Skip to content

Commit 93cbdc6

Browse files
committed
added mismatch forensics analysis
1 parent be9d0e3 commit 93cbdc6

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

interposer/tapedeck.py

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# Copyright (C) 2019 - 2020 Tuono, Inc.
44
# All Rights Reserved
55
#
6+
import difflib
7+
import io
68
import logging
79
import math
8-
import os
910
import pickle # nosec
11+
import pickletools # nosec
1012
import shelve
1113

1214
from contextlib import AbstractContextManager
@@ -334,6 +336,7 @@ def playback(self, context: CallContext, channel: str = "default") -> Any:
334336
uniq = self._advance(context, channel)
335337
recorded = self._tape.get(uniq, RecordedCallNotFoundError(context))
336338
if isinstance(recorded, RecordedCallNotFoundError):
339+
self._forensics(context, channel)
337340
raise recorded
338341

339342
payload = recorded
@@ -398,27 +401,78 @@ def _advance(self, context: CallContext, channel: str) -> str:
398401
our_meta[self.LABEL_HASH] = result
399402
return result
400403

404+
def _forensics(self, context: CallContext, channel: str) -> None:
405+
"""
406+
Perform forensic analysis of RecordedCallNotFoundError and log:
407+
408+
- The recorded pickled context (stored in _call_<channel>_<ordinal>)
409+
- The playback pickled context
410+
- The difference
411+
"""
412+
ordinal = self._call_ordinals[channel]
413+
414+
recorded_raw = self._tape.get(f"_call_{channel}_{ordinal}")
415+
playback_call = self._reduce_call(context)
416+
try:
417+
playback_raw = self._redact(context, return_bytes=True)
418+
finally:
419+
context.call = playback_call
420+
421+
assert recorded_raw != playback_raw, "why did we get RecordedCallNotFoundError?"
422+
423+
if recorded_raw is not None:
424+
recorded_io = io.StringIO()
425+
pickletools.dis(recorded_raw, out=recorded_io)
426+
recorded_transcript = recorded_io.getvalue()
427+
self._log(
428+
logging.DEBUG,
429+
"mismatch",
430+
"recorded",
431+
f"RECORDED CALL IN CHANNEL {channel} ORDINAL {ordinal}:\n\n{recorded_transcript}",
432+
)
433+
else:
434+
self._log(
435+
logging.DEBUG,
436+
"mismatch",
437+
"recorded",
438+
f"NO RECORDED CALL IN CHANNEL {channel} ORDINAL {ordinal}",
439+
)
440+
441+
playback_io = io.StringIO()
442+
pickletools.dis(playback_raw, out=playback_io)
443+
playback_transcript = playback_io.getvalue()
444+
445+
self._log(
446+
logging.DEBUG,
447+
"mismatch",
448+
"playback",
449+
f"PLAYBACK CALL IN CHANNEL {channel} ORDINAL {ordinal}:\n\n{playback_transcript}",
450+
)
451+
452+
if recorded_raw and playback_raw:
453+
differences = list(
454+
difflib.context_diff(
455+
recorded_transcript.splitlines(), playback_transcript.splitlines()
456+
)
457+
)
458+
self._log(logging.DEBUG, "mismatch", "difference", "\n".join(differences))
459+
401460
def _hickle(self, context: CallContext) -> str:
402461
"""
403-
Hash a context using a redacted pickle.
462+
Hash a context using a redacted pickle. In addition we stuff
463+
the original redacted call into the database so we can compare
464+
that call's raw content against a playback call to see why they
465+
are different.
404466
405467
Raises:
406468
PicklingError if something in the context cannot be pickled.
407469
"""
408470
raw = self._redact(context, return_bytes=True)
409-
# if TAPEDECKDEBUG is in the environment we dump out the raw pickles so
410-
# we can use "python3 -m pickletools <file>" to dump out the actual
411-
# raw pickle content and determine why there was a mismatch; to be used
412-
# when playback raises RecordedCallNotFoundError
413-
if "TAPEDECKDEBUG" in os.environ and context:
414-
calldir = Path(str(self.deck) + "-calls")
415-
calldir.mkdir(exist_ok=True)
471+
if self.mode == Mode.Recording:
416472
our_meta = context.meta[self.LABEL_TAPE]
417473
channel = our_meta[self.LABEL_CHANNEL]
418474
ordinal = our_meta[self.LABEL_ORDINAL]
419-
fname = f"{('record' if self.mode == Mode.Recording else 'playback')}-{channel}-{ordinal}.pickle"
420-
with (calldir / fname).open("wb") as fp:
421-
fp.write(raw)
475+
self._tape[f"_call_{channel}_{ordinal}"] = raw
422476
uniq = sha256(raw)
423477
result = uniq.hexdigest()
424478
return result

tests/tapedeck_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,13 @@ def test_recording_secrets(self):
253253
assert token not in str(ex.exception)
254254
assert redacted_token in str(ex.exception)
255255
uut.dump(self.datadir / "dump.yaml")
256+
257+
# now with a misaligned playback
258+
259+
with TapeDeck(self.datadir / "recording", Mode.Playback) as uut:
260+
with self.assertRaises(RecordedCallNotFoundError):
261+
uut.playback(
262+
CallContext(
263+
call=KeeperOfFineSecrets, args=(), kwargs={"sam": "dean"}
264+
)
265+
)

0 commit comments

Comments
 (0)