commit a3829e7a9700bfa748088c18e3afa44a086ed6e2 Author: Arturo Filastò art@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) +
tor-commits@lists.torproject.org