def setUp(self): self.tape_cassette = InMemoryTapeCassette() self.tape_recorder = TapeRecorder(self.tape_cassette) self.tape_recorder.enable_recording()
class TestTapeRecorder(unittest.TestCase): def setUp(self): self.tape_cassette = InMemoryTapeCassette() self.tape_recorder = TapeRecorder(self.tape_cassette) self.tape_recorder.enable_recording() def test_record_and_playback_basic_operation_no_parameters(self): class Operation(object): @self.tape_recorder.operation() def execute(self): return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def _assert_playback_vs_recording(self, playback_result, result): """ :param playback_result: Playback result :type playback_result: playback.tape_recorder.Playback :param result: Operation result :type result: Any """ if six.PY3: self.assertCountEqual(playback_result.recorded_outputs, playback_result.playback_outputs) else: self.assertItemsEqual(playback_result.recorded_outputs, playback_result.playback_outputs) operation_output = next(po for po in playback_result.playback_outputs if TapeRecorder.OPERATION_OUTPUT_ALIAS in po.key) self.assertEqual(result, operation_output.value['args'][0]) self.assertGreater(playback_result.playback_duration, 0) self.assertGreater(playback_result.recorded_duration, 0) def test_record_and_playback_basic_operation_data_interception_no_arguments(self): class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): return self.get_value() @self.tape_recorder.intercept_input('input') def get_value(self): return self.seed instance = Operation(5) result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_data_interception_property(self): class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): return self.get_value @self.tape_recorder.intercept_input('input') @property def get_value(self): return self.seed instance = Operation(5) result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_data_interception_with_arguments(self): class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): val1 = self.get_value(2, b=3) val2 = self.get_value(4, b=6) return val1 + val2 @self.tape_recorder.intercept_input('input') def get_value(self, a, b=2): return (a + b) * self.seed instance = Operation(seed=1) result = instance.execute() self.assertEqual(15, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_static_data_interception_with_arguments(self): class Operation(object): seed = 0 @self.tape_recorder.operation() def execute(self): val1 = self.get_value(2, b=3) val2 = self.get_value(4, b=6) return val1 + val2 @staticmethod @self.tape_recorder.static_intercept_input('input') def get_value(a, b=2): return (a + b) * Operation.seed instance = Operation() Operation.seed = 1 result = instance.execute() self.assertEqual(15, result) Operation.seed = 0 recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_data_interception_with_arguments_and_metadata(self): def operation_metadata_extractor(obj): return {'type': type(obj)} class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation(metadata_extractor=operation_metadata_extractor) def execute(self): val1 = self.get_value(2, b=3) val2 = self.get_value(4, b=6) return val1 + val2 @self.tape_recorder.intercept_input('input') def get_value(self, a, b=2): return (a + b) * self.seed instance = Operation(seed=1) result = instance.execute() self.assertEqual(15, result) recording_id = self.tape_cassette.get_last_recording_id() recording = self.tape_cassette.get_recording(recording_id) # pickle encode decode doesn't reconstruct actuall class if this is an inner class operation_decoded = decode(encode(Operation, unpicklable=False)) self.assertDictContainsSubset({'type': operation_decoded, TapeRecorder.OPERATION_CLASS: operation_decoded}, recording.get_metadata()) playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_no_parameters_with_output(self): class Operation(object): @self.tape_recorder.operation() def execute(self): x = 0 x += self.output(4, arg='a') x += self.output(3, arg='b') return x @self.tape_recorder.intercept_output('output_function') def output(self, value, arg=None): return value instance = Operation() result = instance.execute() self.assertEqual(7, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) self.assertEqual({'args': [4], 'kwargs': {'arg': 'a'}}, playback_result.playback_outputs[0].value) self.assertEqual({'args': [3], 'kwargs': {'arg': 'b'}}, playback_result.playback_outputs[1].value) self.assertNotEqual(playback_result.playback_outputs[0].key, playback_result.playback_outputs[1].key) self.assertIn('output_function', playback_result.playback_outputs[0].key) self.assertIn('output_function', playback_result.playback_outputs[1].key) def test_record_and_playback_basic_operation_no_parameters_with_static_output(self): class Operation(object): @self.tape_recorder.operation() def execute(self): x = 0 x += self.output(4, arg='a') x += self.output(3, arg='b') return x @staticmethod @self.tape_recorder.static_intercept_output('output_function') def output(value, arg=None): return value instance = Operation() result = instance.execute() self.assertEqual(7, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) self.assertEqual({'args': [4], 'kwargs': {'arg': 'a'}}, playback_result.playback_outputs[0].value) self.assertEqual({'args': [3], 'kwargs': {'arg': 'b'}}, playback_result.playback_outputs[1].value) self.assertNotEqual(playback_result.playback_outputs[0].key, playback_result.playback_outputs[1].key) self.assertIn('output_function', playback_result.playback_outputs[0].key) self.assertIn('output_function', playback_result.playback_outputs[1].key) def test_record_and_playback_basic_operation_similar_duration(self): class Operation(object): @self.tape_recorder.operation() def execute(self): sleep(0.5) return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self.assertLessEqual(abs(playback_result.recorded_duration - playback_result.playback_duration), 0.1) def test_record_and_playback_basic_operation_class_extraction_in_metadata(self): class Operation(object): @self.tape_recorder.operation() def execute(self): return 5 @classmethod @self.tape_recorder.class_operation() def class_execute(cls): return 5 instance = Operation() self.assertEqual(5, instance.execute()) recording_id = self.tape_cassette.get_last_recording_id() recording = self.tape_cassette.get_recording(recording_id) # pickle encode decode doesn't reconstruct actuall class if this is an inner class operation_decoded = decode(encode(Operation, unpicklable=False)) self.assertDictContainsSubset({TapeRecorder.OPERATION_CLASS: operation_decoded}, recording.get_metadata()) self.assertEqual(5, instance.class_execute()) recording_id = self.tape_cassette.get_last_recording_id() recording = self.tape_cassette.get_recording(recording_id) # Inner classes are not unpicklable back to class upon decode self.assertDictContainsSubset({TapeRecorder.OPERATION_CLASS: operation_decoded}, recording.get_metadata()) def test_drop_recording_direct_api_call(self): local_tape_recorder = self.tape_recorder class Operation(object): @self.tape_recorder.operation() def execute(self): local_tape_recorder.discard_recording() return 5 instance = Operation() with patch.object(InMemoryTapeCassette, '_save_recording', wraps=self.tape_cassette._save_recording) \ as intercepted_save, \ patch.object(InMemoryTapeCassette, 'abort_recording', wraps=self.tape_cassette.abort_recording) \ as intercepted_abort: result = instance.execute() intercepted_save.assert_not_called() intercepted_abort.assert_called() self.assertEqual(5, result) def test_skip_recording_decorator(self): @self.tape_recorder.recording_params(skipped=True) class Operation(object): @self.tape_recorder.operation() def execute(self): return 5 instance = Operation() with patch.object(InMemoryTapeCassette, 'create_new_recording', wraps=self.tape_cassette.create_new_recording) \ as intercepted: result = instance.execute() intercepted.assert_not_called() self.assertEqual(5, result) def test_input_interception_key_failure(self): class UnencodeableObject(object): def __getstate__(self): raise Exception() class Operation(object): @self.tape_recorder.operation() def execute(self): param = UnencodeableObject() value = self.input(param) return value @self.tape_recorder.intercept_input('input') def input(self, param): return 5 instance = Operation() with patch.object(InMemoryTapeCassette, '_save_recording', wraps=self.tape_cassette._save_recording) \ as intercepted_save, \ patch.object(InMemoryTapeCassette, 'abort_recording', wraps=self.tape_cassette.abort_recording) \ as intercepted_abort: result = instance.execute() intercepted_save.assert_not_called() intercepted_abort.assert_called() self.assertEqual(5, result) def test_input_interception_key_failure_during_playback(self): class UnencodeableObject(object): def __getstate__(self): raise Exception() class Operation(object): def __init__(self, raise_on_get=False): self.raise_on_get = raise_on_get @self.tape_recorder.operation() def execute(self): param = UnencodeableObject() if self.raise_on_get else object() value = self.input(param) return value @self.tape_recorder.intercept_input('input') def input(self, param): return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() with self.assertRaises(InputInterceptionKeyCreationError): self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation(True).execute()) def test_input_interception_key_missing_during_playback(self): class Operation(object): def __init__(self, input_key): self.input_key = input_key @self.tape_recorder.operation() def execute(self): value = self.input(self.input_key) return value @self.tape_recorder.intercept_input('input') def input(self, param): return 5 instance = Operation('key1') result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() with self.assertRaises(RecordingKeyError): self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation('key2').execute()) def test_output_interception_key_missing_during_playback(self): class Operation(object): def __init__(self, output_method): self.output_method = output_method @self.tape_recorder.operation() def execute(self): value = getattr(self, self.output_method)() return value @self.tape_recorder.intercept_output('output_function') def output(self): return 5 @self.tape_recorder.intercept_output('output_missing') def output_missing(self): return 5 instance = Operation('output') result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() with self.assertRaises(RecordingKeyError): self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation('output_missing').execute()) def test_record_and_playback_basic_operation_metadata_extractor_raise_exception(self): def operation_metadata_extractor(*args, **kwargs): raise Exception('Meta exception') class Operation(object): @self.tape_recorder.operation(metadata_extractor=operation_metadata_extractor) def execute(self): return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_save_recording_raise_exception(self): class Operation(object): @self.tape_recorder.operation() def execute(self): return 5 with patch.object(InMemoryTapeCassette, '_save_recording', side_effect=Exception()): instance = Operation() result = instance.execute() self.assertEqual(5, result) def test_record_and_playback_basic_operation_no_parameters_raise_error(self): class Operation(object): @self.tape_recorder.operation() def execute(self): raise ValueError("Error") instance = Operation() with self.assertRaises(ValueError) as e: instance.execute() self.assertEqual("Error", str(e.exception)) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) operation_output = next(po for po in playback_result.playback_outputs if TapeRecorder.OPERATION_OUTPUT_ALIAS in po.key) self.assertEqual(ValueError, type(operation_output.value['args'][0])) self.assertEqual("Error", str(operation_output.value['args'][0])) self.assertEqual(len(playback_result.recorded_outputs), len(playback_result.playback_outputs)) self.assertGreater(playback_result.playback_duration, 0) self.assertGreater(playback_result.recorded_duration, 0) self.assertDictContainsSubset({TapeRecorder.EXCEPTION_IN_OPERATION: True}, playback_result.original_recording.get_metadata()) def test_record_and_playback_basic_operation_data_interception_no_arguments_raise_exception(self): class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): try: self.get_value() except Exception as ex: return str(ex) @self.tape_recorder.intercept_input('input') def get_value(self): raise Exception(self.seed) instance = Operation(5) result = instance.execute() self.assertEqual('5', result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_output_interception_no_arguments_raise_exception(self): class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): try: self.output() except Exception as ex: return str(ex) @self.tape_recorder.intercept_output('output_function') def output(self): raise Exception(self.seed) instance = Operation(5) result = instance.execute() self.assertEqual("5", result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_sampling_rate_class_level_decorator(self): @self.tape_recorder.recording_params(sampling_rate=0.1) class Operation(object): @self.tape_recorder.operation() def execute(self): return 5 instance = Operation() with patch.object(InMemoryTapeCassette, 'save_recording', wraps=self.tape_cassette.create_new_recording) \ as intercepted: calls = 100 for __ in range(calls): result = instance.execute() self.assertEqual(5, result) call_ratio = float(intercepted.call_count)/calls print('Call ratio {}'.format(call_ratio)) self.assertAlmostEqual(0.1, call_ratio, places=1) def test_operation_with_input_dict_as_key(self): class Operation(object): @self.tape_recorder.operation() def execute(self): # We are shuffling here hoping to get different internal order for the dict between calls but still # see we are consistent on input interception is these are equivalent dicts items = list(range(100)) shuffle(items) argument = {'key{}'.format(i): i for i in items} return self.get_value(argument) @self.tape_recorder.intercept_input('input') def get_value(self, json_dict): return json_dict['key5'] instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_operation_with_alias_param_resolver(self): class ValueCreator(object): def __init__(self, name, value): self.name = name self.value = value @self.tape_recorder.intercept_input('input.{name}', alias_params_resolver=lambda s: {'name': s.name}) def get_value(self): return self.value class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): # We check that we are capturing different value interceptions even though we pass same arguments to # the intercepted method by using a param resolver that will provide unique interception key for each # invocation return self.get_value('a', self.seed) + self.get_value('b', self.seed * 2) @staticmethod def get_value(name, value): return ValueCreator(name, value).get_value() instance = Operation(5) result = instance.execute() self.assertEqual(15, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_operation_with_input_interception_data_handler(self): test_self = self class XorDataHandler(InputInterceptionDataHandler): def prepare_input_for_recording(self, interception_key, result, args, kwargs): test_self.assertIn('value_input', interception_key) test_self.assertEqual(5, result) test_self.assertEqual(('x', ), args[1:]) test_self.assertEqual({'b': 'y'}, kwargs) return 5 ^ 3141 def restore_input_from_recording(self, recorded_data, args, kwargs): # Applying same xor twice return original result return recorded_data ^ 3141 class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): return self.get_value('x', b='y') @self.tape_recorder.intercept_input('value_input', data_handler=XorDataHandler()) def get_value(self, a, b=None): return self.seed instance = Operation(5) result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() # Check the recording it self contains the modified value recording = self.tape_cassette.get_recording(recording_id) key = next(k for k in recording.get_all_keys() if 'value_input' in k) self.assertEqual(5 ^ 3141, recording.get_data(key)['value']) playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_input_interception_data_handler_failure(self): class ErrorDataHandler(InputInterceptionDataHandler): def prepare_input_for_recording(self, interception_key, result, args, kwargs): raise Exception('error') def restore_input_from_recording(self, recorded_data, args, kwargs): pass class Operation(object): @self.tape_recorder.operation() def execute(self): return self.get_value() + self.get_value2() @self.tape_recorder.intercept_input('value_input', data_handler=ErrorDataHandler()) def get_value(self): return 5 @self.tape_recorder.intercept_input('value_input2') def get_value2(self): return 5 instance = Operation() with patch.object(InMemoryTapeCassette, '_save_recording', wraps=self.tape_cassette._save_recording) \ as intercepted: result = instance.execute() intercepted.assert_not_called() self.assertEqual(10, result) def test_record_and_playback_basic_operation_data_interception_with_all_arguments_ignored_as_key(self): class Operation(object): @self.tape_recorder.operation() def execute(self): arg1 = random() arg2 = random() val1 = self.get_value(arg1, b=arg2) return val1 @self.tape_recorder.intercept_input('input', capture_args=[]) def get_value(self, a, b=2): return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_basic_operation_data_interception_with_false_capture_args(self): class Operation(object): @self.tape_recorder.operation() def execute(self): arg1 = random() arg2 = random() val1 = self.get_value(arg1, b=arg2) return val1 @self.tape_recorder.intercept_input('input', capture_args=False) def get_value(self, a, b=2): return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_sampling_rate_with_enforced_recording(self): local_tape_recorder = self.tape_recorder test_self = self @self.tape_recorder.recording_params(sampling_rate=0.2) class Operation(object): @self.tape_recorder.operation() def execute(self): local_tape_recorder.force_sample_recording() test_self.assertTrue(local_tape_recorder.is_recording_sample_forced) return 5 instance = Operation() with patch.object(InMemoryTapeCassette, '_save_recording', wraps=self.tape_cassette._save_recording) \ as intercepted: calls = 10 for __ in range(calls): result = instance.execute() self.assertEqual(5, result) call_ratio = float(intercepted.call_count)/calls print('Call ratio {}'.format(call_ratio)) self.assertEqual(1, call_ratio) def test_sampling_rate_with_ignore_enforced_recording(self): local_tape_recorder = self.tape_recorder test_self = self @self.tape_recorder.recording_params(sampling_rate=0.1, ignore_enforced_sampling=True) class Operation(object): @self.tape_recorder.operation() def execute(self): local_tape_recorder.force_sample_recording() test_self.assertFalse(local_tape_recorder.is_recording_sample_forced) return 5 instance = Operation() with patch.object(InMemoryTapeCassette, '_save_recording', wraps=self.tape_cassette._save_recording) \ as intercepted_save,\ patch.object(InMemoryTapeCassette, 'abort_recording', wraps=self.tape_cassette.abort_recording) \ as intercepted_abort: calls = 100 for __ in range(calls): result = instance.execute() self.assertEqual(5, result) expected_aborted = calls - intercepted_save.call_count call_ratio = float(intercepted_save.call_count) / calls print('Call ratio {}'.format(call_ratio)) self.assertAlmostEqual(0.1, call_ratio, places=1) self.assertEqual(expected_aborted, intercepted_abort.call_count) def test_record_and_playback_basic_operation_data_interception_with_specific_arguments_ignored_as_key(self): class Operation(object): @self.tape_recorder.operation() def execute(self): # This is random to see that capture args is only capturing the requested arg index arg1 = random() arg2 = 'b' arg3 = 'c' # This is random to see that capture args is only capturing the requested arg kwargs arg4 = random() arg5 = 'e' val1 = self.get_value(arg1, arg2, c=arg3, d=arg4, e=arg5) return val1 @self.tape_recorder.intercept_input('input', capture_args=[CapturedArg(2, 'b'), CapturedArg(3, 'c'), CapturedArg(None, 'e'), CapturedArg(None, 'f')]) def get_value(self, a, b, c, d=None, e=None, f=None): return 5 instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) def test_record_and_playback_operation_with_output_interception_data_handler(self): test_self = self class XorDataHandler(OutputInterceptionDataHandler): def prepare_output_for_recording(self, interception_key, args, kwargs): test_self.assertIn('output_function', interception_key) test_self.assertEqual((5, ), args) test_self.assertEqual({'arg': 'a'}, kwargs) return args[0] ^ 3141 def restore_output_from_recording(self, recorded_data): # Applying same xor twice return original result return recorded_data ^ 3141 class Operation(object): @self.tape_recorder.operation() def execute(self): return self.output(5, arg='a') @self.tape_recorder.intercept_output('output_function', data_handler=XorDataHandler()) def output(self, value, arg=None): return value instance = Operation() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() # Check the recording it self contains the modified value recording = self.tape_cassette.get_recording(recording_id) key = next(k for k in recording.get_all_keys() if 'output_function' in k) self.assertEqual(5 ^ 3141, recording.get_data(key)) playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) self.assertEqual(result, XorDataHandler().restore_output_from_recording( playback_result.playback_outputs[0].value)) def test_output_interception_data_handler_failure(self): class ErrorDataHandler(OutputInterceptionDataHandler): def prepare_output_for_recording(self, interception_key, args, kwargs): raise Exception('error') def restore_output_from_recording(self, recorded_data): pass class Operation(object): @self.tape_recorder.operation() def execute(self): return self.output(5) + self.output2(5) @self.tape_recorder.intercept_output('output_function', data_handler=ErrorDataHandler()) def output(self, value): return value @self.tape_recorder.intercept_output('output_function2') def output2(self, value): return value instance = Operation() with patch.object(InMemoryTapeCassette, '_save_recording', wraps=self.tape_cassette._save_recording) \ as intercepted: result = instance.execute() intercepted.assert_not_called() self.assertEqual(10, result) def test_record_and_playback_basic_operation_data_interception_inside_interception(self): class Operation(object): def __init__(self, seed=0): self.seed = seed @self.tape_recorder.operation() def execute(self): return self.get_value() @self.tape_recorder.intercept_input('input') def get_value(self): return self.get_inner_value() @self.tape_recorder.intercept_input('inner_input') def get_inner_value(self): return self.seed instance = Operation(5) result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) self._assert_playback_vs_recording(playback_result, result) # Check the recording it self does not contain the inner interception recording = self.tape_cassette.get_recording(recording_id) self.assertIsNone(next((k for k in recording.get_all_keys() if 'inner_input' in k), None)) def test_record_and_playback_basic_operation_new_output_added_post_recording(self): class OperationOld(object): @self.tape_recorder.operation() def execute(self): return self.output(5) @self.tape_recorder.intercept_output('output_function') def output(self, value): return value class OperationNew(object): @self.tape_recorder.operation() def execute(self): value = self.output(5) self.output_new(value) return value @self.tape_recorder.intercept_output('output_function') def output(self, value): return value @self.tape_recorder.intercept_output('output_new_function', fail_on_no_recorded_result=False) def output_new(self, value): return value instance = OperationOld() result = instance.execute() self.assertEqual(5, result) recording_id = self.tape_cassette.get_last_recording_id() playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: OperationNew().execute()) self.assertEqual({'args': [5], 'kwargs': {}}, playback_result.playback_outputs[0].value) self.assertEqual({'args': [5], 'kwargs': {}}, playback_result.playback_outputs[1].value) self.assertNotEqual(playback_result.playback_outputs[0].key, playback_result.playback_outputs[1].key) self.assertIn('output_function', playback_result.playback_outputs[0].key) self.assertIn('output_new_function', playback_result.playback_outputs[1].key) def test_current_recording_id(self): tape_recorder = self.tape_recorder class Operation(object): @self.tape_recorder.operation() def execute(self): return tape_recorder.current_recording_id instance = Operation() result = instance.execute() recording_id = self.tape_cassette.get_last_recording_id() self.assertEqual(recording_id, result) playback_result = self.tape_recorder.play(recording_id, playback_function=lambda recording: Operation().execute()) operation_output = next(po for po in playback_result.playback_outputs if TapeRecorder.OPERATION_OUTPUT_ALIAS in po.key) self.assertEqual(recording_id, operation_output.value['args'][0]) self.tape_recorder.disable_recording() instance = Operation() self.assertIsNone(instance.execute())
class TestPlaybackStudio(unittest.TestCase): def setUp(self): self.tape_cassette = InMemoryTapeCassette() self.tape_recorder = TapeRecorder(self.tape_cassette) self.tape_recorder.enable_recording() def tearDown(self): self.tape_cassette.close() def test_run_two_categories(self): self._test_run_two_operations(use_recording_ids=False) def test_run_specific_recording_ids(self): self._test_run_two_operations(use_recording_ids=True) def _test_run_two_operations(self, use_recording_ids): class A(object): @self.tape_recorder.operation() def execute(self): return 'AAA' class B(object): @self.tape_recorder.operation() def execute(self): return 'BBB' A().execute() A().execute() B().execute() if not use_recording_ids: categories = ['A', 'B', 'C'] recording_ids = None else: categories = None recording_ids = self.tape_cassette.get_all_recording_ids() class MockEqualizerTuner(EqualizerTuner): def create_category_tuning(self, category): if category not in ['A', 'B']: raise Exception("Unsupported") def result_extractor(outputs): return next( o.value['args'][0] for o in outputs if TapeRecorder.OPERATION_OUTPUT_ALIAS in o.key) def comparator(expected, actual, message): return ComparatorResult( EqualityStatus.Equal if expected == actual else EqualityStatus.Different, message) def comparison_data_extractor(recording): return {'message': category} def playback_function(recording): cls_name = recording.get_metadata()[ TapeRecorder.OPERATION_CLASS] if 'studio.A' in list(cls_name.values())[0]: return A().execute() return B().execute() return EqualizerTuning( playback_function=playback_function, result_extractor=result_extractor, comparator=comparator, comparison_data_extractor=comparison_data_extractor) equalizer_tuner = MockEqualizerTuner() start_date = 'a' studio = PlaybackStudio( categories, equalizer_tuner, self.tape_recorder, lookup_properties=RecordingLookupProperties(start_date), recording_ids=recording_ids, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True)) result = studio.play() a_results = list(result['A']) b_results = list(result['B']) self.assertEqual(2, len(a_results)) self.assertTrue(all('AAA' == result.actual for result in a_results)) self.assertTrue(all('AAA' == result.expected for result in a_results)) self.assertTrue( all(EqualityStatus.Equal == result.comparator_status.equality_status for result in a_results)) self.assertTrue( all('A' == result.comparator_status.message for result in a_results)) self.assertEqual(1, len(b_results)) self.assertTrue(all('BBB' == result.actual for result in b_results)) self.assertTrue(all('BBB' == result.expected for result in b_results)) self.assertTrue( all(EqualityStatus.Equal == result.comparator_status.equality_status for result in b_results)) self.assertTrue( all('B' == result.comparator_status.message for result in b_results))
class TestEqualizer(unittest.TestCase): def setUp(self): self.tape_cassette = InMemoryTapeCassette() self.tape_recorder = TapeRecorder(self.tape_cassette) self.tape_recorder.enable_recording() def tearDown(self): self.tape_cassette.close() @parameterized.expand([("compare_in_dedicated_process", True), ("playback_same_process", False), ]) def test_equal_comparison(self, name, compare_in_dedicated_process): class Operation(object): def __init__(self, value=None, multiply_input=1): self._value = value self.multiply_input = multiply_input @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self): return self.input * self.multiply_input Operation(3).execute() Operation(4).execute() Operation(5).execute() playback_counter = [0] def playback_function(recording): playback_counter[0] += 1 if playback_counter[0] == 2: operation = Operation(multiply_input=2) elif playback_counter[0] == 3: operation = Operation(multiply_input=100) else: operation = Operation() return operation.execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) end_date = datetime.utcnow() + timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties(start_date=start_date, end_date=end_date), ) runner = Equalizer(playable_recordings, player, result_extractor=return_value_result_extractor, comparator=exact_comparator, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=compare_in_dedicated_process )) with patch.object(Equalizer, '_play_and_compare_recording', wraps=runner._play_and_compare_recording) as wrapped: comparison = list(runner.run_comparison()) if compare_in_dedicated_process: wrapped.assert_not_called() else: wrapped.assert_called() self.assertEqual(EqualityStatus.Equal, comparison[0].comparator_status.equality_status) self.assertEqual(EqualityStatus.Different, comparison[1].comparator_status.equality_status) self.assertEqual(EqualityStatus.Failed, comparison[2].comparator_status.equality_status) self.assertEqual(3, comparison[0].expected) self.assertEqual(4, comparison[1].expected) self.assertEqual(5, comparison[2].expected) self.assertEqual(3, comparison[0].actual) self.assertEqual(8, comparison[1].actual) self.assertEqual(500, comparison[2].actual) mock.assert_called_with(Operation.__name__, start_date=start_date, end_date=end_date, limit=None, metadata=None) self.assertGreaterEqual(comparison[0].playback.playback_duration, 0) self.assertGreaterEqual(comparison[0].playback.recorded_duration, 0) @parameterized.expand([("compare_in_dedicated_process", True), ("playback_same_process", False), ]) def test_with_exception_comparison(self, name, compare_in_dedicated_process): class Operation(object): def __init__(self, value=None, raise_error=None): self._value = value self.raise_error = raise_error @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self, value=None): if self.raise_error: raise Exception("error") if value is not None: return value return self.input Operation(3).execute() Operation(4).execute() with suppress(Exception): Operation(raise_error=True).execute() playback_counter = [0] def playback_function(recording): playback_counter[0] += 1 if playback_counter[0] == 2: operation = Operation(raise_error=True) elif playback_counter[0] == 3: return Operation().execute(5) else: operation = Operation() return operation.execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids): start_date = datetime.utcnow() - timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties(start_date=start_date), ) runner = Equalizer(playable_recordings, player, result_extractor=return_value_result_extractor, comparator=exact_comparator, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=compare_in_dedicated_process )) comparison = list(runner.run_comparison()) self.assertEqual(EqualityStatus.Equal, comparison[0].comparator_status.equality_status) self.assertEqual(EqualityStatus.Different, comparison[1].comparator_status.equality_status) self.assertEqual(EqualityStatus.Different, comparison[2].comparator_status.equality_status) self.assertEqual(3, comparison[0].expected) self.assertEqual(4, comparison[1].expected) self.assertIsInstance(comparison[2].expected, Exception) self.assertTrue(comparison[1].actual_is_exception) self.assertFalse(comparison[1].expected_is_exception) self.assertEqual(3, comparison[0].actual) self.assertIsInstance(comparison[1].actual, Exception) self.assertEqual(5, comparison[2].actual) self.assertFalse(comparison[2].actual_is_exception) self.assertTrue(comparison[2].expected_is_exception) @parameterized.expand([("compare_in_dedicated_process", True), ("playback_same_process", False), ]) def test_comparison_with_meta_and_limit_filtering(self, name, compare_in_dedicated_process): class Operation(object): def __init__(self, value=None): self._value = value @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation(metadata_extractor=lambda op_self, meta=None: {'meta': meta}) def execute(self, meta=None): return self.input Operation(3).execute('a') Operation(4).execute('b') Operation(5).execute('b') def playback_function(recording): return Operation().execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties( start_date=start_date, metadata={'meta': 'b'}, limit=1), ) runner = Equalizer(playable_recordings, player, result_extractor=return_value_result_extractor, comparator=exact_comparator, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=compare_in_dedicated_process )) comparison = list(runner.run_comparison()) self.assertEqual(1, len(comparison)) self.assertEqual(EqualityStatus.Equal, comparison[0].comparator_status.equality_status) mock.assert_called_with(Operation.__name__, start_date=start_date, end_date=None, limit=1, metadata={'meta': 'b'}) self.assertEqual(4, comparison[0].expected) self.assertEqual(4, comparison[0].actual) @parameterized.expand([("compare_in_dedicated_process", True), ("playback_same_process", False), ]) def test_equal_comparison_with_message(self, name, compare_in_dedicated_process): class Operation(object): def __init__(self, value=None, multiply_input=1): self._value = value self.multiply_input = multiply_input @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self): return self.input * self.multiply_input Operation(3).execute() Operation(4).execute() Operation(5).execute() playback_counter = [0] def playback_function(recording): playback_counter[0] += 1 if playback_counter[0] == 2: operation = Operation(multiply_input=2) elif playback_counter[0] == 3: operation = Operation(multiply_input=100) else: operation = Operation() return operation.execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) end_date = datetime.utcnow() + timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties( start_date=start_date, end_date=end_date) ) runner = Equalizer(playable_recordings, player, result_extractor=return_value_result_extractor, comparator=exact_comparator_with_message, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=compare_in_dedicated_process )) comparison = list(runner.run_comparison()) self.assertEqual(EqualityStatus.Equal, comparison[0].comparator_status.equality_status) self.assertEqual(EqualityStatus.Different, comparison[1].comparator_status.equality_status) self.assertEqual(EqualityStatus.Failed, comparison[2].comparator_status.equality_status) self.assertEqual(EqualityStatus.Equal.name, comparison[0].comparator_status.message) self.assertEqual(EqualityStatus.Different.name, comparison[1].comparator_status.message) self.assertEqual(EqualityStatus.Failed.name, comparison[2].comparator_status.message) @parameterized.expand([("compare_in_dedicated_process", True), ("playback_same_process", False), ]) def test_equal_comparison_comparator_data_extraction(self, name, compare_in_dedicated_process): class Operation(object): def __init__(self, value=None, override_input=None): self._value = value self.override_input = override_input @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self): if self.override_input: return self.override_input return self.input Operation(3).execute() Operation(4).execute() Operation(5).execute() playback_counter = [0] def playback_function(recording): playback_counter[0] += 1 if playback_counter[0] == 2: operation = Operation(override_input=100) else: operation = Operation() return operation.execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) def comparison_data_extractor(recording): self.assertIsNotNone(recording) return {'multiplier': 1} def exact_comparator_with_multiplier(recorded_result, playback_result, multiplier): if playback_result >= 10: playback_result *= 0 recorded_result *= 0 else: playback_result *= multiplier recorded_result *= multiplier return ComparatorResult( EqualityStatus.Equal if recorded_result * multiplier == playback_result * multiplier else EqualityStatus.Different, str(multiplier)) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) end_date = datetime.utcnow() + timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties( start_date=start_date, end_date=end_date) ) runner = Equalizer(playable_recordings, player, result_extractor=return_value_result_extractor, comparator=exact_comparator_with_multiplier, comparison_data_extractor=comparison_data_extractor, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=compare_in_dedicated_process )) comparison = list(runner.run_comparison()) self.assertEqual(EqualityStatus.Equal, comparison[0].comparator_status.equality_status) self.assertEqual(EqualityStatus.Equal, comparison[1].comparator_status.equality_status) self.assertEqual(EqualityStatus.Equal, comparison[2].comparator_status.equality_status) self.assertEqual('1', comparison[0].comparator_status.message) self.assertEqual('1', comparison[1].comparator_status.message) self.assertEqual('1', comparison[2].comparator_status.message) def test_random_sample(self): class Operation(object): def __init__(self, value=None): self._value = value @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self): return self.input for i in range(10): Operation(i).execute() def playback_function(recording): return Operation().execute() random.seed(12) first_list = None with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties( start_date=start_date, random_sample=True, limit=3) ) first_list = list(playable_recordings) self.assertEqual(len(first_list), 3) random.seed(4) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties( start_date=start_date, random_sample=True, limit=3) ) second_list = list(playable_recordings) self.assertNotEqual(second_list, first_list) @parameterized.expand([("compare_in_dedicated_process", True), ("playback_same_process", False), ]) def test_run_with_specific_ids(self, name, compare_in_dedicated_process): class Operation(object): def __init__(self, value=None, override_input=None): self._value = value self.override_input = override_input @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self): if self.override_input: return self.override_input return self.input Operation(3).execute() id1 = self.tape_cassette.get_last_recording_id() Operation(4).execute() Operation(5).execute() id2 = self.tape_cassette.get_last_recording_id() Operation(6).execute() playback_counter = [0] def playback_function(recording): playback_counter[0] += 1 if playback_counter[0] == 2: operation = Operation(override_input=100) else: operation = Operation() return operation.execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) runner = Equalizer([id1, id2], player, result_extractor=return_value_result_extractor, comparator=exact_comparator, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=compare_in_dedicated_process )) comparison = list(runner.run_comparison()) self.assertEqual(id1, comparison[0].playback.original_recording.id) self.assertEqual(id2, comparison[1].playback.original_recording.id) self.assertEqual(2, len(comparison)) def test_equalizer_recycle_process_thread(self): class Operation(object): def __init__(self, value=None): self._value = value @property @self.tape_recorder.intercept_input('input') def input(self): return self._value @self.tape_recorder.operation() def execute(self): return self.input for i in range(20): Operation(i).execute() def playback_function(recording): return Operation().execute() def player(recording_id): return self.tape_recorder.play(recording_id, playback_function) with patch.object(InMemoryTapeCassette, 'iter_recording_ids', wraps=self.tape_cassette.iter_recording_ids) as mock: start_date = datetime.utcnow() - timedelta(hours=1) end_date = datetime.utcnow() + timedelta(hours=1) playable_recordings = find_matching_recording_ids( self.tape_recorder, category=Operation.__name__, lookup_properties=RecordingLookupProperties(start_date=start_date, end_date=end_date), ) runner = Equalizer(playable_recordings, player, result_extractor=return_value_result_extractor, comparator=exact_comparator, compare_execution_config=CompareExecutionConfig( keep_results_in_comparison=True, compare_in_dedicated_process=True, compare_process_recycle_rate=2 )) with patch.object(Equalizer, '_create_new_player_process', wraps=runner._create_new_player_process) as wrapped: comparison = list(runner.run_comparison()) self.assertEqual(10, wrapped.call_count) self.assertEqual(20, len(comparison)) for c in comparison: self.assertEqual(EqualityStatus.Equal, c.comparator_status.equality_status)
# We create the tape recorder singleton that will be used by the endpoints import os from playback.tape_cassettes.asynchronous.async_record_only_tape_cassette import AsyncRecordOnlyTapeCassette from playback.tape_cassettes.file_based.file_based_tape_cassette import FileBasedTapeCassette from playback.tape_recorder import TapeRecorder tape_recorder = TapeRecorder(None) tape_recorder.disable_recording() # For demonstration purpose, persist file in underline recordings dir recordings_path = os.path.dirname(os.path.realpath(__file__)) + '/recordings' def init_recording_mode(): """ Initialize and start the tape cassette and tape recorder for recording mode. in order for the recording not to be blocking and part of the operation latency, we wrap it with the asynchronous tape cassette which will flush the recording to a file in a separate thread, reducing the impact on the thread that executes the operation """ print("Starting recording mode - recordings will be saved to {}".format( recordings_path)) tape_cassette = AsyncRecordOnlyTapeCassette( FileBasedTapeCassette(recordings_path)) tape_cassette.start() tape_recorder.tape_cassette = tape_cassette # This will activate the tape recorder in recording mode tape_recorder.enable_recording()