예제 #1
0
 def setUp(self):
     self.tape_cassette = InMemoryTapeCassette()
     self.tape_recorder = TapeRecorder(self.tape_cassette)
     self.tape_recorder.enable_recording()
예제 #2
0
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)
예제 #3
0
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())
예제 #4
0
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))