From 26e3877fd3479c515acbd5c099833ebc32937f2e Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 28 Jul 2025 18:10:47 +0000 Subject: [PATCH 1/3] gh-137179: Fix flaky test_history_survive_crash test Kill the REPL subprocess once it prints the output from the command immediately before the `time.sleep()`. --- Lib/test/test_pyrepl/test_pyrepl.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 2588a5ba27d776..bc399078e81dc7 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -51,6 +51,7 @@ def run_repl( cwd: str | None = None, skip: bool = False, timeout: float = SHORT_TIMEOUT, + exit_on_output: str | None = None, ) -> tuple[str, int]: temp_dir = None if cwd is None: @@ -64,6 +65,7 @@ def run_repl( cwd=cwd, skip=skip, timeout=timeout, + exit_on_output=exit_on_output, ) finally: if temp_dir is not None: @@ -78,6 +80,7 @@ def _run_repl( cwd: str, skip: bool, timeout: float, + exit_on_output: str | None, ) -> tuple[str, int]: assert pty master_fd, slave_fd = pty.openpty() @@ -123,6 +126,11 @@ def _run_repl( except OSError: break output.append(data) + if exit_on_output is not None: + output = ["".join(output)] + if exit_on_output in output[0]: + process.kill() + break else: os.close(master_fd) process.kill() @@ -1718,12 +1726,15 @@ def test_history_survive_crash(self): commands = "1\n2\n3\nexit()\n" output, exit_code = self.run_repl(commands, env=env, skip=True) + self.assertEqual(exit_code, 0) - commands = "spam\nimport time\ntime.sleep(1000)\nquit\n" - try: - self.run_repl(commands, env=env, timeout=3) - except AssertionError: - pass + # Run until "0xcafe" is printed (as "51966") and then kill the + # process to simulate a crash. Note that the output also includes + # the echoed input commands. + commands = "spam\nimport time\n0xcafe\ntime.sleep(1000)\nquit\n" + output, exit_code = self.run_repl(commands, env=env, timeout=3, + exit_on_output="51966") + self.assertNotEqual(exit_code, 0) history = pathlib.Path(hfile.name).read_text() self.assertIn("2", history) From ff962db9c314e109cf906b27c76ce853c3d00886 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 29 Jul 2025 13:58:10 +0000 Subject: [PATCH 2/3] Assert 0xcafe in output and use default timeout --- Lib/test/test_pyrepl/test_pyrepl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index bc399078e81dc7..57cd293740f7bf 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1732,7 +1732,7 @@ def test_history_survive_crash(self): # process to simulate a crash. Note that the output also includes # the echoed input commands. commands = "spam\nimport time\n0xcafe\ntime.sleep(1000)\nquit\n" - output, exit_code = self.run_repl(commands, env=env, timeout=3, + output, exit_code = self.run_repl(commands, env=env, exit_on_output="51966") self.assertNotEqual(exit_code, 0) @@ -1740,6 +1740,7 @@ def test_history_survive_crash(self): self.assertIn("2", history) self.assertIn("exit()", history) self.assertIn("spam", history) + self.assertIn("0xcafe", history) self.assertIn("import time", history) self.assertNotIn("sleep", history) self.assertNotIn("quit", history) From 6d0656e07aa3069209e1e4d1e19cb11a9131bb05 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 29 Jul 2025 15:30:31 +0000 Subject: [PATCH 3/3] Remove assert 0xcafe in output --- Lib/test/test_pyrepl/test_pyrepl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 57cd293740f7bf..8e4450fdf99ecd 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1740,8 +1740,10 @@ def test_history_survive_crash(self): self.assertIn("2", history) self.assertIn("exit()", history) self.assertIn("spam", history) - self.assertIn("0xcafe", history) self.assertIn("import time", history) + # History is written after each command's output is printed to the + # console, so depending on how quickly the process is killed, + # the last command may or may not be written to the history file. self.assertNotIn("sleep", history) self.assertNotIn("quit", history)