def test_transaction(caplog: _pytest.logging.LogCaptureFixture, logger: gluetool.log.ContextAdapter, db: DB) -> None: """ Test whether :py:func:`transaction` behaves correctly when wrapping non-conflicting queries. """ with db.get_session(transactional=True) as session: update = tft.artemis.tasks._guest_state_update_query( 'dummy-guest', GuestState.PROVISIONING, current_state=GuestState.ROUTING).unwrap() insert = sqlalchemy.insert( GuestEvent.__table__).values( # type: ignore[attr-defined] updated=datetime.datetime.utcnow(), guestname='dummy-guest', eventname='dummy-event') with transaction() as r: session.execute(update) session.execute(insert) assert r.success is True requests = SafeQuery.from_session(session, GuestRequest).all().unwrap() assert len(requests) == 1 # TODO: cast shouldn't be needed, sqlalchemy should annouce .state as enum - maybe with more recent stubs? assert cast(GuestState, requests[0].state) == GuestState.PROVISIONING events = SafeQuery.from_session(session, GuestEvent).all().unwrap() assert len(events) == 1 assert events[0].guestname == 'dummy-guest' assert events[0].eventname == 'dummy-event'
def test_session_autocommit_active_only(db: DB, mock_session: MagicMock) -> None: mock_session.transaction.is_active = False with db.get_session(): pass mock_session.commit.assert_not_called() mock_session.close.assert_called_once()
def test_run_doer_exception(logger: gluetool.log.ContextAdapter, db: tft.artemis.db.DB, cancel: threading.Event) -> None: def foo(_logger: gluetool.log.ContextAdapter, _db: tft.artemis.db.DB, _session: sqlalchemy.orm.session.Session, _cancel: threading.Event) -> tft.artemis.tasks.DoerReturnType: raise Exception('foo') with db.get_session() as session: with pytest.raises(Exception, match=r'foo'): tft.artemis.tasks.run_doer(logger, db, session, cancel, cast(tft.artemis.tasks.DoerType, foo), 'test_run_doer_exception')
def test_run_doer(logger: gluetool.log.ContextAdapter, db: tft.artemis.db.DB, cancel: threading.Event) -> None: def foo(_logger: gluetool.log.ContextAdapter, _db: tft.artemis.db.DB, _session: sqlalchemy.orm.session.Session, _cancel: threading.Event, bar: str) -> tft.artemis.tasks.DoerReturnType: assert bar == '79' return tft.artemis.tasks.RESCHEDULE with db.get_session() as session: assert tft.artemis.tasks.run_doer( logger, db, session, cancel, cast(tft.artemis.tasks.DoerType, foo), 'test_run_doer', '79') == tft.artemis.tasks.RESCHEDULE
def test_session_autorollback(db: DB, mock_session: MagicMock) -> None: mock_exception = ValueError('Exception<mock>') try: with db.get_session(): raise mock_exception except Exception as exc: assert exc is mock_exception mock_session.commit.assert_not_called() mock_session.rollback.assert_called_once() mock_session.close.assert_called_once()
def test_session_autocommit(db: DB, mock_session: MagicMock) -> None: with db.get_session() as session: assert session is mock_session mock_session.commit.assert_called_once() mock_session.close.assert_called_once()
def test_session(db: DB) -> None: with db.get_session() as session: assert hasattr(session, 'commit')
def test_transaction_conflict(caplog: _pytest.logging.LogCaptureFixture, logger: gluetool.log.ContextAdapter, db: DB, session: sqlalchemy.orm.session.Session) -> None: """ Test whether :py:func:`transaction` intercepts and reports transaction rollback. """ with db.get_session(transactional=True) as session2, db.get_session( transactional=True) as session3: update1 = tft.artemis.tasks._guest_state_update_query( 'dummy-guest', GuestState.PROVISIONING, current_state=GuestState.ROUTING).unwrap() update2 = tft.artemis.tasks._guest_state_update_query( 'dummy-guest', GuestState.PROMISED, current_state=GuestState.ROUTING).unwrap() insert1 = sqlalchemy.insert( GuestEvent.__table__).values( # type: ignore[attr-defined] updated=datetime.datetime.utcnow(), guestname='dummy-guest', eventname='dummy-event') insert2 = sqlalchemy.insert( GuestEvent.__table__).values( # type: ignore[attr-defined] updated=datetime.datetime.utcnow(), guestname='dummy-guest', eventname='another-dummy-event') # To create conflict, we must "initialize" view of both sessions, by executing a query. This will setup # their initial knowledge - without this step, the second transaction wouldn't run into any conflict because # it would issue its first query when the first transaction has been already committed. # # Imagine two tasks, both loading guest request from DB, then making some decisions, eventually both # trying to change it. The initial DB query sets the stage for both transactions seeing the same DB # state, and only one is allowed to modify the records both touched. SafeQuery.from_session(session2, GuestRequest).all() SafeQuery.from_session(session3, GuestRequest).all() with transaction() as r1: session2.execute(update1) session2.execute(insert1) session2.commit( ) # type: ignore[no-untyped-call] # TODO: untyped commit()?? assert r1.success is True with transaction() as r2: session3.execute(update2) session3.execute(insert2) session2.commit( ) # type: ignore[no-untyped-call] # TODO: untyped commit()?? assert r2.success is False requests = SafeQuery.from_session(session, GuestRequest).all().unwrap() assert len(requests) == 1 # TODO: cast shouldn't be needed, sqlalchemy should annouce .state as enum - maybe with more recent stubs? assert cast(GuestState, requests[0].state) == GuestState.PROVISIONING events = SafeQuery.from_session(session, GuestEvent).all().unwrap() assert len(events) == 1 assert events[0].guestname == 'dummy-guest' assert events[0].eventname == 'dummy-event'