mirror of
				https://github.com/python/cpython.git
				synced 2025-10-30 05:01:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			747 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			747 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import os
 | |
| import random
 | |
| import re
 | |
| import shlex
 | |
| import sys
 | |
| import sysconfig
 | |
| import time
 | |
| import trace
 | |
| 
 | |
| from test.support import (os_helper, MS_WINDOWS, flush_std_streams,
 | |
|                           suppress_immortalization)
 | |
| 
 | |
| from .cmdline import _parse_args, Namespace
 | |
| from .findtests import findtests, split_test_packages, list_cases
 | |
| from .logger import Logger
 | |
| from .pgo import setup_pgo_tests
 | |
| from .result import State, TestResult
 | |
| from .results import TestResults, EXITCODE_INTERRUPTED
 | |
| from .runtests import RunTests, HuntRefleak
 | |
| from .setup import setup_process, setup_test_dir
 | |
| from .single import run_single_test, PROGRESS_MIN_TIME
 | |
| from .tsan import setup_tsan_tests
 | |
| from .utils import (
 | |
|     StrPath, StrJSON, TestName, TestList, TestTuple, TestFilter,
 | |
|     strip_py_suffix, count, format_duration,
 | |
|     printlist, get_temp_dir, get_work_dir, exit_timeout,
 | |
|     display_header, cleanup_temp_dir, print_warning,
 | |
|     is_cross_compiled, get_host_runner,
 | |
|     EXIT_TIMEOUT)
 | |
| 
 | |
| 
 | |
| class Regrtest:
 | |
|     """Execute a test suite.
 | |
| 
 | |
|     This also parses command-line options and modifies its behavior
 | |
|     accordingly.
 | |
| 
 | |
|     tests -- a list of strings containing test names (optional)
 | |
|     testdir -- the directory in which to look for tests (optional)
 | |
| 
 | |
|     Users other than the Python test suite will certainly want to
 | |
|     specify testdir; if it's omitted, the directory containing the
 | |
|     Python test suite is searched for.
 | |
| 
 | |
|     If the tests argument is omitted, the tests listed on the
 | |
|     command-line will be used.  If that's empty, too, then all *.py
 | |
|     files beginning with test_ will be used.
 | |
| 
 | |
|     The other default arguments (verbose, quiet, exclude,
 | |
|     single, randomize, use_resources, trace, coverdir,
 | |
|     print_slow, and random_seed) allow programmers calling main()
 | |
|     directly to set the values that would normally be set by flags
 | |
|     on the command line.
 | |
|     """
 | |
|     def __init__(self, ns: Namespace, _add_python_opts: bool = False):
 | |
|         # Log verbosity
 | |
|         self.verbose: int = int(ns.verbose)
 | |
|         self.quiet: bool = ns.quiet
 | |
|         self.pgo: bool = ns.pgo
 | |
|         self.pgo_extended: bool = ns.pgo_extended
 | |
|         self.tsan: bool = ns.tsan
 | |
| 
 | |
|         # Test results
 | |
|         self.results: TestResults = TestResults()
 | |
|         self.first_state: str | None = None
 | |
| 
 | |
|         # Logger
 | |
|         self.logger = Logger(self.results, self.quiet, self.pgo)
 | |
| 
 | |
|         # Actions
 | |
|         self.want_header: bool = ns.header
 | |
|         self.want_list_tests: bool = ns.list_tests
 | |
|         self.want_list_cases: bool = ns.list_cases
 | |
|         self.want_wait: bool = ns.wait
 | |
|         self.want_cleanup: bool = ns.cleanup
 | |
|         self.want_rerun: bool = ns.rerun
 | |
|         self.want_run_leaks: bool = ns.runleaks
 | |
|         self.want_bisect: bool = ns.bisect
 | |
| 
 | |
|         self.ci_mode: bool = (ns.fast_ci or ns.slow_ci)
 | |
|         self.want_add_python_opts: bool = (_add_python_opts
 | |
|                                            and ns._add_python_opts)
 | |
| 
 | |
|         # Select tests
 | |
|         self.match_tests: TestFilter = ns.match_tests
 | |
|         self.exclude: bool = ns.exclude
 | |
|         self.fromfile: StrPath | None = ns.fromfile
 | |
|         self.starting_test: TestName | None = ns.start
 | |
