Skip to content

Commit 22179f0

Browse files
committed
support skipping last node by index if list
1 parent ec575d7 commit 22179f0

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

localstack_snapshot/snapshots/prototype.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
SNAPSHOT_LOGGER = logging.getLogger(__name__)
2626
SNAPSHOT_LOGGER.setLevel(logging.DEBUG if os.environ.get("DEBUG_SNAPSHOT") else logging.WARNING)
2727

28+
_PLACEHOLDER_VALUE = "$__marker__$"
29+
2830

2931
class SnapshotMatchResult:
3032
def __init__(self, a: dict, b: dict, key: str = ""):
@@ -218,7 +220,7 @@ def _assert_all(
218220
self.skip_verification_paths = skip_verification_paths or []
219221
if skip_verification_paths:
220222
SNAPSHOT_LOGGER.warning(
221-
f"Snapshot verification disabled for paths: {skip_verification_paths}"
223+
"Snapshot verification disabled for paths: %s", skip_verification_paths
222224
)
223225

224226
if self.update:
@@ -306,7 +308,7 @@ def _transform(self, tmp: dict) -> dict:
306308
try:
307309
replaced_tmp[key] = json.loads(dumped_value)
308310
except JSONDecodeError:
309-
SNAPSHOT_LOGGER.error(f"could not decode json-string:\n{tmp}")
311+
SNAPSHOT_LOGGER.error("could not decode json-string:\n%s", tmp)
310312
return {}
311313

312314
return replaced_tmp
@@ -365,6 +367,21 @@ def build_full_path_nodes(field_match: DatumInContext):
365367

366368
return full_path_nodes[::-1][1:] # reverse the list and remove Root()/$
367369

370+
def _remove_placeholder(_tmp):
371+
"""Traverse the object and remove any values in a list that would be equal to the placeholder"""
372+
if isinstance(_tmp, dict):
373+
for k, v in _tmp.items():
374+
if isinstance(v, dict):
375+
_remove_placeholder(v)
376+
elif isinstance(v, list):
377+
_tmp[k] = _remove_placeholder(v)
378+
elif isinstance(_tmp, list):
379+
return [_remove_placeholder(item) for item in _tmp if item != _PLACEHOLDER_VALUE]
380+
381+
return _tmp
382+
383+
has_placeholder = False
384+
368385
for path in self.skip_verification_paths:
369386
matches = parse(path).find(tmp) or []
370387
for m in matches:
@@ -378,7 +395,24 @@ def build_full_path_nodes(field_match: DatumInContext):
378395
helper = helper.get(p, None)
379396
if not helper:
380397
continue
398+
381399
if (
382400
isinstance(helper, dict) and full_path[-1] in helper.keys()
383401
): # might have been deleted already
384402
del helper[full_path[-1]]
403+
elif isinstance(helper, list):
404+
try:
405+
index = int(full_path[-1].lstrip("[").rstrip("]"))
406+
# we need to set a placeholder value as the skips are based on index
407+
# if we are to pop the values, the next skip index will have shifted and won't be correct
408+
helper[index] = _PLACEHOLDER_VALUE
409+
has_placeholder = True
410+
except ValueError:
411+
SNAPSHOT_LOGGER.warning(
412+
"Snapshot skip path '%s' was not applied as it was invalid for that snapshot",
413+
path,
414+
exc_info=SNAPSHOT_LOGGER.isEnabledFor(logging.DEBUG),
415+
)
416+
417+
if has_placeholder:
418+
_remove_placeholder(tmp)

tests/test_snapshots.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,80 @@ def test_non_homogeneous_list(self):
191191
sm.match("key1", [{"key2": "value1"}, "value2", 3])
192192
sm._assert_all()
193193

194+
def test_list_as_last_node_in_skip_verification_path(self):
195+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
196+
sm.recorded_state = {"key_a": {"aaa": ["item1", "item2", "item3"]}}
197+
sm.match(
198+
"key_a",
199+
{"aaa": ["item1", "different-value"]},
200+
)
201+
202+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
203+
sm._assert_all()
204+
ctx.match("Parity snapshot failed")
205+
206+
skip_path = ["$..aaa[1]", "$..aaa[2]"]
207+
sm._assert_all(skip_verification_paths=skip_path)
208+
209+
skip_path = ["$..aaa.1", "$..aaa.2"]
210+
sm._assert_all(skip_verification_paths=skip_path)
211+
212+
def test_list_as_last_node_in_skip_verification_path_complex(self):
213+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
214+
sm.recorded_state = {
215+
"key_a": {
216+
"aaa": [{"aab": ["aac", "aad"]}, {"aab": ["aac", "aad"]}, {"aab": ["aac", "aad"]}]
217+
}
218+
}
219+
sm.match(
220+
"key_a",
221+
{
222+
"aaa": [
223+
{"aab": ["aac", "bad-value"], "bbb": "value"},
224+
{"aab": ["aac", "aad", "bad-value"]},
225+
{"aab": ["bad-value", "aad"]},
226+
]
227+
},
228+
)
229+
230+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
231+
sm._assert_all()
232+
ctx.match("Parity snapshot failed")
233+
234+
skip_path = [
235+
"$..aaa[0].aab[1]",
236+
"$..aaa[0].bbb",
237+
"$..aaa[1].aab[2]",
238+
"$..aaa[2].aab[0]",
239+
]
240+
sm._assert_all(skip_verification_paths=skip_path)
241+
242+
skip_path = [
243+
"$..aaa.0..aab.1",
244+
"$..aaa.0..bbb",
245+
"$..aaa.1..aab.2",
246+
"$..aaa.2..aab.0",
247+
]
248+
sm._assert_all(skip_verification_paths=skip_path)
249+
250+
def test_list_as_mid_node_in_skip_verification_path(self):
251+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
252+
sm.recorded_state = {"key_a": {"aaa": [{"aab": "value1"}, {"aab": "value2"}]}}
253+
sm.match(
254+
"key_a",
255+
{"aaa": [{"aab": "value1"}, {"aab": "bad-value"}]},
256+
)
257+
258+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
259+
sm._assert_all()
260+
ctx.match("Parity snapshot failed")
261+
262+
skip_path = ["$..aaa[1].aab"]
263+
sm._assert_all(skip_verification_paths=skip_path)
264+
265+
skip_path = ["$..aaa.1.aab"]
266+
sm._assert_all(skip_verification_paths=skip_path)
267+
194268

195269
def test_json_diff_format():
196270
path = ["Records", 1]

0 commit comments

Comments
 (0)