[tor-commits] [ooni-probe/master] Store the state of the currently running tests, their progress and estimated time to completion

art at torproject.org art at torproject.org
Mon Nov 26 02:41:00 UTC 2012


commit a3829e7a9700bfa748088c18e3afa44a086ed6e2
Author: Arturo Filastò <art at fuffa.org>
Date:   Mon Nov 26 03:38:47 2012 +0100

    Store the state of the currently running tests, their progress and estimated time to completion
    * Display every 5 seconds a summary of all the running tests and their progress
    * Refactor input unit and nettest
---
 ooni/config.py          |    1 +
 ooni/inputunit.py       |   27 +++++++++++++-----
 ooni/nettest.py         |   22 ++++++++++-----
 ooni/oonicli.py         |   15 ++++++++++-
 ooni/reporter.py        |    2 +-
 ooni/runner.py          |   66 ++++++++++++++++++++++++++++++++--------------
 tests/test_inputunit.py |   15 +++++++++-
 7 files changed, 108 insertions(+), 40 deletions(-)

diff --git a/ooni/config.py b/ooni/config.py
index d86f4d7..69842b6 100644
--- a/ooni/config.py
+++ b/ooni/config.py
@@ -14,6 +14,7 @@ from ooni.utils import Storage
 reports = Storage()
 scapyFactory = None
 stateDict = None
+state = Storage()
 
 # XXX refactor this to use a database
 resume_lock = defer.DeferredLock()
diff --git a/ooni/inputunit.py b/ooni/inputunit.py
index 65d578c..2ef89d8 100644
--- a/ooni/inputunit.py
+++ b/ooni/inputunit.py
@@ -9,7 +9,6 @@
 # :authors: Arturo Filastò
 # :license: see included LICENSE file
 
-
 class InputUnitFactory(object):
     """
     This is a factory that takes the size of input units to be generated a set
@@ -20,27 +19,39 @@ class InputUnitFactory(object):
     all the elements in memory to be able to produce InputUnits.
     """
     inputUnitSize = 10
+    length = None
     def __init__(self, inputs=[]):
+        """
+        Args:
+            inputs (iterable): inputs *must* be an iterable.
+        """
         self._inputs = iter(inputs)
-        self._idx = 0
+        self.inputs = iter(inputs)
         self._ended = False
 
     def __iter__(self):
         return self
 
+    def __len__(self):
+        """
+        Returns the number of input units in the input unit factory.
+        """
+        if not self.length:
+            self.length = sum(1 for _ in self._inputs)/self.inputUnitSize
+        return self.length
+
     def next(self):
         input_unit_elements = []
 
         if self._ended:
             raise StopIteration
 
-        for i in xrange(self._idx, self._idx + self.inputUnitSize):
+        for i in xrange(self.inputUnitSize):
             try:
-                input_unit_elements.append(self._inputs.next())
+                input_unit_elements.append(self.inputs.next())
             except StopIteration:
                 self._ended = True
                 break
-        self._idx += self.inputUnitSize
 
         if not input_unit_elements:
             raise StopIteration
@@ -55,12 +66,12 @@ class InputUnit(object):
     def __init__(self, inputs=[]):
         self._inputs = iter(inputs)
 
-    def __repr__(self):
+    def __str__(self):
         return "<%s inputs=%s>" % (self.__class__, self._inputs)
 
     def __add__(self, inputs):
-        for input in inputs:
-            self._inputs.append(input)
+        for i in inputs:
+            self._inputs.append(i)
 
     def __iter__(self):
         return self
diff --git a/ooni/nettest.py b/ooni/nettest.py
index e96fba1..e0393e7 100644
--- a/ooni/nettest.py
+++ b/ooni/nettest.py
@@ -22,6 +22,7 @@ from ooni.utils import log
 class NoPostProcessor(Exception):
     pass
 