|         self.cmdline_args: TestList = ns.args
 | |
| 
 | |
|         # Workers
 | |
|         self.single_process: bool = ns.single_process
 | |
|         if self.single_process or ns.use_mp is None:
 | |
|             num_workers = 0   # run sequentially in a single process
 | |
|         elif ns.use_mp <= 0:
 | |
|             num_workers = -1  # run in parallel, use the number of CPUs
 | |
|         else:
 | |
|             num_workers = ns.use_mp  # run in parallel
 | |
|         self.num_workers: int = num_workers
 | |
|         self.worker_json: StrJSON | None = ns.worker_json
 | |
| 
 | |
|         # Options to run tests
 | |
|         self.fail_fast: bool = ns.failfast
 | |
|         self.fail_env_changed: bool = ns.fail_env_changed
 | |
|         self.fail_rerun: bool = ns.fail_rerun
 | |
|         self.forever: bool = ns.forever
 | |
|         self.output_on_failure: bool = ns.verbose3
 | |
|         self.timeout: float | None = ns.timeout
 | |
|         if ns.huntrleaks:
 | |
|             warmups, runs, filename = ns.huntrleaks
 | |
|             filename = os.path.abspath(filename)
 | |
|             self.hunt_refleak: HuntRefleak | None = HuntRefleak(warmups, runs, filename)
 | |
|         else:
 | |
|             self.hunt_refleak = None
 | |
|         self.test_dir: StrPath | None = ns.testdir
 | |
|         self.junit_filename: StrPath | None = ns.xmlpath
 | |
|         self.memory_limit: str | None = ns.memlimit
 | |
|         self.gc_threshold: int | None = ns.threshold
 | |
|         self.use_resources: tuple[str, ...] = tuple(ns.use_resources)
 | |
|         if ns.python:
 | |
|             self.python_cmd: tuple[str, ...] | None = tuple(ns.python)
 | |
|         else:
 | |
|             self.python_cmd = None
 | |
|         self.coverage: bool = ns.trace
 | |
|         self.coverage_dir: StrPath | None = ns.coverdir
 | |
|         self.tmp_dir: StrPath | None = ns.tempdir
 | |
| 
 | |
|         # Randomize
 | |
|         self.randomize: bool = ns.randomize
 | |
|         if ('SOURCE_DATE_EPOCH' in os.environ
 | |
|             # don't use the variable if empty
 | |
|             and os.environ['SOURCE_DATE_EPOCH']
 | |
|         ):
 | |
|             self.randomize = False
 | |
|             # SOURCE_DATE_EPOCH should be an integer, but use a string to not
 | |
|             # fail if it's not integer. random.seed() accepts a string.
 | |
|             # https://reproducible-builds.org/docs/source-date-epoch/
 | |
|             self.random_seed: int | str = os.environ['SOURCE_DATE_EPOCH']
 | |
|         elif ns.random_seed is None:
 | |
|             self.random_seed = random.getrandbits(32)
 | |
|         else:
 | |
|             self.random_seed = ns.random_seed
 | |
| 
 | |
|         # tests
 | |
|         self.first_runtests: RunTests | None = None
 | |
| 
 | |
|         # used by --slowest
 | |
|         self.print_slowest: bool = ns.print_slow
 | |
| 
 | |
|         # used to display the progress bar "[ 3/100]"
 | |
|         self.start_time = time.perf_counter()
 | |
| 
 | |
|         # used by --single
 | |
|         self.single_test_run: bool = ns.single
 | |
|         self.next_single_test: TestName | None = None
 | |
|         self.next_single_filename: StrPath | None = None
 | |
| 
 | |
|     def log(self, line=''):
 | |
|         self.logger.log(line)
 | |
| 
 | |
|     def find_tests(self, tests: TestList | None = None) -> tuple[TestTuple, TestList | None]:
 | |
|         if self.single_test_run:
 | |
|             self.next_single_filename = os.path.join(self.tmp_dir, 'pynexttest')
 | |
|             try:
 | |
|                 with open(self.next_single_filename, 'r') as fp:
 | |
|                     next_test = fp.read().strip()
 | |
|                     tests = [next_test]
 | |
|             except OSError:
 | |
|                 pass
 | |
| 
 | |
|         if self.fromfile:
 | |
|             tests = []
 | |
|             # regex to match 'test_builtin' in line:
 | |
