Skip to content

Commit 2f03413

Browse files
authored
[update_lib] auto-mark original contents recovery (#6960)
1 parent f7b2660 commit 2f03413

File tree

2 files changed

+174
-2
lines changed

2 files changed

+174
-2
lines changed

scripts/update_lib/cmd_auto_mark.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ def auto_mark_file(
745745
# Strip reason-less markers so those tests fail normally and we capture
746746
# their error messages during the test run.
747747
contents = test_path.read_text(encoding="utf-8")
748+
original_contents = contents
748749
contents, stripped_tests = strip_reasonless_expected_failures(contents)
749750
if stripped_tests:
750751
test_path.write_text(contents, encoding="utf-8")
@@ -761,11 +762,21 @@ def auto_mark_file(
761762
and not results.tests
762763
and not results.unexpected_successes
763764
):
765+
# Restore original contents before raising
766+
if stripped_tests:
767+
test_path.write_text(original_contents, encoding="utf-8")
764768
raise TestRunError(
765769
f"Test run failed for {test_name}. "
766770
f"Output: {results.stdout[-500:] if results.stdout else '(no output)'}"
767771
)
768772

773+
# If the run crashed (incomplete), restore original file so that markers
774+
# for tests that never ran are preserved. Only observed results will be
775+
# re-applied below.
776+
if not results.tests_result and stripped_tests:
777+
test_path.write_text(original_contents, encoding="utf-8")
778+
stripped_tests = set()
779+
769780
contents = test_path.read_text(encoding="utf-8")
770781

771782
all_failing_tests, unexpected_successes, error_messages = collect_test_changes(
@@ -863,11 +874,13 @@ def auto_mark_directory(
863874
# Strip reason-less markers from ALL files before running tests so those
864875
# tests fail normally and we capture their error messages.
865876
stripped_per_file: dict[pathlib.Path, set[tuple[str, str]]] = {}
877+
original_per_file: dict[pathlib.Path, str] = {}
866878
for test_file in test_files:
867879
contents = test_file.read_text(encoding="utf-8")
868-
contents, stripped = strip_reasonless_expected_failures(contents)
880+
stripped_contents, stripped = strip_reasonless_expected_failures(contents)
869881
if stripped:
870-
test_file.write_text(contents, encoding="utf-8")
882+
original_per_file[test_file] = contents
883+
test_file.write_text(stripped_contents, encoding="utf-8")
871884
stripped_per_file[test_file] = stripped
872885

873886
test_name = get_test_module_name(test_dir)
@@ -882,11 +895,21 @@ def auto_mark_directory(
882895
and not results.tests
883896
and not results.unexpected_successes
884897
):
898+
# Restore original contents before raising
899+
for fpath, original in original_per_file.items():
900+
fpath.write_text(original, encoding="utf-8")
885901
raise TestRunError(
886902
f"Test run failed for {test_name}. "
887903
f"Output: {results.stdout[-500:] if results.stdout else '(no output)'}"
888904
)
889905

906+
# If the run crashed (incomplete), restore original files so that markers
907+
# for tests that never ran are preserved.
908+
if not results.tests_result and original_per_file:
909+
for fpath, original in original_per_file.items():
910+
fpath.write_text(original, encoding="utf-8")
911+
stripped_per_file.clear()
912+
890913
total_added = 0
891914
total_removed = 0
892915
total_regressions = 0

scripts/update_lib/tests/test_auto_mark.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,5 +932,154 @@ def test_auto_mark_directory_no_results_raises(self):
932932
auto_mark_directory(test_dir, verbose=False)
933933

934934

935+
class TestAutoMarkFileRestoresOnCrash(unittest.TestCase):
936+
"""Stripped markers must be restored when the test runner crashes."""
937+
938+
def test_stripped_markers_restored_when_crash(self):
939+
"""Markers stripped before run must be restored for unobserved tests on crash."""
940+
test_code = f"""\
941+
import unittest
942+
943+
class TestA(unittest.TestCase):
944+
@unittest.expectedFailure # {COMMENT}
945+
def test_foo(self):
946+
pass
947+
948+
@unittest.expectedFailure # {COMMENT}
949+
def test_bar(self):
950+
pass
951+
952+
@unittest.expectedFailure # {COMMENT}
953+
def test_baz(self):
954+
pass
955+
"""
956+
with tempfile.TemporaryDirectory() as tmpdir:
957+
test_file = pathlib.Path(tmpdir) / "test_example.py"
958+
test_file.write_text(test_code)
959+
960+
# Simulate a crashed run that only observed test_foo (failed)
961+
# test_bar and test_baz never ran due to crash
962+
mock_result = TestResult()
963+
mock_result.tests_result = "" # no Tests result line (crash)
964+
mock_result.tests = [
965+
Test(
966+
name="test_foo",
967+
path="test.test_example.TestA.test_foo",
968+
result="fail",
969+
error_message="AssertionError: 1 != 2",
970+
),
971+
]
972+
973+
with mock.patch(
974+
"update_lib.cmd_auto_mark.run_test", return_value=mock_result
975+
):
976+
auto_mark_file(test_file, verbose=False)
977+
978+
contents = test_file.read_text()
979+
# test_bar and test_baz were not observed — their markers must be restored
980+
self.assertIn("def test_bar", contents)
981+
self.assertIn("def test_baz", contents)
982+
# Count expectedFailure markers: all 3 should be present
983+
self.assertEqual(contents.count("expectedFailure"), 3, contents)
984+
985+
def test_stripped_markers_removed_when_complete_run(self):
986+
"""Markers are properly removed when the run completes normally."""
987+
test_code = f"""\
988+
import unittest
989+
990+
class TestA(unittest.TestCase):
991+
@unittest.expectedFailure # {COMMENT}
992+
def test_foo(self):
993+
pass
994+
995+
@unittest.expectedFailure # {COMMENT}
996+
def test_bar(self):
997+
pass
998+
"""
999+
with tempfile.TemporaryDirectory() as tmpdir:
1000+
test_file = pathlib.Path(tmpdir) / "test_example.py"
1001+
test_file.write_text(test_code)
1002+
1003+
# Simulate a complete run where test_foo fails but test_bar passes
1004+
mock_result = TestResult()
1005+
mock_result.tests_result = "FAILURE" # normal completion
1006+
mock_result.tests = [
1007+
Test(
1008+
name="test_foo",
1009+
path="test.test_example.TestA.test_foo",
1010+
result="fail",
1011+
error_message="AssertionError",
1012+
),
1013+
]
1014+
# test_bar passes → shows as unexpected success
1015+
mock_result.unexpected_successes = [
1016+
Test(
1017+
name="test_bar",
1018+
path="test.test_example.TestA.test_bar",
1019+
result="unexpected success",
1020+
),
1021+
]
1022+
1023+
with mock.patch(
1024+
"update_lib.cmd_auto_mark.run_test", return_value=mock_result
1025+
):
1026+
auto_mark_file(test_file, verbose=False)
1027+
1028+
contents = test_file.read_text()
1029+
# test_foo should still have marker (re-added)
1030+
self.assertEqual(contents.count("expectedFailure"), 1, contents)
1031+
self.assertIn("def test_foo", contents)
1032+
1033+
1034+
class TestAutoMarkDirectoryRestoresOnCrash(unittest.TestCase):
1035+
"""Stripped markers must be restored for directory runs that crash."""
1036+
1037+
def test_stripped_markers_restored_when_crash(self):
1038+
test_code = f"""\
1039+
import unittest
1040+
1041+
class TestA(unittest.TestCase):
1042+
@unittest.expectedFailure # {COMMENT}
1043+
def test_foo(self):
1044+
pass
1045+
1046+
@unittest.expectedFailure # {COMMENT}
1047+
def test_bar(self):
1048+
pass
1049+
"""
1050+
with tempfile.TemporaryDirectory() as tmpdir:
1051+
test_dir = pathlib.Path(tmpdir) / "test_example"
1052+
test_dir.mkdir()
1053+
test_file = test_dir / "test_sub.py"
1054+
test_file.write_text(test_code)
1055+
1056+
mock_result = TestResult()
1057+
mock_result.tests_result = "" # crash
1058+
mock_result.tests = [
1059+
Test(
1060+
name="test_foo",
1061+
path="test.test_example.test_sub.TestA.test_foo",
1062+
result="fail",
1063+
),
1064+
]
1065+
1066+
with (
1067+
mock.patch(
1068+
"update_lib.cmd_auto_mark.run_test", return_value=mock_result
1069+
),
1070+
mock.patch(
1071+
"update_lib.cmd_auto_mark.get_test_module_name",
1072+
side_effect=lambda p: (
1073+
"test_example" if p == test_dir else "test_example.test_sub"
1074+
),
1075+
),
1076+
):
1077+
auto_mark_directory(test_dir, verbose=False)
1078+
1079+
contents = test_file.read_text()
1080+
# Both markers must be present (unobserved test_bar restored)
1081+
self.assertEqual(contents.count("expectedFailure"), 2, contents)
1082+
1083+
9351084
if __name__ == "__main__":
9361085
unittest.main()

0 commit comments

Comments
 (0)