+
 class NetTestCase(object):
     """
     This is the base of the OONI nettest universe. When you write a nettest
@@ -150,17 +151,22 @@ class NetTestCase(object):
             if not self.localOptions[required_option]:
                 raise usage.UsageError("%s not specified!" % required_option)
 
-    def _processOptions(self, options=None):
+    def _processOptions(self):
         if self.inputFilename:
-            self.inputs = self.inputProcessor(self.inputFilename)
-
-        self._checkRequiredOptions()
+            inputProcessor = self.inputProcessor
+            inputFilename = self.inputFilename
+            class inputProcessorIterator(object):
+                """
+                Here we convert the input processor generator into an iterator
+                so that we can run it twice.
+                """
+                def __iter__(self):
+                    return inputProcessor(inputFilename)
+            self.inputs = inputProcessorIterator()
 
-        # XXX perhaps we may want to name and version to be inside of a
-        # different method that is not called options.
         return {'inputs': self.inputs,
-                'name': self.name,
-                'version': self.version}
+                'name': self.name, 'version': self.version
+               }
 
     def __repr__(self):
         return "<%s inputs=%s>" % (self.__class__, self.inputs)
diff --git a/ooni/oonicli.py b/ooni/oonicli.py
index 3a8b3df..2b0991b 100644
--- a/ooni/oonicli.py
+++ b/ooni/oonicli.py
@@ -15,7 +15,7 @@ import random
 import time
 import yaml
 
-from twisted.internet import defer, reactor
+from twisted.internet import defer, reactor, task
 from twisted.application import app
 from twisted.python import usage, failure
 from twisted.python.util import spewer
@@ -78,6 +78,15 @@ class Options(usage.Options):
         except:
             raise usage.UsageError("No test filename specified!")
 
+def updateStatusBar():
+    for test_filename in config.state.keys():
+        # The ETA is not updated so we we will not print it out for the
+        # moment.
+        eta = config.state[test_filename].eta()
+        progress = config.state[test_filename].progress()
+        progress_bar_frmt = "[%s] %s%%" % (test_filename, progress)
+        print progress_bar_frmt
+
 def testsEnded(*arg, **kw):
     """
     You can place here all the post shutdown tasks.
@@ -123,6 +132,10 @@ def run():
     d2 = defer.DeferredList(deck_dl)
     d2.addBoth(testsEnded)
 
+    # Print every 5 second the list of current tests running
+    l = task.LoopingCall(updateStatusBar)
+    l.start(5.0)
+
     if config.start_reactor:
         log.debug("Starting reactor")
         reactor.run()
diff --git a/ooni/reporter.py b/ooni/reporter.py
index 3cc77d7..dac9aba 100644
--- a/ooni/reporter.py
+++ b/ooni/reporter.py
@@ -284,7 +284,7 @@ class OONIBReporter(OReporter):
         bodyProducer = StringProducer(json.dumps(request))
 
         try:
