Skip to content

Commit c827c51

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

File tree

2 files changed

+167
-2
lines changed

2 files changed

+167
-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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,137 @@ 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": [
217+
{"aab": ["aac", "aad"]},
218+
{"aab": ["aac", "aad"]},
219+
{"aab": ["aac", "aad"]},
220+
]
221+
}
222+
}
223+
sm.match(
224+
"key_a",
225+
{
226+
"aaa": [
227+
{"aab": ["aac", "bad-value"], "bbb": "value"},
228+
{"aab": ["aac", "aad", "bad-value"]},
229+
{"aab": ["bad-value", "aad"]},
230+
]
231+
},
232+
)
233+
234+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
235+
sm._assert_all()
236+
ctx.match("Parity snapshot failed")
237+
238+
skip_path = [
239+
"$..aaa[0].aab[1]",
240+
"$..aaa[0].bbb",
241+
"$..aaa[1].aab[2]",
242+
"$..aaa[2].aab[0]",
243+
]
244+
sm._assert_all(skip_verification_paths=skip_path)
245+
246+
skip_path = [
247+
"$..aaa.0..aab.1",
248+
"$..aaa.0..bbb",
249+
"$..aaa.1..aab.2",
250+
"$..aaa.2..aab.0",
251+
]
252+
sm._assert_all(skip_verification_paths=skip_path)
253+
254+
def test_list_as_mid_node_in_skip_verification_path(self):
255+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
256+
sm.recorded_state = {"key_a": {"aaa": [{"aab": "value1"}, {"aab": "value2"}]}}
257+
sm.match(
258+
"key_a",
259+
{"aaa": [{"aab": "value1"}, {"aab": "bad-value"}]},
260+
)
261+
262+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
263+
sm._assert_all()
264+
ctx.match("Parity snapshot failed")
265+
266+
skip_path = ["$..aaa[1].aab"]
267+
sm._assert_all(skip_verification_paths=skip_path)
268+
269+
skip_path = ["$..aaa.1.aab"]
270+
sm._assert_all(skip_verification_paths=skip_path)
271+
272+
def test_list_as_last_node_in_skip_verification_path_nested(self):
273+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
274+
sm.recorded_state = {
275+
"key_a": {
276+
"aaa": [
277+
"bbb",
278+
"ccc",
279+
[
280+
"ddd",
281+
"eee",
282+
[
283+
"fff",
284+
"ggg",
285+
],
286+
],
287+
]
288+
}
289+
}
290+
sm.match(
291+
"key_a",
292+
{
293+
"aaa": [
294+
"bbb",
295+
"ccc",
296+
[
297+
"bad-value",
298+
"eee",
299+
[
300+
"fff",
301+
"ggg",
302+
],
303+
],
304+
]
305+
},
306+
)
307+
308+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
309+
sm._assert_all()
310+
ctx.match("Parity snapshot failed")
311+
312+
skip_path = ["$..aaa[2][0]"]
313+
sm._assert_all(skip_verification_paths=skip_path)
314+
315+
skip_path = ["$..aaa.2[0]"]
316+
sm._assert_all(skip_verification_paths=skip_path)
317+
318+
# these 2 will actually skip almost everything, as they will match every first element of any list inside `aaa`
319+
skip_path = ["$..aaa..[0]"]
320+
sm._assert_all(skip_verification_paths=skip_path)
321+
322+
skip_path = ["$..aaa..0"]
323+
sm._assert_all(skip_verification_paths=skip_path)
324+
194325

195326
def test_json_diff_format():
196327
path = ["Records", 1]

0 commit comments

Comments
 (0)