async def test_finish_commit(raise_exception, tmp_path):
    """
    Tests that the session is automatically committed if and only if the context was not
    exited with an exception.

    """
    db_path = tmp_path / "test.db"
    engine = create_engine(f"sqlite:///{db_path}", poolclass=NullPool)
    with engine.begin() as connection:
        connection.execute(text("CREATE TABLE foo (id INTEGER PRIMARY KEY)"))

        component = SQLAlchemyComponent(url={
            "drivername": "sqlite",
            "database": str(db_path)
        }, )
        with ExitStack() as stack:
            async with Context() as ctx:
                await component.start(ctx)
                session = ctx.require_resource(Session)
                session.execute(text("INSERT INTO foo (id) VALUES(3)"))
                if raise_exception:
                    stack.enter_context(pytest.raises(Exception,
                                                      match="dummy"))
                    raise Exception("dummy")

        rows = connection.execute(text("SELECT * FROM foo")).fetchall()
        assert len(rows) == (0 if raise_exception else 1)
Exemple #2
0
async def test_multiple_engines():
    component = SQLAlchemyComponent(engines={'db1': {}, 'db2': {}}, url='sqlite:///:memory:')
    async with Context() as ctx:
        await component.start(ctx)

        engine1 = ctx.require_resource(Engine, 'db1')
        engine2 = ctx.require_resource(Engine, 'db2')
        assert ctx.db1.bind is engine1
        assert ctx.db2.bind is engine2
async def test_component_start_async():
    """Test that the component creates all the expected (asynchronous) resources."""
    url = URL.create("sqlite+aiosqlite", database=":memory:")
    component = SQLAlchemyComponent(url=url)
    async with Context() as ctx:
        await component.start(ctx)

        ctx.require_resource(AsyncEngine)
        ctx.require_resource(sessionmaker)
        ctx.require_resource(AsyncSession)
Exemple #4
0
async def test_bind():
    """Test that a Connection can be passed as "bind" in place of "url"."""
    engine = create_engine('sqlite:///:memory:')
    connection = engine.connect()
    component = SQLAlchemyComponent(bind=connection)
    async with Context() as ctx:
        await component.start(ctx)

        assert ctx.require_resource(Engine) is engine
        assert ctx.sql.bind is connection
Exemple #5
0
async def test_component_start(poolclass):
    """Test that the component creates all the expected resources."""
    component = SQLAlchemyComponent(url='sqlite:///:memory:', poolclass=poolclass)
    async with Context() as ctx:
        await component.start(ctx)

        engine = ctx.require_resource(Engine)
        ctx.require_resource(sessionmaker)
        assert ctx.sql is ctx.require_resource(Session)
        assert ctx.sql.bind is engine
Exemple #6
0
async def test_memory_leak():
    """Test that creating a session in a context does not leak memory."""
    component = SQLAlchemyComponent(url='sqlite:///:memory:')
    async with Context() as ctx:
        await component.start(ctx)
        assert isinstance(ctx.sql, Session)

    del ctx
    gc.collect()  # needed on PyPy
    assert next((x for x in gc.get_objects() if isinstance(x, Context)), None) is None
async def test_bind_async():
    """Test that a Connection can be passed as "bind" in place of "url"."""
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    connection = await engine.connect()
    component = SQLAlchemyComponent(bind=connection)
    async with Context() as ctx:
        await component.start(ctx)

        assert ctx.require_resource(AsyncEngine) is engine
        assert ctx.require_resource(AsyncSession).bind is connection

    await connection.close()
async def test_close_twice_async(asyncpg_url):
    """Test that closing a session releases connection resources, but remains usable."""
    component = SQLAlchemyComponent(url=asyncpg_url)
    async with Context() as ctx:
        await component.start(ctx)
        session = ctx.require_resource(AsyncSession)
        pool = session.bind.pool
        await session.execute(text("SELECT 1"))
        assert pool.checkedout() == 1
        await session.close()
        assert pool.checkedout() == 0
        await session.execute(text("SELECT 1"))
        assert pool.checkedout() == 1

    assert pool.checkedout() == 0
Exemple #9
0
async def test_ready_callback(asynchronous):
    def ready_callback(engine, factory):
        nonlocal engine2, factory2
        engine2 = engine
        factory2 = factory

    async def ready_callback_async(engine, factory):
        nonlocal engine2, factory2
        engine2 = engine
        factory2 = factory

    engine2 = factory2 = None
    callback = ready_callback_async if asynchronous else ready_callback
    component = SQLAlchemyComponent(url='sqlite:///:memory:', ready_callback=callback)
    async with Context() as ctx:
        await component.start(ctx)

        engine = ctx.require_resource(Engine)
        factory = ctx.require_resource(sessionmaker)
        assert engine is engine2
        assert factory is factory2
