edq.testing.run
Discover and run unit tests (via Python's unittest package) that live in this project's base package (the parent of this package).
1""" 2Discover and run unit tests (via Python's unittest package) 3that live in this project's base package 4(the parent of this package). 5""" 6 7import argparse 8import os 9import re 10import sys 11import typing 12import unittest 13 14DEFAULT_TEST_FILENAME_PATTERN: str = '*_test.py' 15 16def _collect_tests(suite: typing.Union[unittest.TestCase, unittest.suite.TestSuite]) -> typing.List[unittest.TestCase]: 17 """ 18 Collect and return tests (unittest.TestCase) from the target directory. 19 """ 20 21 if (isinstance(suite, unittest.TestCase)): 22 return [suite] 23 24 if (not isinstance(suite, unittest.suite.TestSuite)): 25 raise ValueError(f"Unknown test type: '{str(type(suite))}'.") 26 27 test_cases = [] 28 for test_object in suite: 29 test_cases += _collect_tests(test_object) 30 31 return test_cases 32 33def run(args: argparse.Namespace) -> int: 34 """ 35 Discover and run unit tests. 36 This function may change your working directory. 37 Will raise if tests fail to load (e.g. syntax errors) and a suggested exit code otherwise. 38 """ 39 40 if (args.work_dir is not None): 41 os.chdir(args.work_dir) 42 43 if (args.path_additions is not None): 44 for path in args.path_additions: 45 sys.path.append(path) 46 47 if (args.test_dirs is None): 48 args.test_dirs = ['.'] 49 50 runner = unittest.TextTestRunner(verbosity = 3) 51 test_cases = [] 52 53 for test_dir in args.test_dirs: 54 discovered_suite = unittest.TestLoader().discover(test_dir, pattern = args.filename_pattern) 55 test_cases += _collect_tests(discovered_suite) 56 57 tests = unittest.suite.TestSuite() 58 59 for test_case in test_cases: 60 if (isinstance(test_case, unittest.loader._FailedTest)): # type: ignore[attr-defined] 61 raise ValueError(f"Failed to load test: '{test_case.id()}'.") from test_case._exception 62 63 if (args.pattern is None or re.search(args.pattern, test_case.id())): 64 tests.addTest(test_case) 65 else: 66 print(f"Skipping {test_case.id()} because of match pattern.") 67 68 result = runner.run(tests) 69 faults = len(result.errors) + len(result.failures) 70 71 if (not result.wasSuccessful()): 72 # This value will be used as an exit status, so it should not be larger than a byte. 73 # (Some higher values are used specially, so just keep it at a round number.) 74 return max(1, min(faults, 100)) 75 76 return 0 77 78def main() -> int: 79 """ Parse the CLI arguments and run tests. """ 80 81 args = _get_parser().parse_args() 82 return run(args) 83 84def _get_parser() -> argparse.ArgumentParser: 85 """ Build a parser for CLI arguments. """ 86 87 parser = argparse.ArgumentParser(description = 'Run unit tests discovered in this project.') 88 89 parser.add_argument('--work-dir', dest = 'work_dir', 90 action = 'store', type = str, default = os.getcwd(), 91 help = 'Set the working directory when running tests, defaults to the current working directory (%(default)s).') 92 93 parser.add_argument('--tests-dir', dest = 'test_dirs', 94 action = 'append', 95 help = 'Discover tests from these directories. Defaults to the current directory.') 96 97 parser.add_argument('--add-path', dest = 'path_additions', 98 action = 'append', 99 help = 'If supplied, add this path the sys.path before running tests.') 100 101 parser.add_argument('--filename-pattern', dest = 'filename_pattern', 102 action = 'store', type = str, default = DEFAULT_TEST_FILENAME_PATTERN, 103 help = 'The pattern to use to find test files (default: %(default)s).') 104 105 parser.add_argument('pattern', 106 action = 'store', type = str, default = None, nargs = '?', 107 help = 'If supplied, only tests with names matching this pattern will be run. This pattern is used directly in re.search().') 108 109 return parser 110 111if __name__ == '__main__': 112 sys.exit(main())
DEFAULT_TEST_FILENAME_PATTERN: str =
'*_test.py'
def
run(args: argparse.Namespace) -> int:
34def run(args: argparse.Namespace) -> int: 35 """ 36 Discover and run unit tests. 37 This function may change your working directory. 38 Will raise if tests fail to load (e.g. syntax errors) and a suggested exit code otherwise. 39 """ 40 41 if (args.work_dir is not None): 42 os.chdir(args.work_dir) 43 44 if (args.path_additions is not None): 45 for path in args.path_additions: 46 sys.path.append(path) 47 48 if (args.test_dirs is None): 49 args.test_dirs = ['.'] 50 51 runner = unittest.TextTestRunner(verbosity = 3) 52 test_cases = [] 53 54 for test_dir in args.test_dirs: 55 discovered_suite = unittest.TestLoader().discover(test_dir, pattern = args.filename_pattern) 56 test_cases += _collect_tests(discovered_suite) 57 58 tests = unittest.suite.TestSuite() 59 60 for test_case in test_cases: 61 if (isinstance(test_case, unittest.loader._FailedTest)): # type: ignore[attr-defined] 62 raise ValueError(f"Failed to load test: '{test_case.id()}'.") from test_case._exception 63 64 if (args.pattern is None or re.search(args.pattern, test_case.id())): 65 tests.addTest(test_case) 66 else: 67 print(f"Skipping {test_case.id()} because of match pattern.") 68 69 result = runner.run(tests) 70 faults = len(result.errors) + len(result.failures) 71 72 if (not result.wasSuccessful()): 73 # This value will be used as an exit status, so it should not be larger than a byte. 74 # (Some higher values are used specially, so just keep it at a round number.) 75 return max(1, min(faults, 100)) 76 77 return 0
Discover and run unit tests. This function may change your working directory. Will raise if tests fail to load (e.g. syntax errors) and a suggested exit code otherwise.
def
main() -> int:
79def main() -> int: 80 """ Parse the CLI arguments and run tests. """ 81 82 args = _get_parser().parse_args() 83 return run(args)
Parse the CLI arguments and run tests.