|             # '0:00:00 [  4/400] test_builtin -- test_dict took 1 sec'
 | |
|             regex = re.compile(r'\btest_[a-zA-Z0-9_]+\b')
 | |
|             with open(os.path.join(os_helper.SAVEDCWD, self.fromfile)) as fp:
 | |
|                 for line in fp:
 | |
|                     line = line.split('#', 1)[0]
 | |
|                     line = line.strip()
 | |
|                     match = regex.search(line)
 | |
|                     if match is not None:
 | |
|                         tests.append(match.group())
 | |
| 
 | |
|         strip_py_suffix(tests)
 | |
| 
 | |
|         if self.pgo:
 | |
|             # add default PGO tests if no tests are specified
 | |
|             setup_pgo_tests(self.cmdline_args, self.pgo_extended)
 | |
| 
 | |
|         if self.tsan:
 | |
|             setup_tsan_tests(self.cmdline_args)
 | |
| 
 | |
|         exclude_tests = set()
 | |
|         if self.exclude:
 | |
|             for arg in self.cmdline_args:
 | |
|                 exclude_tests.add(arg)
 | |
|             self.cmdline_args = []
 | |
| 
 | |
|         alltests = findtests(testdir=self.test_dir,
 | |
|                              exclude=exclude_tests)
 | |
| 
 | |
|         if not self.fromfile:
 | |
|             selected = tests or self.cmdline_args
 | |
|             if selected:
 | |
|                 selected = split_test_packages(selected)
 | |
|             else:
 | |
|                 selected = alltests
 | |
|         else:
 | |
|             selected = tests
 | |
| 
 | |
|         if self.single_test_run:
 | |
|             selected = selected[:1]
 | |
|             try:
 | |
|                 pos = alltests.index(selected[0])
 | |
|                 self.next_single_test = alltests[pos + 1]
 | |
|             except IndexError:
 | |
|                 pass
 | |
| 
 | |
|         # Remove all the selected tests that precede start if it's set.
 | |
|         if self.starting_test:
 | |
|             try:
 | |
|                 del selected[:selected.index(self.starting_test)]
 | |
|             except ValueError:
 | |
|                 print(f"Cannot find starting test: {self.starting_test}")
 | |
|                 sys.exit(1)
 | |
| 
 | |
|         random.seed(self.random_seed)
 | |
|         if self.randomize:
 | |
|             random.shuffle(selected)
 | |
| 
 | |
|         return (tuple(selected), tests)
 | |
| 
 | |
|     @staticmethod
 | |
|     def list_tests(tests: TestTuple):
 | |
|         for name in tests:
 | |
|             print(name)
 | |
| 
 | |
|     def _rerun_failed_tests(self, runtests: RunTests):
 | |
|         # Configure the runner to re-run tests
 | |
|         if self.num_workers == 0 and not self.single_process:
 | |
|             # Always run tests in fresh processes to have more deterministic
 | |
|             # initial state. Don't re-run tests in parallel but limit to a
 | |
|             # single worker process to have side effects (on the system load
 | |
|             # and timings) between tests.
 | |
|             self.num_workers = 1
 | |
| 
 | |
|         tests, match_tests_dict = self.results.prepare_rerun()
 | |
| 
 | |
|         # Re-run failed tests
 | |
|         runtests = runtests.copy(
 | |
|             tests=tests,
 | |
|             rerun=True,
 | |
|             verbose=True,
 | |
|             forever=False,
 | |
|             fail_fast=False,
 | |
|             match_tests_dict=match_tests_dict,
 | |
|             output_on_failure=False)
 | |
|         self.logger.set_tests(runtests)
 | |
| 
 | |
|         msg = f"Re-running {len(tests)} failed tests in verbose mode"
 | |
|         if not self.single_process:
 | |
|             msg = f"{msg} in subprocesses"
 | |
|             self.log(msg)
 | |
|             self._run_tests_mp(runtests, self.num_workers)
 | |
|         else:
 | |
|             self.log(msg)
 | |
|             self.run_tests_sequentially(runtests)
 | |
|         return runtests
 | |
| 
 | |
|     def rerun_failed_tests(self, runtests: RunTests):
 | |
|         if self.python_cmd:
 | |
|             # Temp patch for https://github.com/python/cpython/issues/94052
 | |
