"""Provides a runner to execute unit tests with a given runner It is intended to be used as a launcher script for a given unit test file. The reports are output to the current working directory. """ from __future__ import (absolute_import, division, print_function) import imp import os import sys from xmlrunner import XMLTestRunner from xmlrunner.result import _TestInfo, _XMLTestResult, safe_unicode import unittest class GroupedNameTestInfo(_TestInfo): """ Overrides these default xmlrunner class to used a different test naming scheme """ def __init__(self, test_result, test_method, outcome=_TestInfo.SUCCESS, err=None, subTest=None): super(GroupedNameTestInfo, self).__init__(test_result, test_method, outcome, err, subTest) def id(self): return self.test_id class GroupedNameTestResult(_XMLTestResult): """ A hack to allow us to prefix the test suite name with a prefix we choose allowing to output to be organized by Jenkins. """ testcase_prefix = None def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1, elapsed_times=True, properties=None): super(GroupedNameTestResult, self).__init__(stream, descriptions, verbosity, elapsed_times, properties, infoclass=GroupedNameTestInfo) def _get_info_by_testcase(self): """ Organizes test results by TestCase module. This information is used during the report generation, where a XML report will be created for each TestCase. """ tests_by_testcase = {} if self.testcase_prefix is None: self.testcase_prefix = "" for tests in (self.successes, self.failures, self.errors, self.skipped): for test_info in tests: if isinstance(test_info, tuple): # This is a skipped, error or a failure test case test_info = test_info[0] testcase_name = self.testcase_prefix + test_info.test_name if testcase_name not in tests_by_testcase: tests_by_testcase[testcase_name] = [] tests_by_testcase[testcase_name].append(test_info) return tests_by_testcase def main(argv): """ Runs the test files through the xml runner :param argv: List of command line arguments """ if len(argv) != 2: raise ValueError("Usage: testrunner <path-to-test-file>") pathname = argv[1] if not os.path.exists(pathname): raise ValueError("Test file not found '{}'".format(pathname)) if not os.path.isfile(pathname): raise ValueError("Test path '{}' is not a file".format(pathname)) # Add the directory of the test to the Python path sys.path.insert(0, os.path.dirname(pathname)) # Load the test and copy over any module variables so that we have # the same environment defined here test_module = imp.load_source(module_name(pathname), pathname) test_module_globals = dir(test_module) this_globals = globals() for key in test_module_globals: this_globals[key] = getattr(test_module, key) # create runner & execute runner = XMLTestRunner(output='.', outsuffix='', resultclass=result_class(pathname)) unittest.main( module=test_module, # We've processed the test source so don't let unittest try to reparse it # This forces it to load the tests from the supplied module argv=(argv[0],), testRunner=runner, # these make sure that some options that are not applicable # remain hidden from the help menu. failfast=False, buffer=False, catchbreak=False ) def module_name(pathname): """ Returns a Python module name for the given pathname using the standard rules :param pathname: Path to a python file :return: A module name to give to the import mechanism """ return os.path.splitext(os.path.basename(pathname))[0] def result_class(pathname): """ Returns a result class that can be passed to XMLTestRunner that customizes the test naming to suite our needs. Note that this is only suitable for running tests from a single file. :return: A sub class of _XMLTestResult """ directory_path, _ = os.path.split(pathname) directory_name = os.path.relpath(directory_path, os.path.dirname(directory_path)) class_ = GroupedNameTestResult class_.testcase_prefix = "python." + directory_name + "." return class_ if __name__ == "__main__": main(sys.argv)