1import os 2import random 3import re 4import shlex 5import sys 6import sysconfig 7import time 8import trace 9 10from test.support import (os_helper, MS_WINDOWS, flush_std_streams, 11 suppress_immortalization) 12 13from .cmdline import _parse_args, Namespace 14from .findtests import findtests, split_test_packages, list_cases 15from .logger import Logger 16from .pgo import setup_pgo_tests 17from .result import State, TestResult 18from .results import TestResults, EXITCODE_INTERRUPTED 19from .runtests import RunTests, HuntRefleak 20from .setup import setup_process, setup_test_dir 21from .single import run_single_test, PROGRESS_MIN_TIME 22from .tsan import setup_tsan_tests 23from .utils import ( 24 StrPath, StrJSON, TestName, TestList, TestTuple, TestFilter, 25 strip_py_suffix, count, format_duration, 26 printlist, get_temp_dir, get_work_dir, exit_timeout, 27 display_header, cleanup_temp_dir, print_warning, 28 is_cross_compiled, get_host_runner, 29 EXIT_TIMEOUT) 30 31 32class Regrtest: 33 """Execute a test suite. 34 35 This also parses command-line options and modifies its behavior 36 accordingly. 37 38 tests -- a list of strings containing test names (optional) 39 testdir -- the directory in which to look for tests (optional) 40 41 Users other than the Python test suite will certainly want to 42 specify testdir; if it's omitted, the directory containing the 43 Python test suite is searched for. 44 45 If the tests argument is omitted, the tests listed on the 46 command-line will be used. If that's empty, too, then all *.py 47 files beginning with test_ will be used. 48 49 The other default arguments (verbose, quiet, exclude, 50 single, randomize, use_resources, trace, coverdir, 51 print_slow, and random_seed) allow programmers calling main() 52 directly to set the values that would normally be set by flags 53 on the command line. 54 """ 55 def __init__(self, ns: Namespace, _add_python_opts: bool = False): 56 # Log verbosity 57 self.verbose: int = int(ns.verbose) 58 self.quiet: bool = ns.quiet 59 self.pgo: bool = ns.pgo 60 self.pgo_extended: bool = ns.pgo_extended 61 self.tsan: bool = ns.tsan 62 63 # Test results 64 self.results: TestResults = TestResults() 65 self.first_state: str | None = None 66 67 # Logger 68 self.logger = Logger(self.results, self.quiet, self.pgo) 69 70 # Actions 71 self.want_header: bool = ns.header 72 self.want_list_tests: bool = ns.list_tests 73 self.want_list_cases: bool = ns.list_cases 74 self.want_wait: bool = ns.wait 75 self.want_cleanup: bool = ns.cleanup 76 self.want_rerun: bool = ns.rerun 77 self.want_run_leaks: bool = ns.runleaks 78 self.want_bisect: bool = ns.bisect 79 80 self.ci_mode: bool = (ns.fast_ci or ns.slow_ci) 81 self.want_add_python_opts: bool = (_add_python_opts 82 and ns._add_python_opts) 83 84 # Select tests 85 self.match_tests: TestFilter = ns.match_tests 86 self.exclude: bool = ns.exclude 87 self.fromfile: StrPath | None = ns.fromfile 88 self.starting_test: TestName | None = ns.start 89 self.cmdline_args: TestList = ns.args 90 91 # Workers 92 self.single_process: bool = ns.single_process 93 if self.single_process or ns.use_mp is None: 94 num_workers = 0 # run sequentially in a single process 95 elif ns.use_mp <= 0: 96 num_workers = -1 # run in parallel, use the number of CPUs 97 else: 98 num_workers = ns.use_mp # run in parallel 99 self.num_workers: int = num_workers 100 self.worker_json: StrJSON | None = ns.worker_json 101 102 # Options to run tests 103 self.fail_fast: bool = ns.failfast 104 self.fail_env_changed: bool = ns.fail_env_changed 105 self.fail_rerun: bool = ns.fail_rerun 106 self.forever: bool = ns.forever 107 self.output_on_failure: bool = ns.verbose3 108 self.timeout: float | None = ns.timeout 109 if ns.huntrleaks: 110 warmups, runs, filename = ns.huntrleaks 111 filename = os.path.abspath(filename) 112 self.hunt_refleak: HuntRefleak | None = HuntRefleak(warmups, runs, filename) 113 else: 114 self.hunt_refleak = None 115 self.test_dir: StrPath | None = ns.testdir 116 self.junit_filename: StrPath | None = ns.xmlpath 117 self.memory_limit: str | None = ns.memlimit 118 self.gc_threshold: int | None = ns.threshold 119 self.use_resources: tuple[str, ...] = tuple(ns.use_resources) 120 if ns.python: 121 self.python_cmd: tuple[str, ...] | None = tuple(ns.python) 122 else: 123 self.python_cmd = None 124 self.coverage: bool = ns.trace 125 self.coverage_dir: StrPath | None = ns.coverdir 126 self._tmp_dir: StrPath | None = ns.tempdir 127 128 # Randomize 129 self.randomize: bool = ns.randomize 130 if ('SOURCE_DATE_EPOCH' in os.environ 131 # don't use the variable if empty 132 and os.environ['SOURCE_DATE_EPOCH'] 133 ): 134 self.randomize = False 135 # SOURCE_DATE_EPOCH should be an integer, but use a string to not 136 # fail if it's not integer. random.seed() accepts a string. 137 # https://reproducible-builds.org/docs/source-date-epoch/ 138 self.random_seed: int | str = os.environ['SOURCE_DATE_EPOCH'] 139 elif ns.random_seed is None: 140 self.random_seed = random.getrandbits(32) 141 else: 142 self.random_seed = ns.random_seed 143 144 # tests 145 self.first_runtests: RunTests | None = None 146 147 # used by --slowest 148 self.print_slowest: bool = ns.print_slow 149 150 # used to display the progress bar "[ 3/100]" 151 self.start_time = time.perf_counter() 152 153 # used by --single 154 self.single_test_run: bool = ns.single 155 self.next_single_test: TestName | None = None 156 self.next_single_filename: StrPath | None = None 157 158 def log(self, line=''): 159 self.logger.log(line) 160 161 def find_tests(self, tests: TestList | None = None) -> tuple[TestTuple, TestList | None]: 162 if tests is None: 163 tests = [] 164 if self.single_test_run: 165 self.next_single_filename = os.path.join(self.tmp_dir, 'pynexttest') 166 try: 167 with open(self.next_single_filename, 'r') as fp: 168 next_test = fp.read().strip() 169 tests = [next_test] 170 except OSError: 171 pass 172 173 if self.fromfile: 174 tests = [] 175 # regex to match 'test_builtin' in line: 176 # '0:00:00 [ 4/400] test_builtin -- test_dict took 1 sec' 177 regex = re.compile(r'\btest_[a-zA-Z0-9_]+\b') 178 with open(os.path.join(os_helper.SAVEDCWD, self.fromfile)) as fp: 179 for line in fp: 180 line = line.split('#', 1)[0] 181 line = line.strip() 182 match = regex.search(line) 183 if match is not None: 184 tests.append(match.group()) 185 186 strip_py_suffix(tests) 187 188 if self.pgo: 189 # add default PGO tests if no tests are specified 190 setup_pgo_tests(self.cmdline_args, self.pgo_extended) 191 192 if self.tsan: 193 setup_tsan_tests(self.cmdline_args) 194 195 exclude_tests = set() 196 if self.exclude: 197 for arg in self.cmdline_args: 198 exclude_tests.add(arg) 199 self.cmdline_args = [] 200 201 alltests = findtests(testdir=self.test_dir, 202 exclude=exclude_tests) 203 204 if not self.fromfile: 205 selected = tests or self.cmdline_args 206 if selected: 207 selected = split_test_packages(selected) 208 else: 209 selected = alltests 210 else: 211 selected = tests 212 213 if self.single_test_run: 214 selected = selected[:1] 215 try: 216 pos = alltests.index(selected[0]) 217 self.next_single_test = alltests[pos + 1] 218 except IndexError: 219 pass 220 221 # Remove all the selected tests that precede start if it's set. 222 if self.starting_test: 223 try: 224 del selected[:selected.index(self.starting_test)] 225 except ValueError: 226 print(f"Cannot find starting test: {self.starting_test}") 227 sys.exit(1) 228 229 random.seed(self.random_seed) 230 if self.randomize: 231 random.shuffle(selected) 232 233 return (tuple(selected), tests) 234 235 @staticmethod 236 def list_tests(tests: TestTuple): 237 for name in tests: 238 print(name) 239 240 def _rerun_failed_tests(self, runtests: RunTests): 241 # Configure the runner to re-run tests 242 if self.num_workers == 0 and not self.single_process: 243 # Always run tests in fresh processes to have more deterministic 244 # initial state. Don't re-run tests in parallel but limit to a 245 # single worker process to have side effects (on the system load 246 # and timings) between tests. 247 self.num_workers = 1 248 249 tests, match_tests_dict = self.results.prepare_rerun() 250 251 # Re-run failed tests 252 runtests = runtests.copy( 253 tests=tests, 254 rerun=True, 255 verbose=True, 256 forever=False, 257 fail_fast=False, 258 match_tests_dict=match_tests_dict, 259 output_on_failure=False) 260 self.logger.set_tests(runtests) 261 262 msg = f"Re-running {len(tests)} failed tests in verbose mode" 263 if not self.single_process: 264 msg = f"{msg} in subprocesses" 265 self.log(msg) 266 self._run_tests_mp(runtests, self.num_workers) 267 else: 268 self.log(msg) 269 self.run_tests_sequentially(runtests) 270 return runtests 271 272 def rerun_failed_tests(self, runtests: RunTests): 273 if self.python_cmd: 274 # Temp patch for https://github.com/python/cpython/issues/94052 275 self.log( 276 "Re-running failed tests is not supported with --python " 277 "host runner option." 278 ) 279 return 280 281 self.first_state = self.get_state() 282 283 print() 284 rerun_runtests = self._rerun_failed_tests(runtests) 285 286 if self.results.bad: 287 print(count(len(self.results.bad), 'test'), "failed again:") 288 printlist(self.results.bad) 289 290 self.display_result(rerun_runtests) 291 292 def _run_bisect(self, runtests: RunTests, test: str, progress: str) -> bool: 293 print() 294 title = f"Bisect {test}" 295 if progress: 296 title = f"{title} ({progress})" 297 print(title) 298 print("#" * len(title)) 299 print() 300 301 cmd = runtests.create_python_cmd() 302 cmd.extend([ 303 "-u", "-m", "test.bisect_cmd", 304 # Limit to 25 iterations (instead of 100) to not abuse CI resources 305 "--max-iter", "25", 306 "-v", 307 # runtests.match_tests is not used (yet) for bisect_cmd -i arg 308 ]) 309 cmd.extend(runtests.bisect_cmd_args()) 310 cmd.append(test) 311 print("+", shlex.join(cmd), flush=True) 312 313 flush_std_streams() 314 315 import subprocess 316 proc = subprocess.run(cmd, timeout=runtests.timeout) 317 exitcode = proc.returncode 318 319 title = f"{title}: exit code {exitcode}" 320 print(title) 321 print("#" * len(title)) 322 print(flush=True) 323 324 if exitcode: 325 print(f"Bisect failed with exit code {exitcode}") 326 return False 327 328 return True 329 330 def run_bisect(self, runtests: RunTests) -> None: 331 tests, _ = self.results.prepare_rerun(clear=False) 332 333 for index, name in enumerate(tests, 1): 334 if len(tests) > 1: 335 progress = f"{index}/{len(tests)}" 336 else: 337 progress = "" 338 if not self._run_bisect(runtests, name, progress): 339 return 340 341 def display_result(self, runtests): 342 # If running the test suite for PGO then no one cares about results. 343 if runtests.pgo: 344 return 345 346 state = self.get_state() 347 print() 348 print(f"== Tests result: {state} ==") 349 350 self.results.display_result(runtests.tests, 351 self.quiet, self.print_slowest) 352 353 def run_test( 354 self, test_name: TestName, runtests: RunTests, tracer: trace.Trace | None 355 ) -> TestResult: 356 if tracer is not None: 357 # If we're tracing code coverage, then we don't exit with status 358 # if on a false return value from main. 359 cmd = ('result = run_single_test(test_name, runtests)') 360 namespace = dict(locals()) 361 tracer.runctx(cmd, globals=globals(), locals=namespace) 362 result = namespace['result'] 363 result.covered_lines = list(tracer.counts) 364 else: 365 result = run_single_test(test_name, runtests) 366 367 self.results.accumulate_result(result, runtests) 368 369 return result 370 371 def run_tests_sequentially(self, runtests) -> None: 372 if self.coverage: 373 tracer = trace.Trace(trace=False, count=True) 374 else: 375 tracer = None 376 377 save_modules = set(sys.modules) 378 379 jobs = runtests.get_jobs() 380 if jobs is not None: 381 tests = count(jobs, 'test') 382 else: 383 tests = 'tests' 384 msg = f"Run {tests} sequentially in a single process" 385 if runtests.timeout: 386 msg += " (timeout: %s)" % format_duration(runtests.timeout) 387 self.log(msg) 388 389 previous_test = None 390 tests_iter = runtests.iter_tests() 391 for test_index, test_name in enumerate(tests_iter, 1): 392 start_time = time.perf_counter() 393 394 text = test_name 395 if previous_test: 396 text = '%s -- %s' % (text, previous_test) 397 self.logger.display_progress(test_index, text) 398 399 result = self.run_test(test_name, runtests, tracer) 400 401 # Unload the newly imported test modules (best effort finalization) 402 new_modules = [module for module in sys.modules 403 if module not in save_modules and 404 module.startswith(("test.", "test_"))] 405 for module in new_modules: 406 sys.modules.pop(module, None) 407 # Remove the attribute of the parent module. 408 parent, _, name = module.rpartition('.') 409 try: 410 delattr(sys.modules[parent], name) 411 except (KeyError, AttributeError): 412 pass 413 414 if result.must_stop(self.fail_fast, self.fail_env_changed): 415 break 416 417 previous_test = str(result) 418 test_time = time.perf_counter() - start_time 419 if test_time >= PROGRESS_MIN_TIME: 420 previous_test = "%s in %s" % (previous_test, format_duration(test_time)) 421 elif result.state == State.PASSED: 422 # be quiet: say nothing if the test passed shortly 423 previous_test = None 424 425 if previous_test: 426 print(previous_test) 427 428 def get_state(self): 429 state = self.results.get_state(self.fail_env_changed) 430 if self.first_state: 431 state = f'{self.first_state} then {state}' 432 return state 433 434 def _run_tests_mp(self, runtests: RunTests, num_workers: int) -> None: 435 from .run_workers import RunWorkers 436 RunWorkers(num_workers, runtests, self.logger, self.results).run() 437 438 def finalize_tests(self, coverage: trace.CoverageResults | None) -> None: 439 if self.next_single_filename: 440 if self.next_single_test: 441 with open(self.next_single_filename, 'w') as fp: 442 fp.write(self.next_single_test + '\n') 443 else: 444 os.unlink(self.next_single_filename) 445 446 if coverage is not None: 447 # uses a new-in-Python 3.13 keyword argument that mypy doesn't know about yet: 448 coverage.write_results(show_missing=True, summary=True, # type: ignore[call-arg] 449 coverdir=self.coverage_dir, 450 ignore_missing_files=True) 451 452 if self.want_run_leaks: 453 os.system("leaks %d" % os.getpid()) 454 455 if self.junit_filename: 456 self.results.write_junit(self.junit_filename) 457 458 def display_summary(self) -> None: 459 if self.first_runtests is None: 460 raise ValueError( 461 "Should never call `display_summary()` before calling `_run_test()`" 462 ) 463 464 duration = time.perf_counter() - self.logger.start_time 465 filtered = bool(self.match_tests) 466 467 # Total duration 468 print() 469 print("Total duration: %s" % format_duration(duration)) 470 471 self.results.display_summary(self.first_runtests, filtered) 472 473 # Result 474 state = self.get_state() 475 print(f"Result: {state}") 476 477 def create_run_tests(self, tests: TestTuple): 478 return RunTests( 479 tests, 480 fail_fast=self.fail_fast, 481 fail_env_changed=self.fail_env_changed, 482 match_tests=self.match_tests, 483 match_tests_dict=None, 484 rerun=False, 485 forever=self.forever, 486 pgo=self.pgo, 487 pgo_extended=self.pgo_extended, 488 output_on_failure=self.output_on_failure, 489 timeout=self.timeout, 490 verbose=self.verbose, 491 quiet=self.quiet, 492 hunt_refleak=self.hunt_refleak, 493 test_dir=self.test_dir, 494 use_junit=(self.junit_filename is not None), 495 coverage=self.coverage, 496 memory_limit=self.memory_limit, 497 gc_threshold=self.gc_threshold, 498 use_resources=self.use_resources, 499 python_cmd=self.python_cmd, 500 randomize=self.randomize, 501 random_seed=self.random_seed, 502 ) 503 504 def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int: 505 if self.hunt_refleak and self.hunt_refleak.warmups < 3: 506 msg = ("WARNING: Running tests with --huntrleaks/-R and " 507 "less than 3 warmup repetitions can give false positives!") 508 print(msg, file=sys.stdout, flush=True) 509 510 if self.num_workers < 0: 511 # Use all CPUs + 2 extra worker processes for tests 512 # that like to sleep 513 # 514 # os.process.cpu_count() is new in Python 3.13; 515 # mypy doesn't know about it yet 516 self.num_workers = (os.process_cpu_count() or 1) + 2 # type: ignore[attr-defined] 517 518 # For a partial run, we do not need to clutter the output. 519 if (self.want_header 520 or not(self.pgo or self.quiet or self.single_test_run 521 or tests or self.cmdline_args)): 522 display_header(self.use_resources, self.python_cmd) 523 524 print("Using random seed:", self.random_seed) 525 526 runtests = self.create_run_tests(selected) 527 self.first_runtests = runtests 528 self.logger.set_tests(runtests) 529 530 setup_process() 531 532 if (runtests.hunt_refleak is not None) and (not self.num_workers): 533 # gh-109739: WindowsLoadTracker thread interferes with refleak check 534 use_load_tracker = False 535 else: 536 # WindowsLoadTracker is only needed on Windows 537 use_load_tracker = MS_WINDOWS 538 539 if use_load_tracker: 540 self.logger.start_load_tracker() 541 try: 542 if self.num_workers: 543 self._run_tests_mp(runtests, self.num_workers) 544 else: 545 # gh-117783: don't immortalize deferred objects when tracking 546 # refleaks. Only releveant for the free-threaded build. 547 with suppress_immortalization(runtests.hunt_refleak): 548 self.run_tests_sequentially(runtests) 549 550 coverage = self.results.get_coverage_results() 551 self.display_result(runtests) 552 553 if self.want_rerun and self.results.need_rerun(): 554 self.rerun_failed_tests(runtests) 555 556 if self.want_bisect and self.results.need_rerun(): 557 self.run_bisect(runtests) 558 finally: 559 if use_load_tracker: 560 self.logger.stop_load_tracker() 561 562 self.display_summary() 563 self.finalize_tests(coverage) 564 565 return self.results.get_exitcode(self.fail_env_changed, 566 self.fail_rerun) 567 568 def run_tests(self, selected: TestTuple, tests: TestList | None) -> int: 569 os.makedirs(self.tmp_dir, exist_ok=True) 570 work_dir = get_work_dir(self.tmp_dir) 571 572 # Put a timeout on Python exit 573 with exit_timeout(): 574 # Run the tests in a context manager that temporarily changes the 575 # CWD to a temporary and writable directory. If it's not possible 576 # to create or change the CWD, the original CWD will be used. 577 # The original CWD is available from os_helper.SAVEDCWD. 578 with os_helper.temp_cwd(work_dir, quiet=True): 579 # When using multiprocessing, worker processes will use 580 # work_dir as their parent temporary directory. So when the 581 # main process exit, it removes also subdirectories of worker 582 # processes. 583 return self._run_tests(selected, tests) 584 585 def _add_cross_compile_opts(self, regrtest_opts): 586 # WASM/WASI buildbot builders pass multiple PYTHON environment 587 # variables such as PYTHONPATH and _PYTHON_HOSTRUNNER. 588 keep_environ = bool(self.python_cmd) 589 environ = None 590 591 # Are we using cross-compilation? 592 cross_compile = is_cross_compiled() 593 594 # Get HOSTRUNNER 595 hostrunner = get_host_runner() 596 597 if cross_compile: 598 # emulate -E, but keep PYTHONPATH + cross compile env vars, 599 # so test executable can load correct sysconfigdata file. 600 keep = { 601 '_PYTHON_PROJECT_BASE', 602 '_PYTHON_HOST_PLATFORM', 603 '_PYTHON_SYSCONFIGDATA_NAME', 604 "_PYTHON_SYSCONFIGDATA_PATH", 605 'PYTHONPATH' 606 } 607 old_environ = os.environ 608 new_environ = { 609 name: value for name, value in os.environ.items() 610 if not name.startswith(('PYTHON', '_PYTHON')) or name in keep 611 } 612 # Only set environ if at least one variable was removed 613 if new_environ != old_environ: 614 environ = new_environ 615 keep_environ = True 616 617 if cross_compile and hostrunner: 618 if self.num_workers == 0 and not self.single_process: 619 # For now use only two cores for cross-compiled builds; 620 # hostrunner can be expensive. 621 regrtest_opts.extend(['-j', '2']) 622 623 # If HOSTRUNNER is set and -p/--python option is not given, then 624 # use hostrunner to execute python binary for tests. 625 if not self.python_cmd: 626 buildpython = sysconfig.get_config_var("BUILDPYTHON") 627 python_cmd = f"{hostrunner} {buildpython}" 628 regrtest_opts.extend(["--python", python_cmd]) 629 keep_environ = True 630 631 return (environ, keep_environ) 632 633 def _add_ci_python_opts(self, python_opts, keep_environ): 634 # --fast-ci and --slow-ci add options to Python: 635 # "-u -W default -bb -E" 636 637 # Unbuffered stdout and stderr 638 if not sys.stdout.write_through: 639 python_opts.append('-u') 640 641 # Add warnings filter 'default' 642 if 'default' not in sys.warnoptions: 643 python_opts.extend(('-W', 'default')) 644 645 # Error on bytes/str comparison 646 if sys.flags.bytes_warning < 2: 647 python_opts.append('-bb') 648 649 if not keep_environ: 650 # Ignore PYTHON* environment variables 651 if not sys.flags.ignore_environment: 652 python_opts.append('-E') 653 654 def _execute_python(self, cmd, environ): 655 # Make sure that messages before execv() are logged 656 sys.stdout.flush() 657 sys.stderr.flush() 658 659 cmd_text = shlex.join(cmd) 660 try: 661 print(f"+ {cmd_text}", flush=True) 662 663 if hasattr(os, 'execv') and not MS_WINDOWS: 664 os.execv(cmd[0], cmd) 665 # On success, execv() do no return. 666 # On error, it raises an OSError. 667 else: 668 import subprocess 669 with subprocess.Popen(cmd, env=environ) as proc: 670 try: 671 proc.wait() 672 except KeyboardInterrupt: 673 # There is no need to call proc.terminate(): on CTRL+C, 674 # SIGTERM is also sent to the child process. 675 try: 676 proc.wait(timeout=EXIT_TIMEOUT) 677 except subprocess.TimeoutExpired: 678 proc.kill() 679 proc.wait() 680 sys.exit(EXITCODE_INTERRUPTED) 681 682 sys.exit(proc.returncode) 683 except Exception as exc: 684 print_warning(f"Failed to change Python options: {exc!r}\n" 685 f"Command: {cmd_text}") 686 # continue executing main() 687 688 def _add_python_opts(self): 689 python_opts = [] 690 regrtest_opts = [] 691 692 environ, keep_environ = self._add_cross_compile_opts(regrtest_opts) 693 if self.ci_mode: 694 self._add_ci_python_opts(python_opts, keep_environ) 695 696 if (not python_opts) and (not regrtest_opts) and (environ is None): 697 # Nothing changed: nothing to do 698 return 699 700 # Create new command line 701 cmd = list(sys.orig_argv) 702 if python_opts: 703 cmd[1:1] = python_opts 704 if regrtest_opts: 705 cmd.extend(regrtest_opts) 706 cmd.append("--dont-add-python-opts") 707 708 self._execute_python(cmd, environ) 709 710 def _init(self): 711 # Set sys.stdout encoder error handler to backslashreplace, 712 # similar to sys.stderr error handler, to avoid UnicodeEncodeError 713 # when printing a traceback or any other non-encodable character. 714 sys.stdout.reconfigure(errors="backslashreplace") 715 716 if self.junit_filename and not os.path.isabs(self.junit_filename): 717 self.junit_filename = os.path.abspath(self.junit_filename) 718 719 strip_py_suffix(self.cmdline_args) 720 721 self._tmp_dir = get_temp_dir(self._tmp_dir) 722 723 @property 724 def tmp_dir(self) -> StrPath: 725 if self._tmp_dir is None: 726 raise ValueError( 727 "Should never use `.tmp_dir` before calling `.main()`" 728 ) 729 return self._tmp_dir 730 731 def main(self, tests: TestList | None = None): 732 if self.want_add_python_opts: 733 self._add_python_opts() 734 735 self._init() 736 737 if self.want_cleanup: 738 cleanup_temp_dir(self.tmp_dir) 739 sys.exit(0) 740 741 if self.want_wait: 742 input("Press any key to continue...") 743 744 setup_test_dir(self.test_dir) 745 selected, tests = self.find_tests(tests) 746 747 exitcode = 0 748 if self.want_list_tests: 749 self.list_tests(selected) 750 elif self.want_list_cases: 751 list_cases(selected, 752 match_tests=self.match_tests, 753 test_dir=self.test_dir) 754 else: 755 exitcode = self.run_tests(selected, tests) 756 757 sys.exit(exitcode) 758 759 760def main(tests=None, _add_python_opts=False, **kwargs): 761 """Run the Python suite.""" 762 ns = _parse_args(sys.argv[1:], **kwargs) 763 Regrtest(ns, _add_python_opts=_add_python_opts).main(tests=tests) 764