Tue, 23 Jan 2018 15:30:48 +0200
added test for collinearity, fixed bowtie test
| 16 | 1 | from warnings import warn |
| 2 | ||
| 3 | def report_element(bad_object, type, error_name, args): | |
| 12 | 4 | return { |
| 16 | 5 | 'type': type, |
| 6 | 'object': bad_object, | |
| 7 | 'name': error_name, | |
| 8 | 'args': args, | |
| 13 | 9 | } |
| 10 | ||
| 17 | 11 | def warning(bad_object, error_name, **args): |
| 16 | 12 | return report_element(bad_object, 'warning', error_name, args) |
| 13 | ||
| 17 | 14 | def error(bad_object, error_name, **args): |
| 16 | 15 | return report_element(bad_object, 'error', error_name, args) |
| 16 | ||
| 17 | def test_discovery(): | |
| 18 | ''' | |
| 19 | Finds all test modules and yields their names. | |
| 20 | ''' | |
| 21 | from pkgutil import walk_packages | |
| 22 | import tests | |
| 23 | yield from sorted( | |
| 24 | 'tests.' + result.name | |
| 25 | for result in walk_packages(tests.__path__) | |
| 26 | ) | |
| 27 | ||
| 28 | def do_manifest_integrity_checks(test_suite, module): | |
| 29 | ''' | |
| 30 | Runs integrity checks on a given module's manifest. | |
| 31 | ''' | |
| 32 | def check_for_extra_keys(): | |
| 33 | extra_keys = module.manifest.keys() - test_suite.keys() | |
| 34 | if extra_keys: | |
| 35 | warn(str.format( | |
| 36 | '{}: extra keys in manifest: {}', | |
| 37 | module.__name__, | |
| 38 | ', '.join(map(str, extra_keys)) | |
| 39 | )) | |
| 40 | def check_for_manifest_duplicates(): | |
| 41 | for key in test_suite.keys(): | |
| 42 | duplicates = module.manifest[key].keys() & test_suite[key].keys() | |
| 43 | if duplicates: | |
| 44 | warn(str.format( | |
| 45 | '{}: redefined {} in manifests: {}', | |
| 46 | module.__name__, | |
| 47 | key, | |
| 48 | duplicates, | |
| 49 | )) | |
| 50 | check_for_extra_keys() | |
| 51 | check_for_manifest_duplicates() | |
| 52 | ||
| 53 | def load_tests(): | |
| 54 | ''' | |
| 55 | Imports test modules and combines their manifests into a test suite. | |
| 56 | ''' | |
| 57 | test_suite = {'tests': {}, 'messages': {}} | |
| 58 | for module_name in test_discovery(): | |
| 59 | from importlib import import_module | |
| 60 | module = import_module(module_name) | |
| 61 | if hasattr(module, 'manifest'): | |
| 62 | do_manifest_integrity_checks(test_suite, module) | |
| 63 | # Merge the data from the manifest | |
| 64 | for key in module.manifest.keys() & test_suite.keys(): | |
| 65 | test_suite[key].update(module.manifest[key]) | |
| 66 | else: | |
| 67 | warn(str.format('Module {} does not have a manifest', module_name)) | |
| 68 | return test_suite | |
| 69 | ||
| 17 | 70 | def check_model(model, test_suite = None): |
| 71 | if not test_suite: | |
| 72 | test_suite = load_tests() | |
| 73 | report = [] | |
| 74 | line_numbers = { | |
| 75 | element: (i, i + 1 + model.body_offset) | |
| 76 | for i, element in enumerate(model.body) | |
| 77 | } | |
| 78 | for test_name, test_function in test_suite['tests'].items(): | |
| 79 | problems = test_function(model) | |
| 80 | for problem in problems: | |
| 81 | problem['body-index'], problem['line-number'] \ | |
| 82 | = line_numbers[problem['object']] | |
| 83 | del problem['object'] | |
| 84 | report.append(problem) | |
| 85 | return report | |
| 86 | ||
| 29 | 87 | def problem_text(problem, test_suite): |
| 88 | message = test_suite['messages'][problem['name']] | |
| 89 | if callable(message): | |
| 90 | message = message(**problem['args']) | |
| 91 | return message | |
| 92 | ||
| 93 | def format_report_html(report, model, test_suite): | |
| 94 | result = [] | |
| 95 | for problem in report: | |
| 96 | ldraw_code = model.body[problem['body-index']].textual_representation() | |
| 97 | message = str.format( | |
| 98 | '<li class="{problem_type}">{model_name}:{line_number}:' | |
| 99 | '{problem_type}: {message}<br />{ldraw_code}</li>', | |
| 100 | model_name = model.name, | |
| 101 | line_number = problem['line-number'], | |
| 102 | problem_type = problem['type'], | |
| 103 | message = problem_text(problem, test_suite), | |
| 104 | ldraw_code = ldraw_code, | |
| 105 | ) | |
| 106 | result.append((problem['line-number'], message)) | |
| 107 | return '\n'.join( | |
| 108 | problem[1] | |
| 109 | for problem in sorted(result) | |
| 110 | ) | |
| 111 | ||
| 17 | 112 | def format_report(report, model, test_suite): |
| 18 | 113 | import colorama |
| 114 | colorama.init() | |
| 17 | 115 | result = [] |
| 116 | for problem in report: | |
| 18 | 117 | if problem['type'] == 'error': |
| 118 | text_colour = colorama.Fore.LIGHTRED_EX | |
| 119 | elif problem['type'] == 'warning': | |
| 120 | text_colour = colorama.Fore.LIGHTYELLOW_EX | |
| 121 | else: | |
| 122 | text_colour = '' | |
| 19 | 123 | ldraw_code = model.body[problem['body-index']].textual_representation() |
| 17 | 124 | message = str.format( |
| 18 | 125 | '{text_colour}{model_name}:{line_number}: {problem_type}: {message}' |
| 19 | 126 | '{colour_reset}\n\t{ldraw_code}', |
| 18 | 127 | text_colour = text_colour, |
| 128 | model_name = model.name, | |
| 129 | line_number = problem['line-number'], | |
| 130 | problem_type = problem['type'], | |
| 29 | 131 | message = problem_text(problem, test_suite), |
| 18 | 132 | colour_reset = colorama.Fore.RESET, |
| 19 | 133 | ldraw_code = ldraw_code, |
| 17 | 134 | ) |
| 135 | result.append((problem['line-number'], message)) | |
| 136 | return '\n'.join( | |
| 137 | problem[1] | |
| 138 | for problem in sorted(result) | |
| 139 | ) | |
| 140 | ||
| 16 | 141 | if __name__ == '__main__': |
| 142 | from pprint import pprint | |
| 143 | pprint(load_tests()) |