commit 3dd9ea0d9222ddf22e63c7f5dcf8e7032e6fb494 Author: Damian Johnson atagar@torproject.org Date: Sat Oct 8 16:04:08 2011 -0700
Running tests based on input arguments
Making the test runner accept arguments for the type of tests to be ran. The integration tests especially will take a while when they're implemented so letting the user specify the use cases for those.
This included copying and refactoring some basic utilities from arm for enumerations and terminal text attributes. --- run_tests.py | 102 ++++++++++++++++++++++++++++++++- stem/types.py | 2 +- stem/util/__init__.py | 6 ++ stem/util/enum.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ stem/util/term.py | 57 +++++++++++++++++++ 5 files changed, 314 insertions(+), 3 deletions(-)
diff --git a/run_tests.py b/run_tests.py index 9debfc2..e1c0811 100755 --- a/run_tests.py +++ b/run_tests.py @@ -4,10 +4,108 @@ Runs unit and integration tests. """
+import sys +import getopt import unittest import test.unit.version
+from stem.util import enum, term + +OPT = "uit:h" +OPT_EXPANDED = ["unit", "integ", "targets=", "help"] +DIVIDER = "=" * 80 + +# Configurations that the intergration tests can be ran with. Attributs are +# tuples of the test runner and description. +TARGETS = enum.Enum(*[(v, v) for v in ("NONE", "NO_CONTROL", "NO_AUTH", "COOKIE", "PASSWORD", "SOCKET")]) +TARGET_ATTR = { + TARGETS.NONE: (None, "No running tor instance."), + TARGETS.NO_CONTROL: (None, "Basic client, no control port or socket."), + TARGETS.NO_AUTH: (None, "Basic client, control port with no authenticaion."), + TARGETS.COOKIE: (None, "Basic client, control port with cookie authenticaion."), + TARGETS.PASSWORD: (None, "Basic client, control port wiht password authentication."), + TARGETS.SOCKET: (None, "Basic client, control socket."), +} + +HELP_MSG = """Usage runTests.py [OPTION] +Runs tests for the stem library. + + -u, --unit runs unit tests + -i, --integ runs integration tests + -t, --target comma separated list of tor configurations to use for the + integration tests (all are used by default) + -h, --help presents this help + + Integration targets: + %s +""" + if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(test.unit.version.TestVerionFunctions) - unittest.TextTestRunner(verbosity=2).run(suite) + run_unit_tests = False + run_integ_tests = False + integ_targets = TARGETS.values() + + # 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)" + 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 ("-t", "--targets"): + integ_targets = arg.split(",") + + # validates the targets + if not integ_targets: + print "No targets provided" + sys.exit(1) + + for target in integ_targets: + if not target in TARGETS.values(): + print "Invalid integration target: %s" % target + sys.exit(1) + elif opt in ("-h", "--help"): + # Prints usage information and quits. This includes a listing of the + # valid integration targets. + + # gets the longest target length so we can show the entries in columns + target_name_length = max([len(name) for name in TARGETS.values()]) + description_format = "%%-%is - %%s" % target_name_length + + target_lines = [] + for target in TARGETS.values(): + target_lines.append(description_format % (target, TARGET_ATTR[target][1])) + + 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() + + if run_unit_tests: + print "%s\nUnit Tests\n%s\n" % (DIVIDER, DIVIDER) + + suite = unittest.TestLoader().loadTestsFromTestCase(test.unit.version.TestVerionFunctions) + unittest.TextTestRunner(verbosity=2).run(suite) + + print "" + + if run_integ_tests: + print "%s\nIntegration Tests\n%s\n" % (DIVIDER, DIVIDER) + + for target in integ_targets: + runner, description = TARGET_ATTR[target] + + print "Configuration: %s - %s" % (target, description) + + if runner: + pass # TODO: implement + else: + print " %s" % term.format("Unimplemented", term.Color.RED, term.Attr.BOLD) + + print ""
diff --git a/stem/types.py b/stem/types.py index 58845fe..f4583e3 100644 --- a/stem/types.py +++ b/stem/types.py @@ -76,7 +76,7 @@ def get_version(version_str): Returns: types.Version instance
- Throws: + Raises: ValueError if input isn't a valid tor version """
diff --git a/stem/util/__init__.py b/stem/util/__init__.py new file mode 100644 index 0000000..e079d62 --- /dev/null +++ b/stem/util/__init__.py @@ -0,0 +1,6 @@ +""" +Utility functions used by the stem library. +""" + +__all__ = ["enum", "term"] + diff --git a/stem/util/enum.py b/stem/util/enum.py new file mode 100644 index 0000000..d7745ec --- /dev/null +++ b/stem/util/enum.py @@ -0,0 +1,150 @@ +""" +Basic enumeration, providing ordered types for collections. These can be +constructed as simple type listings, ie: +>>> insects = Enum("ANT", "WASP", "LADYBUG", "FIREFLY") +>>> insects.ANT +'Ant' +>>> insects.values() +['Ant', 'Wasp', 'Ladybug', 'Firefly'] + +with overwritten string counterparts: +>>> pets = Enum(("DOG", "Skippy"), "CAT", ("FISH", "Nemo")) +>>> pets.DOG +'Skippy' +>>> pets.CAT +'Cat' + +or with entirely custom string components as an unordered enum with: +>>> pets = LEnum(DOG="Skippy", CAT="Kitty", FISH="Nemo") +>>> pets.CAT +'Kitty' +""" + +def to_camel_case(label, word_divider = " "): + """ + Converts the given string to camel case, ie: + >>> to_camel_case("I_LIKE_PEPPERJACK!") + 'I Like Pepperjack!' + + Arguments: + label (str) - input string to be converted + word_divider (str) - string used to replace underscores + """ + + words = [] + for entry in label.split("_"): + if len(entry) == 0: words.append("") + elif len(entry) == 1: words.append(entry.upper()) + else: words.append(entry[0].upper() + entry[1:].lower()) + + return word_divider.join(words) + +class Enum: + """ + Basic enumeration. + """ + + def __init__(self, *args): + # ordered listings of our keys and values + keys, values = [], [] + + for entry in args: + if isinstance(entry, str): + key, val = entry, to_camel_case(entry) + elif isinstance(entry, tuple) and len(entry) == 2: + key, val = entry + else: raise ValueError("Unrecognized input: %s" % args) + + keys.append(key) + values.append(val) + self.__dict__[key] = val + + self._keys = tuple(keys) + self._values = tuple(values) + + def keys(self): + """ + Provides an ordered listing of the enumeration keys in this set. + + Returns: + tuple with our enum keys + """ + + return self._keys + + def values(self): + """ + Provides an ordered listing of the enumerations in this set. + + Returns: + tuple with our enum values + """ + + return self._values + + def index_of(self, value): + """ + Provides the index of the given value in the collection. + + Arguments: + value - entry to be looked up + + Returns: + integer index of the given entry + + Raises: + ValueError if no such element exists + """ + + return self._values.index(value) + + def next(self, value): + """ + Provides the next enumeration after the given value. + + Arguments: + value - enumeration for which to get the next entry + + Returns: + enum value following the given entry + + Raises: + ValueError if no such element exists + """ + + if not value in self._values: + raise ValueError("No such enumeration exists: %s (options: %s)" % (value, ", ".join(self._values))) + + next_index = (self._values.index(value) + 1) % len(self._values) + return self._values[next_index] + + def previous(self, value): + """ + Provides the previous enumeration before the given value. + + Arguments: + value - enumeration for which to get the previous entry + + Returns: + enum value proceeding the given entry + + Raises: + ValueError if no such element exists + """ + + if not value in self._values: + raise ValueError("No such enumeration exists: %s (options: %s)" % (value, ", ".join(self._values))) + + prev_index = (self._values.index(value) - 1) % len(self._values) + return self._values[prev_index] + +class LEnum(Enum): + """ + Enumeration that accepts custom string mappings. + """ + + def __init__(self, **args): + Enum.__init__(self) + self.__dict__.update(args) + self._values = sorted(args.values()) + diff --git a/stem/util/term.py b/stem/util/term.py new file mode 100644 index 0000000..7f3449e --- /dev/null +++ b/stem/util/term.py @@ -0,0 +1,57 @@ +""" +Utilities for working with the terminal. +""" + +import enum + +TERM_COLORS = ("BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE") + +Color = enum.Enum(*TERM_COLORS) +BgColor = enum.Enum(*["BG_" + color for color in TERM_COLORS]) +Attr = enum.Enum("BOLD", "UNDERLINE", "HILIGHT") + +# mappings of terminal attribute enums to their ANSI escape encoding +FG_ENCODING = dict([(Color.values()[i], str(30 + i)) for i in range(8)]) +BG_ENCODING = dict([(BgColor.values()[i], str(40 + i)) for i in range(8)]) +ATTR_ENCODING = {Attr.BOLD: "1", Attr.UNDERLINE: "4", Attr.HILIGHT: "7"} + +CSI = "\x1B[%sm" +RESET = CSI % "0" + +def format(msg, *attr): + """ + Simple terminal text formatting, using ANSI escape sequences from: + https://secure.wikimedia.org/wikipedia/en/wiki/ANSI_escape_code#CSI_codes + + toolkits providing similar capabilities: + * django.utils.termcolors + https://code.djangoproject.com/browser/django/trunk/django/utils/termcolors.... + + * termcolor + http://pypi.python.org/pypi/termcolor + + * colorama + http://pypi.python.org/pypi/colorama + + Arguments: + msg (str) - string to be formatted + attr (str) - text attributes, this can be Color, BgColor, or Attr enums and + are case insensitive (so strings like "red" are fine) + + Returns: + string wrapped with ANSI escape encodings, starting with the given + attributes and ending with a reset + """ + + encodings = [] + for text_attr in attr: + text_attr, encoding = enum.to_camel_case(text_attr), None + encoding = FG_ENCODING.get(text_attr, encoding) + encoding = BG_ENCODING.get(text_attr, encoding) + encoding = ATTR_ENCODING.get(text_attr, encoding) + if encoding: encodings.append(encoding) + + if encodings: + return (CSI % ";".join(encodings)) + msg + RESET + else: return msg +