|             self.log(
 | |
|                 "Re-running failed tests is not supported with --python "
 | |
|                 "host runner option."
 | |
|             )
 | |
|             return
 | |
| 
 | |
|         self.first_state = self.get_state()
 | |
| 
 | |
|         print()
 | |
|         rerun_runtests = self._rerun_failed_tests(runtests)
 | |
| 
 | |
|         if self.results.bad:
 | |
|             print(count(len(self.results.bad), 'test'), "failed again:")
 | |
|             printlist(self.results.bad)
 | |
| 
 | |
|         self.display_result(rerun_runtests)
 | |
| 
 | |
|     def _run_bisect(self, runtests: RunTests, test: str, progress: str) -> bool:
 | |
|         print()
 | |
|         title = f"Bisect {test}"
 | |
|         if progress:
 | |
|             title = f"{title} ({progress})"
 | |
|         print(title)
 | |
|         print("#" * len(title))
 | |
|         print()
 | |
| 
 | |
|         cmd = runtests.create_python_cmd()
 | |
|         cmd.extend([
 | |
|             "-u", "-m", "test.bisect_cmd",
 | |
|             # Limit to 25 iterations (instead of 100) to not abuse CI resources
 | |
|             "--max-iter", "25",
 | |
|             "-v",
 | |
|             # runtests.match_tests is not used (yet) for bisect_cmd -i arg
 | |
|         ])
 | |
|         cmd.extend(runtests.bisect_cmd_args())
 | |
|         cmd.append(test)
 | |
|         print("+", shlex.join(cmd), flush=True)
 | |
| 
 | |
|         flush_std_streams()
 | |
| 
 | |
|         import subprocess
 | |
|         proc = subprocess.run(cmd, timeout=runtests.timeout)
 | |
|         exitcode = proc.returncode
 | |
| 
 | |
|         title = f"{title}: exit code {exitcode}"
 | |
|         print(title)
 | |
|         print("#" * len(title))
 | |
|         print(flush=True)
 | |
| 
 | |
|         if exitcode:
 | |
|             print(f"Bisect failed with exit code {exitcode}")
 | |
|             return False
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     def run_bisect(self, runtests: RunTests) -> None:
 | |
|         tests, _ = self.results.prepare_rerun(clear=False)
 | |
| 
 | |
|         for index, name in enumerate(tests, 1):
 | |
|             if len(tests) > 1:
 | |
|                 progress = f"{index}/{len(tests)}"
 | |
|             else:
 | |
|                 progress = ""
 | |
|             if not self._run_bisect(runtests, name, progress):
 | |
|                 return
 | |
| 
 | |
|     def display_result(self, runtests):
 | |
|         # If running the test suite for PGO then no one cares about results.
 | |
|         if runtests.pgo:
 | |
|             return
 | |
| 
 | |
|         state = self.get_state()
 | |
|         print()
 | |
|         print(f"== Tests result: {state} ==")
 | |
| 
 | |
|         self.results.display_result(runtests.tests,
 | |
|                                     self.quiet, self.print_slowest)
 | |
| 
 | |
|     def run_test(
 | |
|         self, test_name: TestName, runtests: RunTests, tracer: trace.Trace | None
 | |
|     ) -> TestResult:
 | |
|         if tracer is not None:
 | |
|             # If we're tracing code coverage, then we don't exit with status
 | |
|             # if on a false return value from main.
 | |
|             cmd = ('result = run_single_test(test_name, runtests)')
 | |
|             namespace = dict(locals())
 | |
|             tracer.runctx(cmd, globals=globals(), locals=namespace)
 | |
|             result = namespace['result']
 | |
|             result.covered_lines = list(tracer.counts)
 | |
|         else:
 | |
|             result = run_single_test(test_name, runtests)
 | |
| 
 | |
|         self.results.accumulate_result(result, runtests)
 | |
| 
 | |
|         return result
 | |
| 
 | |
|     def run_tests_sequentially(self, runtests) -> None:
 | |
|         if self.coverage:
 | |
|             tracer = trace.Trace(trace=False, count=True)
 | |
|         else:
 | |
|             tracer = None
 | |
| 
 | |
|         save_modules = set(sys.modules)
 | |
| 
 | |
|         jobs = runtests.get_jobs()
 | |
|         if jobs is not None:
 | |
|             tests = count(jobs, 'test')
 | |
|         else:
 | |
