1 from warnings import warn |
1 from warnings import warn |
2 |
2 |
3 def report_element(bad_object, type, error_name, args): |
3 class ProblemType: |
4 return { |
4 severities = ['error', 'notice'] # in descending order |
5 'type': type, |
5 def __init__(self, name, severity, message): |
6 'object': bad_object, |
6 if severity not in ProblemType.severities: |
7 'name': error_name, |
7 raise ValueError(str.format( |
8 'args': args, |
8 'bad severity {severity!r}', |
9 } |
9 severity = severity, |
|
10 )) |
|
11 self.name = name |
|
12 self.severity = severity |
|
13 self.message = message |
|
14 def __call__(self, bad_object, **args): |
|
15 return Problem( |
|
16 problem_class = self, |
|
17 bad_object = bad_object, |
|
18 **args, |
|
19 ) |
10 |
20 |
11 def warning(bad_object, error_name, **args): |
21 class Problem: |
12 return report_element(bad_object, 'warning', error_name, args) |
22 def __init__(self, problem_class, bad_object, **args): |
|
23 self.problem_class = problem_class |
|
24 self.severity = problem_class.severity |
|
25 self.object = bad_object |
|
26 self.args = args |
|
27 def __str__(self): |
|
28 if callable(self.problem_class.message): |
|
29 return self.problem_class.message(**self.args) |
|
30 else: |
|
31 return self.problem_class.message |
13 |
32 |
14 def error(bad_object, error_name, **args): |
33 def problem_type(problem_name, **args): |
15 return report_element(bad_object, 'error', error_name, args) |
34 def wrapper(function): |
|
35 if not hasattr(function, 'ldcheck_problem_types'): |
|
36 function.ldcheck_problem_types = {} |
|
37 new_type = ProblemType(name = problem_name, **args) |
|
38 function.ldcheck_problem_types[problem_name] = new_type |
|
39 return function |
|
40 return wrapper |
16 |
41 |
17 def notice(bad_object, error_name, **args): |
42 def report_problem(problem_name, *, bad_object, **args): |
18 return report_element(bad_object, 'notice', error_name, args) |
43 return {'type': problem_name, 'bad-object': bad_object, 'args': args} |
19 |
44 |
20 def name_of_package(package): |
45 def name_of_package(package): |
21 if isinstance(package, tuple): |
46 if isinstance(package, tuple): |
22 return package[1] |
47 return package[1] |
23 else: |
48 else: |
32 yield from sorted( |
57 yield from sorted( |
33 'tests.' + name_of_package(result) |
58 'tests.' + name_of_package(result) |
34 for result in walk_packages(tests.__path__) |
59 for result in walk_packages(tests.__path__) |
35 ) |
60 ) |
36 |
61 |
37 def do_manifest_integrity_checks(test_suite, module): |
|
38 ''' |
|
39 Runs integrity checks on a given module's manifest. |
|
40 ''' |
|
41 def check_for_extra_keys(): |
|
42 extra_keys = module.manifest.keys() - test_suite.keys() |
|
43 if extra_keys: |
|
44 warn(str.format( |
|
45 '{}: extra keys in manifest: {}', |
|
46 module.__name__, |
|
47 ', '.join(map(str, extra_keys)) |
|
48 )) |
|
49 def check_for_manifest_duplicates(): |
|
50 for key in test_suite.keys(): |
|
51 duplicates = module.manifest[key].keys() & test_suite[key].keys() |
|
52 if duplicates: |
|
53 warn(str.format( |
|
54 '{}: redefined {} in manifests: {}', |
|
55 module.__name__, |
|
56 key, |
|
57 duplicates, |
|
58 )) |
|
59 check_for_extra_keys() |
|
60 check_for_manifest_duplicates() |
|
61 |
|
62 def load_tests(): |
62 def load_tests(): |
63 ''' |
63 ''' |
64 Imports test modules and combines their manifests into a test suite. |
64 Imports test modules and combines their manifests into a test suite. |
65 ''' |
65 ''' |
66 test_suite = {'tests': {}, 'messages': {}} |
66 test_suite = {'tests': []} |
67 for module_name in test_discovery(): |
67 for module_name in test_discovery(): |
68 from importlib import import_module |
68 from importlib import import_module |
69 module = import_module(module_name) |
69 module = import_module(module_name) |
70 if hasattr(module, 'manifest'): |
70 if hasattr(module, 'manifest'): |
71 do_manifest_integrity_checks(test_suite, module) |
|
72 # Merge the data from the manifest |
71 # Merge the data from the manifest |
73 for key in module.manifest.keys() & test_suite.keys(): |
72 test_suite['tests'] += module.manifest['tests'] |
74 test_suite[key].update(module.manifest[key]) |
|
75 else: |
73 else: |
76 warn(str.format('Module {} does not have a manifest', module_name)) |
74 warn(str.format('Module {} does not have a manifest', module_name)) |
|
75 test_suite['tests'].sort(key = lambda f: f.__name__) |
77 return test_suite |
76 return test_suite |
78 |
77 |
79 def problem_key(problem): |
78 def problem_key(problem): |
80 problem_hierarchy = ['error', 'warning', 'notice'] |
79 rank = ProblemType.severities.index(problem.severity) # sort by severity |
81 return (problem_hierarchy.index(problem['type']), problem['line-number']) |
80 return (rank, problem.line_number) |
|
81 |
|
82 def build_problem(test_function, problem_params): |
|
83 problem_name = problem_params['type'] |
|
84 problem_type = test_function.ldcheck_problem_types[problem_name] |
|
85 problem_object = problem_type( |
|
86 bad_object = problem_params['bad-object'], |
|
87 **problem_params['args'], |
|
88 ) |
|
89 return problem_object |
82 |
90 |
83 def check_model(model, test_suite = None): |
91 def check_model(model, test_suite = None): |
84 if not test_suite: |
92 if not test_suite: |
85 test_suite = load_tests() |
93 test_suite = load_tests() |
86 problems = [] |
94 problems = [] |
87 line_numbers = { |
95 line_numbers = { |
88 element: (i, i + 1) |
96 element: (i, i + 1) |
89 for i, element in enumerate(model.body) |
97 for i, element in enumerate(model.body) |
90 } |
98 } |
91 for test_name, test_function in test_suite['tests'].items(): |
99 for test_function in test_suite['tests']: |
92 for problem in test_function(model): |
100 for problem_params in test_function(model): |
93 problem['body-index'], problem['line-number'] \ |
101 problem = build_problem(test_function, problem_params) |
94 = line_numbers[problem['object']] |
102 # add line numbers to the problem |
95 del problem['object'] |
103 problem.body_index, problem.line_number \ |
|
104 = line_numbers[problem.object] |
|
105 problem.object = None |
96 problems.append(problem) |
106 problems.append(problem) |
97 return { |
107 return { |
98 'passed': not any( |
108 'passed': not any( |
99 problem['type'] == 'error' |
109 problem.severity == 'error' |
100 for problem in problems |
110 for problem in problems |
101 ), |
111 ), |
102 'problems': sorted(problems, key = problem_key), |
112 'problems': sorted(problems, key = problem_key), |
103 } |
113 } |
104 |
114 |
105 def problem_text(problem, test_suite): |
115 def problem_text(problem, test_suite): |
106 message = test_suite['messages'][problem['name']] |
116 message = problem.problem_class.message |
107 if callable(message): |
117 if callable(message): |
108 message = message(**problem['args']) |
118 message = message(**problem.args) |
109 return message |
119 return message |
110 |
120 |
111 def format_report_html(report, model, test_suite): |
121 def format_report_html(report, model, test_suite): |
112 messages = [] |
122 messages = [] |
113 for problem in report['problems']: |
123 for problem in report['problems']: |
114 ldraw_code = model.body[problem['body-index']].textual_representation() |
124 ldraw_code = model.body[problem.body_index].textual_representation() |
115 message = str.format( |
125 message = str.format( |
116 '<li class="{problem_type}">{model_name}:{line_number}:' |
126 '<li class="{problem_type}">{model_name}:{line_number}:' |
117 '{problem_type}: {message}<br />{ldraw_code}</li>', |
127 '{problem_type}: {message}<br />{ldraw_code}</li>', |
118 model_name = model.name, |
128 model_name = model.name, |
119 line_number = problem['line-number'], |
129 line_number = problem.line_number, |
120 problem_type = problem['type'], |
130 problem_type = problem.severity, |
121 message = problem_text(problem, test_suite), |
131 message = problem_text(problem, test_suite), |
122 ldraw_code = ldraw_code, |
132 ldraw_code = ldraw_code, |
123 ) |
133 ) |
124 messages.append(message) |
134 messages.append(message) |
125 return '\n'.join(messages) |
135 return '\n'.join(messages) |
127 def format_report(report, model, test_suite): |
137 def format_report(report, model, test_suite): |
128 import colorama |
138 import colorama |
129 colorama.init() |
139 colorama.init() |
130 messages = [] |
140 messages = [] |
131 for problem in report['problems']: |
141 for problem in report['problems']: |
132 if problem['type'] == 'error': |
142 if problem.severity == 'error': |
133 text_colour = colorama.Fore.LIGHTRED_EX |
143 text_colour = colorama.Fore.LIGHTRED_EX |
134 elif problem['type'] == 'warning': |
144 elif problem.severity == 'notice': |
135 text_colour = colorama.Fore.LIGHTYELLOW_EX |
|
136 elif problem['type'] == 'notice': |
|
137 text_colour = colorama.Fore.LIGHTBLUE_EX |
145 text_colour = colorama.Fore.LIGHTBLUE_EX |
138 else: |
146 else: |
139 text_colour = '' |
147 text_colour = '' |
140 ldraw_code = model.body[problem['body-index']].textual_representation() |
148 ldraw_code = model.body[problem.body_index].textual_representation() |
141 message = str.format( |
149 message = str.format( |
142 '{text_colour}{model_name}:{line_number}: {problem_type}: {message}' |
150 '{text_colour}{model_name}:{line_number}: {problem_type}: {message}' |
143 '{colour_reset}\n\t{ldraw_code}', |
151 '{colour_reset}\n\t{ldraw_code}', |
144 text_colour = text_colour, |
152 text_colour = text_colour, |
145 model_name = model.name, |
153 model_name = model.name, |
146 line_number = problem['line-number'], |
154 line_number = problem.line_number, |
147 problem_type = problem['type'], |
155 problem_type = problem.severity, |
148 message = problem_text(problem, test_suite), |
156 message = problem_text(problem, test_suite), |
149 colour_reset = colorama.Fore.RESET, |
157 colour_reset = colorama.Fore.RESET, |
150 ldraw_code = ldraw_code, |
158 ldraw_code = ldraw_code, |
151 ) |
159 ) |
152 messages.append(message) |
160 messages.append(message) |
153 return '\n'.join(messages) |
161 return '\n'.join(messages) |
154 |
162 |
|
163 def all_warning_types(test_suite): |
|
164 for test_function in test_suite['tests']: |
|
165 yield from test_function.ldcheck_problem_types.values() |
|
166 |
155 if __name__ == '__main__': |
167 if __name__ == '__main__': |
156 from pprint import pprint |
168 from pprint import pprint |
157 pprint(load_tests()) |
169 pprint(load_tests()) |