diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index 3e7428c4ad3797..36e4df70192e32 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -252,6 +252,9 @@ def _create_parser(): help='no output unless one or more tests fail') group.add_argument('-o', '--slowest', action='store_true', dest='print_slow', help='print the slowest 10 tests') + group.add_argument('--duration', dest="durationpath", metavar='DURATION_FILE', + help='writes information about the duration ' + 'of the tests to a file') group.add_argument('--header', action='store_true', help='print header with interpreter info') diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 9e7a7d60880091..46cb642292ecf2 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -123,6 +123,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False): self.coverage: bool = ns.trace self.coverage_dir: StrPath | None = ns.coverdir self.tmp_dir: StrPath | None = ns.tempdir + self.duration_filename: StrPath | None = ns.durationpath # Randomize self.randomize: bool = ns.randomize @@ -445,6 +446,9 @@ def finalize_tests(self, coverage: trace.CoverageResults | None) -> None: if self.junit_filename: self.results.write_junit(self.junit_filename) + if self.duration_filename: + self.results.write_duration(self.duration_filename) + def display_summary(self): duration = time.perf_counter() - self.logger.start_time filtered = bool(self.match_tests) @@ -484,6 +488,7 @@ def create_run_tests(self, tests: TestTuple): python_cmd=self.python_cmd, randomize=self.randomize, random_seed=self.random_seed, + with_duration=(self.duration_filename is not None), ) def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int: @@ -691,14 +696,22 @@ def _add_python_opts(self): self._execute_python(cmd, environ) + def normalize_path(self, filename: StrPath) -> StrPath: + if not os.path.isabs(filename): + return os.path.abspath(filename) + return filename + 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) + if self.junit_filename: + self.junit_filename = self.normalize_path(self.junit_filename) + + if self.duration_filename: + self.duration_filename = self.normalize_path(self.duration_filename) strip_py_suffix(self.cmdline_args) diff --git a/Lib/test/libregrtest/results.py b/Lib/test/libregrtest/results.py index 85c82052eae19b..ea8c660f990313 100644 --- a/Lib/test/libregrtest/results.py +++ b/Lib/test/libregrtest/results.py @@ -192,6 +192,12 @@ def write_junit(self, filename: StrPath): for s in ET.tostringlist(root): f.write(s) + def write_duration(self, filename: StrPath): + self.test_times.sort(reverse=True) + with open(filename, 'w', encoding='utf8') as f: + for test_time, test in self.test_times: + f.write(f"{test}: {format_duration(test_time)}\n") + def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool): if print_slowest: self.test_times.sort(reverse=True) diff --git a/Lib/test/libregrtest/runtests.py b/Lib/test/libregrtest/runtests.py index 8e9779524c56a2..1ebc8f1ac0663b 100644 --- a/Lib/test/libregrtest/runtests.py +++ b/Lib/test/libregrtest/runtests.py @@ -100,6 +100,7 @@ class RunTests: python_cmd: tuple[str, ...] | None randomize: bool random_seed: int | str + with_duration: bool def copy(self, **override) -> 'RunTests': state = dataclasses.asdict(self) diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 809abd7e92d65f..648bc04b3097de 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -482,6 +482,11 @@ def test_xml_huntrleaks(self): self.assertEqual(regrtest.hunt_refleak.runs, 12) self.assertIsNone(regrtest.junit_filename) + def test_duration(self): + args = ['--duration', 'output'] + regrtest = self.create_regrtest(args) + self.assertIsNotNone(regrtest.duration_filename) + @dataclasses.dataclass(slots=True) class Rerun: diff --git a/Misc/NEWS.d/next/Tests/2024-05-14-23-14-01.gh-issue-119050.Nvdq9L.rst b/Misc/NEWS.d/next/Tests/2024-05-14-23-14-01.gh-issue-119050.Nvdq9L.rst new file mode 100644 index 00000000000000..4e2a01ff8dd1a6 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2024-05-14-23-14-01.gh-issue-119050.Nvdq9L.rst @@ -0,0 +1,3 @@ +Add a ``--duration`` option to the test suite. +This option is similar to the already existing ``--slowest``, +but writes the duration of each test to a file specified by the user.