|             tests = 'tests'
 | |
|         msg = f"Run {tests} sequentially in a single process"
 | |
|         if runtests.timeout:
 | |
|             msg += " (timeout: %s)" % format_duration(runtests.timeout)
 | |
|         self.log(msg)
 | |
| 
 | |
|         previous_test = None
 | |
|         tests_iter = runtests.iter_tests()
 | |
|         for test_index, test_name in enumerate(tests_iter, 1):
 | |
|             start_time = time.perf_counter()
 | |
| 
 | |
|             text = test_name
 | |
|             if previous_test:
 | |
|                 text = '%s -- %s' % (text, previous_test)
 | |
|             self.logger.display_progress(test_index, text)
 | |
| 
 | |
|             result = self.run_test(test_name, runtests, tracer)
 | |
| 
 | |
|             # Unload the newly imported test modules (best effort finalization)
 | |
|             new_modules = [module for module in sys.modules
 | |
|                            if module not in save_modules and
 | |
|                                 module.startswith(("test.", "test_"))]
 | |
|             for module in new_modules:
 | |
|                 sys.modules.pop(module, None)
 | |
|                 # Remove the attribute of the parent module.
 | |
|                 parent, _, name = module.rpartition('.')
 | |
|                 try:
 | |
|                     delattr(sys.modules[parent], name)
 | |
|                 except (KeyError, AttributeError):
 | |
|                     pass
 | |
| 
 | |
|             if result.must_stop(self.fail_fast, self.fail_env_changed):
 | |
|                 break
 | |
| 
 | |
|             previous_test = str(result)
 | |
|             test_time = time.perf_counter() - start_time
 | |
|             if test_time >= PROGRESS_MIN_TIME:
 | |
|                 previous_test = "%s in %s" % (previous_test, format_duration(test_time))
 | |
|             elif result.state == State.PASSED:
 | |
|                 # be quiet: say nothing if the test passed shortly
 | |
|                 previous_test = None
 | |
| 
 | |
|         if previous_test:
 | |
|             print(previous_test)
 | |
| 
 | |
|     def get_state(self):
 | |
|         state = self.results.get_state(self.fail_env_changed)
 | |
|         if self.first_state:
 | |
|             state = f'{self.first_state} then {state}'
 | |
|         return state
 | |
| 
 | |
|     def _run_tests_mp(self, runtests: RunTests, num_workers: int) -> None:
 | |
|         from .run_workers import RunWorkers
 | |
|         RunWorkers(num_workers, runtests, self.logger, self.results).run()
 | |
| 
 | |
|     def finalize_tests(self, coverage: trace.CoverageResults | None) -> None:
 | |
|         if self.next_single_filename:
 | |
|             if self.next_single_test:
 | |
|                 with open(self.next_single_filename, 'w') as fp:
 | |
|                     fp.write(self.next_single_test + '\n')
 | |
|             else:
 | |
|                 os.unlink(self.next_single_filename)
 | |
| 
 | |
|         if coverage is not None:
 | |
|             # uses a new-in-Python 3.13 keyword argument that mypy doesn't know about yet:
 | |
|             coverage.write_results(show_missing=True, summary=True,  # type: ignore[call-arg]
 | |
|                                    coverdir=self.coverage_dir,
 | |
|                                    ignore_missing_files=True)
 | |
| 
 | |
|         if self.want_run_leaks:
 | |
|             os.system("leaks %d" % os.getpid())
 | |
| 
 | |
|         if self.junit_filename:
 | |
|             self.results.write_junit(self.junit_filename)
 | |
| 
 | |
|     def display_summary(self):
 | |
|         duration = time.perf_counter() - self.logger.start_time
 | |
|         filtered = bool(self.match_tests)
 | |
| 
 | |
|         # Total duration
 | |
|         print()
 | |
|         print("Total duration: %s" % format_duration(duration))
 | |
| 
 | |
|         self.results.display_summary(self.first_runtests, filtered)
 | |
| 
 | |
|         # Result
 | |
|         state = self.get_state()
 | |
|         print(f"Result: {state}")
 | |
| 
 | |
|     def create_run_tests(self, tests: TestTuple):
 | |
