1import faulthandler
2import locale
3import os
4import platform
5import random
6import re
7import sys
8import sysconfig
9import tempfile
10import time
11import unittest
12from test.libregrtest.cmdline import _parse_args
13from test.libregrtest.runtest import (
14    findtests, runtest, get_abs_module, is_failed,
15    STDTESTS, NOTTESTS, PROGRESS_MIN_TIME,
16    Passed, Failed, EnvChanged, Skipped, ResourceDenied, Interrupted,
17    ChildError, DidNotRun)
18from test.libregrtest.setup import setup_tests
19from test.libregrtest.pgo import setup_pgo_tests
20from test.libregrtest.utils import removepy, count, format_duration, printlist
21from test import support
22from test.support import os_helper
23from test.support import threading_helper
24
25
26# bpo-38203: Maximum delay in seconds to exit Python (call Py_Finalize()).
27# Used to protect against threading._shutdown() hang.
28# Must be smaller than buildbot "1200 seconds without output" limit.
29EXIT_TIMEOUT = 120.0
30
31# gh-90681: When rerunning tests, we might need to rerun the whole
32# class or module suite if some its life-cycle hooks fail.
33# Test level hooks are not affected.
34_TEST_LIFECYCLE_HOOKS = frozenset((
35    'setUpClass', 'tearDownClass',
36    'setUpModule', 'tearDownModule',
37))
38
39EXITCODE_BAD_TEST = 2
40EXITCODE_INTERRUPTED = 130
41EXITCODE_ENV_CHANGED = 3
42EXITCODE_NO_TESTS_RAN = 4
43
44
45class Regrtest:
46    """Execute a test suite.
47
48    This also parses command-line options and modifies its behavior
49    accordingly.
50
51    tests -- a list of strings containing test names (optional)
52    testdir -- the directory in which to look for tests (optional)
53
54    Users other than the Python test suite will certainly want to
55    specify testdir; if it's omitted, the directory containing the
56    Python test suite is searched for.
57
58    If the tests argument is omitted, the tests listed on the
59    command-line will be used.  If that's empty, too, then all *.py
60    files beginning with test_ will be used.
61
62    The other default arguments (verbose, quiet, exclude,
63    single, randomize, use_resources, trace, coverdir,
64    print_slow, and random_seed) allow programmers calling main()
65    directly to set the values that would normally be set by flags
66    on the command line.
67    """
68    def __init__(self):
69        # Namespace of command line options
70        self.ns = None
71
72        # tests
73        self.tests = []
74        self.selected = []
75
76        # test results
77        self.good = []
78        self.bad = []
79        self.skipped = []
80        self.resource_denieds = []
81        self.environment_changed = []
82        self.run_no_tests = []
83        self.need_rerun = []
84        self.rerun = []
85        self.first_result = None
86        self.interrupted = False
87
88        # used by --slow
89        self.test_times = []
90
91        # used by --coverage, trace.Trace instance
92        self.tracer = None
93
94        # used to display the progress bar "[ 3/100]"
95        self.start_time = time.monotonic()
96        self.test_count = ''
97        self.test_count_width = 1
98
99        # used by --single
100        self.next_single_test = None
101        self.next_single_filename = None
102
103        # used by --junit-xml
104        self.testsuite_xml = None
105
106        # misc
107        self.win_load_tracker = None
108        self.tmp_dir = None
109        self.worker_test_name = None
110
111    def get_executed(self):
112        return (set(self.good) | set(self.bad) | set(self.skipped)
113                | set(self.resource_denieds) | set(self.environment_changed)
114                | set(self.run_no_tests))
115
116    def accumulate_result(self, result, rerun=False):
117        test_name = result.name
118
119        if not isinstance(result, (ChildError, Interrupted)) and not rerun:
120            self.test_times.append((result.duration_sec, test_name))
121
122        if isinstance(result, Passed):
123            self.good.append(test_name)
124        elif isinstance(result, ResourceDenied):
125            self.skipped.append(test_name)
126            self.resource_denieds.append(test_name)
127        elif isinstance(result, Skipped):
128            self.skipped.append(test_name)
129        elif isinstance(result, EnvChanged):
130            self.environment_changed.append(test_name)
131        elif isinstance(result, Failed):
132            if not rerun:
133                self.bad.append(test_name)
134                self.need_rerun.append(result)
135        elif isinstance(result, DidNotRun):
136            self.run_no_tests.append(test_name)
137        elif isinstance(result, Interrupted):
138            self.interrupted = True
139        else:
140            raise ValueError("invalid test result: %r" % result)
141
142        if rerun and not isinstance(result, (Failed, Interrupted)):
143            self.bad.remove(test_name)
144
145        xml_data = result.xml_data
146        if xml_data:
147            import xml.etree.ElementTree as ET
148            for e in xml_data:
149                try:
150                    self.testsuite_xml.append(ET.fromstring(e))
151                except ET.ParseError:
152                    print(xml_data, file=sys.__stderr__)
153                    raise
154
155    def log(self, line=''):
156        empty = not line
157
158        # add the system load prefix: "load avg: 1.80 "
159        load_avg = self.getloadavg()
160        if load_avg is not None:
161            line = f"load avg: {load_avg:.2f} {line}"
162
163        # add the timestamp prefix:  "0:01:05 "
164        test_time = time.monotonic() - self.start_time
165
166        mins, secs = divmod(int(test_time), 60)
167        hours, mins = divmod(mins, 60)
168        test_time = "%d:%02d:%02d" % (hours, mins, secs)
169
170        line = f"{test_time} {line}"
171        if empty:
172            line = line[:-1]
173
174        print(line, flush=True)
175
176    def display_progress(self, test_index, text):
177        if self.ns.quiet:
178            return
179
180        # "[ 51/405/1] test_tcl passed"
181        line = f"{test_index:{self.test_count_width}}{self.test_count}"
182        fails = len(self.bad) + len(self.environment_changed)
183        if fails and not self.ns.pgo:
184            line = f"{line}/{fails}"
185        self.log(f"[{line}] {text}")
186
187    def parse_args(self, kwargs):
188        ns = _parse_args(sys.argv[1:], **kwargs)
189
190        if ns.xmlpath:
191            support.junit_xml_list = self.testsuite_xml = []
192
193        worker_args = ns.worker_args
194        if worker_args is not None:
195            from test.libregrtest.runtest_mp import parse_worker_args
196            ns, test_name = parse_worker_args(ns.worker_args)
197            ns.worker_args = worker_args
198            self.worker_test_name = test_name
199
200        # Strip .py extensions.
201        removepy(ns.args)
202
203        if ns.huntrleaks:
204            warmup, repetitions, _ = ns.huntrleaks
205            if warmup < 1 or repetitions < 1:
206                msg = ("Invalid values for the --huntrleaks/-R parameters. The "
207                       "number of warmups and repetitions must be at least 1 "
208                       "each (1:1).")
209                print(msg, file=sys.stderr, flush=True)
210                sys.exit(2)
211
212        if ns.tempdir:
213            ns.tempdir = os.path.expanduser(ns.tempdir)
214
215        self.ns = ns
216
217    def find_tests(self, tests):
218        self.tests = tests
219
220        if self.ns.single:
221            self.next_single_filename = os.path.join(self.tmp_dir, 'pynexttest')
222            try:
223                with open(self.next_single_filename, 'r') as fp:
224                    next_test = fp.read().strip()
225                    self.tests = [next_test]
226            except OSError:
227                pass
228
229        if self.ns.fromfile:
230            self.tests = []
231            # regex to match 'test_builtin' in line:
232            # '0:00:00 [  4/400] test_builtin -- test_dict took 1 sec'
233            regex = re.compile(r'\btest_[a-zA-Z0-9_]+\b')
234            with open(os.path.join(os_helper.SAVEDCWD, self.ns.fromfile)) as fp:
235                for line in fp:
236                    line = line.split('#', 1)[0]
237                    line = line.strip()
238                    match = regex.search(line)
239                    if match is not None:
240                        self.tests.append(match.group())
241
242        removepy(self.tests)
243
244        if self.ns.pgo:
245            # add default PGO tests if no tests are specified
246            setup_pgo_tests(self.ns)
247
248        stdtests = STDTESTS[:]
249        nottests = NOTTESTS.copy()
250        if self.ns.exclude:
251            for arg in self.ns.args:
252                if arg in stdtests:
253                    stdtests.remove(arg)
254                nottests.add(arg)
255            self.ns.args = []
256
257        # if testdir is set, then we are not running the python tests suite, so
258        # don't add default tests to be executed or skipped (pass empty values)
259        if self.ns.testdir:
260            alltests = findtests(self.ns.testdir, list(), set())
261        else:
262            alltests = findtests(self.ns.testdir, stdtests, nottests)
263
264        if not self.ns.fromfile:
265            self.selected = self.tests or self.ns.args or alltests
266        else:
267            self.selected = self.tests
268        if self.ns.single:
269            self.selected = self.selected[:1]
270            try:
271                pos = alltests.index(self.selected[0])
272                self.next_single_test = alltests[pos + 1]
273            except IndexError:
274                pass
275
276        # Remove all the selected tests that precede start if it's set.
277        if self.ns.start:
278            try:
279                del self.selected[:self.selected.index(self.ns.start)]
280            except ValueError:
281                print("Couldn't find starting test (%s), using all tests"
282                      % self.ns.start, file=sys.stderr)
283
284        if self.ns.randomize:
285            if self.ns.random_seed is None:
286                self.ns.random_seed = random.randrange(10000000)
287            random.seed(self.ns.random_seed)
288            random.shuffle(self.selected)
289
290    def list_tests(self):
291        for name in self.selected:
292            print(name)
293
294    def _list_cases(self, suite):
295        for test in suite:
296            if isinstance(test, unittest.loader._FailedTest):
297                continue
298            if isinstance(test, unittest.TestSuite):
299                self._list_cases(test)
300            elif isinstance(test, unittest.TestCase):
301                if support.match_test(test):
302                    print(test.id())
303
304    def list_cases(self):
305        support.verbose = False
306        support.set_match_tests(self.ns.match_tests, self.ns.ignore_tests)
307
308        for test_name in self.selected:
309            abstest = get_abs_module(self.ns, test_name)
310            try:
311                suite = unittest.defaultTestLoader.loadTestsFromName(abstest)
312                self._list_cases(suite)
313            except unittest.SkipTest:
314                self.skipped.append(test_name)
315
316        if self.skipped:
317            print(file=sys.stderr)
318            print(count(len(self.skipped), "test"), "skipped:", file=sys.stderr)
319            printlist(self.skipped, file=sys.stderr)
320
321    def rerun_failed_tests(self):
322        self.log()
323
324        if self.ns.python:
325            # Temp patch for https://github.com/python/cpython/issues/94052
326            self.log(
327                "Re-running failed tests is not supported with --python "
328                "host runner option."
329            )
330            return
331
332        self.ns.verbose = True
333        self.ns.failfast = False
334        self.ns.verbose3 = False
335
336        self.first_result = self.get_tests_result()
337
338        self.log("Re-running failed tests in verbose mode")
339        rerun_list = list(self.need_rerun)
340        self.need_rerun.clear()
341        for result in rerun_list:
342            test_name = result.name
343            self.rerun.append(test_name)
344
345            errors = result.errors or []
346            failures = result.failures or []
347            error_names = [
348                self.normalize_test_name(test_full_name, is_error=True)
349                for (test_full_name, *_) in errors]
350            failure_names = [
351                self.normalize_test_name(test_full_name)
352                for (test_full_name, *_) in failures]
353            self.ns.verbose = True
354            orig_match_tests = self.ns.match_tests
355            if errors or failures:
356                if self.ns.match_tests is None:
357                    self.ns.match_tests = []
358                self.ns.match_tests.extend(error_names)
359                self.ns.match_tests.extend(failure_names)
360                matching = "matching: " + ", ".join(self.ns.match_tests)
361                self.log(f"Re-running {test_name} in verbose mode ({matching})")
362            else:
363                self.log(f"Re-running {test_name} in verbose mode")
364            result = runtest(self.ns, test_name)
365            self.ns.match_tests = orig_match_tests
366
367            self.accumulate_result(result, rerun=True)
368
369            if isinstance(result, Interrupted):
370                break
371
372        if self.bad:
373            print(count(len(self.bad), 'test'), "failed again:")
374            printlist(self.bad)
375
376        self.display_result()
377
378    def normalize_test_name(self, test_full_name, *, is_error=False):
379        short_name = test_full_name.split(" ")[0]
380        if is_error and short_name in _TEST_LIFECYCLE_HOOKS:
381            # This means that we have a failure in a life-cycle hook,
382            # we need to rerun the whole module or class suite.
383            # Basically the error looks like this:
384            #    ERROR: setUpClass (test.test_reg_ex.RegTest)
385            # or
386            #    ERROR: setUpModule (test.test_reg_ex)
387            # So, we need to parse the class / module name.
388            lpar = test_full_name.index('(')
389            rpar = test_full_name.index(')')
390            return test_full_name[lpar + 1: rpar].split('.')[-1]
391        return short_name
392
393    def display_result(self):
394        # If running the test suite for PGO then no one cares about results.
395        if self.ns.pgo:
396            return
397
398        print()
399        print("== Tests result: %s ==" % self.get_tests_result())
400
401        if self.interrupted:
402            print("Test suite interrupted by signal SIGINT.")
403
404        omitted = set(self.selected) - self.get_executed()
405        if omitted:
406            print()
407            print(count(len(omitted), "test"), "omitted:")
408            printlist(omitted)
409
410        if self.good and not self.ns.quiet:
411            print()
412            if (not self.bad
413                and not self.skipped
414                and not self.interrupted
415                and len(self.good) > 1):
416                print("All", end=' ')
417            print(count(len(self.good), "test"), "OK.")
418
419        if self.ns.print_slow:
420            self.test_times.sort(reverse=True)
421            print()
422            print("10 slowest tests:")
423            for test_time, test in self.test_times[:10]:
424                print("- %s: %s" % (test, format_duration(test_time)))
425
426        if self.bad:
427            print()
428            print(count(len(self.bad), "test"), "failed:")
429            printlist(self.bad)
430
431        if self.environment_changed:
432            print()
433            print("{} altered the execution environment:".format(
434                     count(len(self.environment_changed), "test")))
435            printlist(self.environment_changed)
436
437        if self.skipped and not self.ns.quiet:
438            print()
439            print(count(len(self.skipped), "test"), "skipped:")
440            printlist(self.skipped)
441
442        if self.rerun:
443            print()
444            print("%s:" % count(len(self.rerun), "re-run test"))
445            printlist(self.rerun)
446
447        if self.run_no_tests:
448            print()
449            print(count(len(self.run_no_tests), "test"), "run no tests:")
450            printlist(self.run_no_tests)
451
452    def run_tests_sequential(self):
453        if self.ns.trace:
454            import trace
455            self.tracer = trace.Trace(trace=False, count=True)
456
457        save_modules = sys.modules.keys()
458
459        msg = "Run tests sequentially"
460        if self.ns.timeout:
461            msg += " (timeout: %s)" % format_duration(self.ns.timeout)
462        self.log(msg)
463
464        previous_test = None
465        for test_index, test_name in enumerate(self.tests, 1):
466            start_time = time.monotonic()
467
468            text = test_name
469            if previous_test:
470                text = '%s -- %s' % (text, previous_test)
471            self.display_progress(test_index, text)
472
473            if self.tracer:
474                # If we're tracing code coverage, then we don't exit with status
475                # if on a false return value from main.
476                cmd = ('result = runtest(self.ns, test_name); '
477                       'self.accumulate_result(result)')
478                ns = dict(locals())
479                self.tracer.runctx(cmd, globals=globals(), locals=ns)
480                result = ns['result']
481            else:
482                result = runtest(self.ns, test_name)
483                self.accumulate_result(result)
484
485            if isinstance(result, Interrupted):
486                break
487
488            previous_test = str(result)
489            test_time = time.monotonic() - start_time
490            if test_time >= PROGRESS_MIN_TIME:
491                previous_test = "%s in %s" % (previous_test, format_duration(test_time))
492            elif isinstance(result, Passed):
493                # be quiet: say nothing if the test passed shortly
494                previous_test = None
495
496            # Unload the newly imported modules (best effort finalization)
497            for module in sys.modules.keys():
498                if module not in save_modules and module.startswith("test."):
499                    support.unload(module)
500
501            if self.ns.failfast and is_failed(result, self.ns):
502                break
503
504        if previous_test:
505            print(previous_test)
506
507    def _test_forever(self, tests):
508        while True:
509            for test_name in tests:
510                yield test_name
511                if self.bad:
512                    return
513                if self.ns.fail_env_changed and self.environment_changed:
514                    return
515
516    def display_header(self):
517        # Print basic platform information
518        print("==", platform.python_implementation(), *sys.version.split())
519        print("==", platform.platform(aliased=True),
520                      "%s-endian" % sys.byteorder)
521        print("== cwd:", os.getcwd())
522        cpu_count = os.cpu_count()
523        if cpu_count:
524            print("== CPU count:", cpu_count)
525        print("== encodings: locale=%s, FS=%s"
526              % (locale.getencoding(), sys.getfilesystemencoding()))
527
528    def get_tests_result(self):
529        result = []
530        if self.bad:
531            result.append("FAILURE")
532        elif self.ns.fail_env_changed and self.environment_changed:
533            result.append("ENV CHANGED")
534        elif not any((self.good, self.bad, self.skipped, self.interrupted,
535            self.environment_changed)):
536            result.append("NO TEST RUN")
537
538        if self.interrupted:
539            result.append("INTERRUPTED")
540
541        if not result:
542            result.append("SUCCESS")
543
544        result = ', '.join(result)
545        if self.first_result:
546            result = '%s then %s' % (self.first_result, result)
547        return result
548
549    def run_tests(self):
550        # For a partial run, we do not need to clutter the output.
551        if (self.ns.header
552            or not(self.ns.pgo or self.ns.quiet or self.ns.single
553                   or self.tests or self.ns.args)):
554            self.display_header()
555
556        if self.ns.huntrleaks:
557            warmup, repetitions, _ = self.ns.huntrleaks
558            if warmup < 3:
559                msg = ("WARNING: Running tests with --huntrleaks/-R and less than "
560                        "3 warmup repetitions can give false positives!")
561                print(msg, file=sys.stdout, flush=True)
562
563        if self.ns.randomize:
564            print("Using random seed", self.ns.random_seed)
565
566        if self.ns.forever:
567            self.tests = self._test_forever(list(self.selected))
568            self.test_count = ''
569            self.test_count_width = 3
570        else:
571            self.tests = iter(self.selected)
572            self.test_count = '/{}'.format(len(self.selected))
573            self.test_count_width = len(self.test_count) - 1
574
575        if self.ns.use_mp:
576            from test.libregrtest.runtest_mp import run_tests_multiprocess
577            # If we're on windows and this is the parent runner (not a worker),
578            # track the load average.
579            if sys.platform == 'win32' and self.worker_test_name is None:
580                from test.libregrtest.win_utils import WindowsLoadTracker
581
582                try:
583                    self.win_load_tracker = WindowsLoadTracker()
584                except PermissionError as error:
585                    # Standard accounts may not have access to the performance
586                    # counters.
587                    print(f'Failed to create WindowsLoadTracker: {error}')
588
589            try:
590                run_tests_multiprocess(self)
591            finally:
592                if self.win_load_tracker is not None:
593                    self.win_load_tracker.close()
594                    self.win_load_tracker = None
595        else:
596            self.run_tests_sequential()
597
598    def finalize(self):
599        if self.next_single_filename:
600            if self.next_single_test:
601                with open(self.next_single_filename, 'w') as fp:
602                    fp.write(self.next_single_test + '\n')
603            else:
604                os.unlink(self.next_single_filename)
605
606        if self.tracer:
607            r = self.tracer.results()
608            r.write_results(show_missing=True, summary=True,
609                            coverdir=self.ns.coverdir)
610
611        print()
612        duration = time.monotonic() - self.start_time
613        print("Total duration: %s" % format_duration(duration))
614        print("Tests result: %s" % self.get_tests_result())
615
616        if self.ns.runleaks:
617            os.system("leaks %d" % os.getpid())
618
619    def save_xml_result(self):
620        if not self.ns.xmlpath and not self.testsuite_xml:
621            return
622
623        import xml.etree.ElementTree as ET
624        root = ET.Element("testsuites")
625
626        # Manually count the totals for the overall summary
627        totals = {'tests': 0, 'errors': 0, 'failures': 0}
628        for suite in self.testsuite_xml:
629            root.append(suite)
630            for k in totals:
631                try:
632                    totals[k] += int(suite.get(k, 0))
633                except ValueError:
634                    pass
635
636        for k, v in totals.items():
637            root.set(k, str(v))
638
639        xmlpath = os.path.join(os_helper.SAVEDCWD, self.ns.xmlpath)
640        with open(xmlpath, 'wb') as f:
641            for s in ET.tostringlist(root):
642                f.write(s)
643
644    def fix_umask(self):
645        if support.is_emscripten:
646            # Emscripten has default umask 0o777, which breaks some tests.
647            # see https://github.com/emscripten-core/emscripten/issues/17269
648            old_mask = os.umask(0)
649            if old_mask == 0o777:
650                os.umask(0o027)
651            else:
652                os.umask(old_mask)
653
654    def set_temp_dir(self):
655        if self.ns.tempdir:
656            self.tmp_dir = self.ns.tempdir
657
658        if not self.tmp_dir:
659            # When tests are run from the Python build directory, it is best practice
660            # to keep the test files in a subfolder.  This eases the cleanup of leftover
661            # files using the "make distclean" command.
662            if sysconfig.is_python_build():
663                self.tmp_dir = sysconfig.get_config_var('abs_builddir')
664                if self.tmp_dir is None:
665                    # bpo-30284: On Windows, only srcdir is available. Using
666                    # abs_builddir mostly matters on UNIX when building Python
667                    # out of the source tree, especially when the source tree
668                    # is read only.
669                    self.tmp_dir = sysconfig.get_config_var('srcdir')
670                self.tmp_dir = os.path.join(self.tmp_dir, 'build')
671            else:
672                self.tmp_dir = tempfile.gettempdir()
673
674        self.tmp_dir = os.path.abspath(self.tmp_dir)
675
676    def create_temp_dir(self):
677        os.makedirs(self.tmp_dir, exist_ok=True)
678
679        # Define a writable temp dir that will be used as cwd while running
680        # the tests. The name of the dir includes the pid to allow parallel
681        # testing (see the -j option).
682        # Emscripten and WASI have stubbed getpid(), Emscripten has only
683        # milisecond clock resolution. Use randint() instead.
684        if sys.platform in {"emscripten", "wasi"}:
685            nounce = random.randint(0, 1_000_000)
686        else:
687            nounce = os.getpid()
688        if self.worker_test_name is not None:
689            test_cwd = 'test_python_worker_{}'.format(nounce)
690        else:
691            test_cwd = 'test_python_{}'.format(nounce)
692        test_cwd += os_helper.FS_NONASCII
693        test_cwd = os.path.join(self.tmp_dir, test_cwd)
694        return test_cwd
695
696    def cleanup(self):
697        import glob
698
699        path = os.path.join(glob.escape(self.tmp_dir), 'test_python_*')
700        print("Cleanup %s directory" % self.tmp_dir)
701        for name in glob.glob(path):
702            if os.path.isdir(name):
703                print("Remove directory: %s" % name)
704                os_helper.rmtree(name)
705            else:
706                print("Remove file: %s" % name)
707                os_helper.unlink(name)
708
709    def main(self, tests=None, **kwargs):
710        self.parse_args(kwargs)
711
712        self.set_temp_dir()
713
714        self.fix_umask()
715
716        if self.ns.cleanup:
717            self.cleanup()
718            sys.exit(0)
719
720        test_cwd = self.create_temp_dir()
721
722        try:
723            # Run the tests in a context manager that temporarily changes the CWD
724            # to a temporary and writable directory. If it's not possible to
725            # create or change the CWD, the original CWD will be used.
726            # The original CWD is available from os_helper.SAVEDCWD.
727            with os_helper.temp_cwd(test_cwd, quiet=True):
728                # When using multiprocessing, worker processes will use test_cwd
729                # as their parent temporary directory. So when the main process
730                # exit, it removes also subdirectories of worker processes.
731                self.ns.tempdir = test_cwd
732
733                self._main(tests, kwargs)
734        except SystemExit as exc:
735            # bpo-38203: Python can hang at exit in Py_Finalize(), especially
736            # on threading._shutdown() call: put a timeout
737            if threading_helper.can_start_thread:
738                faulthandler.dump_traceback_later(EXIT_TIMEOUT, exit=True)
739
740            sys.exit(exc.code)
741
742    def getloadavg(self):
743        if self.win_load_tracker is not None:
744            return self.win_load_tracker.getloadavg()
745
746        if hasattr(os, 'getloadavg'):
747            return os.getloadavg()[0]
748
749        return None
750
751    def _main(self, tests, kwargs):
752        if self.worker_test_name is not None:
753            from test.libregrtest.runtest_mp import run_tests_worker
754            run_tests_worker(self.ns, self.worker_test_name)
755
756        if self.ns.wait:
757            input("Press any key to continue...")
758
759        support.PGO = self.ns.pgo
760        support.PGO_EXTENDED = self.ns.pgo_extended
761
762        setup_tests(self.ns)
763
764        self.find_tests(tests)
765
766        if self.ns.list_tests:
767            self.list_tests()
768            sys.exit(0)
769
770        if self.ns.list_cases:
771            self.list_cases()
772            sys.exit(0)
773
774        self.run_tests()
775        self.display_result()
776
777        if self.ns.verbose2 and self.bad:
778            self.rerun_failed_tests()
779
780        self.finalize()
781
782        self.save_xml_result()
783
784        if self.bad:
785            sys.exit(2)
786        if self.interrupted:
787            sys.exit(130)
788        if self.ns.fail_env_changed and self.environment_changed:
789            sys.exit(3)
790        sys.exit(0)
791
792
793def main(tests=None, **kwargs):
794    """Run the Python suite."""
795    Regrtest().main(tests=tests, **kwargs)
796