Exemple #10
0
async def test_finish_commit(raise_exception, executor, commit_executor, tmpdir):
    """
    Tests that the session is automatically committed if and only if the context was not exited
    with an exception.

    """
    db_path = tmpdir.join('test.db')
    engine = create_engine('sqlite:///%s' % db_path, poolclass=NullPool)
    engine.execute('CREATE TABLE foo (id INTEGER PRIMARY KEY)')

    component = SQLAlchemyComponent(
        url={'drivername': 'sqlite', 'database': str(db_path)},
        commit_executor=executor if commit_executor == 'instance' else commit_executor)
    ctx = Context()
    ctx.add_resource(executor, types=[Executor])
    await component.start(ctx)
    ctx.sql.execute('INSERT INTO foo (id) VALUES(3)')
    await ctx.close(Exception('dummy') if raise_exception else None)

    rows = engine.execute('SELECT * FROM foo').fetchall()
    assert len(rows) == (0 if raise_exception else 1)
async def test_session_event_async(request, asyncpg_url, psycopg2_url):
    """Test that creating a session in a context does not leak memory."""
    listener_session: Session
    listener_thread: Thread

    def listener(session: Session) -> None:
        nonlocal listener_session, listener_thread
        try:
            async_session = get_resource(AsyncSession)
        except NoCurrentContext:
            return

        if async_session and session is async_session.sync_session:
            listener_session = session
            listener_thread = current_thread()

    listen(Session, "before_commit", listener)
    request.addfinalizer(lambda: remove(Session, "before_commit", listener))
    component = SQLAlchemyComponent(url=asyncpg_url)
    async with Context() as ctx:
        await component.start(ctx)
        dbsession = ctx.require_resource(AsyncSession)
        await dbsession.run_sync(
            lambda session: Person.metadata.create_all(session.bind))
        dbsession.add(Person(name="Test person"))

    assert listener_session is dbsession.sync_session
    assert listener_thread is current_thread()

    engine = create_engine(psycopg2_url)
    with Session(engine) as sess:
        sess.add(Person(name="Test person 2"))
        sess.commit()

    engine.dispose()

    assert listener_session is dbsession.sync_session
    assert listener_thread is current_thread()
async def test_session_event_sync(psycopg2_url):
    """Test that creating a session in a context does not leak memory."""
    listener_session: Session
    listener_thread: Thread

    def listener(session: Session) -> None:
        nonlocal listener_session, listener_thread
        current_context()
        listener_session = session
        listener_thread = current_thread()

    component = SQLAlchemyComponent(url=psycopg2_url)
    async with Context() as ctx:
        await component.start(ctx)
        Person.metadata.create_all(ctx.require_resource(Engine))
        session_factory = ctx.require_resource(sessionmaker)
        listen(session_factory, "before_commit", listener)

        dbsession = ctx.require_resource(Session)
        dbsession.add(Person(name="Test person"))

    assert listener_session is dbsession
    assert listener_thread != current_thread()
def connect_test_database(url: Union[str, URL], **engine_kwargs) -> Connection:
    """
    Connect to the given database and drop any existing tables in it.

    For SQLite URLs pointing to a file, the target database file will be deleted and a new one is
    created in its place.

    :param url: connection URL for the database
    :param engine_kwargs: additional keyword arguments passed to
        :meth:`asphalt.sqlalchemy.component.SQLAlchemyComponent.create_engine`
    :return: a connection object

    """
    assert check_argument_types()
    _context_attr, engine = SQLAlchemyComponent.configure_engine(url=url, **engine_kwargs)

    if engine.dialect.name == 'sqlite':
        # SQLite does not support dropping constraints and it's faster to just delete the file
        if engine.url.database not in (None, ':memory:') and os.path.isfile(engine.url.database):
            os.remove(engine.url.database)

        connection = engine.connect()
    else:
        # Reflect the schema to get the list of the tables and constraints left over from the
        # previous run
        connection = engine.connect()
        metadata = MetaData(connection, reflect=True)

        # Drop all the foreign key constraints so we can drop the tables in any order
        for table in metadata.tables.values():
            for fk in table.foreign_keys:
                connection.execute(DropConstraint(fk.constraint))

        # Drop the tables
        metadata.drop_all()

    return connection