@@ -1043,6 +1043,83 @@ def orig2():
10431043 assert list (fun ()) == [123 ]
10441044
10451045
1046+ @pytest .mark .xfail (reason = 'cache write errors after yielding currently restart the source iterator' , strict = True )
1047+ def test_defensive_write_error_after_yield_does_not_duplicate (
1048+ tmp_path : Path ,
1049+ restore_settings ,
1050+ ) -> None :
1051+ """
1052+ If cache writing fails after yielding an item, fallback must not restart the source iterator and duplicate emitted items.
1053+ """
1054+ settings .THROW_ON_ERROR = False
1055+
1056+ calls = 0
1057+ first = BB (xx = 1 , yy = 2 )
1058+ second = BB (xx = 3 , yy = 4 )
1059+
1060+ # deliberately specify the wrong type, so cache writing fails after yielding the first item
1061+ @cachew (tmp_path , cls = AA )
1062+ def fun () -> Iterator [BB ]:
1063+ nonlocal calls
1064+ calls += 1
1065+ yield first
1066+ yield second
1067+
1068+ # Current buggy result is [first, first, second]: one item yielded before cache writing fails, then full fallback.
1069+ # Expected result is [first, second], with no restarted source iterator after anything was yielded.
1070+ assert list (fun ()) == [first , second ]
1071+ assert calls == 1
1072+
1073+
1074+ @pytest .mark .xfail (reason = 'cache read errors after yielding currently restart the source iterator' , strict = True )
1075+ def test_defensive_read_error_after_yield_does_not_duplicate (
1076+ tmp_path : Path ,
1077+ restore_settings ,
1078+ ) -> None :
1079+ """
1080+ If cache reading fails after yielding an item, fallback must not restart the source iterator and duplicate emitted items.
1081+ """
1082+ settings .THROW_ON_ERROR = False
1083+
1084+ calls = 0
1085+
1086+ class Item (NamedTuple ):
1087+ value : Any
1088+
1089+ first = Item (value = [1 ])
1090+ second = Item (value = 2 )
1091+
1092+ # First populate the cache with a looser schema.
1093+ @cachew (tmp_path )
1094+ def fun () -> Iterator [Item ]:
1095+ nonlocal calls
1096+ calls += 1
1097+ yield first
1098+ yield second
1099+
1100+ assert list (fun ()) == [first , second ]
1101+ assert calls == 1
1102+
1103+ class Item (NamedTuple ): # type: ignore[no-redef]
1104+ value : list [int ]
1105+
1106+ first = Item (value = [1 ])
1107+ second = Item (value = [2 ])
1108+
1109+ # Then reuse the same function and type names, so the cache hash still matches, but the second cached item no longer loads.
1110+ @cachew (tmp_path ) # type: ignore[no-redef]
1111+ def fun () -> Iterator [Item ]:
1112+ nonlocal calls
1113+ calls += 1
1114+ yield first
1115+ yield second
1116+
1117+ # Current buggy result is [first, first, second]: one item loaded from cache, then full fallback.
1118+ # Expected result is [first, second], with no restarted source iterator after anything was yielded.
1119+ assert list (fun ()) == [first , second ]
1120+ assert calls == 1
1121+
1122+
10461123@pytest .mark .parametrize ('throw' , [False , True ])
10471124def test_bad_annotation (* , tmp_path : Path , throw : bool ) -> None :
10481125 """
0 commit comments