|         return RunTests(
 | |
|             tests,
 | |
|             fail_fast=self.fail_fast,
 | |
|             fail_env_changed=self.fail_env_changed,
 | |
|             match_tests=self.match_tests,
 | |
|             match_tests_dict=None,
 | |
|             rerun=False,
 | |
|             forever=self.forever,
 | |
|             pgo=self.pgo,
 | |
|             pgo_extended=self.pgo_extended,
 | |
|             output_on_failure=self.output_on_failure,
 | |
|             timeout=self.timeout,
 | |
|             verbose=self.verbose,
 | |
|             quiet=self.quiet,
 | |
|             hunt_refleak=self.hunt_refleak,
 | |
|             test_dir=self.test_dir,
 | |
|             use_junit=(self.junit_filename is not None),
 | |
|             coverage=self.coverage,
 | |
|             memory_limit=self.memory_limit,
 | |
|             gc_threshold=self.gc_threshold,
 | |
|             use_resources=self.use_resources,
 | |
|             python_cmd=self.python_cmd,
 | |
|             randomize=self.randomize,
 | |
|             random_seed=self.random_seed,
 | |
|         )
 | |
| 
 | |
|     def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
 | |
|         if self.hunt_refleak and self.hunt_refleak.warmups < 3:
 | |
|             msg = ("WARNING: Running tests with --huntrleaks/-R and "
 | |
|                    "less than 3 warmup repetitions can give false positives!")
 | |
|             print(msg, file=sys.stdout, flush=True)
 | |
| 
 | |
|         if self.num_workers < 0:
 | |
|             # Use all CPUs + 2 extra worker processes for tests
 | |
|             # that like to sleep
 | |
|             #
 | |
|             # os.process.cpu_count() is new in Python 3.13;
 | |
|             # mypy doesn't know about it yet
 | |
|             self.num_workers = (os.process_cpu_count() or 1) + 2  # type: ignore[attr-defined]
 | |
| 
 | |
|         # For a partial run, we do not need to clutter the output.
 | |
|         if (self.want_header
 | |
|             or not(self.pgo or self.quiet or self.single_test_run
 | |
|                    or tests or self.cmdline_args)):
 | |
|             display_header(self.use_resources, self.python_cmd)
 | |
| 
 | |
|         print("Using random seed:", self.random_seed)
 | |
| 
 | |
|         runtests = self.create_run_tests(selected)
 | |
|         self.first_runtests = runtests
 | |
|         self.logger.set_tests(runtests)
 | |
| 
 | |
|         setup_process()
 | |
| 
 | |
|         if (runtests.hunt_refleak is not None) and (not self.num_workers):
 | |
|             # gh-109739: WindowsLoadTracker thread interfers with refleak check
 | |
|             use_load_tracker = False
 | |
|         else:
 | |
|             # WindowsLoadTracker is only needed on Windows
 | |
|             use_load_tracker = MS_WINDOWS
 | |
| 
 | |
|         if use_load_tracker:
 | |
|             self.logger.start_load_tracker()
 | |
|         try:
 | |
|             if self.num_workers:
 | |
|                 self._run_tests_mp(runtests, self.num_workers)
 | |
|             else:
 | |
|                 # gh-117783: don't immortalize deferred objects when tracking
 | |
|                 # refleaks. Only relevant for the free-threaded build.
 | |
|                 with suppress_immortalization(runtests.hunt_refleak):
 | |
|                     self.run_tests_sequentially(runtests)
 | |
| 
 | |
|             coverage = self.results.get_coverage_results()
 | |
|             self.display_result(runtests)
 | |
| 
 | |
|             if self.want_rerun and self.results.need_rerun():
 | |
|                 self.rerun_failed_tests(runtests)
 | |
| 
 | |
|             if self.want_bisect and self.results.need_rerun():
 | |
|                 self.run_bisect(runtests)
 | |
|         finally:
 | |
|             if use_load_tracker:
 | |
|                 self.logger.stop_load_tracker()
 | |
| 
 | |
|         self.display_summary()
 | |
|         self.finalize_tests(coverage)
 | |
| 
 | |
|         return self.results.get_exitcode(self.fail_env_changed,
 | |
|                                          self.fail_rerun)
 | |
| 
 | |
|     def run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
 | |
|         os.makedirs(self.tmp_dir, exist_ok=True)
 | |
|         work_dir = get_work_dir(self.tmp_dir)
 | |
| 
 | |
|         # Put a timeout on Python exit
 | |
