Skip to content

Commit 272fe21

Browse files
committed
tests: add a couple of xfail tests that reproduce possible duplicate items emission
1 parent 43194bf commit 272fe21

1 file changed

Lines changed: 77 additions & 0 deletions

File tree

src/cachew/tests/test_cachew.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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])
10471124
def test_bad_annotation(*, tmp_path: Path, throw: bool) -> None:
10481125
"""

0 commit comments

Comments
 (0)