commit 17bf4d37a0b7d2c85378922da529f61502126819 Author: Arturo Filastò art@fuffa.org Date: Sat Mar 1 18:59:20 2014 +0100
Add unittests for oonicli.
Allows to programatically run nettests. --- .travis.yml | 1 + bin/ooniprobe | 6 ++- ooni/director.py | 16 +++--- ooni/nettest.py | 11 ++-- ooni/oonicli.py | 31 +++++------- ooni/reporter.py | 11 ++-- ooni/tasks.py | 8 +-- ooni/tests/__init__.py | 1 + ooni/tests/mocks.py | 3 -- ooni/tests/test_director.py | 48 ++++++++++-------- ooni/tests/test_oonicli.py | 115 ++++++++++++++++++++++++++++++++++++++++++ ooni/tests/test_templates.py | 4 -- ooni/utils/log.py | 37 +++++++++----- 13 files changed, 210 insertions(+), 82 deletions(-)
diff --git a/.travis.yml b/.travis.yml index 718e405..6bce942 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python before_install: - sudo apt-get install tor libpcap-dev libgeoip-dev + - sudo /etc/init.d/tor start python: - "2.7" # command to install dependencies diff --git a/bin/ooniprobe b/bin/ooniprobe index 695b137..5e277e8 100755 --- a/bin/ooniprobe +++ b/bin/ooniprobe @@ -14,4 +14,8 @@ copy_reg._reduce_ex = patched_reduce_ex # from ooni.oonicli import run # run() from ooni.oonicli import runWithDirector -runWithDirector() +d = runWithDirector() +@d.addBoth +def cb(result): + reactor.stop() +reactor.run() diff --git a/ooni/director.py b/ooni/director.py index f3d646a..acf50cc 100644 --- a/ooni/director.py +++ b/ooni/director.py @@ -217,7 +217,6 @@ class Director(object): net_test_loader: an instance of :class:ooni.nettest.NetTestLoader """ - if config.privacy.includepcap: if not config.reports.pcap: config.reports.pcap = config.generatePcapFilename(net_test_loader.testDetails) @@ -231,14 +230,14 @@ class Director(object): yield net_test.report.open()
yield net_test.initializeInputProcessor() - self.measurementManager.schedule(net_test.generateMeasurements()) - - self.activeNetTests.append(net_test) - - yield net_test.done - yield report.close() + try: + self.activeNetTests.append(net_test) + self.measurementManager.schedule(net_test.generateMeasurements())
- self.netTestDone(net_test) + yield net_test.done + yield report.close() + finally: + self.netTestDone(net_test)
def startSniffing(self): """ Start sniffing with Scapy. Exits if required privileges (root) are not @@ -287,6 +286,7 @@ class Director(object): Called when we read from stdout that Tor has reached 100%. """ log.debug("Building a TorState") + config.tor.protocol = proto state = TorState(proto.tor_protocol) state.post_bootstrap.addCallback(state_complete) state.post_bootstrap.addErrback(setup_failed) diff --git a/ooni/nettest.py b/ooni/nettest.py index 943aeba..774c962 100644 --- a/ooni/nettest.py +++ b/ooni/nettest.py @@ -495,6 +495,9 @@ class NetTest(object): self.done = defer.Deferred()
self.state = NetTestState(self.done) + + def __str__(self): + return ' '.join(tc.name for tc, _ in self.testCases)
def doneReport(self, report_results): """ @@ -642,7 +645,6 @@ class NetTestCase(object): inputFilename = None
report = {} - report['errors'] = []
usageOptions = usage.Options
@@ -660,6 +662,7 @@ class NetTestCase(object): This is the internal setup method to be overwritten by templates. """ self.report = {} + self.inputs = None
def setUp(self): """ @@ -742,13 +745,13 @@ class NetTestCase(object): a generator that will yield one item from the file based on the inputProcessor. """ - if self.inputs: - return self.inputs - if self.inputFileSpecified: self.inputFilename = self.localOptions[self.inputFile[0]] return self.inputProcessor(self.inputFilename)
+ if self.inputs: + return self.inputs + return None
def _checkValidOptions(self): diff --git a/ooni/oonicli.py b/ooni/oonicli.py index 904f7fb..f75b31c 100644 --- a/ooni/oonicli.py +++ b/ooni/oonicli.py @@ -92,16 +92,7 @@ def parseOptions():
return dict(cmd_line_options)
-def shutdown(result): - """ - This will get called once all the operations that need to be done in the - current oonicli session have been completed. - """ - log.debug("Halting reactor") - try: reactor.stop() - except: pass - -def runWithDirector(): +def runWithDirector(logging=True, start_tor=True): """ Instance the director, parse command line options and start an ooniprobe test! @@ -110,8 +101,11 @@ def runWithDirector(): config.global_options = global_options config.set_paths() config.read_config_file() + if not start_tor: + config.advanced.start_tor = False
- log.start(global_options['logfile']) + if logging: + log.start(global_options['logfile'])
if config.privacy.includepcap: try: @@ -172,7 +166,7 @@ def runWithDirector(): sys.exit(2)
def setup_nettest(_): - try: + try: return deck.setup() except errors.UnableToLoadDeckInput as error: return defer.failure.Failure(error) @@ -241,7 +235,8 @@ def runWithDirector(): raise errors.TorNotRunning
test_details = net_test_loader.testDetails - yaml_reporter = YAMLReporter(test_details) + yaml_reporter = YAMLReporter(test_details, + report_filename=global_options['reportfile']) reporters = [yaml_reporter]
if collector: @@ -252,15 +247,13 @@ def runWithDirector(): except errors.InvalidOONIBCollectorAddress, e: raise e
- log.debug("adding callback for startNetTest") - director.startNetTest(net_test_loader, reporters) - - director.allTestsDone.addBoth(shutdown) + netTestDone = director.startNetTest(net_test_loader, reporters) + return netTestDone
def start(): d.addCallback(setup_nettest) d.addCallback(post_director_start) d.addErrback(director_startup_failed) + return d
- reactor.callWhenRunning(start) - reactor.run() + return start() diff --git a/ooni/reporter.py b/ooni/reporter.py index c8d0748..3159428 100644 --- a/ooni/reporter.py +++ b/ooni/reporter.py @@ -165,15 +165,16 @@ class YAMLReporter(OReporter): the destination directory of the report
""" - def __init__(self, test_details, report_destination='.'): + def __init__(self, test_details, report_destination='.', report_filename=None): self.reportDestination = report_destination
if not os.path.isdir(report_destination): raise InvalidDestination - - report_filename = "report-" + \ - test_details['test_name'] + "-" + \ - otime.timestamp() + ".yamloo" + + if not report_filename: + report_filename = "report-" + \ + test_details['test_name'] + "-" + \ + otime.timestamp() + ".yamloo"
report_path = os.path.join(self.reportDestination, report_filename)
diff --git a/ooni/tasks.py b/ooni/tasks.py index efca9b0..578d8a9 100644 --- a/ooni/tasks.py +++ b/ooni/tasks.py @@ -105,11 +105,11 @@ class Measurement(TaskWithTimeout): """ self.testInstance = test_instance self.testInstance.input = test_input + self.testInstance._setUp() if 'input' not in self.testInstance.report.keys(): - self.testInstance.report = {'input': test_input} - self.testInstance._setUp() - self.testInstance._start_time = time.time() - self.testInstance.setUp() + self.testInstance.report['input'] = test_input + self.testInstance._start_time = time.time() + self.testInstance.setUp()
self.netTestMethod = getattr(self.testInstance, test_method)
diff --git a/ooni/tests/__init__.py b/ooni/tests/__init__.py index 1b5c949..5663ccf 100644 --- a/ooni/tests/__init__.py +++ b/ooni/tests/__init__.py @@ -1,3 +1,4 @@ from ooni.settings import config
config.logging = False +config.advanced.debug = False diff --git a/ooni/tests/mocks.py b/ooni/tests/mocks.py index f849344..3fd75aa 100644 --- a/ooni/tests/mocks.py +++ b/ooni/tests/mocks.py @@ -1,13 +1,10 @@ from twisted.python import failure from twisted.internet import defer
-from ooni.settings import config from ooni.tasks import BaseTask, TaskWithTimeout from ooni.nettest import NetTest from ooni.managers import TaskManager
-config.logging = False - class MockMeasurementFailOnce(BaseTask): def run(self): f = open('dummyTaskFailOnce.txt', 'w') diff --git a/ooni/tests/test_director.py b/ooni/tests/test_director.py index c1ad524..9d86720 100644 --- a/ooni/tests/test_director.py +++ b/ooni/tests/test_director.py @@ -6,7 +6,30 @@ from ooni.director import Director from twisted.internet import defer from twisted.trial import unittest
+from txtorcon import TorControlProtocol +proto = MagicMock() +proto.tor_protocol = TorControlProtocol() + +mock_TorState = MagicMock() +# We use the instance of mock_TorState so that the mock caching will +# return the same instance when TorState is created. +mts = mock_TorState() +mts.protocol.get_conf = lambda x: defer.succeed({'SocksPort': '4242'}) +mts.post_bootstrap = defer.succeed(mts) + +# Set the tor_protocol to be already fired +state = MagicMock() +proto.tor_protocol.post_bootstrap = defer.succeed(state) + +mock_launch_tor = MagicMock() +mock_launch_tor.return_value = defer.succeed(proto) + class TestDirector(unittest.TestCase): + def tearDown(self): + config.tor_state = None + config.tor.socks_port = None + config.tor.control_port = None + def test_get_net_tests(self): director = Director() nettests = director.getNetTests() @@ -14,34 +37,15 @@ class TestDirector(unittest.TestCase): assert 'dnsconsistency' in nettests assert 'http_header_field_manipulation' in nettests assert 'traceroute' in nettests - + + @patch('ooni.director.TorState', mock_TorState) + @patch('ooni.director.launch_tor', mock_launch_tor) def test_start_tor(self): - from txtorcon import TorControlProtocol - proto = MagicMock() - proto.tor_protocol = TorControlProtocol() - - mock_TorState = MagicMock() - # We use the instance of mock_TorState so that the mock caching will - # return the same instance when TorState is created. - mts = mock_TorState() - mts.protocol.get_conf = lambda x: defer.succeed({'SocksPort': '4242'}) - mts.post_bootstrap = defer.succeed(mts) - - # Set the tor_protocol to be already fired - state = MagicMock() - proto.tor_protocol.post_bootstrap = defer.succeed(state) - - mock_launch_tor = MagicMock() - mock_launch_tor.return_value = defer.succeed(proto) - - @patch('ooni.director.TorState', mock_TorState) - @patch('ooni.director.launch_tor', mock_launch_tor) @defer.inlineCallbacks def director_start_tor(): director = Director() yield director.startTor() assert config.tor.socks_port == 4242 assert config.tor.control_port == 4242 - config.tor_state = None
return director_start_tor() diff --git a/ooni/tests/test_oonicli.py b/ooni/tests/test_oonicli.py new file mode 100644 index 0000000..ab10000 --- /dev/null +++ b/ooni/tests/test_oonicli.py @@ -0,0 +1,115 @@ +import os +import sys +import yaml +import signal + +from twisted.internet import base, defer +from twisted.trial import unittest + +from ooni.settings import config +from ooni.oonicli import runWithDirector + +def verify_header(header): + assert 'input_hashes' in header.keys() + assert 'options' in header.keys() + assert 'probe_asn' in header.keys() + assert 'probe_cc' in header.keys() + assert 'probe_ip' in header.keys() + assert 'software_name' in header.keys() + assert 'software_version' in header.keys() + assert 'test_name' in header.keys() + assert 'test_version' in header.keys() + +def verify_entry(entry): + assert 'input' in entry + + +class TestRunDirector(unittest.TestCase): + def setUp(self): + config.tor.socks_port = 9050 + config.tor.control_port = None + with open('example-input.txt', 'w+') as f: + f.write('http://torproject.org/%5Cn') + f.write('http://bridges.torproject.org/%5Cn') + f.write('http://blog.torproject.org/%5Cn') + + def tearDown(self): + os.remove('test_report.yaml') + os.remove('example-input.txt') + + @defer.inlineCallbacks + def run_test(self, test_name, args, verify_function): + output_file = 'test_report.yaml' + sys.argv = ['', '-n', '-o', output_file, test_name] + sys.argv.extend(args) + yield runWithDirector(False, False) + with open(output_file) as f: + entries = yaml.safe_load_all(f) + header = entries.next() + try: + first_entry = entries.next() + except StopIteration: + raise Exception("Missing entry in report") + verify_header(header) + verify_entry(first_entry) + verify_function(first_entry) + + @defer.inlineCallbacks + def test_http_requests(self): + def verify_function(entry): + assert 'body_length_match' in entry + assert 'body_proportion' in entry + assert 'control_failure' in entry + assert 'experiment_failure' in entry + assert 'factor' in entry + assert 'headers_diff' in entry + assert 'headers_match' in entry + yield self.run_test('blocking/http_requests', + ['-u', 'http://torproject.org/'], + verify_function) + + @defer.inlineCallbacks + def test_http_requests_with_file(self): + def verify_function(entry): + assert 'body_length_match' in entry + assert 'body_proportion' in entry + assert 'control_failure' in entry + assert 'experiment_failure' in entry + assert 'factor' in entry + assert 'headers_diff' in entry + assert 'headers_match' in entry + yield self.run_test('blocking/http_requests', + ['-f', 'example-input.txt'], + verify_function) + + @defer.inlineCallbacks + def test_dnsconsistency(self): + def verify_function(entry): + assert 'queries' in entry + assert 'control_resolver' in entry + assert 'tampering' in entry + assert len(entry['tampering']) == 1 + yield self.run_test('blocking/dnsconsistency', + ['-b', '8.8.8.8:53', + '-t', '8.8.8.8', + '-f', 'example-input.txt'], + verify_function) + + @defer.inlineCallbacks + def test_http_header_field_manipulation(self): + def verify_function(entry): + assert 'agent' in entry + assert 'requests' in entry + assert 'socksproxy' in entry + assert 'tampering' in entry + assert 'header_field_name' in entry['tampering'] + assert 'header_field_number' in entry['tampering'] + assert 'header_field_value' in entry['tampering'] + assert 'header_name_capitalization' in entry['tampering'] + assert 'header_name_diff' in entry['tampering'] + assert 'request_line_capitalization' in entry['tampering'] + assert 'total' in entry['tampering'] + + yield self.run_test('manipulation/http_header_field_manipulation', + ['-b', 'http://64.9.225.221'], + verify_function) diff --git a/ooni/tests/test_templates.py b/ooni/tests/test_templates.py index 987c7fa..66960c3 100644 --- a/ooni/tests/test_templates.py +++ b/ooni/tests/test_templates.py @@ -1,13 +1,9 @@ -from ooni.settings import config - from ooni.templates import httpt
from twisted.internet.error import DNSLookupError from twisted.internet import reactor, defer from twisted.trial import unittest
-config.logging = False - class TestHTTPT(unittest.TestCase): def setUp(self): from twisted.web.resource import Resource diff --git a/ooni/utils/log.py b/ooni/utils/log.py index e0ad5d9..379b5c7 100644 --- a/ooni/utils/log.py +++ b/ooni/utils/log.py @@ -24,25 +24,38 @@ class LogWithNoPrefix(txlog.FileLogObserver): util.untilConcludes(self.write, "%s\n" % text) util.untilConcludes(self.flush) # Hoorj!
-def start(logfile=None, application_name="ooniprobe"): - daily_logfile = None +class OONILogger(object): + def start(self, logfile=None, application_name="ooniprobe"): + daily_logfile = None + + if not logfile: + logfile = os.path.expanduser(config.basic.logfile) + + log_folder = os.path.dirname(logfile) + log_filename = os.path.basename(logfile)
- if not logfile: - logfile = os.path.expanduser(config.basic.logfile) + daily_logfile = DailyLogFile(log_filename, log_folder)
- log_folder = os.path.dirname(logfile) - log_filename = os.path.basename(logfile) + txlog.msg("Starting %s on %s (%s UTC)" % (application_name, otime.prettyDateNow(), + otime.utcPrettyDateNow())) + + self.fileObserver = txlog.FileLogObserver(daily_logfile) + self.stdoutObserver = LogWithNoPrefix(sys.stdout)
- daily_logfile = DailyLogFile(log_filename, log_folder) + txlog.startLoggingWithObserver(self.stdoutObserver.emit) + txlog.addObserver(self.fileObserver.emit)
- txlog.msg("Starting %s on %s (%s UTC)" % (application_name, otime.prettyDateNow(), - otime.utcPrettyDateNow())) + def stop(self): + self.stdoutObserver.stop() + self.fileObserver.stop()
- txlog.startLoggingWithObserver(LogWithNoPrefix(sys.stdout).emit) - txlog.addObserver(txlog.FileLogObserver(daily_logfile).emit) +oonilogger = OONILogger() + +def start(logfile=None, application_name="ooniprobe"): + oonilogger.start(logfile, application_name)
def stop(): - print "Stopping OONI" + oonilogger.stop()
def msg(msg, *arg, **kw): if config.logging:
tor-commits@lists.torproject.org