|         with exit_timeout():
 | |
|             # Run the tests in a context manager that temporarily changes the
 | |
|             # CWD to a temporary and writable directory. If it's not possible
 | |
|             # to create or change the CWD, the original CWD will be used.
 | |
|             # The original CWD is available from os_helper.SAVEDCWD.
 | |
|             with os_helper.temp_cwd(work_dir, quiet=True):
 | |
|                 # When using multiprocessing, worker processes will use
 | |
|                 # work_dir as their parent temporary directory. So when the
 | |
|                 # main process exit, it removes also subdirectories of worker
 | |
|                 # processes.
 | |
|                 return self._run_tests(selected, tests)
 | |
| 
 | |
|     def _add_cross_compile_opts(self, regrtest_opts):
 | |
|         # WASM/WASI buildbot builders pass multiple PYTHON environment
 | |
|         # variables such as PYTHONPATH and _PYTHON_HOSTRUNNER.
 | |
|         keep_environ = bool(self.python_cmd)
 | |
|         environ = None
 | |
| 
 | |
|         # Are we using cross-compilation?
 | |
|         cross_compile = is_cross_compiled()
 | |
| 
 | |
|         # Get HOSTRUNNER
 | |
|         hostrunner = get_host_runner()
 | |
| 
 | |
|         if cross_compile:
 | |
|             # emulate -E, but keep PYTHONPATH + cross compile env vars,
 | |
|             # so test executable can load correct sysconfigdata file.
 | |
|             keep = {
 | |
|                 '_PYTHON_PROJECT_BASE',
 | |
|                 '_PYTHON_HOST_PLATFORM',
 | |
|                 '_PYTHON_SYSCONFIGDATA_NAME',
 | |
|                 'PYTHONPATH'
 | |
|             }
 | |
|             old_environ = os.environ
 | |
|             new_environ = {
 | |
|                 name: value for name, value in os.environ.items()
 | |
|                 if not name.startswith(('PYTHON', '_PYTHON')) or name in keep
 | |
|             }
 | |
|             # Only set environ if at least one variable was removed
 | |
|             if new_environ != old_environ:
 | |
|                 environ = new_environ
 | |
|             keep_environ = True
 | |
| 
 | |
|         if cross_compile and hostrunner:
 | |
|             if self.num_workers == 0 and not self.single_process:
 | |
|                 # For now use only two cores for cross-compiled builds;
 | |
|                 # hostrunner can be expensive.
 | |
|                 regrtest_opts.extend(['-j', '2'])
 | |
| 
 | |
|             # If HOSTRUNNER is set and -p/--python option is not given, then
 | |
|             # use hostrunner to execute python binary for tests.
 | |
|             if not self.python_cmd:
 | |
|                 buildpython = sysconfig.get_config_var("BUILDPYTHON")
 | |
|                 python_cmd = f"{hostrunner} {buildpython}"
 | |
|                 regrtest_opts.extend(["--python", python_cmd])
 | |
|                 keep_environ = True
 | |
| 
 | |
|         return (environ, keep_environ)
 | |
| 
 | |
|     def _add_ci_python_opts(self, python_opts, keep_environ):
 | |
|         # --fast-ci and --slow-ci add options to Python:
 | |
|         # "-u -W default -bb -E"
 | |
| 
 | |
|         # Unbuffered stdout and stderr
 | |
|         if not sys.stdout.write_through:
 | |
|             python_opts.append('-u')
 | |
| 
 | |
|         # Add warnings filter 'default'
 | |
|         if 'default' not in sys.warnoptions:
 | |
|             python_opts.extend(('-W', 'default'))
 | |
| 
 | |
|         # Error on bytes/str comparison
 | |
|         if sys.flags.bytes_warning < 2:
 | |
|             python_opts.append('-bb')
 | |
| 
 | |
|         if not keep_environ:
 | |
|             # Ignore PYTHON* environment variables
 | |
|             if not sys.flags.ignore_environment:
 | |
|                 python_opts.append('-E')
 | |
| 
 | |
|     def _execute_python(self, cmd, environ):
 | |
|         # Make sure that messages before execv() are logged
 | |
|         sys.stdout.flush()
 | |
|         sys.stderr.flush()
 | |
| 
 | |
|         cmd_text = shlex.join(cmd)
 | |
|         try:
 | |
