[tor-commits] [ooni-probe/master] Add unittests for oonicli.

art at torproject.org art at torproject.org
Fri Mar 7 15:45:36 UTC 2014


commit 17bf4d37a0b7d2c85378922da529f61502126819
Author: Arturo Filastò <art at 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()
+ at 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/\n')
+            f.write('http://bridges.torproject.org/\n')
+            f.write('http://blog.torproject.org/\n')
+    
+    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:





More information about the tor-commits mailing list