mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			272 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import sys
 | |
| import trace
 | |
| 
 | |
| from .runtests import RunTests
 | |
| from .result import State, TestResult, TestStats, Location
 | |
| from .utils import (
 | |
|     StrPath, TestName, TestTuple, TestList, FilterDict,
 | |
|     printlist, count, format_duration)
 | |
| 
 | |
| 
 | |
| # Python uses exit code 1 when an exception is not caught
 | |
| # argparse.ArgumentParser.error() uses exit code 2
 | |
| EXITCODE_BAD_TEST = 2
 | |
| EXITCODE_ENV_CHANGED = 3
 | |
| EXITCODE_NO_TESTS_RAN = 4
 | |
| EXITCODE_RERUN_FAIL = 5
 | |
| EXITCODE_INTERRUPTED = 130   # 128 + signal.SIGINT=2
 | |
| 
 | |
| 
 | |
| class TestResults:
 | |
|     def __init__(self) -> None:
 | |
|         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 = []
 | |
|         self.rerun_results: list[TestResult] = []
 | |
| 
 | |
|         self.interrupted: bool = False
 | |
|         self.worker_bug: bool = False
 | |
|         self.test_times: list[tuple[float, TestName]] = []
 | |
|         self.stats = TestStats()
 | |
|         # used by --junit-xml
 | |
|         self.testsuite_xml: list = []
 | |
|         # used by -T with -j
 | |
|         self.covered_lines: set[Location] = set()
 | |
| 
 | |
|     def is_all_good(self) -> bool:
 | |
|         return (not self.bad
 | |
|                 and not self.skipped
 | |
|                 and not self.interrupted
 | |
|                 and not self.worker_bug)
 | |
| 
 | |
|     def get_executed(self) -> set[TestName]:
 | |
|         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) -> bool:
 | |
|         return not any((self.good, self.bad, self.skipped, self.interrupted,
 | |
|                         self.env_changed))
 | |
| 
 | |
|     def get_state(self, fail_env_changed: bool) -> str:
 | |
|         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")
 | |
|         if self.worker_bug:
 | |
|             state.append("WORKER BUG")
 | |
|         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
 | |
|         elif self.worker_bug:
 | |
|             exitcode = EXITCODE_BAD_TEST
 | |
|         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)
 | |
|                 self.rerun_results.append(result)
 | |
|             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)
 | |
|                     self.rerun_results.append(result)
 | |
|                 else:
 | |
|                     raise ValueError(f"invalid test state: {result.state!r}")
 | |
| 
 | |
|         if result.state == State.WORKER_BUG:
 | |
|             self.worker_bug = True
 | |
| 
 | |
|         if result.has_meaningful_duration() and not rerun:
 | |
|             if result.duration is None:
 | |
|                 raise ValueError("result.duration is None")
 | |
|             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)
 | |
|         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)
 | |
|         xml_data = result.xml_data
 | |
|         if xml_data:
 | |
|             self.add_junit(xml_data)
 | |
| 
 | |
|     def get_coverage_results(self) -> trace.CoverageResults:
 | |
|         counts = {loc: 1 for loc in self.covered_lines}
 | |
|         return trace.CoverageResults(counts=counts)
 | |
| 
 | |
|     def need_rerun(self):
 | |
|         return bool(self.rerun_results)
 | |
| 
 | |
|     def prepare_rerun(self, *, clear: bool = True) -> tuple[TestTuple, FilterDict]:
 | |
|         tests: TestList = []
 | |
|         match_tests_dict = {}
 | |
|         for result in self.rerun_results:
 | |
|             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
 | |
| 
 | |
|         if clear:
 | |
|             # Clear previously failed tests
 | |
|             self.rerun_bad.extend(self.bad)
 | |
|             self.bad.clear()
 | |
|             self.env_changed.clear()
 | |
|             self.rerun_results.clear()
 | |
| 
 | |
|         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)
 | |
| 
 | |
|     def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool):
 | |
|         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)))
 | |
| 
 | |
|         all_tests = []
 | |
|         omitted = set(tests) - self.get_executed()
 | |
| 
 | |
|         # less important
 | |
|         all_tests.append((sorted(omitted), "test", "{} omitted:"))
 | |
|         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:"))
 | |
| 
 | |
|         # 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:"))
 | |
| 
 | |
|         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)
 | |
| 
 | |
|         if self.good and not quiet:
 | |
|             print()
 | |
|             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)
 | |
| 
 | |
|         if self.interrupted:
 | |
|             print()
 | |
|             print("Test suite interrupted by signal SIGINT.")
 | |
| 
 | |
|     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:,}')
 | |
|         print(f"Total tests: {' '.join(report)}")
 | |
| 
 | |
|         # 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)}')
 | |
|         print(f"Total test files: {' '.join(report)}")
 | 
