def test_single_processor(self): mock_processor = MagicMock() router = RoutingBackend(processors=[ mock_processor ]) router.send(self.sample_event) mock_processor.assert_called_once_with(self.sample_event)
def test_single_backend(self): mock_backend = MagicMock() router = RoutingBackend(backends={ 'mock0': mock_backend }) router.send(self.sample_event) mock_backend.send.assert_called_once_with(self.sample_event)
def test_backend_failure(self): backends = {str(i): MagicMock() for i in range(5)} backends['1'].send.side_effect = RuntimeError router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in backends.itervalues(): backend.send.assert_called_once_with(self.sample_event)
def test_multiple_processors(self): processors = [MagicMock() for __ in range(5)] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event)
def test_multiple_backends(self): backends = { str(i): MagicMock() for i in range(5) } router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event)
def test_backend_failure(self): backends = { str(i): MagicMock() for i in range(5) } backends['1'].send.side_effect = RuntimeError router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in six.itervalues(backends): backend.send.assert_called_once_with(self.sample_event)
def test_multiple_processors(self): processors = [ MagicMock() for __ in range(5) ] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event)
def test_multiple_backends_and_processors(self): backends = {str(i): MagicMock() for i in range(5)} processors = [MagicMock() for __ in range(5)] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(backends=backends, processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event)
def test_multiple_backends_and_processors(self): backends = { str(i): MagicMock() for i in range(5) } processors = [ MagicMock() for __ in range(5) ] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(backends=backends, processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event)
def test_nested_routing_with_abort(self): mock_abort_processing = MagicMock() mock_abort_processing.side_effect = EventEmissionExit left_backend = MagicMock() left_router = RoutingBackend(backends={'0': left_backend}, processors=[mock_abort_processing]) right_backend = MagicMock() right_router = RoutingBackend(backends={'0': right_backend}) root_router = RoutingBackend(backends={ 'left': left_router, 'right': right_router, }) root_router.send(self.sample_event) right_backend.send.assert_called_once_with(self.sample_event) self.assertEqual(len(left_backend.mock_calls), 0) mock_abort_processing.assert_called_once_with(self.sample_event)
def test_backend_call_order(self): class OrderRecordingBackend(object): """Keep track of the order that the backends are called in""" def __init__(self, name, call_order): self._name = name self._order = call_order def send(self, event): # pylint: disable=unused-argument """Do nothing except record that this was called""" self._order.append(self._name) call_order = [] backends = { str(i): OrderRecordingBackend(str(i), call_order) for i in range(5) } router = RoutingBackend(backends=backends) router.send(self.sample_event) self.assertEqual(call_order, ['0', '1', '2', '3', '4'])
def test_backend_without_send(self): with self.assertRaisesRegexp( # pylint: disable=deprecated-method,useless-suppression ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={'a': object()})
def setUp(self): super(TestRoutingBackend, self).setUp() self.sample_event = {'name': sentinel.name} self.mock_backend = MagicMock() self.router = RoutingBackend(backends={'0': self.mock_backend})
def test_non_callable_processor_simple_type(self): with self.assertRaisesRegexp(ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=['b'])
def test_non_callable_processor(self): with self.assertRaisesRegexp(ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=[object()])
def test_backend_without_send(self): with self.assertRaisesRegexp( ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={'a': object()})
def test_non_callable_backend(self): with self.assertRaisesRegexp( ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={'a': 'b'})
def setUp(self): self.sample_event = {'name': sentinel.name} self.mock_backend = MagicMock() self.router = RoutingBackend(backends={'0': self.mock_backend})
class TestRoutingBackend(TestCase): """Test the routing backend""" def setUp(self): self.sample_event = {'name': sentinel.name} self.mock_backend = MagicMock() self.router = RoutingBackend(backends={'0': self.mock_backend}) def test_non_callable_backend(self): with self.assertRaisesRegexp( ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={'a': 'b'}) def test_backend_without_send(self): with self.assertRaisesRegexp( ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={'a': object()}) def test_non_callable_processor(self): with self.assertRaisesRegexp(ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=[object()]) def test_non_callable_processor_simple_type(self): with self.assertRaisesRegexp(ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=['b']) def test_single_processor(self): mock_processor = MagicMock() router = RoutingBackend(processors=[mock_processor]) router.send(self.sample_event) mock_processor.assert_called_once_with(self.sample_event) def test_single_backend(self): mock_backend = MagicMock() router = RoutingBackend(backends={'mock0': mock_backend}) router.send(self.sample_event) mock_backend.send.assert_called_once_with(self.sample_event) def test_multiple_backends(self): backends = {str(i): MagicMock() for i in range(5)} router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event) def test_backend_failure(self): backends = {str(i): MagicMock() for i in range(5)} backends['1'].send.side_effect = RuntimeError router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in backends.itervalues(): backend.send.assert_called_once_with(self.sample_event) def test_multiple_processors(self): processors = [MagicMock() for __ in range(5)] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event) def test_multiple_backends_and_processors(self): backends = {str(i): MagicMock() for i in range(5)} processors = [MagicMock() for __ in range(5)] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(backends=backends, processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event) def test_callable_class_processor(self): class SampleProcessor(object): """An event processing class""" def __call__(self, event): """Modify the event type""" event['name'] = sentinel.changed_name self.router.register_processor(SampleProcessor()) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.changed_name}) def assert_single_event_emitted(self, event): """Assert that the mock backend is called exactly once with the provided event""" self.mock_backend.send.assert_called_once_with(event) def test_function_processor(self): def change_name(event): """Modify the event type of the event""" event['name'] = sentinel.changed_name return event self.router.register_processor(change_name) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.changed_name}) def test_processor_chain(self): def change_name(event): """Modify the event type of the event""" event['name'] = sentinel.changed_name return event def inject_fields(event): """Add a couple fields to the event""" event['other'] = sentinel.other event['to_remove'] = sentinel.to_remove return event def remove_field(event): """Remove a field to the event""" self.assertEquals(event['to_remove'], sentinel.to_remove) del event['to_remove'] return event def ensure_modified_event(event): """Assert the first processor added a field to the event""" self.assertEquals(event['name'], sentinel.changed_name) self.assertEquals(event['other'], sentinel.other) return event self.router.register_processor(change_name) self.router.register_processor(inject_fields) self.router.register_processor(remove_field) self.router.register_processor(ensure_modified_event) self.router.send(self.sample_event) self.assert_single_event_emitted({ 'name': sentinel.changed_name, 'other': sentinel.other }) def test_processor_failure(self): def always_fail(event): # pylint: disable=unused-argument """Always raises an error""" raise ValueError def change_name(event): """Modify the event type of the event""" event['name'] = sentinel.changed_name return event self.router.register_processor(always_fail) self.router.register_processor(change_name) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.changed_name}) def test_processor_returns_none(self): def return_none(event): # pylint: disable=unused-argument """Don't return the event""" pass self.router.register_processor(return_none) self.router.send(self.sample_event) self.assert_single_event_emitted(self.sample_event) def test_processor_modifies_the_same_event_object(self): def forget_return(event): """Modify the event without returning it""" event['name'] = sentinel.forgotten_return def ensure_name_changed(event): """Assert the event type has been modified even though the event wasn't returned""" self.assertEquals(event['name'], sentinel.forgotten_return) self.router.register_processor(forget_return) self.router.register_processor(ensure_name_changed) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.forgotten_return}) def test_processor_abort(self): def abort_processing(event): # pylint: disable=unused-argument """Always abort processing""" raise EventEmissionExit def fail_if_called(event): # pylint: disable=unused-argument """Fail the test immediately if this is called""" self.fail('This processor should never be called') self.router.register_processor(abort_processing) self.router.register_processor(fail_if_called) self.router.send(self.sample_event) self.assertEqual(len(self.mock_backend.mock_calls), 0) def test_nested_routing_with_abort(self): mock_abort_processing = MagicMock() mock_abort_processing.side_effect = EventEmissionExit left_backend = MagicMock() left_router = RoutingBackend(backends={'0': left_backend}, processors=[mock_abort_processing]) right_backend = MagicMock() right_router = RoutingBackend(backends={'0': right_backend}) root_router = RoutingBackend(backends={ 'left': left_router, 'right': right_router, }) root_router.send(self.sample_event) right_backend.send.assert_called_once_with(self.sample_event) self.assertEqual(len(left_backend.mock_calls), 0) mock_abort_processing.assert_called_once_with(self.sample_event) def test_backend_call_order(self): class OrderRecordingBackend(object): """Keep track of the order that the backends are called in""" def __init__(self, name, call_order): self._name = name self._order = call_order def send(self, event): # pylint: disable=unused-argument """Do nothing except record that this was called""" self._order.append(self._name) call_order = [] backends = { str(i): OrderRecordingBackend(str(i), call_order) for i in range(5) } router = RoutingBackend(backends=backends) router.send(self.sample_event) self.assertEqual(call_order, ['0', '1', '2', '3', '4'])
def __init__(self, backends=None, context_locator=None, processors=None): self.routing_backend = RoutingBackend(backends=backends, processors=processors) self.context_locator = context_locator or DefaultContextLocator()
class Tracker(object): """ Track application events. Holds references to a set of backends that will be used to persist any events that are emitted. """ def __init__(self, backends=None, context_locator=None, processors=None): self.routing_backend = RoutingBackend(backends=backends, processors=processors) self.context_locator = context_locator or DefaultContextLocator() @property def located_context(self): """ The thread local context for this tracker. """ return self.context_locator.get() def get_backend(self, name): """Gets the backend that was configured with `name`""" return self.backends[name] @property def processors(self): """The list of registered processors""" return self.routing_backend.processors @property def backends(self): """The dictionary of registered backends""" return self.routing_backend.backends def emit(self, name=None, data=None): """ Emit an event annotated with the UTC time when this function was called. `name` is a unique identification string for an event that has already been registered. `data` is a dictionary mapping field names to the value to include in the event. Note that all values provided must be serializable. """ event = { 'name': name or UNKNOWN_EVENT_TYPE, 'timestamp': datetime.now(UTC), 'data': data or {}, 'context': self.resolve_context() } self.routing_backend.send(event) def resolve_context(self): """ Create a new dictionary that corresponds to the union of all of the contexts that have been entered but not exited at this point. """ merged = dict() for context in self.located_context.values(): merged.update(context) return merged def enter_context(self, name, ctx): """ Enter a named context. Any events emitted after calling this method will contain all of the key-value pairs included in `ctx` unless overridden by a context that is entered after this call. """ self.located_context[name] = ctx def exit_context(self, name): """ Exit a named context. This will remove all key-value pairs associated with this context from any events emitted after it is removed. """ del self.located_context[name] @contextmanager def context(self, name, ctx): """ Execute the block with the given context applied. This manager ensures that the context is removed even if an exception is raised within the context. """ self.enter_context(name, ctx) try: yield finally: self.exit_context(name)
def test_non_callable_processor_simple_type(self): with self.assertRaisesRegexp( # pylint: disable=deprecated-method,useless-suppression ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=['b'])
class TestRoutingBackend(TestCase): """Test the routing backend""" def setUp(self): super(TestRoutingBackend, self).setUp() self.sample_event = {'name': sentinel.name} self.mock_backend = MagicMock() self.router = RoutingBackend(backends={'0': self.mock_backend}) def test_non_callable_backend(self): with self.assertRaisesRegexp( # pylint: disable=deprecated-method,useless-suppression ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={ 'a': 'b' }) def test_backend_without_send(self): with self.assertRaisesRegexp( # pylint: disable=deprecated-method,useless-suppression ValueError, r'Backend \w+ does not have a callable "send" method.'): RoutingBackend(backends={ 'a': object() }) def test_non_callable_processor(self): with self.assertRaisesRegexp( # pylint: disable=deprecated-method,useless-suppression ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=[ object() ]) def test_non_callable_processor_simple_type(self): with self.assertRaisesRegexp( # pylint: disable=deprecated-method,useless-suppression ValueError, r'Processor \w+ is not callable.'): RoutingBackend(processors=[ 'b' ]) def test_single_processor(self): mock_processor = MagicMock() router = RoutingBackend(processors=[ mock_processor ]) router.send(self.sample_event) mock_processor.assert_called_once_with(self.sample_event) def test_single_backend(self): mock_backend = MagicMock() router = RoutingBackend(backends={ 'mock0': mock_backend }) router.send(self.sample_event) mock_backend.send.assert_called_once_with(self.sample_event) def test_multiple_backends(self): backends = { str(i): MagicMock() for i in range(5) } router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event) def test_backend_failure(self): backends = { str(i): MagicMock() for i in range(5) } backends['1'].send.side_effect = RuntimeError router = RoutingBackend(backends=backends) router.send(self.sample_event) for backend in six.itervalues(backends): backend.send.assert_called_once_with(self.sample_event) def test_multiple_processors(self): processors = [ MagicMock() for __ in range(5) ] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event) def test_multiple_backends_and_processors(self): backends = { str(i): MagicMock() for i in range(5) } processors = [ MagicMock() for __ in range(5) ] for processor in processors: processor.return_value = self.sample_event router = RoutingBackend(backends=backends, processors=processors) router.send(self.sample_event) for processor in processors: processor.assert_called_once_with(self.sample_event) for backend in backends.values(): backend.send.assert_called_once_with(self.sample_event) def test_callable_class_processor(self): class SampleProcessor(object): """An event processing class""" def __call__(self, event): """Modify the event type""" event['name'] = sentinel.changed_name self.router.register_processor(SampleProcessor()) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.changed_name}) def assert_single_event_emitted(self, event): """Assert that the mock backend is called exactly once with the provided event""" self.mock_backend.send.assert_called_once_with(event) def test_function_processor(self): def change_name(event): """Modify the event type of the event""" event['name'] = sentinel.changed_name return event self.router.register_processor(change_name) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.changed_name}) def test_processor_chain(self): def change_name(event): """Modify the event type of the event""" event['name'] = sentinel.changed_name return event def inject_fields(event): """Add a couple fields to the event""" event['other'] = sentinel.other event['to_remove'] = sentinel.to_remove return event def remove_field(event): """Remove a field to the event""" self.assertEqual(event['to_remove'], sentinel.to_remove) del event['to_remove'] return event def ensure_modified_event(event): """Assert the first processor added a field to the event""" self.assertEqual(event['name'], sentinel.changed_name) self.assertEqual(event['other'], sentinel.other) return event self.router.register_processor(change_name) self.router.register_processor(inject_fields) self.router.register_processor(remove_field) self.router.register_processor(ensure_modified_event) self.router.send(self.sample_event) self.assert_single_event_emitted( { 'name': sentinel.changed_name, 'other': sentinel.other } ) def test_processor_failure(self): def always_fail(event): # pylint: disable=unused-argument """Always raises an error""" raise ValueError def change_name(event): """Modify the event type of the event""" event['name'] = sentinel.changed_name return event self.router.register_processor(always_fail) self.router.register_processor(change_name) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.changed_name}) def test_processor_returns_none(self): def return_none(event): # pylint: disable=unused-argument """Don't return the event""" pass self.router.register_processor(return_none) self.router.send(self.sample_event) self.assert_single_event_emitted(self.sample_event) def test_processor_modifies_the_same_event_object(self): def forget_return(event): """Modify the event without returning it""" event['name'] = sentinel.forgotten_return def ensure_name_changed(event): """Assert the event type has been modified even though the event wasn't returned""" self.assertEqual(event['name'], sentinel.forgotten_return) self.router.register_processor(forget_return) self.router.register_processor(ensure_name_changed) self.router.send(self.sample_event) self.assert_single_event_emitted({'name': sentinel.forgotten_return}) def test_processor_abort(self): def abort_processing(event): # pylint: disable=unused-argument """Always abort processing""" raise EventEmissionExit def fail_if_called(event): # pylint: disable=unused-argument """Fail the test immediately if this is called""" self.fail('This processor should never be called') self.router.register_processor(abort_processing) self.router.register_processor(fail_if_called) self.router.send(self.sample_event) self.assertEqual(len(self.mock_backend.mock_calls), 0) def test_nested_routing_with_abort(self): mock_abort_processing = MagicMock() mock_abort_processing.side_effect = EventEmissionExit left_backend = MagicMock() left_router = RoutingBackend(backends={'0': left_backend}, processors=[mock_abort_processing]) right_backend = MagicMock() right_router = RoutingBackend(backends={'0': right_backend}) root_router = RoutingBackend(backends={ 'left': left_router, 'right': right_router, }) root_router.send(self.sample_event) right_backend.send.assert_called_once_with(self.sample_event) self.assertEqual(len(left_backend.mock_calls), 0) mock_abort_processing.assert_called_once_with(self.sample_event) def test_backend_call_order(self): class OrderRecordingBackend(object): """Keep track of the order that the backends are called in""" def __init__(self, name, call_order): self._name = name self._order = call_order def send(self, event): # pylint: disable=unused-argument """Do nothing except record that this was called""" self._order.append(self._name) call_order = [] backends = { str(i): OrderRecordingBackend(str(i), call_order) for i in range(5) } router = RoutingBackend(backends=backends) router.send(self.sample_event) self.assertEqual(call_order, ['0', '1', '2', '3', '4'])