|             print(f"+ {cmd_text}", flush=True)
 | |
| 
 | |
|             if hasattr(os, 'execv') and not MS_WINDOWS:
 | |
|                 os.execv(cmd[0], cmd)
 | |
|                 # On success, execv() do no return.
 | |
|                 # On error, it raises an OSError.
 | |
|             else:
 | |
|                 import subprocess
 | |
|                 with subprocess.Popen(cmd, env=environ) as proc:
 | |
|                     try:
 | |
|                         proc.wait()
 | |
|                     except KeyboardInterrupt:
 | |
|                         # There is no need to call proc.terminate(): on CTRL+C,
 | |
|                         # SIGTERM is also sent to the child process.
 | |
|                         try:
 | |
|                             proc.wait(timeout=EXIT_TIMEOUT)
 | |
|                         except subprocess.TimeoutExpired:
 | |
|                             proc.kill()
 | |
|                             proc.wait()
 | |
|                             sys.exit(EXITCODE_INTERRUPTED)
 | |
| 
 | |
|                 sys.exit(proc.returncode)
 | |
|         except Exception as exc:
 | |
|             print_warning(f"Failed to change Python options: {exc!r}\n"
 | |
|                           f"Command: {cmd_text}")
 | |
|             # continue executing main()
 | |
| 
 | |
|     def _add_python_opts(self):
 | |
|         python_opts = []
 | |
|         regrtest_opts = []
 | |
| 
 | |
|         environ, keep_environ = self._add_cross_compile_opts(regrtest_opts)
 | |
|         if self.ci_mode:
 | |
|             self._add_ci_python_opts(python_opts, keep_environ)
 | |
| 
 | |
|         if (not python_opts) and (not regrtest_opts) and (environ is None):
 | |
|             # Nothing changed: nothing to do
 | |
|             return
 | |
| 
 | |
|         # Create new command line
 | |
|         cmd = list(sys.orig_argv)
 | |
|         if python_opts:
 | |
|             cmd[1:1] = python_opts
 | |
|         if regrtest_opts:
 | |
|             cmd.extend(regrtest_opts)
 | |
|         cmd.append("--dont-add-python-opts")
 | |
| 
 | |
|         self._execute_python(cmd, environ)
 | |
| 
 | |
|     def _init(self):
 | |
|         # Set sys.stdout encoder error handler to backslashreplace,
 | |
|         # similar to sys.stderr error handler, to avoid UnicodeEncodeError
 | |
|         # when printing a traceback or any other non-encodable character.
 | |
|         sys.stdout.reconfigure(errors="backslashreplace")
 | |
| 
 | |
|         if self.junit_filename and not os.path.isabs(self.junit_filename):
 | |
|             self.junit_filename = os.path.abspath(self.junit_filename)
 | |
| 
 | |
|         strip_py_suffix(self.cmdline_args)
 | |
| 
 | |
|         self.tmp_dir = get_temp_dir(self.tmp_dir)
 | |
| 
 | |
|     def main(self, tests: TestList | None = None):
 | |
|         if self.want_add_python_opts:
 | |
|             self._add_python_opts()
 | |
| 
 | |
|         self._init()
 | |
| 
 | |
|         if self.want_cleanup:
 | |
|             cleanup_temp_dir(self.tmp_dir)
 | |
|             sys.exit(0)
 | |
| 
 | |
|         if self.want_wait:
 | |
|             input("Press any key to continue...")
 | |
| 
 | |
|         setup_test_dir(self.test_dir)
 | |
|         selected, tests = self.find_tests(tests)
 | |
| 
 | |
|         exitcode = 0
 | |
|         if self.want_list_tests:
 | |
|             self.list_tests(selected)
 | |
|         elif self.want_list_cases:
 | |
|             list_cases(selected,
 | |
|                        match_tests=self.match_tests,
 | |
|                        test_dir=self.test_dir)
 | |
|         else:
 | |
|             exitcode = self.run_tests(selected, tests)
 | |
| 
 | |
|         sys.exit(exitcode)
 | |
| 
 | |
| 
 | |
| def main(tests=None, _add_python_opts=False, **kwargs):
 | |
|     """Run the Python suite."""
 | |
|     ns = _parse_args(sys.argv[1:], **kwargs)
 | |
|     Regrtest(ns, _add_python_opts=_add_python_opts).main(tests=tests)
 | 
