From 7ea7ce89cb3f6312f26443a43c6e2f9abd684e7a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 23 Sep 2026 02:12:40 -0400 Subject: [PATCH] [trace_replay] (P7): persist cancelled TCP attempts in results JSON --- tools/trace_replay/run_tcp_matrix.py | 14 +++++++++++++- tools/trace_replay/test_run_tcp_matrix.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/tools/trace_replay/run_tcp_matrix.py b/tools/trace_replay/run_tcp_matrix.py index a07a8ea6..c4926837 100644 --- a/tools/trace_replay/run_tcp_matrix.py +++ b/tools/trace_replay/run_tcp_matrix.py @@ -520,8 +520,20 @@ def main(argv=None): try: run_case(plan, args, row) except KeyboardInterrupt: - row.update(status="cancelled", returncode=130) + # An outer timeout sends SIGTERM, which our handler turns + # into KeyboardInterrupt. Preserve a terminal row in BOTH + # ledgers; otherwise the wrapper sees rc=124 but no + # results.json and cannot distinguish cancellation from a + # driver that never wrote evidence. + row.update(status="cancelled", returncode=130, + completed_at_ns=time.time_ns()) + row.setdefault("seconds", round((row["completed_at_ns"] - row["started_at_ns"]) / 1e9, 3)) atomic_json(checkpoint_path, checkpoint) + selected.append({**row, "resumed": False}) + atomic_json(args.out / "results.json", selected) + print(json.dumps({k: row.get(k) for k in + ("case", "backend", "status", "returncode", "seconds", "timeout_kind", "error")}), + flush=True) return 130 except Exception as error: row.update(status="failed", returncode=1, error=str(error)) diff --git a/tools/trace_replay/test_run_tcp_matrix.py b/tools/trace_replay/test_run_tcp_matrix.py index c1a15e42..2beb57e4 100644 --- a/tools/trace_replay/test_run_tcp_matrix.py +++ b/tools/trace_replay/test_run_tcp_matrix.py @@ -10,6 +10,7 @@ import tempfile import textwrap import types import unittest +from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parent)) import run_tcp_matrix as matrix @@ -153,6 +154,23 @@ class MatrixTests(unittest.TestCase): self.assertEqual(len(self.checkpoint()),2) self.assertTrue(all(row['status']=='failed' and row['returncode']==7 for row in self.checkpoint())) + def test_interrupted_run_keeps_a_terminal_result_and_reruns_on_resume(self): + with mock.patch.object(matrix, 'run_case', side_effect=KeyboardInterrupt): + self.assertEqual(self.run_main(), 130) + checkpoint = self.checkpoint() + self.assertEqual(len(checkpoint), 1) + self.assertEqual(checkpoint[0]['status'], 'cancelled') + self.assertEqual(checkpoint[0]['returncode'], 130) + self.assertGreaterEqual(checkpoint[0]['seconds'], 0) + self.assertIn('completed_at_ns', checkpoint[0]) + result = matrix.read_json(self.out/'results.json') + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['status'], 'cancelled') + self.assertFalse(result[0]['resumed']) + self.assertEqual(self.run_main('--resume'), 0) + self.assertEqual(len(self.checkpoint()), 2) + self.assertEqual(matrix.read_json(self.out/'results.json')[0]['status'], 'passed') + def test_timeout_with_passed_json_is_not_resumed_as_success(self): self.write_inputs('timeout') self.assertEqual(self.run_main('--idle-seconds','.2'),1)