| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  | import trace | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 09:02:35 +02:00
										 |  |  | from .runtests import RunTests | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  | from .result import State, TestResult, TestStats, Location | 
					
						
							| 
									
										
										
										
											2023-09-11 09:02:35 +02:00
										 |  |  | from .utils import ( | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  |     StrPath, TestName, TestTuple, TestList, FilterDict, | 
					
						
							|  |  |  |     printlist, count, format_duration) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  | # Python uses exit code 1 when an exception is not caught | 
					
						
							| 
									
										
										
										
											2023-09-26 17:22:50 +02:00
										 |  |  | # argparse.ArgumentParser.error() uses exit code 2 | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | EXITCODE_BAD_TEST = 2 | 
					
						
							|  |  |  | EXITCODE_ENV_CHANGED = 3 | 
					
						
							|  |  |  | EXITCODE_NO_TESTS_RAN = 4 | 
					
						
							|  |  |  | EXITCODE_RERUN_FAIL = 5 | 
					
						
							| 
									
										
										
										
											2023-09-26 17:22:50 +02:00
										 |  |  | EXITCODE_INTERRUPTED = 130   # 128 + signal.SIGINT=2 | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TestResults: | 
					
						
							|  |  |  |     def __init__(self): | 
					
						
							|  |  |  |         self.bad: TestList = [] | 
					
						
							|  |  |  |         self.good: TestList = [] | 
					
						
							|  |  |  |         self.rerun_bad: TestList = [] | 
					
						
							|  |  |  |         self.skipped: TestList = [] | 
					
						
							|  |  |  |         self.resource_denied: TestList = [] | 
					
						
							|  |  |  |         self.env_changed: TestList = [] | 
					
						
							|  |  |  |         self.run_no_tests: TestList = [] | 
					
						
							|  |  |  |         self.rerun: TestList = [] | 
					
						
							| 
									
										
										
										
											2023-09-25 16:21:01 +02:00
										 |  |  |         self.rerun_results: list[TestResult] = [] | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self.interrupted: bool = False | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |         self.worker_bug: bool = False | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         self.test_times: list[tuple[float, TestName]] = [] | 
					
						
							|  |  |  |         self.stats = TestStats() | 
					
						
							|  |  |  |         # used by --junit-xml | 
					
						
							| 
									
										
										
										
											2023-11-30 23:00:14 +00:00
										 |  |  |         self.testsuite_xml: list = [] | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  |         # used by -T with -j | 
					
						
							|  |  |  |         self.covered_lines: set[Location] = set() | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |     def is_all_good(self): | 
					
						
							|  |  |  |         return (not self.bad | 
					
						
							|  |  |  |                 and not self.skipped | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |                 and not self.interrupted | 
					
						
							|  |  |  |                 and not self.worker_bug) | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |     def get_executed(self): | 
					
						
							|  |  |  |         return (set(self.good) | set(self.bad) | set(self.skipped) | 
					
						
							|  |  |  |                 | set(self.resource_denied) | set(self.env_changed) | 
					
						
							|  |  |  |                 | set(self.run_no_tests)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def no_tests_run(self): | 
					
						
							|  |  |  |         return not any((self.good, self.bad, self.skipped, self.interrupted, | 
					
						
							|  |  |  |                         self.env_changed)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_state(self, fail_env_changed): | 
					
						
							|  |  |  |         state = [] | 
					
						
							|  |  |  |         if self.bad: | 
					
						
							|  |  |  |             state.append("FAILURE") | 
					
						
							|  |  |  |         elif fail_env_changed and self.env_changed: | 
					
						
							|  |  |  |             state.append("ENV CHANGED") | 
					
						
							|  |  |  |         elif self.no_tests_run(): | 
					
						
							|  |  |  |             state.append("NO TESTS RAN") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.interrupted: | 
					
						
							|  |  |  |             state.append("INTERRUPTED") | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |         if self.worker_bug: | 
					
						
							|  |  |  |             state.append("WORKER BUG") | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         if not state: | 
					
						
							|  |  |  |             state.append("SUCCESS") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return ', '.join(state) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_exitcode(self, fail_env_changed, fail_rerun): | 
					
						
							|  |  |  |         exitcode = 0 | 
					
						
							|  |  |  |         if self.bad: | 
					
						
							|  |  |  |             exitcode = EXITCODE_BAD_TEST | 
					
						
							|  |  |  |         elif self.interrupted: | 
					
						
							|  |  |  |             exitcode = EXITCODE_INTERRUPTED | 
					
						
							|  |  |  |         elif fail_env_changed and self.env_changed: | 
					
						
							|  |  |  |             exitcode = EXITCODE_ENV_CHANGED | 
					
						
							|  |  |  |         elif self.no_tests_run(): | 
					
						
							|  |  |  |             exitcode = EXITCODE_NO_TESTS_RAN | 
					
						
							|  |  |  |         elif fail_rerun and self.rerun: | 
					
						
							|  |  |  |             exitcode = EXITCODE_RERUN_FAIL | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |         elif self.worker_bug: | 
					
						
							|  |  |  |             exitcode = EXITCODE_BAD_TEST | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         return exitcode | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def accumulate_result(self, result: TestResult, runtests: RunTests): | 
					
						
							|  |  |  |         test_name = result.test_name | 
					
						
							|  |  |  |         rerun = runtests.rerun | 
					
						
							|  |  |  |         fail_env_changed = runtests.fail_env_changed | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         match result.state: | 
					
						
							|  |  |  |             case State.PASSED: | 
					
						
							|  |  |  |                 self.good.append(test_name) | 
					
						
							|  |  |  |             case State.ENV_CHANGED: | 
					
						
							|  |  |  |                 self.env_changed.append(test_name) | 
					
						
							| 
									
										
										
										
											2023-09-25 16:21:01 +02:00
										 |  |  |                 self.rerun_results.append(result) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |             case State.SKIPPED: | 
					
						
							|  |  |  |                 self.skipped.append(test_name) | 
					
						
							|  |  |  |             case State.RESOURCE_DENIED: | 
					
						
							|  |  |  |                 self.resource_denied.append(test_name) | 
					
						
							|  |  |  |             case State.INTERRUPTED: | 
					
						
							|  |  |  |                 self.interrupted = True | 
					
						
							|  |  |  |             case State.DID_NOT_RUN: | 
					
						
							|  |  |  |                 self.run_no_tests.append(test_name) | 
					
						
							|  |  |  |             case _: | 
					
						
							|  |  |  |                 if result.is_failed(fail_env_changed): | 
					
						
							|  |  |  |                     self.bad.append(test_name) | 
					
						
							| 
									
										
										
										
											2023-09-25 16:21:01 +02:00
										 |  |  |                     self.rerun_results.append(result) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     raise ValueError(f"invalid test state: {result.state!r}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |         if result.state == State.WORKER_BUG: | 
					
						
							|  |  |  |             self.worker_bug = True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         if result.has_meaningful_duration() and not rerun: | 
					
						
							| 
									
										
										
										
											2023-12-01 14:54:33 +00:00
										 |  |  |             if result.duration is None: | 
					
						
							|  |  |  |                 raise ValueError("result.duration is None") | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |             self.test_times.append((result.duration, test_name)) | 
					
						
							|  |  |  |         if result.stats is not None: | 
					
						
							|  |  |  |             self.stats.accumulate(result.stats) | 
					
						
							|  |  |  |         if rerun: | 
					
						
							|  |  |  |             self.rerun.append(test_name) | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  |         if result.covered_lines: | 
					
						
							|  |  |  |             # we don't care about trace counts so we don't have to sum them up | 
					
						
							|  |  |  |             self.covered_lines.update(result.covered_lines) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         xml_data = result.xml_data | 
					
						
							|  |  |  |         if xml_data: | 
					
						
							| 
									
										
										
										
											2023-09-11 05:27:37 +02:00
										 |  |  |             self.add_junit(xml_data) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  |     def get_coverage_results(self) -> trace.CoverageResults: | 
					
						
							|  |  |  |         counts = {loc: 1 for loc in self.covered_lines} | 
					
						
							|  |  |  |         return trace.CoverageResults(counts=counts) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |     def need_rerun(self): | 
					
						
							| 
									
										
										
										
											2023-09-25 16:21:01 +02:00
										 |  |  |         return bool(self.rerun_results) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |     def prepare_rerun(self, *, clear: bool = True) -> tuple[TestTuple, FilterDict]: | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         tests: TestList = [] | 
					
						
							|  |  |  |         match_tests_dict = {} | 
					
						
							| 
									
										
										
										
											2023-09-25 16:21:01 +02:00
										 |  |  |         for result in self.rerun_results: | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |             tests.append(result.test_name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             match_tests = result.get_rerun_match_tests() | 
					
						
							|  |  |  |             # ignore empty match list | 
					
						
							|  |  |  |             if match_tests: | 
					
						
							|  |  |  |                 match_tests_dict[result.test_name] = match_tests | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |         if clear: | 
					
						
							|  |  |  |             # Clear previously failed tests | 
					
						
							|  |  |  |             self.rerun_bad.extend(self.bad) | 
					
						
							|  |  |  |             self.bad.clear() | 
					
						
							|  |  |  |             self.env_changed.clear() | 
					
						
							|  |  |  |             self.rerun_results.clear() | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         return (tuple(tests), match_tests_dict) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def add_junit(self, xml_data: list[str]): | 
					
						
							|  |  |  |         import xml.etree.ElementTree as ET | 
					
						
							|  |  |  |         for e in xml_data: | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 self.testsuite_xml.append(ET.fromstring(e)) | 
					
						
							|  |  |  |             except ET.ParseError: | 
					
						
							|  |  |  |                 print(xml_data, file=sys.__stderr__) | 
					
						
							|  |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def write_junit(self, filename: StrPath): | 
					
						
							|  |  |  |         if not self.testsuite_xml: | 
					
						
							|  |  |  |             # Don't create empty XML file | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         import xml.etree.ElementTree as ET | 
					
						
							|  |  |  |         root = ET.Element("testsuites") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Manually count the totals for the overall summary | 
					
						
							|  |  |  |         totals = {'tests': 0, 'errors': 0, 'failures': 0} | 
					
						
							|  |  |  |         for suite in self.testsuite_xml: | 
					
						
							|  |  |  |             root.append(suite) | 
					
						
							|  |  |  |             for k in totals: | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     totals[k] += int(suite.get(k, 0)) | 
					
						
							|  |  |  |                 except ValueError: | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for k, v in totals.items(): | 
					
						
							|  |  |  |             root.set(k, str(v)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         with open(filename, 'wb') as f: | 
					
						
							|  |  |  |             for s in ET.tostringlist(root): | 
					
						
							|  |  |  |                 f.write(s) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 05:27:37 +02:00
										 |  |  |     def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool): | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |         if print_slowest: | 
					
						
							|  |  |  |             self.test_times.sort(reverse=True) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  |             print("10 slowest tests:") | 
					
						
							|  |  |  |             for test_time, test in self.test_times[:10]: | 
					
						
							|  |  |  |                 print("- %s: %s" % (test, format_duration(test_time))) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |         all_tests = [] | 
					
						
							|  |  |  |         omitted = set(tests) - self.get_executed() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # less important | 
					
						
							|  |  |  |         all_tests.append((omitted, "test", "{} omitted:")) | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |         if not quiet: | 
					
						
							|  |  |  |             all_tests.append((self.skipped, "test", "{} skipped:")) | 
					
						
							|  |  |  |             all_tests.append((self.resource_denied, "test", "{} skipped (resource denied):")) | 
					
						
							|  |  |  |         all_tests.append((self.run_no_tests, "test", "{} run no tests:")) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-30 22:48:26 +02:00
										 |  |  |         # more important | 
					
						
							|  |  |  |         all_tests.append((self.env_changed, "test", "{} altered the execution environment (env changed):")) | 
					
						
							|  |  |  |         all_tests.append((self.rerun, "re-run test", "{}:")) | 
					
						
							|  |  |  |         all_tests.append((self.bad, "test", "{} failed:")) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |         for tests_list, count_text, title_format in all_tests: | 
					
						
							|  |  |  |             if tests_list: | 
					
						
							|  |  |  |                 print() | 
					
						
							|  |  |  |                 count_text = count(len(tests_list), count_text) | 
					
						
							|  |  |  |                 print(title_format.format(count_text)) | 
					
						
							|  |  |  |                 printlist(tests_list) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |         if self.good and not quiet: | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |             print() | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |             text = count(len(self.good), "test") | 
					
						
							|  |  |  |             text = f"{text} OK." | 
					
						
							|  |  |  |             if (self.is_all_good() and len(self.good) > 1): | 
					
						
							|  |  |  |                 text = f"All {text}" | 
					
						
							|  |  |  |             print(text) | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |         if self.interrupted: | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  |             print() | 
					
						
							| 
									
										
										
										
											2023-09-25 15:50:15 +02:00
										 |  |  |             print("Test suite interrupted by signal SIGINT.") | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def display_summary(self, first_runtests: RunTests, filtered: bool): | 
					
						
							|  |  |  |         # Total tests | 
					
						
							|  |  |  |         stats = self.stats | 
					
						
							|  |  |  |         text = f'run={stats.tests_run:,}' | 
					
						
							|  |  |  |         if filtered: | 
					
						
							|  |  |  |             text = f"{text} (filtered)" | 
					
						
							|  |  |  |         report = [text] | 
					
						
							|  |  |  |         if stats.failures: | 
					
						
							|  |  |  |             report.append(f'failures={stats.failures:,}') | 
					
						
							|  |  |  |         if stats.skipped: | 
					
						
							|  |  |  |             report.append(f'skipped={stats.skipped:,}') | 
					
						
							| 
									
										
										
										
											2023-09-15 18:01:28 +01:00
										 |  |  |         print(f"Total tests: {' '.join(report)}") | 
					
						
							| 
									
										
										
										
											2023-09-10 04:30:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # Total test files | 
					
						
							|  |  |  |         all_tests = [self.good, self.bad, self.rerun, | 
					
						
							|  |  |  |                      self.skipped, | 
					
						
							|  |  |  |                      self.env_changed, self.run_no_tests] | 
					
						
							|  |  |  |         run = sum(map(len, all_tests)) | 
					
						
							|  |  |  |         text = f'run={run}' | 
					
						
							|  |  |  |         if not first_runtests.forever: | 
					
						
							|  |  |  |             ntest = len(first_runtests.tests) | 
					
						
							|  |  |  |             text = f"{text}/{ntest}" | 
					
						
							|  |  |  |         if filtered: | 
					
						
							|  |  |  |             text = f"{text} (filtered)" | 
					
						
							|  |  |  |         report = [text] | 
					
						
							|  |  |  |         for name, tests in ( | 
					
						
							|  |  |  |             ('failed', self.bad), | 
					
						
							|  |  |  |             ('env_changed', self.env_changed), | 
					
						
							|  |  |  |             ('skipped', self.skipped), | 
					
						
							|  |  |  |             ('resource_denied', self.resource_denied), | 
					
						
							|  |  |  |             ('rerun', self.rerun), | 
					
						
							|  |  |  |             ('run_no_tests', self.run_no_tests), | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             if tests: | 
					
						
							|  |  |  |                 report.append(f'{name}={len(tests)}') | 
					
						
							| 
									
										
										
										
											2023-09-15 18:01:28 +01:00
										 |  |  |         print(f"Total test files: {' '.join(report)}") |