diff options
author | Trent Mick <trentm@gmail.com> | 2010-09-02 10:07:16 -0700 |
---|---|---|
committer | Trent Mick <trentm@gmail.com> | 2010-09-02 10:07:16 -0700 |
commit | a35c26a8b8ca9fa04ff93bca0c1afa6b575df01e (patch) | |
tree | 8c97a474bce2bdc84c7a7fe005db2cc2bb3a0ea6 | |
parent | 5854624220bb6d50017210ebd9c99694c1947e50 (diff) | |
download | appdirs-a35c26a8b8ca9fa04ff93bca0c1afa6b575df01e.tar.gz |
add a starter test suite
-rw-r--r-- | test/api.doctests | 28 | ||||
-rwxr-xr-x | test/test.py | 31 | ||||
-rw-r--r-- | test/test_appdirs.py | 24 | ||||
-rw-r--r-- | test/testlib.py | 721 |
4 files changed, 804 insertions, 0 deletions
diff --git a/test/api.doctests b/test/api.doctests new file mode 100644 index 0000000..ad5bd06 --- /dev/null +++ b/test/api.doctests @@ -0,0 +1,28 @@ + +>>> import appdirs +>>> hasattr(appdirs, "__version__") +True +>>> hasattr(appdirs, "__version_info__") +True + +>>> type(appdirs.user_data_dir('MyApp', 'MyCompany')) == type('') +True +>>> type(appdirs.site_data_dir('MyApp', 'MyCompany')) == type('') +True +>>> type(appdirs.user_cache_dir('MyApp', 'MyCompany')) == type('') +True +>>> type(appdirs.user_log_dir('MyApp', 'MyCompany')) == type('') +True + + +>>> dirs = appdirs.AppDirs('MyApp', 'MyCompany', version='1.0') +>>> type(dirs.user_data_dir) == type('') +True +>>> type(dirs.site_data_dir) == type('') +True +>>> type(dirs.user_cache_dir) == type('') +True +>>> type(dirs.user_log_dir) == type('') +True + + diff --git a/test/test.py b/test/test.py new file mode 100755 index 0000000..38ded06 --- /dev/null +++ b/test/test.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# Copyright (c) 2010 ActiveState Software Inc. +# License: MIT (http://www.opensource.org/licenses/mit-license.php) + +"""The appdirs test suite entry point.""" + +import os +from os.path import exists, join, abspath, dirname, normpath +import sys +import logging + +import testlib + +log = logging.getLogger("test") +testdir_from_ns = { + None: os.curdir, +} + +def setup(): + top_dir = dirname(dirname(abspath(__file__))) + lib_dir = join(top_dir, "lib") + sys.path.insert(0, lib_dir) + +if __name__ == "__main__": + logging.basicConfig() + setup() + default_tags = [] + retval = testlib.harness(testdir_from_ns=testdir_from_ns, + default_tags=default_tags) + sys.exit(retval) + diff --git a/test/test_appdirs.py b/test/test_appdirs.py new file mode 100644 index 0000000..f8faa3a --- /dev/null +++ b/test/test_appdirs.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# Copyright (c) 2010 ActiveState Software Inc. +# License: MIT (http://www.opensource.org/licenses/mit-license.php) + +import os +import sys +from pprint import pprint +import unittest +import doctest + +from testlib import TestError, TestSkipped, tag + + +class DocTestsTestCase(unittest.TestCase): + def test_api(self): + if sys.version_info[:2] < (2,4): + raise TestSkipped("no DocFileTest in Python <=2.3") + test = doctest.DocFileTest("api.doctests") + test.runTest() + + def test_internal(self): + import appdirs + doctest.testmod(appdirs) + diff --git a/test/testlib.py b/test/testlib.py new file mode 100644 index 0000000..6c7bc3a --- /dev/null +++ b/test/testlib.py @@ -0,0 +1,721 @@ +#!python +# Copyright (c) 2000-2008 ActiveState Software Inc. +# License: MIT License (http://www.opensource.org/licenses/mit-license.php) + +""" + test suite harness + + Usage: + + test --list [<tags>...] # list available tests modules + test [<tags>...] # run test modules + + Options: + -v, --verbose more verbose output + -q, --quiet don't print anything except if a test fails + -d, --debug log debug information + -h, --help print this text and exit + -l, --list Just list the available test modules. You can also + specify tags to play with module filtering. + -n, --no-default-tags Ignore default tags + -L <directive> Specify a logging level via + <logname>:<levelname> + For example: + codeintel.db:DEBUG + This option can be used multiple times. + + By default this will run all tests in all available "test_*" modules. + Tags can be specified to control which tests are run. For example: + + test python # run tests with the 'python' tag + test python cpln # run tests with both 'python' and 'cpln' tags + test -- -python # exclude tests with the 'python' tag + # (the '--' is necessary to end the option list) + + The full name and base name of a test module are implicit tags for that + module, e.g. module "test_xdebug.py" has tags "test_xdebug" and "xdebug". + A TestCase's class name (with and without "TestCase") is an implicit + tag for an test_* methods. A "test_foo" method also has "test_foo" + and "foo" implicit tags. + + Tags can be added explicitly added: + - to modules via a __tags__ global list; and + - to individual test_* methods via a "tags" attribute list (you can + use the testlib.tag() decorator for this). +""" +#TODO: +# - Document how tests are found (note the special "test_cases()" and +# "test_suite_class" hooks). +# - See the optparse "TODO" below. +# - Make the quiet option actually quiet. + +__version_info__ = (0, 6, 6) +__version__ = '.'.join(map(str, __version_info__)) + + +import os +from os.path import join, basename, dirname, abspath, splitext, \ + isfile, isdir, normpath, exists +import sys +import getopt +import glob +import time +import types +import tempfile +import unittest +from pprint import pprint +import imp +import optparse +import logging +import textwrap +import traceback + + + +#---- globals and exceptions + +log = logging.getLogger("test") + + + +#---- exports generally useful to test cases + +class TestError(Exception): + pass + +class TestSkipped(Exception): + """Raise this to indicate that a test is being skipped. + + ConsoleTestRunner knows to interpret these at NOT failures. + """ + pass + +class TestFailed(Exception): + pass + +def tag(*tags): + """Decorator to add tags to test_* functions. + + Example: + class MyTestCase(unittest.TestCase): + @testlib.tag("knownfailure") + def test_foo(self): + #... + """ + def decorate(f): + if not hasattr(f, "tags"): + f.tags = [] + f.tags += tags + return f + return decorate + + +#---- timedtest decorator +# Use this to assert that a test completes in a given amount of time. +# This is from http://www.artima.com/forums/flat.jsp?forum=122&thread=129497 +# Including here, becase it might be useful. +# NOTE: Untested and I suspect some breakage. + +TOLERANCE = 0.05 + +class DurationError(AssertionError): pass + +def timedtest(max_time, tolerance=TOLERANCE): + """ timedtest decorator + decorates the test method with a timer + when the time spent by the test exceeds + max_time in seconds, an Assertion error is thrown. + """ + def _timedtest(function): + def wrapper(*args, **kw): + start_time = time.time() + try: + function(*args, **kw) + finally: + total_time = time.time() - start_time + if total_time > max_time + tolerance: + raise DurationError(('Test was too long (%.2f s)' + % total_time)) + return wrapper + + return _timedtest + + + +#---- module api + +class Test(object): + def __init__(self, ns, testmod, testcase, testfn_name, + testsuite_class=None): + self.ns = ns + self.testmod = testmod + self.testcase = testcase + self.testfn_name = testfn_name + self.testsuite_class = testsuite_class + # Give each testcase some extra testlib attributes for useful + # introspection on TestCase instances later on. + self.testcase._testlib_shortname_ = self.shortname() + self.testcase._testlib_explicit_tags_ = self.explicit_tags() + self.testcase._testlib_implicit_tags_ = self.implicit_tags() + def __str__(self): + return self.shortname() + def __repr__(self): + return "<Test %s>" % self.shortname() + def shortname(self): + bits = [self._normname(self.testmod.__name__), + self._normname(self.testcase.__class__.__name__), + self._normname(self.testfn_name)] + if self.ns: + bits.insert(0, self.ns) + return '/'.join(bits) + def _flatten_tags(self, tags): + """Split tags with '/' in them into multiple tags. + + '/' is the reserved tag separator and allowing tags with + embedded '/' results in one being unable to select those via + filtering. As long as tag order is stable then presentation of + these subsplit tags should be fine. + """ + flattened = [] + for t in tags: + flattened += t.split('/') + return flattened + def explicit_tags(self): + tags = [] + if hasattr(self.testmod, "__tags__"): + tags += self.testmod.__tags__ + if hasattr(self.testcase, "__tags__"): + tags += self.testcase.__tags__ + testfn = getattr(self.testcase, self.testfn_name) + if hasattr(testfn, "tags"): + tags += testfn.tags + return self._flatten_tags(tags) + def implicit_tags(self): + tags = [ + self.testmod.__name__.lower(), + self._normname(self.testmod.__name__), + self.testcase.__class__.__name__.lower(), + self._normname(self.testcase.__class__.__name__), + self.testfn_name, + self._normname(self.testfn_name), + ] + if self.ns: + tags.insert(0, self.ns) + return self._flatten_tags(tags) + def tags(self): + return self.explicit_tags() + self.implicit_tags() + def doc(self): + testfn = getattr(self.testcase, self.testfn_name) + return testfn.__doc__ or "" + def _normname(self, name): + if name.startswith("test_"): + return name[5:].lower() + elif name.startswith("test"): + return name[4:].lower() + elif name.endswith("TestCase"): + return name[:-8].lower() + else: + return name + + +def testmod_paths_from_testdir(testdir): + """Generate test module paths in the given dir.""" + for path in glob.glob(join(testdir, "test_*.py")): + yield path + + for path in glob.glob(join(testdir, "test_*")): + if not isdir(path): continue + if not isfile(join(path, "__init__.py")): continue + yield path + +def testmods_from_testdir(testdir): + """Generate test modules in the given test dir. + + Modules are imported with 'testdir' first on sys.path. + """ + testdir = normpath(testdir) + for testmod_path in testmod_paths_from_testdir(testdir): + testmod_name = splitext(basename(testmod_path))[0] + log.debug("import test module '%s'", testmod_path) + try: + iinfo = imp.find_module(testmod_name, [dirname(testmod_path)]) + testabsdir = abspath(testdir) + sys.path.insert(0, testabsdir) + old_dir = os.getcwd() + os.chdir(testdir) + try: + testmod = imp.load_module(testmod_name, *iinfo) + finally: + os.chdir(old_dir) + sys.path.remove(testabsdir) + except TestSkipped: + _, ex, _ = sys.exc_info() + log.warn("'%s' module skipped: %s", testmod_name, ex) + except Exception: + _, ex, _ = sys.exc_info() + log.warn("could not import test module '%s': %s (skipping, " + "run with '-d' for full traceback)", + testmod_path, ex) + if log.isEnabledFor(logging.DEBUG): + traceback.print_exc() + else: + yield testmod + +def testcases_from_testmod(testmod): + """Gather tests from a 'test_*' module. + + Returns a list of TestCase-subclass instances. One instance for each + found test function. + + In general the normal unittest TestLoader.loadTests*() semantics are + used for loading tests with some differences: + - TestCase subclasses beginning with '_' are skipped (presumed to be + internal). + - If the module has a top-level "test_cases", it is called for a list of + TestCase subclasses from which to load tests (can be a generator). This + allows for run-time setup of test cases. + - If the module has a top-level "test_suite_class", it is used to group + all test cases from that module into an instance of that TestSuite + subclass. This allows for overriding of test running behaviour. + """ + class TestListLoader(unittest.TestLoader): + suiteClass = list + + loader = TestListLoader() + if hasattr(testmod, "test_cases"): + try: + for testcase_class in testmod.test_cases(): + if testcase_class.__name__.startswith("_"): + log.debug("skip private TestCase class '%s'", + testcase_class.__name__) + continue + for testcase in loader.loadTestsFromTestCase(testcase_class): + yield testcase + except Exception: + _, ex, _ = sys.exc_info() + testmod_path = testmod.__file__ + if testmod_path.endswith(".pyc"): + testmod_path = testmod_path[:-1] + log.warn("error running test_cases() in '%s': %s (skipping, " + "run with '-d' for full traceback)", + testmod_path, ex) + if log.isEnabledFor(logging.DEBUG): + traceback.print_exc() + else: + class_names_skipped = [] + for testcases in loader.loadTestsFromModule(testmod): + for testcase in testcases: + class_name = testcase.__class__.__name__ + if class_name in class_names_skipped: + pass + elif class_name.startswith("_"): + log.debug("skip private TestCase class '%s'", class_name) + class_names_skipped.append(class_name) + else: + yield testcase + + +def tests_from_manifest(testdir_from_ns): + """Return a list of `testlib.Test` instances for each test found in + the manifest. + + There will be a test for + (a) each "test*" function of + (b) each TestCase-subclass in + (c) each "test_*" Python module in + (d) each test dir in the manifest. + + If a "test_*" module has a top-level "test_suite_class", it will later + be used to group all test cases from that module into an instance of that + TestSuite subclass. This allows for overriding of test running behaviour. + """ + for ns, testdir in testdir_from_ns.items(): + for testmod in testmods_from_testdir(testdir): + if hasattr(testmod, "test_suite_class"): + testsuite_class = testmod.test_suite_class + if not issubclass(testsuite_class, unittest.TestSuite): + testmod_path = testmod.__file__ + if testmod_path.endswith(".pyc"): + testmod_path = testmod_path[:-1] + log.warn("'test_suite_class' of '%s' module is not a " + "subclass of 'unittest.TestSuite': ignoring", + testmod_path) + else: + testsuite_class = None + for testcase in testcases_from_testmod(testmod): + try: + yield Test(ns, testmod, testcase, + testcase._testMethodName, + testsuite_class) + except AttributeError: + # Python 2.4 and older: + yield Test(ns, testmod, testcase, + testcase._TestCase__testMethodName, + testsuite_class) + +def tests_from_manifest_and_tags(testdir_from_ns, tags): + include_tags = [tag.lower() for tag in tags if not tag.startswith('-')] + exclude_tags = [tag[1:].lower() for tag in tags if tag.startswith('-')] + + for test in tests_from_manifest(testdir_from_ns): + test_tags = [t.lower() for t in test.tags()] + + matching_exclude_tags = [t for t in exclude_tags if t in test_tags] + if matching_exclude_tags: + #log.debug("test '%s' matches exclude tag(s) '%s': skipping", + # test.shortname(), "', '".join(matching_exclude_tags)) + continue + + if not include_tags: + yield test + else: + for tag in include_tags: + if tag not in test_tags: + #log.debug("test '%s' does not match tag '%s': skipping", + # test.shortname(), tag) + break + else: + #log.debug("test '%s' matches tags: %s", test.shortname(), + # ' '.join(tags)) + yield test + +def test(testdir_from_ns, tags=[], setup_func=None): + log.debug("test(testdir_from_ns=%r, tags=%r, ...)", + testdir_from_ns, tags) + if setup_func is not None: + setup_func() + tests = list(tests_from_manifest_and_tags(testdir_from_ns, tags)) + if not tests: + return None + + # Groups test cases into a test suite class given by their test module's + # "test_suite_class" hook, if any. + suite = unittest.TestSuite() + suite_for_testmod = None + testmod = None + for test in tests: + if test.testmod != testmod: + if suite_for_testmod is not None: + suite.addTest(suite_for_testmod) + suite_for_testmod = (test.testsuite_class or unittest.TestSuite)() + testmod = test.testmod + suite_for_testmod.addTest(test.testcase) + if suite_for_testmod is not None: + suite.addTest(suite_for_testmod) + + runner = ConsoleTestRunner(sys.stdout) + result = runner.run(suite) + return result + +def list_tests(testdir_from_ns, tags): + # Say I have two test_* modules: + # test_python.py: + # __tags__ = ["guido"] + # class BasicTestCase(unittest.TestCase): + # def test_def(self): + # def test_class(self): + # class ComplexTestCase(unittest.TestCase): + # def test_foo(self): + # def test_bar(self): + # test_perl/__init__.py: + # __tags__ = ["larry", "wall"] + # class BasicTestCase(unittest.TestCase): + # def test_sub(self): + # def test_package(self): + # class EclecticTestCase(unittest.TestCase): + # def test_foo(self): + # def test_bar(self): + # The short-form list output for this should look like: + # python/basic/def [guido] + # python/basic/class [guido] + # python/complex/foo [guido] + # python/complex/bar [guido] + # perl/basic/sub [larry, wall] + # perl/basic/package [larry, wall] + # perl/eclectic/foo [larry, wall] + # perl/eclectic/bar [larry, wall] + log.debug("list_tests(testdir_from_ns=%r, tags=%r)", + testdir_from_ns, tags) + + tests = list(tests_from_manifest_and_tags(testdir_from_ns, tags)) + if not tests: + return + + WIDTH = 78 + if log.isEnabledFor(logging.INFO): # long-form + for i, t in enumerate(tests): + if i: + print() + testfile = t.testmod.__file__ + if testfile.endswith(".pyc"): + testfile = testfile[:-1] + print("%s:" % t.shortname()) + print(" from: %s#%s.%s" % (testfile, + t.testcase.__class__.__name__, t.testfn_name)) + wrapped = textwrap.fill(' '.join(t.tags()), WIDTH-10) + print(" tags: %s" % _indent(wrapped, 8, True)) + if t.doc(): + print(_indent(t.doc(), width=2)) + else: + for t in tests: + line = t.shortname() + ' ' + if t.explicit_tags(): + line += '[%s]' % ' '.join(t.explicit_tags()) + print(line) + + +#---- text test runner that can handle TestSkipped reasonably + +class ConsoleTestResult(unittest.TestResult): + """A test result class that can print formatted text results to a stream. + + Used by ConsoleTestRunner. + """ + separator1 = '=' * 70 + separator2 = '-' * 70 + + def __init__(self, stream): + unittest.TestResult.__init__(self) + self.skips = [] + self.stream = stream + + def getDescription(self, test): + if test._testlib_explicit_tags_: + return "%s [%s]" % (test._testlib_shortname_, + ', '.join(test._testlib_explicit_tags_)) + else: + return test._testlib_shortname_ + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + self.stream.write(self.getDescription(test)) + self.stream.write(" ... ") + + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + self.stream.write("ok\n") + + def addSkip(self, test, err): + why = str(err[1]) + self.skips.append((test, why)) + self.stream.write("skipped (%s)\n" % why) + + def addError(self, test, err): + if isinstance(err[1], TestSkipped): + self.addSkip(test, err) + else: + unittest.TestResult.addError(self, test, err) + self.stream.write("ERROR\n") + + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + self.stream.write("FAIL\n") + + def printSummary(self): + self.stream.write('\n') + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavour, errors): + for test, err in errors: + self.stream.write(self.separator1 + '\n') + self.stream.write("%s: %s\n" + % (flavour, self.getDescription(test))) + self.stream.write(self.separator2 + '\n') + self.stream.write("%s\n" % err) + + +class ConsoleTestRunner(object): + """A test runner class that displays results on the console. + + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + + Differences with unittest.TextTestRunner: + - adds support for *skipped* tests (those that raise TestSkipped) + - no verbosity option (only have equiv of verbosity=2) + - test "short desc" is it 3-level tag name (e.g. 'foo/bar/baz' where + that identifies: 'test_foo.py::BarTestCase.test_baz'. + """ + def __init__(self, stream=sys.stderr): + self.stream = stream + + def run(self, test_or_suite, test_result_class=ConsoleTestResult): + """Run the given test case or test suite.""" + result = test_result_class(self.stream) + start_time = time.time() + test_or_suite.run(result) + time_taken = time.time() - start_time + + result.printSummary() + self.stream.write(result.separator2 + '\n') + self.stream.write("Ran %d test%s in %.3fs\n\n" + % (result.testsRun, result.testsRun != 1 and "s" or "", + time_taken)) + details = [] + num_skips = len(result.skips) + if num_skips: + details.append("%d skip%s" + % (num_skips, (num_skips != 1 and "s" or ""))) + if not result.wasSuccessful(): + num_failures = len(result.failures) + if num_failures: + details.append("%d failure%s" + % (num_failures, (num_failures != 1 and "s" or ""))) + num_errors = len(result.errors) + if num_errors: + details.append("%d error%s" + % (num_errors, (num_errors != 1 and "s" or ""))) + self.stream.write("FAILED (%s)\n" % ', '.join(details)) + elif details: + self.stream.write("OK (%s)\n" % ', '.join(details)) + else: + self.stream.write("OK\n") + return result + + + +#---- internal support stuff + +# Recipe: indent (0.2.1) +def _indent(s, width=4, skip_first_line=False): + """_indent(s, [width=4]) -> 's' indented by 'width' spaces + + The optional "skip_first_line" argument is a boolean (default False) + indicating if the first line should NOT be indented. + """ + lines = s.splitlines(1) + indentstr = ' '*width + if skip_first_line: + return indentstr.join(lines) + else: + return indentstr + indentstr.join(lines) + + + + + +#---- mainline + +#TODO: pass in add_help_option=False and add it ourself here. +## Optparse's handling of the doc passed in for -h|--help handling is +## abysmal. Hence we'll stick with getopt. +#def _parse_opts(args): +# """_parse_opts(args) -> (options, tags)""" +# usage = "usage: %prog [OPTIONS...] [TAGS...]" +# parser = optparse.OptionParser(prog="test", usage=usage, +# description=__doc__) +# parser.add_option("-v", "--verbose", dest="log_level", +# action="store_const", const=logging.DEBUG, +# help="more verbose output") +# parser.add_option("-q", "--quiet", dest="log_level", +# action="store_const", const=logging.WARNING, +# help="quieter output") +# parser.add_option("-l", "--list", dest="action", +# action="store_const", const="list", +# help="list available tests") +# parser.set_defaults(log_level=logging.INFO, action="test") +# opts, raw_tags = parser.parse_args() +# +# # Trim '.py' from user-supplied tags. They might have gotten there +# # via shell expansion. +# ... +# +# return opts, raw_tags + +def _parse_opts(args, default_tags): + """_parse_opts(args) -> (log_level, action, tags)""" + opts, raw_tags = getopt.getopt(args, "hvqdlL:n", + ["help", "verbose", "quiet", "debug", "list", "no-default-tags"]) + log_level = logging.WARN + action = "test" + no_default_tags = False + for opt, optarg in opts: + if opt in ("-h", "--help"): + action = "help" + elif opt in ("-v", "--verbose"): + log_level = logging.INFO + elif opt in ("-q", "--quiet"): + log_level = logging.ERROR + elif opt in ("-d", "--debug"): + log_level = logging.DEBUG + elif opt in ("-l", "--list"): + action = "list" + elif opt in ("-n", "--no-default-tags"): + no_default_tags = True + elif opt == "-L": + # Optarg is of the form '<logname>:<levelname>', e.g. + # "codeintel:DEBUG", "codeintel.db:INFO". + lname, llevelname = optarg.split(':', 1) + llevel = getattr(logging, llevelname) + logging.getLogger(lname).setLevel(llevel) + + # Clean up the given tags. + if no_default_tags: + tags = [] + else: + tags = default_tags + for raw_tag in raw_tags: + if splitext(raw_tag)[1] in (".py", ".pyc", ".pyo", ".pyw") \ + and exists(raw_tag): + # Trim '.py' from user-supplied tags if it looks to be from + # shell expansion. + tags.append(splitext(raw_tag)[0]) + elif '/' in raw_tag: + # Split one '/' to allow the shortname from the test listing + # to be used as a filter. + tags += raw_tag.split('/') + else: + tags.append(raw_tag) + + return log_level, action, tags + + +def harness(testdir_from_ns={None: os.curdir}, argv=sys.argv, + setup_func=None, default_tags=None): + """Convenience mainline for a test harness "test.py" script. + + "testdir_from_ns" (optional) is basically a set of directories in + which to look for test cases. It is a dict with: + <namespace>: <testdir> + where <namespace> is a (short) string that becomes part of the + included test names and an implicit tag for filtering those + tests. By default the current dir is use with an empty namespace: + {None: os.curdir} + "setup_func" (optional) is a callable that will be called once + before any tests are run to prepare for the test suite. It + is not called if no tests will be run. + "default_tags" (optional) + + Typically, if you have a number of test_*.py modules you can create + a test harness, "test.py", for them that looks like this: + + #!/usr/bin/env python + if __name__ == "__main__": + retval = testlib.harness() + sys.exit(retval) + """ + if not logging.root.handlers: + logging.basicConfig() + try: + log_level, action, tags = _parse_opts(argv[1:], default_tags or []) + except getopt.error: + _, ex, _ = sys.exc_info() + log.error(str(ex) + " (did you need a '--' before a '-TAG' argument?)") + return 1 + log.setLevel(log_level) + + if action == "help": + print(__doc__) + return 0 + if action == "list": + return list_tests(testdir_from_ns, tags) + elif action == "test": + result = test(testdir_from_ns, tags, setup_func=setup_func) + if result is None: + return None + return len(result.errors) + len(result.failures) + else: + raise TestError("unexpected action/mode: '%s'" % action) + + |