commit e7cb3b59298088b4a8adf35364ff6d775559d27a Author: Damian Johnson atagar@torproject.org Date: Sun Jan 22 19:10:41 2012 -0800
Overhaul of run_tests.py
Several substantial changes to the run_tests.py script to improve readability: - splitting arg parsing from the rest of the main function - adding a config sync method to keep config dictinaries in sync with the main configuration (this will be especially important for arm since it allows for proper runtime configuration editing) - moving remaining print functions into test/output.py - lots of general cleanup
Remaining todo items from this... - still need to add testing for the config listeners - we should note the module that failed with the failures at the end - we still need multi-line config entries - the --no-color option was added but not yet implemented - the RUN_NONE target looks to be broken --- run_tests.py | 243 +++++++++++++++++++++++++-------------------------- stem/util/conf.py | 88 ++++++++++++++++--- test/output.py | 34 +++++++ test/settings.cfg | 8 +- test/testrc.sample | 14 +++ 5 files changed, 247 insertions(+), 140 deletions(-)
diff --git a/run_tests.py b/run_tests.py index df9c538..364d411 100755 --- a/run_tests.py +++ b/run_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python
""" -Runs unit and integration tests. +Runs unit and integration tests. For usage information run this with '--help'. """
import os @@ -33,9 +33,37 @@ import stem.util.log as log import stem.util.term as term
OPT = "uic:l:t:h" -OPT_EXPANDED = ["unit", "integ", "config=", "targets=", "log=", "tor=", "help"] +OPT_EXPANDED = ["unit", "integ", "config=", "targets=", "log=", "tor=", "no-color", "help"] DIVIDER = "=" * 70
+CONFIG = { + "test.arg.unit": False, + "test.arg.integ": False, + "test.arg.log": None, + "test.arg.tor": "tor", + "test.arg.no_color": False, + "target.config": {}, + "target.description": {}, + "target.prereq": {}, + "target.torrc": {}, +} + +TARGETS = stem.util.enum.Enum(*[(v, v) for v in ( + "ONLINE", + "RELATIVE", + "RUN_NONE", + "RUN_OPEN", + "RUN_PASSWORD", + "RUN_COOKIE", + "RUN_MULTIPLE", + "RUN_SOCKET", + "RUN_SCOOKIE", + "RUN_PTRACE", + "RUN_ALL", +)]) + +DEFAULT_RUN_TARGET = TARGETS.RUN_OPEN + # Tests are ordered by the dependencies so the lowest level tests come first. # This is because a problem in say, controller message parsing, will cause all # higher level tests to fail too. Hence we want the test that most narrowly @@ -61,18 +89,7 @@ INTEG_TESTS = ( test.integ.connection.connect.TestConnect, )
-# Integration tests above the basic suite. -TARGETS = stem.util.enum.Enum(*[(v, v) for v in ("ONLINE", "RELATIVE", "RUN_NONE", "RUN_OPEN", "RUN_PASSWORD", "RUN_COOKIE", "RUN_MULTIPLE", "RUN_SOCKET", "RUN_SCOOKIE", "RUN_PTRACE", "RUN_ALL")]) - -CONFIG = { - "target.config": {}, - "target.description": {}, - "target.prereq": {}, - "target.torrc": {}, -} - -DEFAULT_RUN_TARGET = TARGETS.RUN_OPEN - +# TODO: move into settings.cfg when we have multi-line options HELP_MSG = """Usage runTests.py [OPTION] Runs tests for the stem library.
@@ -83,74 +100,34 @@ Runs tests for the stem library. -l, --log RUNLEVEL includes logging output with test results, runlevels: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR --tor PATH custom tor binary to run testing against + --no-color displays testing output without color -h, --help presents this help
- Integration targets: - %s -""" - -# TODO: add an option to disable output coloring? - -HEADER_ATTR = (term.Color.CYAN, term.Attr.BOLD) -CATEGORY_ATTR = (term.Color.GREEN, term.Attr.BOLD) -DEFAULT_TEST_ATTR = (term.Color.CYAN,) - -TEST_OUTPUT_ATTR = { - "... ok": (term.Color.GREEN,), - "... FAIL": (term.Color.RED, term.Attr.BOLD), - "... ERROR": (term.Color.RED, term.Attr.BOLD), - "... skipped": (term.Color.BLUE,), -} - -def print_divider(msg, is_header = False): - attr = HEADER_ATTR if is_header else CATEGORY_ATTR - print term.format("%s\n%s\n%s\n" % (DIVIDER, msg.center(70), DIVIDER), *attr) - -def print_logging(logging_buffer): - if not logging_buffer.is_empty(): - for entry in logging_buffer: - print term.format(entry.replace("\n", "\n "), term.Color.MAGENTA) - - print + Integration targets:"""
-if __name__ == '__main__': - # loads the builtin testing configuration - settings_path = os.path.join(test.runner.STEM_BASE, "test", "settings.cfg") - - test_config = stem.util.conf.get_config("test") - test_config.load(settings_path) - test_config.update(CONFIG) - - # parses target.torrc as csv values and convert to runner Torrc enums - for target in CONFIG["target.torrc"]: - CONFIG["target.torrc"][target] = [] - - for opt in test_config.get_str_csv("target.torrc", [], sub_key = target): - if opt in test.runner.Torrc.keys(): - CONFIG["target.torrc"][target].append(test.runner.Torrc[opt]) - else: - print "'%s' isn't a test.runner.Torrc enumeration" % opt - sys.exit(1) +def load_user_configuration(test_config): + """ + Parses our commandline arguments, loading our custom test configuration if + '--config' was provided and then appending arguments to that. This does some + sanity checking on the input, printing an error and quitting if validation + fails. + """
- start_time = time.time() - run_unit_tests = False - run_integ_tests = False - config_path = None - override_targets = [] - logging_runlevel = None - tor_cmd = "tor" + arg_overrides, config_path = {}, None
- # parses user input, noting any issues try: opts, args = getopt.getopt(sys.argv[1:], OPT, OPT_EXPANDED) except getopt.GetoptError, exc: - print str(exc) + " (for usage provide --help)" + print "%s (for usage provide --help)" % exc sys.exit(1)
for opt, arg in opts: - if opt in ("-u", "--unit"): run_unit_tests = True - elif opt in ("-i", "--integ"): run_integ_tests = True - elif opt in ("-c", "--config"): config_path = os.path.abspath(arg) + if opt in ("-u", "--unit"): + arg_overrides["test.arg.unit"] = "true" + elif opt in ("-i", "--integ"): + arg_overrides["test.arg.integ"] = "true" + elif opt in ("-c", "--config"): + config_path = os.path.abspath(arg) elif opt in ("-t", "--targets"): integ_targets = arg.split(",")
@@ -164,67 +141,75 @@ if __name__ == '__main__': print "Invalid integration target: %s" % target sys.exit(1) else: - override_targets.append(target) + target_config = test_config.get("target.config", {}).get(target) + if target_config: arg_overrides[target_config] = "true" elif opt in ("-l", "--log"): - logging_runlevel = arg.upper() - - if not logging_runlevel in log.LOG_VALUES: - print "'%s' isn't a logging runlevel, use one of the following instead:" % arg - print " TRACE, DEBUG, INFO, NOTICE, WARN, ERROR" - sys.exit(1) + arg_overrides["test.arg.log"] = arg.upper() elif opt in ("--tor"): - if not os.path.exists(arg): - print "Unable to start tor, '%s' does not exists." % arg - sys.exit(1) - - tor_cmd = arg + arg_overrides["test.arg.tor"] = arg elif opt in ("-h", "--help"): # Prints usage information and quits. This includes a listing of the # valid integration targets.
+ print HELP_MSG + # gets the longest target length so we can show the entries in columns - target_name_length = max([len(name) for name in TARGETS]) - description_format = "%%-%is - %%s" % target_name_length + target_name_length = max(map(len, TARGETS)) + description_format = " %%-%is - %%s" % target_name_length
- target_lines = [] for target in TARGETS: - target_lines.append(description_format % (target, CONFIG["target.description"].get(target, ""))) + print description_format % (target, CONFIG["target.description"].get(target, "")) + + print
- print HELP_MSG % "\n ".join(target_lines) sys.exit()
- if not run_unit_tests and not run_integ_tests: - print "Nothing to run (for usage provide --help)\n" - sys.exit() + # load a testrc if '--config' was given, then apply arguments
if config_path: - print_divider("TESTING CONFIG", True) - print - try: - sys.stdout.write(term.format("Loading test configuration (%s)... " % config_path, term.Color.BLUE, term.Attr.BOLD)) test_config.load(config_path) - sys.stdout.write(term.format("done\n", term.Color.BLUE, term.Attr.BOLD)) - - for config_key in test_config.keys(): - key_entry = " %s => " % config_key - - # if there's multiple values then list them on separate lines - value_div = ",\n" + (" " * len(key_entry)) - value_entry = value_div.join(test_config.get_value(config_key, multiple = True)) - - sys.stdout.write(term.format(key_entry + value_entry + "\n", term.Color.BLUE)) except IOError, exc: - sys.stdout.write(term.format("failed (%s)\n" % exc, term.Color.RED, term.Attr.BOLD)) - - print + print "Unable to load testing configuration at '%s': %s" % (config_path, exc) + sys.exit(1)
- # Set the configuration flag for our '--target' arguments. This is meant to - # override our configuration flags if both set a target. + for key, value in arg_overrides.items(): + test_config.set(key, value)
- for target in override_targets: - target_config = CONFIG["target.config"].get(target) - if target_config: test_config.set(target_config, "true") + # basic validation on user input + + log_config = CONFIG["test.arg.log"] + if log_config and not log_config in log.LOG_VALUES: + print "'%s' isn't a logging runlevel, use one of the following instead:" % log_config + print " TRACE, DEBUG, INFO, NOTICE, WARN, ERROR" + sys.exit(1) + + tor_config = CONFIG["test.arg.tor"] + if not os.path.exists(tor_config) and not stem.util.system.is_available(tor_config): + print "Unable to start tor, '%s' does not exists." % tor_config + sys.exit(1) + +if __name__ == '__main__': + start_time = time.time() + + # loads and validates our various configurations + test_config = stem.util.conf.get_config("test") + test_config.sync(CONFIG) + + settings_path = os.path.join(test.runner.STEM_BASE, "test", "settings.cfg") + test_config.load(settings_path) + + load_user_configuration(test_config) + + if not CONFIG["test.arg.unit"] and not CONFIG["test.arg.integ"]: + print "Nothing to run (for usage provide --help)\n" + sys.exit() + + # if we have verbose logging then provide the testing config + our_level = stem.util.log.logging_level(CONFIG["test.arg.log"]) + info_level = stem.util.log.logging_level(stem.util.log.INFO) + + if our_level <= info_level: test.output.print_config(test_config)
error_tracker = test.output.ErrorTracker() output_filters = ( @@ -235,14 +220,14 @@ if __name__ == '__main__': )
stem_logger = log.get_logger() - logging_buffer = log.LogBuffer(logging_runlevel) + logging_buffer = log.LogBuffer(CONFIG["test.arg.log"]) stem_logger.addHandler(logging_buffer)
- if run_unit_tests: - print_divider("UNIT TESTS", True) + if CONFIG["test.arg.unit"]: + test.output.print_divider("UNIT TESTS", True)
for test_class in UNIT_TESTS: - print_divider(test_class.__module__) + test.output.print_divider(test_class.__module__) suite = unittest.TestLoader().loadTestsFromTestCase(test_class) test_results = StringIO.StringIO() unittest.TextTestRunner(test_results, verbosity=2).run(suite) @@ -250,12 +235,12 @@ if __name__ == '__main__': sys.stdout.write(test.output.apply_filters(test_results.getvalue(), *output_filters)) print
- print_logging(logging_buffer) + test.output.print_logging(logging_buffer)
- if run_integ_tests: - print_divider("INTEGRATION TESTS", True) + if CONFIG["test.arg.integ"]: + test.output.print_divider("INTEGRATION TESTS", True) integ_runner = test.runner.get_runner()
# Queue up all the targets with torrc options we want to run against. @@ -288,7 +273,7 @@ if __name__ == '__main__': if target_prereq: # lazy loaded to skip system call if we don't have any prereqs if not our_version: - our_version = stem.version.get_system_tor_version(tor_cmd) + our_version = stem.version.get_system_tor_version(CONFIG["test.arg.tor"])
if our_version < stem.version.Requirement[target_prereq]: skip_targets.append(target) @@ -297,13 +282,23 @@ if __name__ == '__main__': if target in skip_targets: continue
try: - integ_runner.start(tor_cmd, extra_torrc_opts = CONFIG["target.torrc"].get(target, [])) + # converts the 'target.torrc' csv into a list of test.runner.Torrc enums + torrc_opts = [] + + for opt in test_config.get_str_csv("target.torrc", [], sub_key = target): + if opt in test.runner.Torrc.keys(): + torrc_opts.append(test.runner.Torrc[opt]) + else: + print "'%s' isn't a test.runner.Torrc enumeration" % opt + sys.exit(1) + + integ_runner.start(CONFIG["test.arg.tor"], extra_torrc_opts = torrc_opts)
print term.format("Running tests...", term.Color.BLUE, term.Attr.BOLD) print
for test_class in INTEG_TESTS: - print_divider(test_class.__module__) + test.output.print_divider(test_class.__module__) suite = unittest.TestLoader().loadTestsFromTestCase(test_class) test_results = StringIO.StringIO() unittest.TextTestRunner(test_results, verbosity=2).run(suite) @@ -311,7 +306,7 @@ if __name__ == '__main__': sys.stdout.write(test.output.apply_filters(test_results.getvalue(), *output_filters)) print
- print_logging(logging_buffer) + test.output.print_logging(logging_buffer) except OSError: pass finally: diff --git a/stem/util/conf.py b/stem/util/conf.py index d4e9775..68a332a 100644 --- a/stem/util/conf.py +++ b/stem/util/conf.py @@ -18,6 +18,8 @@ Config - Custom configuration. |- save - writes the current configuration to a file |- clear - empties our loaded configuration contents |- update - replaces mappings in a dictionary with the config's values + |- add_listener - notifies the given listener when an update occures + |- sync - keeps a dictionary synchronized with our config |- keys - provides keys in the loaded configuration |- set - sets the given key/value pair |- unused_keys - provides keys that have never been requested @@ -33,15 +35,28 @@ import stem.util.log as log
CONFS = {} # mapping of identifier to singleton instances of configs
+class SyncListener: + def __init__(self, config_dict, interceptor): + self.config_dict = config_dict + self.interceptor = interceptor + + def update(self, config, key): + if key in self.config_dict: + new_value = config.get(key, self.config_dict[key]) + if new_value == self.config_dict[key]: return # no change + + if self.interceptor: + interceptor_value = self.interceptor(key, new_value) + if interceptor_value: new_value = interceptor_value + + self.config_dict[key] = new_value + # TODO: methods that will be needed if we want to allow for runtime # customization... # # Config.set(key, value) - accepts any type that the get() method does, # updating our contents with the string conversion # -# Config.addListener(functor) - allow other classes to have callbacks for when -# the configuration is changed (either via load() or set()) -# # Config.save(path) - writes our current configurations, ideally merging them # with the file that exists there so commenting and such are preserved
@@ -131,6 +146,7 @@ class Config(): self._path = None # location we last loaded from or saved to self._contents = {} # configuration key/value pairs self._raw_contents = [] # raw contents read from configuration file + self._listeners = [] # functors to be notified of config changes
# used for both _contents and _raw_contents access self._contents_lock = threading.RLock() @@ -240,6 +256,45 @@ class Config(): if type(val) == type(conf_mappings[entry]): conf_mappings[entry] = val
+ def add_listener(self, listener, backfill = True): + """ + Registers the given function to be notified of configuration updates. + Listeners are expected to be functors which accept (config, key). + + Arguments: + listener (functor) - function to be notified when our configuration is + changed + backfill (bool) - calls the function with our current values if true + """ + + self._contents_lock.acquire() + self._listeners.append(listener) + + if backfill: + for key in self.keys(): + listener(key) + + self._contents_lock.release() + + def sync(self, config_dict, interceptor = None): + """ + Synchronizes a dictionary with our current configuration (like the 'update' + method), and registers it to be updated whenever our configuration changes. + + If an interceptor is provided then this is called just prior to assigning + new values to the config_dict. The interceptor function is expected to + accept the (key, value) for the new values and return what we should + actually insert into the dictionary. If this returns None then the value is + updated as normal. + + Arguments: + config_dict (dict) - dictionary to keep synchronized with our + configuration + interceptor (functor) - function referred to prior to assigning values + """ + + self.add_listener(SyncListener(config_dict, interceptor).update) + def keys(self): """ Provides all keys in the currently loaded configuration. @@ -273,15 +328,24 @@ class Config(): the values are appended """
- if isinstance(value, str): - if not overwrite and key in self._contents: self._contents[key].append(value) - else: self._contents[key] = [value] - elif isinstance(value, list) or isinstance(value, tuple): - if not overwrite and key in self._contents: - self._contents[key] += value - else: self._contents[key] = value - else: - raise ValueError("Config.set() only accepts str, list, or tuple. Provided value was a '%s'" % type(value)) + try: + self._contents_lock.acquire() + + if isinstance(value, str): + if not overwrite and key in self._contents: self._contents[key].append(value) + else: self._contents[key] = [value] + + for listener in self._listeners: listener(self, key) + elif isinstance(value, list) or isinstance(value, tuple): + if not overwrite and key in self._contents: + self._contents[key] += value + else: self._contents[key] = value + + for listener in self._listeners: listener(self, key) + else: + raise ValueError("Config.set() only accepts str, list, or tuple. Provided value was a '%s'" % type(value)) + finally: + self._contents_lock.release()
def get(self, key, default = None): """ diff --git a/test/output.py b/test/output.py index 6c00d28..327482a 100644 --- a/test/output.py +++ b/test/output.py @@ -9,6 +9,10 @@ import logging import stem.util.enum import stem.util.term as term
+DIVIDER = "=" * 70 +HEADER_ATTR = (term.Color.CYAN, term.Attr.BOLD) +CATEGORY_ATTR = (term.Color.GREEN, term.Attr.BOLD) + LineType = stem.util.enum.Enum("OK", "FAIL", "ERROR", "SKIPPED", "CONTENT")
LINE_ENDINGS = { @@ -26,6 +30,36 @@ LINE_ATTR = { LineType.CONTENT: (term.Color.CYAN,), }
+def print_divider(msg, is_header = False): + attr = HEADER_ATTR if is_header else CATEGORY_ATTR + print term.format("%s\n%s\n%s\n" % (DIVIDER, msg.center(70), DIVIDER), *attr) + +def print_logging(logging_buffer): + if not logging_buffer.is_empty(): + for entry in logging_buffer: + print term.format(entry.replace("\n", "\n "), term.Color.MAGENTA) + + print + +def print_config(test_config): + print_divider("TESTING CONFIG", True) + + try: + print term.format("Test configuration... ", term.Color.BLUE, term.Attr.BOLD) + + for config_key in test_config.keys(): + key_entry = " %s => " % config_key + + # if there's multiple values then list them on separate lines + value_div = ",\n" + (" " * len(key_entry)) + value_entry = value_div.join(test_config.get_value(config_key, multiple = True)) + + print term.format(key_entry + value_entry, term.Color.BLUE) + except IOError, exc: + sys.stdout.write(term.format("failed (%s)\n" % exc, term.Color.RED, term.Attr.BOLD)) + + print + def apply_filters(testing_output, *filters): """ Gets the tests results, possably processed through a series of filters. The diff --git a/test/settings.cfg b/test/settings.cfg index 3766dd4..bf90ee0 100644 --- a/test/settings.cfg +++ b/test/settings.cfg @@ -13,8 +13,8 @@ # Configuration option with which the target is synced. If an option is set via # both the config and '--target' argument then the argument takes precedence.
-target.config ONLINE => test.target.online -target.config RELATIVE => test.target.relative_data_dir +target.config ONLINE => test.target.online +target.config RELATIVE => test.target.relative_data_dir target.config RUN_NONE => test.target.run.none target.config RUN_OPEN => test.target.run.open target.config RUN_PASSWORD => test.target.run.password @@ -27,8 +27,8 @@ target.config RUN_ALL => test.target.run.all
# The '--help' description of the target.
-target.description ONLINE => Includes tests that require network activity. -target.description RELATIVE => Uses a relative path for tor's data directory. +target.description ONLINE => Includes tests that require network activity. +target.description RELATIVE => Uses a relative path for tor's data directory. target.description RUN_NONE => Configuration without a way for controllers to connect. target.description RUN_OPEN => Configuration with an open control port (default). target.description RUN_PASSWORD => Configuration with password authentication. diff --git a/test/testrc.sample b/test/testrc.sample index 8c89b04..25ca9aa 100644 --- a/test/testrc.sample +++ b/test/testrc.sample @@ -1,5 +1,12 @@ # Integration Test Settings # +# test.arg.unit +# test.arg.integ +# test.arg.log +# test.arg.tor +# test.arg.no_color +# Default values for runner arguments. +# # test.integ.test_directory # Path used for our data directory and any temporary test resources. Relative # paths are expanded in reference to the location of 'run_tests.py'. @@ -32,8 +39,15 @@ # authentication configurations. If the 'all' option is set then the other # flags are ignored.
+test.arg.unit false +test.arg.integ false +test.arg.log +test.arg.tor tor +test.arg.no_color false + test.integ.test_directory ./test/data test.integ.log ./test/data/log + test.target.online false test.target.relative_data_dir false test.target.run.none false
tor-commits@lists.torproject.org