commit c4ad2411046deb08f17902320cb8dd21cc25f367 Author: Arturo Filastò arturo@filasto.net Date: Tue Sep 11 10:38:35 2012 +0000
Import basic stuff from Twisted trial --- .gitignore | 1 + README.md | 10 + bin/ooniprobe | 16 ++- ooni/oonicli.py | 397 +++++++++++++++++++++++++++++++++++++++++++ ooni/oonitests/bridget.py | 373 ---------------------------------------- ooni/plugins/new_bridget.py | 2 +- ooni/protocols/http.py | 13 +- ooni/reporter.py | 13 ++ ooni/runner.py | 7 + 9 files changed, 448 insertions(+), 384 deletions(-)
diff --git a/.gitignore b/.gitignore index 7f270bb..6b1b9ce 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ proxy-lists/italy-http-ips.txt private/* /ooni/plugins/dropin.cache oonib/oonibackend.conf +ooni/assets/* diff --git a/README.md b/README.md index b39cb2b..123d837 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,16 @@ To list the help for a specific test:
python ooniprobe.py httpt --help
+## Virtualenv way (Recommended) + + virtualenv2 ENV/ + source ENV/bin/activate + pip install twisted Scapy + +To install the most up to date scapy version (requires mercurial): + + pip install hg+http://hg.secdev.org/scapy +
# More details
diff --git a/bin/ooniprobe b/bin/ooniprobe index 5c87831..1f0c26d 100755 --- a/bin/ooniprobe +++ b/bin/ooniprobe @@ -1,5 +1,11 @@ -#!/bin/sh -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -echo $ROOT -export PYTHONPATH=$PYTHONPATH:$ROOT -python $ROOT/ooni/ooniprobe.py $1 +#!/usr/bin/python2 +# startup script based on twisted trial +# See http://twistedmatrix.com/ +import os, sys + +sys.path[:] = map(os.path.abspath, sys.path) + +sys.path.insert(0, os.path.abspath(os.getcwd())) + +from ooni.oonicli import run +run() diff --git a/ooni/oonicli.py b/ooni/oonicli.py new file mode 100644 index 0000000..8ace160 --- /dev/null +++ b/ooni/oonicli.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 +# +# oonicli +# ********* +# +# oonicli is the next generation ooniprober. It based off of twisted's trial +# unit testing framework. +# +# :copyright: (c) 2012 by Arturo Filastò +# :license: see LICENSE for more details. +# +# original copyright (c) by Twisted Matrix Laboratories. + + +import sys, os, random, gc, time, warnings + +from twisted.internet import defer +from twisted.application import app +from twisted.python import usage, reflect, failure +from twisted.python.filepath import FilePath +from twisted import plugin +from twisted.python.util import spewer +from twisted.python.compat import set +from twisted.trial import runner, itrial, reporter + + +# Yea, this is stupid. Leave it for for command-line compatibility for a +# while, though. +TBFORMAT_MAP = { + 'plain': 'default', + 'default': 'default', + 'emacs': 'brief', + 'brief': 'brief', + 'cgitb': 'verbose', + 'verbose': 'verbose' + } + + +def _parseLocalVariables(line): + """ + Accepts a single line in Emacs local variable declaration format and + returns a dict of all the variables {name: value}. + Raises ValueError if 'line' is in the wrong format. + + See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html + """ + paren = '-*-' + start = line.find(paren) + len(paren) + end = line.rfind(paren) + if start == -1 or end == -1: + raise ValueError("%r not a valid local variable declaration" % (line,)) + items = line[start:end].split(';') + localVars = {} + for item in items: + if len(item.strip()) == 0: + continue + split = item.split(':') + if len(split) != 2: + raise ValueError("%r contains invalid declaration %r" + % (line, item)) + localVars[split[0].strip()] = split[1].strip() + return localVars + + +def loadLocalVariables(filename): + """ + Accepts a filename and attempts to load the Emacs variable declarations + from that file, simulating what Emacs does. + + See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html + """ + f = file(filename, "r") + lines = [f.readline(), f.readline()] + f.close() + for line in lines: + try: + return _parseLocalVariables(line) + except ValueError: + pass + return {} + + +def getTestModules(filename): + testCaseVar = loadLocalVariables(filename).get('test-case-name', None) + if testCaseVar is None: + return [] + return testCaseVar.split(',') + + +def isTestFile(filename): + """ + Returns true if 'filename' looks like a file containing unit tests. + False otherwise. Doesn't care whether filename exists. + """ + basename = os.path.basename(filename) + return (basename.startswith('test_') + and os.path.splitext(basename)[1] == ('.py')) + + +def _reporterAction(): + return usage.CompleteList([p.longOpt for p in + plugin.getPlugins(itrial.IReporter)]) + +class Options(usage.Options): + + optParameters = [ + ['parallelism', 'n', 10, "Specify the number of parallel tests to run"], + ['output', 'o', 'report.log', "Specify output report file"], + ['log', 'l', 'oonicli.log', "Specify output log file"] + ] + + def opt_version(self): + """ + Display OONI version and exit. + """ + print "OONI version:", __version__ + sys.exit(0) + + +class Options(usage.Options, app.ReactorSelectionMixin): + synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...] + """ % (os.path.basename(sys.argv[0]),) + + longdesc = ("ooniprobe loads and executes a suite or a set of suites of" + "network tests. These are loaded from modules, packages and" + "files listed on the command line") + + optFlags = [["help", "h"], + ["rterrors", "e", "realtime errors, print out tracebacks as " + "soon as they occur"], + ["debug", "b", "Run tests in the Python debugger. Will load " + "'.pdbrc' from current directory if it exists."], + ["debug-stacktraces", "B", "Report Deferred creation and " + "callback stack traces"], + ["nopm", None, "don't automatically jump into debugger for " + "postmorteming of exceptions"], + ["dry-run", 'n', "do everything but run the tests"], + ["force-gc", None, "Have Trial run gc.collect() before and " + "after each test case."], + ["profile", None, "Run tests under the Python profiler"], + ["unclean-warnings", None, + "Turn dirty reactor errors into warnings"], + ["until-failure", "u", "Repeat test until it fails"], + ["no-recurse", "N", "Don't recurse into packages"], + ['help-reporters', None, + "Help on available output plugins (reporters)"] + ] + + optParameters = [ + ["logfile", "l", "test.log", "log file name"], + ["random", "z", None, + "Run tests in random order using the specified seed"], + ['temp-directory', None, '_trial_temp', + 'Path to use as working directory for tests.'], + ['reporter', None, 'verbose', + 'The reporter to use for this test run. See --help-reporters for ' + 'more info.']] + + compData = usage.Completions( + optActions={"tbformat": usage.CompleteList(["plain", "emacs", "cgitb"]), + "reporter": _reporterAction, + "logfile": usage.CompleteFiles(descr="log file name"), + "random": usage.Completer(descr="random seed")}, + extraActions=[usage.CompleteFiles( + "*.py", descr="file | module | package | TestCase | testMethod", + repeat=True)], + ) + + fallbackReporter = reporter.TreeReporter + tracer = None + + def __init__(self): + self['tests'] = set() + usage.Options.__init__(self) + + + def coverdir(self): + """ + Return a L{FilePath} representing the directory into which coverage + results should be written. + """ + coverdir = 'coverage' + result = FilePath(self['temp-directory']).child(coverdir) + print "Setting coverage directory to %s." % (result.path,) + return result + + + def opt_coverage(self): + """ + Generate coverage information in the I{coverage} file in the + directory specified by the I{trial-temp} option. + """ + import trace + self.tracer = trace.Trace(count=1, trace=0) + sys.settrace(self.tracer.globaltrace) + + + def opt_testmodule(self, filename): + """ + Filename to grep for test cases (-*- test-case-name) + """ + # If the filename passed to this parameter looks like a test module + # we just add that to the test suite. + # + # If not, we inspect it for an Emacs buffer local variable called + # 'test-case-name'. If that variable is declared, we try to add its + # value to the test suite as a module. + # + # This parameter allows automated processes (like Buildbot) to pass + # a list of files to Trial with the general expectation of "these files, + # whatever they are, will get tested" + if not os.path.isfile(filename): + sys.stderr.write("File %r doesn't exist\n" % (filename,)) + return + filename = os.path.abspath(filename) + if isTestFile(filename): + self['tests'].add(filename) + else: + self['tests'].update(getTestModules(filename)) + + + def opt_spew(self): + """ + Print an insanely verbose log of everything that happens. Useful + when debugging freezes or locks in complex code. + """ + sys.settrace(spewer) + + + def opt_help_reporters(self): + synopsis = ("OONI's output can be customized using plugins called " + "Reporters. You can\nselect any of the following " + "reporters using --reporter=<foo>\n") + print synopsis + for p in plugin.getPlugins(itrial.IReporter): + print ' ', p.longOpt, '\t', p.description + print + sys.exit(0) + + + def opt_disablegc(self): + """ + Disable the garbage collector + """ + gc.disable() + + + def opt_tbformat(self, opt): + """ + Specify the format to display tracebacks with. Valid formats are + 'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib + cgitb.text function + """ + try: + self['tbformat'] = TBFORMAT_MAP[opt] + except KeyError: + raise usage.UsageError( + "tbformat must be 'plain', 'emacs', or 'cgitb'.") + + + def opt_recursionlimit(self, arg): + """ + see sys.setrecursionlimit() + """ + try: + sys.setrecursionlimit(int(arg)) + except (TypeError, ValueError): + raise usage.UsageError( + "argument to recursionlimit must be an integer") + + + def opt_random(self, option): + try: + self['random'] = long(option) + except ValueError: + raise usage.UsageError( + "Argument to --random must be a positive integer") + else: + if self['random'] < 0: + raise usage.UsageError( + "Argument to --random must be a positive integer") + elif self['random'] == 0: + self['random'] = long(time.time() * 100) + + + def opt_without_module(self, option): + """ + Fake the lack of the specified modules, separated with commas. + """ + for module in option.split(","): + if module in sys.modules: + warnings.warn("Module '%s' already imported, " + "disabling anyway." % (module,), + category=RuntimeWarning) + sys.modules[module] = None + + + def parseArgs(self, *args): + self['tests'].update(args) + + + def _loadReporterByName(self, name): + for p in plugin.getPlugins(itrial.IReporter): + qual = "%s.%s" % (p.module, p.klass) + if p.longOpt == name: + return reflect.namedAny(qual) + raise usage.UsageError("Only pass names of Reporter plugins to " + "--reporter. See --help-reporters for " + "more info.") + + + def postOptions(self): + # Only load reporters now, as opposed to any earlier, to avoid letting + # application-defined plugins muck up reactor selecting by importing + # t.i.reactor and causing the default to be installed. + self['reporter'] = self._loadReporterByName(self['reporter']) + + if 'tbformat' not in self: + self['tbformat'] = 'default' + if self['nopm']: + if not self['debug']: + raise usage.UsageError("you must specify --debug when using " + "--nopm ") + failure.DO_POST_MORTEM = False + + + +def _initialDebugSetup(config): + # do this part of debug setup first for easy debugging of import failures + if config['debug']: + failure.startDebugMode() + if config['debug'] or config['debug-stacktraces']: + defer.setDebugging(True) + + + +def _getSuite(config): + loader = _getLoader(config) + recurse = not config['no-recurse'] + return loader.loadByNames(config['tests'], recurse) + + + +def _getLoader(config): + loader = runner.TestLoader() + if config['random']: + randomer = random.Random() + randomer.seed(config['random']) + loader.sorter = lambda x : randomer.random() + print 'Running tests shuffled with seed %d\n' % config['random'] + if not config['until-failure']: + loader.suiteFactory = runner.DestructiveTestSuite + return loader + + + +def _makeRunner(config): + mode = None + if config['debug']: + mode = runner.TrialRunner.DEBUG + if config['dry-run']: + mode = runner.TrialRunner.DRY_RUN + return runner.TrialRunner(config['reporter'], + mode=mode, + profile=config['profile'], + logfile=config['logfile'], + tracebackFormat=config['tbformat'], + realTimeErrors=config['rterrors'], + uncleanWarnings=config['unclean-warnings'], + workingDirectory=config['temp-directory'], + forceGarbageCollection=config['force-gc']) + + + +def run(): + if len(sys.argv) == 1: + sys.argv.append("--help") + config = Options() + try: + config.parseOptions() + except usage.error, ue: + raise SystemExit, "%s: %s" % (sys.argv[0], ue) + _initialDebugSetup(config) + trialRunner = _makeRunner(config) + suite = _getSuite(config) + if config['until-failure']: + test_result = trialRunner.runUntilFailure(suite) + else: + test_result = trialRunner.run(suite) + if config.tracer: + sys.settrace(None) + results = config.tracer.results() + results.write_results(show_missing=1, summary=False, + coverdir=config.coverdir().path) + sys.exit(not test_result.wasSuccessful()) + diff --git a/ooni/oonitests/bridget.py b/ooni/oonitests/bridget.py deleted file mode 100644 index a613f61..0000000 --- a/ooni/oonitests/bridget.py +++ /dev/null @@ -1,373 +0,0 @@ -# -*- coding: UTF-8 -""" - bridgeT - ******* - - an OONI test (we call them Plugoos :P) aimed - at detecting if a set of Tor bridges are working or not. - - :copyright: (c) 2012 by Arturo Filastò - :license: BSD, see LICENSE for more details. -""" -import os -import sys -import errno -import time -import random -import re -import glob -import socks -import socket -from shutil import rmtree -from subprocess import Popen, PIPE -from datetime import datetime - -import shutil -import gevent -from gevent import socket -import fcntl -from plugoo.assets import Asset -from plugoo.tests import Test -import urllib2 -import httplib -import json - -try: - from TorCtl import TorCtl -except: - print "Error TorCtl not installed!" - -__plugoo__ = "BridgeT" -__desc__ = "BridgeT, for testing Tor Bridge reachability" -ONIONOO_URL="http://85.214.195.203/summary/search/" - -class SocksiPyConnection(httplib.HTTPConnection): - def __init__(self, proxytype, proxyaddr, proxyport = None, rdns = True, - username = None, password = None, *args, **kwargs): - self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) - httplib.HTTPConnection.__init__(self, *args, **kwargs) - - def connect(self): - self.sock = socks.socksocket() - self.sock.setproxy(*self.proxyargs) - if isinstance(self.timeout, float): - self.sock.settimeout(self.timeout) - self.sock.connect((self.host, self.port)) - -class SocksiPyHandler(urllib2.HTTPHandler): - def __init__(self, *args, **kwargs): - self.args = args - self.kw = kwargs - urllib2.HTTPHandler.__init__(self) - - def http_open(self, req): - def build(host, port=None, strict=None, timeout=0): - conn = SocksiPyConnection(*self.args, host=host, port=port, - strict=strict, timeout=timeout, **self.kw) - return conn - return self.do_open(build, req) - -class BridgeTAsset(Asset): - def __init__(self, file=None): - self = Asset.__init__(self, file) - -class BridgeT(Test): - # This is the timeout value after which - # we will give up - timeout = 20 - # These are the modules that should be torified - modules = [urllib2] - - def tor_greater_than(self, version): - """ - Checks if the currently installed version of Tor is greater - than the required version. - - :version The version of Tor to check against for greater than or equal - """ - fullstring = os.popen("tor --version").read().split('\n')[-2] - v_array = fullstring.split(' ')[2].split('-') - minor = v_array[1:] - v_array = v_array[0].split('.') - minor_p = version.split('-')[1:] - v_array_p = version.split('-')[0].split('.') - - for i, x in enumerate(v_array): - try: - if i > len(v_array_p): - break - - if int(x) > int(v_array_p[i]): - self.logger.debug("The Tor version is greater than %s" % version) - return True - elif int(x) == int(v_array_p[i]): - self.logger.debug("The Tor version is greater than %s" % version) - continue - else: - self.logger.debug("You run an outdated version of Tor: %s (< %s)" % (fullstring, version)) - return False - except: - self.logger.error("Error in parsing your Tor version string: %s" % fullstring) - return False - - self.logger.debug("The Tor version is equal to %s" % version) - return True - # XXX currently don't consider the minor parts of the version - # (alpha, dev, beta, etc.) - - def free_port(self, port): - s = socket.socket() - try: - s.bind(('127.0.0.1', port)) - s.close() - return True - except: - self.logger.warn("The randomly chosen port was already taken!") - s.close() - return False - - def writetorrc(self, bridge): - """ - Write the torrc file for the tor process to be used - to test the bridge. - - :bridge the bridge to be tested - """ - self.failures = [] - prange = (49152, 65535) - - # register Tor to an ephemeral port - socksport = random.randint(prange[0], prange[1]) - # Keep on trying to get a new port if the chosen one is already - # taken. - while not self.free_port(socksport): - socksport = random.randint(prange[0], prange[1]) - controlport = random.randint(prange[0], prange[1]) - while not self.free_port(controlport): - controlport = random.randint(prange[0], prange[1]) - - randomname = "tor_"+str(random.randint(0, 424242424242)) - datadir = "/tmp/" + randomname - if bridge.startswith("obfs://"): - obfsbridge = bridge.split("//")[1] - - self.logger.debug("Genearting torrc file for obfs bridge") - torrc = """SocksPort %s -UseBridges 1 -Bridge obfs2 %s -DataDirectory %s -ClientTransportPlugin obfs2 exec /usr/local/bin/obfsproxy --managed -ControlPort %s -Log info file %s -""" % (socksport, obfsbridge, datadir, controlport, os.path.join(datadir,'tor.log')) - else: - self.logger.debug("Generating torrc file for bridge") - if self.tor_greater_than('0.2.3.2'): - - torrc = """SocksPort %s -UseBridges 1 -bridge %s -DataDirectory %s -usemicrodescriptors 0 -ControlPort %s -Log info file %s -""" % (socksport, bridge, datadir, controlport, os.path.join(datadir,'tor.log')) - else: - torrc = """SocksPort %s -UseBridges 1 -bridge %s -DataDirectory %s -ControlPort %s -Log info file %s -""" % (socksport, bridge, datadir, controlport, os.path.join(datadir,'tor.log')) - - with open(randomname, "wb") as f: - f.write(torrc) - - os.mkdir(datadir) - return (randomname, datadir, controlport, socksport) - - def parsebridgeinfo(self, output): - ret = {} - fields = ['router', 'platform', 'opt', 'published', 'uptime', 'bandwidth'] - - for x in output.split("\n"): - cfield = x.split(' ') - if cfield[0] in fields: - #not sure if hellais did this on purpose, but this overwrites - #the previous entries. For ex, 'opt' has multiple entries and - #only the last value is stored - ret[cfield[0]] = ' '.join(cfield[1:]) - if cfield[1] == 'fingerprint': - ret['fingerprint'] = ''.join(cfield[2:]) - return ret - - #Can't use @torify as it doesn't support concurrency right now - def download_file(self, socksport): - opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, - '127.0.0.1', int(socksport))) - - time_start=time.time() - f = opener.open('http://38.229.72.16/bwauth.torproject.org/256k') - f.read() - time_end = time.time() - print (time_end-time_start) - return str(256/(time_end-time_start)) + " KB/s" - - def is_public(self, fp, socksport): - opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5,'127.0.0.1',int(socksport))) - response = opener.open(str(ONIONOO_URL)+str(fp)) - reply = json.loads(response.read()) - if reply['bridges'] or reply['relays']: - return True - return False - - def connect(self, bridge, timeout=None): - bridgeinfo = None - bandwidth = None - public = None - if not timeout: - if self.config.tests.tor_bridges_timeout: - self.timeout = self.config.tests.tor_bridges_timeout - timeout = self.timeout - torrc, tordir, controlport, socksport = self.writetorrc(bridge) - cmd = ["tor", "-f", torrc] - - tupdate = time.time() - debugupdate = time.time() - - try: - p = Popen(cmd, stdout=PIPE) - except: - self.logger.error("Error in starting Tor (do you have tor installed?)") - - self.logger.info("Testing bridge: %s" % bridge) - while True: - o = "" - try: - o = p.stdout.read(4096) - if o: - self.logger.debug(str(o)) - if re.search("100%", o): - self.logger.info("Success in connecting to %s" % bridge) - - print "%s bridge works" % bridge - # print "%s controlport" % controlport - try: - c = TorCtl.connect('127.0.0.1', controlport) - bridgeinfo = self.parsebridgeinfo(c.get_info('dir/server/all')['dir/server/all']) - c.close() - except: - self.logger.error("Error in connecting to Tor Control port") - - # XXX disable the public checking - #public = self.is_public(bridgeinfo['fingerprint'], socksport) - #self.logger.info("Public: %s" % public) - - bandwidth = self.download_file(socksport) - self.logger.info("Bandwidth: %s" % bandwidth) - - try: - p.stdout.close() - except: - self.logger.error("Error in closing stdout FD.") - - try: - os.unlink(os.path.join(os.getcwd(), torrc)) - rmtree(tordir) - except: - self.logger.error("Error in unlinking files.") - - p.terminate() - return { - 'Time': datetime.now(), - 'Bridge': bridge, - 'Working': True, - 'Descriptor': bridgeinfo, - 'Calculated bandwidth': bandwidth, - 'Public': public - } - - if re.search("%", o): - # Keep updating the timeout if there is progress - self.logger.debug("Updating time...") - tupdate = time.time() - #print o - continue - - except IOError: - ex = sys.exc_info()[1] - if ex[0] != errno.EAGAIN: - self.logger.error("Error IOError: EAGAIN") - raise - sys.exc_clear() - print "In this exception 1" - - try: - # Set the timeout for the socket wait - ct = timeout-(time.time() - tupdate) - socket.wait_read(p.stdout.fileno(), timeout=ct) - - except: - lfh = open(os.path.join(tordir, 'tor.log'), 'r') - log = lfh.readlines() - lfh.close() - self.logger.info("%s bridge does not work (%s s timeout)" % (bridge, timeout)) - print "%s bridge does not work (%s s timeout)" % (bridge, timeout) - self.failures.append(bridge) - p.stdout.close() - os.unlink(os.path.join(os.getcwd(), torrc)) - rmtree(tordir) - p.terminate() - return { - 'Time': datetime.now(), - 'Bridge': bridge, - 'Working': False, - 'Descriptor': {}, - 'Log': log - } - - def experiment(self, *a, **kw): - # this is just a dirty hack - bridge = kw['data'] - print "Experiment" - config = self.config - - return self.connect(bridge) - - def clean(self): - for infile in glob.glob('tor_*'): - os.remove(infile) - - def print_failures(self): - if self.failures: - for item in self.failures: - print "Offline : %s" % item - else: - print "All online" - - # For logging TorCtl event msgs - #class LogHandler: - #def msg(self, severity, message): - # print "[%s] %s"%(severity, message) - -def run(ooni, assets=None): - """ - Run the test - """ - - config = ooni.config - urls = [] - - bridges = BridgeTAsset(os.path.join(config.main.assetdir, \ - config.tests.tor_bridges)) - - bridgelist = [bridges] - - bridget = BridgeT(ooni) - ooni.logger.info("Starting bridget test") - bridget.run(bridgelist) - bridget.print_failures() - bridget.clean() - ooni.logger.info("Testing completed!") - diff --git a/ooni/plugins/new_bridget.py b/ooni/plugins/new_bridget.py index 3e4db56..b26455e 100644 --- a/ooni/plugins/new_bridget.py +++ b/ooni/plugins/new_bridget.py @@ -27,7 +27,7 @@ class bridgetArgs(usage.Options): class bridgetTest(OONITest): implements(IPlugin, ITest)
- shortName = "bridget" + shortName = "newbridget" description = "bridget" requirements = None options = bridgetArgs diff --git a/ooni/protocols/http.py b/ooni/protocols/http.py index 2b38f28..09bb9b9 100644 --- a/ooni/protocols/http.py +++ b/ooni/protocols/http.py @@ -84,11 +84,7 @@ class HTTPTest(OONITest): """ pass
- - def experiment(self, args): - log.msg("Running experiment") - url = self.local_options['url'] if 'url' not in args else args['url'] - + def doRequest(self, url): d = self.build_request(url) def finished(data): return data @@ -97,6 +93,13 @@ class HTTPTest(OONITest): d.addCallback(finished) return d
+ def experiment(self, args): + log.msg("Running experiment") + url = self.local_options['url'] if 'url' not in args else args['url'] + + d = self.doRequest(url) + return d + def _cbResponse(self, response): self.response['headers'] = list(response.headers.getAllRawHeaders()) self.response['code'] = response.code diff --git a/ooni/reporter.py b/ooni/reporter.py new file mode 100644 index 0000000..0ecf2ea --- /dev/null +++ b/ooni/reporter.py @@ -0,0 +1,13 @@ +from twisted.trial import reporter + +class TestResult(reporter.TestResult): + """ + Accumulates the results of several ooni.nettest.TestCases. + + The output format of a TestResult is YAML and it will contain all the basic + information that a test result should contain. + """ + def __init__(self): + super(TestResult, self).__init__() + + diff --git a/ooni/runner.py b/ooni/runner.py new file mode 100644 index 0000000..d7caa9d --- /dev/null +++ b/ooni/runner.py @@ -0,0 +1,7 @@ +from twisted.trial import runner + + +class TestLoader(runner.TestLoader): + pass + +