def test_all_finished_handlers_called_despite_one_failing(): handler_a = MagicMock(spec=ImportOperationHandler) handler_b = MagicMock(spec=ImportOperationHandler) def handle_finished_a(operation): # b is called after we are handler_b.on_import_finished.assert_not_called() # Our handler fails for some reason... raise ValueError('boom') def handle_finished_b(operation): # a has been called before us (and failed) assert handler_a.on_import_finished.call_count == 1 handler_a.on_import_finished.side_effect = handle_finished_a handler_b.on_import_finished.side_effect = handle_finished_b with handlers_attached(handler_a, handler_b): with pytest.raises(ImportOperationError) as excinfo: perform_import('foo', ImportType.FULL_SYNC, []) assert str(excinfo.value) == 'handler raised from on_import_finished()' assert handler_a.on_import_finished.call_count == 1 assert handler_b.on_import_finished.call_count == 1
def test_all_failed_handlers_called_despite_one_failing(): ''' When invoking failed handlers, all get invoked, even if a preceding failed handler itself raises an error. ''' handler_a = MagicMock(spec=ImportOperationHandler) handler_b = MagicMock(spec=ImportOperationHandler) def handle_failed_a(operation): # b is called after we are handler_b.on_import_failed.assert_not_called() # Our handler fails for some reason... raise ValueError('boom') def handle_failed_b(operation): # a has been called before us (and failed) assert handler_a.on_import_failed.call_count == 1 handler_a.on_import_failed.side_effect = handle_failed_a handler_b.on_import_failed.side_effect = handle_failed_b with handlers_attached(handler_a, handler_b): with pytest.raises(ImportOperationError) as excinfo: perform_import('foo', ImportType.FULL_SYNC, # Fails due to division by zero (1/0 for _ in [1])) assert str(excinfo.value) == 'record generator raised exception' assert handler_a.on_import_failed.call_count == 1 assert handler_b.on_import_failed.call_count == 1
def test_import_failed_called_when_record_is_invalid(mock_iop_handler): with pytest.raises(ImportOperationError): perform_import('foo', ImportType.FULL_SYNC, [object()]) assert mock_iop_handler.on_record_available.call_count == 0 assert mock_iop_handler.on_import_finished.call_count == 0 assert mock_iop_handler.on_import_failed.call_count == 1
def test_importop_has_record_and_import_types(record_type, import_type): with import_started_receiver(MagicMock()) as mock_handler: perform_import(record_type, import_type, []) _, kwargs = mock_handler.call_args iop = kwargs['sender'] assert iop.record_type is record_type assert iop.import_type is import_type
def test_import_failed_called_when_handler_raises(mock_iop_handler): mock_iop_handler.on_record_available.side_effect = ValueError('boom') with pytest.raises(ImportOperationError): perform_import('foo', ImportType.FULL_SYNC, [ ImportRecord([ID(1, 1)], 1) ]) assert mock_iop_handler.on_record_available.call_count == 1 assert mock_iop_handler.on_import_finished.call_count == 0 assert mock_iop_handler.on_import_failed.call_count == 1
def test_import_failed_called_when_record_generator_raises(mock_iop_handler): def records(): yield ImportRecord([ID(1, 1)], 1) raise ValueError('boom') with pytest.raises(ImportOperationError): perform_import('foo', ImportType.FULL_SYNC, records()) assert mock_iop_handler.on_record_available.call_count == 1 assert mock_iop_handler.on_import_finished.call_count == 0 assert mock_iop_handler.on_import_failed.call_count == 1
def test_handlers_get_call_to_finished_func_after_records(mock_iop_handler): def assert_finished_not_called(iop, record): mock_iop_handler.on_import_finished.assert_not_called() mock_iop_handler.on_record_available.side_effect = \ assert_finished_not_called perform_import('foo', ImportType.FULL_SYNC, [ImportRecord([ID(1, 1)], 1)]) assert mock_iop_handler.on_record_available.call_count == 1 assert mock_iop_handler.on_import_finished.call_count == 1
def test_records_are_provided_to_handlers_registered_with_importop( mock_iop_handler, records): # tee in case it's a one shot generator in_records, expected_records = itertools.tee(records) iop = perform_import('foo', ImportType.FULL_SYNC, in_records) mock_iop_handler.on_record_available.assert_has_calls([ call(iop, record) for record in expected_records])
def test_import_failed_called_when_import_started_receiver_raises( mock_iop_handler): ''' Registered handlers get an import failed event when an import_started receiver fails. ''' # Register another import_started receiver which will fail def failing_receiver(*args, **kwargs): raise ValueError('boom') with import_started_receiver(failing_receiver): with pytest.raises(ImportOperationError) as excinfo: perform_import('foo', ImportType.FULL_SYNC, []) assert ('import_started signal receiver raised exception' in str(excinfo.value)) assert mock_iop_handler.on_import_failed.call_count == 1
def test_records_must_be_importrecord_instances(): with pytest.raises(ImportOperationError) as exc_info: perform_import('foo', ImportType.FULL_SYNC, [object()]) assert isinstance(exc_info.value.__cause__, ValueError)
def test_import_type_must_be_importtype_instances(import_type): with pytest.raises(ValueError) as excinfo: perform_import('foo', import_type, []) assert 'import_type was not an ImportType' in str(excinfo.value)
def test_import_started_receiver_receives_importop_as_sender(): with import_started_receiver(MagicMock()) as mock_handler: iop = perform_import('foo', ImportType.FULL_SYNC, []) _, kwargs = mock_handler.call_args assert kwargs['sender'] is iop
def test_import_started_event_is_fired(): with import_started_receiver(MagicMock()) as mock_handler: perform_import('foo', ImportType.FULL_SYNC, []) assert mock_handler.called_once()
def test_import_failed_not_called_on_successful_imports(mock_iop_handler): perform_import('foo', ImportType.FULL_SYNC, []) assert mock_iop_handler.on_import_finished.call_count == 1 assert mock_iop_handler.on_import_failed.call_count == 0
def test_integration(): # A mock "database" of records db = dict() # The generator which def record_handler(import_operation, records): to_update = dict() to_remove = set() for record in records: fid = get_f_id(record) if record.is_deleted(): to_remove.add(fid) else: to_update[fid] = record.data # Yield is required to allow the next record to be made available # and allow other handlers to run. An exception is rail yield if import_operation.import_type is ImportType.FULL_SYNC: db.clear() else: for fid in to_remove: del db[fid] db.update(to_update) def import_listener(*args, sender=None, **kwargs): import_op = sender # We're handling the all-important foo records if import_op.record_type != 'foo': return handler = GeneratorImportOperationHandler(import_op, record_handler) import_op.attach_handler(handler) # Can use @receiver(import_started) decorator in normal app with import_started_receiver(import_listener): # Not a type we're interested in - these get ignored perform_import('bar', ImportType.FULL_SYNC, [ImportRecord([ID('x', 0)], object())]) assert len(db) == 0 perform_import('foo', ImportType.FULL_SYNC, [ ImportRecord([ID('f', 'a')], 'abc'), ImportRecord([ID('f', 'd')], 'def'), ]) assert db == {'a': 'abc', 'd': 'def'} # Update/delete just what's specified perform_import( 'foo', ImportType.PARTIAL_UPDATE, [ # Delete this ImportRecord([ID('f', 'd')], None), # Add these ImportRecord([ID('f', 'g')], 'ghi'), ImportRecord([ID('f', 'j')], 'jkl'), ]) assert db == {'a': 'abc', 'g': 'ghi', 'j': 'jkl'} # Replace everything perform_import('foo', ImportType.FULL_SYNC, [ ImportRecord([ID('f', 'm')], 'mno'), ImportRecord([ID('f', 'p')], 'pqr'), ]) assert db == {'m': 'mno', 'p': 'pqr'}