-            response = yield self.agent.request("PUT", url, 
+            response = yield self.agent.request("PUT", url,
                                 bodyProducer=bodyProducer)
         except:
             # XXX we must trap this in the runner and make sure to report the data later.
diff --git a/ooni/runner.py b/ooni/runner.py
index e7a40fd..942e3b5 100644
--- a/ooni/runner.py
+++ b/ooni/runner.py
@@ -26,7 +26,7 @@ from ooni.nettest import NetTestCase, NoPostProcessor
 
 from ooni import reporter, config
 
-from ooni.utils import log, checkForRoot, NotRootError
+from ooni.utils import log, checkForRoot, NotRootError, Storage
 
 def processTest(obj):
     """
@@ -67,7 +67,7 @@ def processTest(obj):
     try:
         log.debug("processing options")
         tmp_test_case_object = obj()
-        tmp_test_case_object._processOptions(options)
+        tmp_test_case_object._checkRequiredOptions()
 
     except usage.UsageError, e:
         test_name = tmp_test_case_object.name
@@ -120,6 +120,9 @@ def makeTestCases(klass, tests, method_prefix):
         cases.append((klass, method_prefix+test))
     return cases
 
+class NoTestCasesFound(Exception):
+    pass
+
 def loadTestsAndOptions(classes, cmd_line_options):
     """
     Takes a list of test classes and returns their testcases and options.
@@ -134,7 +137,10 @@ def loadTestsAndOptions(classes, cmd_line_options):
             test_cases = makeTestCases(klass, tests, method_prefix)
 
         test_klass = klass()
-        options = test_klass._processOptions(cmd_line_options)
+        options = test_klass._processOptions()
+
+    if not test_cases:
+        raise NoTestCasesFound
 
     return test_cases, options
 
@@ -220,7 +226,8 @@ def runTestCasesWithInputUnit(test_cases, input_unit, oreporter):
     dl = []
     for test_input in input_unit:
         log.debug("Running test with this input %s" % test_input)
-        d = runTestCasesWithInput(test_cases, test_input, oreporter)
+        d = runTestCasesWithInput(test_cases,
+                test_input, oreporter)
         dl.append(d)
     return defer.DeferredList(dl)
 
@@ -322,28 +329,45 @@ def increaseInputUnitIdx(test_filename):
     config.stateDict[test_filename] += 1
     yield updateResumeFile(test_filename)
 
+def setupProgressMeters(test_filename, input_unit_factory, 
+        test_case_number):
+    """
+    Sets up the meters required for keeping track of the current progress of
+    certain tests.
+    """
+    log.msg("Setting up progress meters")
+    if not config.state.test_filename:
+        config.state[test_filename] = Storage()
+
+    config.state[test_filename].per_item_average = 2.0
+
+    input_unit_idx = float(config.stateDict[test_filename])
+    input_unit_items = float(len(input_unit_factory) + 1)
+    test_case_number = float(test_case_number)
+    total_iterations = input_unit_items * test_case_number
+    current_iteration = input_unit_idx * test_case_number
+
+    def progress():
+        return (current_iteration / total_iterations) * 100.0
+
+    config.state[test_filename].progress = progress
+
+    def eta():
+        return (total_iterations - current_iteration) \
+                * config.state[test_filename].per_item_average
+    config.state[test_filename].eta = eta
+
+    config.state[test_filename].input_unit_idx = input_unit_idx
+    config.state[test_filename].input_unit_items = input_unit_items
+
+
 @defer.inlineCallbacks
 def runTestCases(test_cases, options, cmd_line_options):
     log.debug("Running %s" % test_cases)
     log.debug("Options %s" % options)
     log.debug("cmd_line_options %s" % dict(cmd_line_options))
-    try:
-        assert len(options) != 0, "Length of options is zero!"
-    except AssertionError, ae:
-        test_inputs = []
-        log.err(ae)
-    else:
-        try:
-            first = options.pop(0)
-        except:
-            first = options
 
-        if 'inputs' in first:
-            test_inputs = options['inputs']
-        else:
-            log.msg("Could not find inputs!")
-            log.msg("options[0] = %s" % first)
-            test_inputs = [None]
+    test_inputs = options['inputs']
 
     if cmd_line_options['collector']:
         log.msg("Using remote collector, please be patient while we create the report.")
@@ -379,6 +403,8 @@ def runTestCases(test_cases, options, cmd_line_options):
     else:
         config.stateDict[test_filename] = 0
 
+    setupProgressMeters(test_filename, input_unit_factory, len(test_cases))
+
     try:
         for input_unit in input_unit_factory:
             log.debug("Running this input unit %s" % input_unit)
diff --git a/tests/test_inputunit.py b/tests/test_inputunit.py
index f591e23..1f9043c 100644
--- a/tests/test_inputunit.py
+++ b/tests/test_inputunit.py
@@ -1,10 +1,13 @@
 import unittest
 from ooni.inputunit import InputUnit, InputUnitFactory
 
+def dummyGenerator():
+    for x in range(100):
+        yield x
+
 class TestInputUnit(unittest.TestCase):
     def test_input_unit_factory(self):
-        inputs = range(100)
-        inputUnit = InputUnitFactory(inputs)
+        inputUnit = InputUnitFactory(range(100))
         for i in inputUnit:
             self.assertEqual(len(list(i)), inputUnit.inputUnitSize)
 
@@ -16,3 +19,11 @@ class TestInputUnit(unittest.TestCase):
             idx += 1
 
         self.assertEqual(idx, 100)
+
+    def test_input_unit_factory_length(self):
+        inputUnitFactory = InputUnitFactory(range(100))
+        l1 = len(inputUnitFactory)
+        l2 = sum(1 for _ in inputUnitFactory)
+        self.assertEqual(l1, 10)
+        self.assertEqual(l2, 10)
+



More information about the tor-commits mailing list