diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1601c1c6d..cc75e63bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: shell: bash -l {0} run: | set -vxeuo pipefail - coverage run -m pytest -v -m "not slow" + coverage run -m pytest -v coverage report env: # Provide test suite with a PostgreSQL database to use. diff --git a/continuous_integration/scripts/download_sqlite_data.sh b/continuous_integration/scripts/download_sqlite_data.sh new file mode 100755 index 000000000..c496f5134 --- /dev/null +++ b/continuous_integration/scripts/download_sqlite_data.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +curl -fsSLO https://github.com/bluesky/tiled-example-database/releases/latest/download/tiled_test_db_sqlite.db diff --git a/continuous_integration/scripts/start_postgres.sh b/continuous_integration/scripts/start_postgres.sh index 6d4a5a24d..467a72f14 100755 --- a/continuous_integration/scripts/start_postgres.sh +++ b/continuous_integration/scripts/start_postgres.sh @@ -1,5 +1,8 @@ #!/bin/bash set -e -docker run -d --rm --name tiled-test-postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret -d docker.io/postgres +# Try and get the latest postgres test data from the tiled-example-database repository +curl -fsSLO https://github.com/bluesky/tiled-example-database/releases/latest/download/tiled_test_db_pg.sql + +docker run -d --rm --name tiled-test-postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=tiled-example-data -v ./tiled_test_db_pg.sql:/docker-entrypoint-initdb.d/tiled_test_db_pg.sql -d docker.io/postgres:16 docker ps diff --git a/pytest.ini b/pytest.ini index 61306c81b..18a858942 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,6 +5,3 @@ log_cli = 1 log_cli_level = WARNING log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) log_cli_date_format=%Y-%m-%d %H:%M:%S -addopts = --strict-markers -m 'not slow' -markers = - slow: marks tests as slow (deselect with '-m "not slow"') diff --git a/tiled/_tests/conftest.py b/tiled/_tests/conftest.py index f9aa778d2..05be2a1ff 100644 --- a/tiled/_tests/conftest.py +++ b/tiled/_tests/conftest.py @@ -3,8 +3,10 @@ import tempfile from pathlib import Path +import asyncpg import pytest import pytest_asyncio +from sqlalchemy.ext.asyncio import create_async_engine from .. import profiles from ..catalog import from_uri, in_memory @@ -97,7 +99,7 @@ def poll_enumerate(): # To test with postgres, start a container like: # -# docker run --name tiled-test-postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret -d docker.io/postgres +# docker run --name tiled-test-postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret -d docker.io/postgres:16 # and set this env var like: # # TILED_TEST_POSTGRESQL_URI=postgresql+asyncpg://postgres:secret@localhost:5432 @@ -105,29 +107,105 @@ def poll_enumerate(): TILED_TEST_POSTGRESQL_URI = os.getenv("TILED_TEST_POSTGRESQL_URI") -@pytest_asyncio.fixture(params=["sqlite", "postgresql"]) -async def adapter(request, tmpdir): +@pytest_asyncio.fixture +async def postgresql_adapter(request, tmpdir): """ Adapter instance Note that startup() and shutdown() are not called, and must be run either manually (as in the fixture 'a') or via the app (as in the fixture 'client'). """ - if request.param == "sqlite": - adapter = in_memory(writable_storage=str(tmpdir)) + if not TILED_TEST_POSTGRESQL_URI: + raise pytest.skip("No TILED_TEST_POSTGRESQL_URI configured") + # Create temporary database. + async with temp_postgres(TILED_TEST_POSTGRESQL_URI) as uri_with_database_name: + # Build an adapter on it, and initialize the database. + adapter = from_uri( + uri_with_database_name, + writable_storage=str(tmpdir), + init_if_not_exists=True, + ) yield adapter - elif request.param == "postgresql": - if not TILED_TEST_POSTGRESQL_URI: - raise pytest.skip("No TILED_TEST_POSTGRESQL_URI configured") - # Create temporary database. - async with temp_postgres(TILED_TEST_POSTGRESQL_URI) as uri_with_database_name: - # Build an adapter on it, and initialize the database. - adapter = from_uri( - uri_with_database_name, - writable_storage=str(tmpdir), - init_if_not_exists=True, - ) - yield adapter - await adapter.shutdown() - else: - assert False + + +@pytest_asyncio.fixture +async def postgresql_with_example_data_adapter(request, tmpdir): + """ + Adapter instance + + Note that startup() and shutdown() are not called, and must be run + either manually (as in the fixture 'a') or via the app (as in the fixture 'client'). + """ + if not TILED_TEST_POSTGRESQL_URI: + raise pytest.skip("No TILED_TEST_POSTGRESQL_URI configured") + DATABASE_NAME = "tiled-example-data" + uri = TILED_TEST_POSTGRESQL_URI + if uri.endswith("/"): + uri = uri[:-1] + uri_with_database_name = f"{uri}/{DATABASE_NAME}" + engine = create_async_engine(uri_with_database_name) + try: + async with engine.connect(): + pass + except asyncpg.exceptions.InvalidCatalogNameError: + raise pytest.skip( + f"PostgreSQL instance contains no database named {DATABASE_NAME!r}" + ) + adapter = from_uri( + uri_with_database_name, + writable_storage=str(tmpdir), + ) + yield adapter + + +@pytest_asyncio.fixture +async def sqlite_adapter(request, tmpdir): + """ + Adapter instance + + Note that startup() and shutdown() are not called, and must be run + either manually (as in the fixture 'a') or via the app (as in the fixture 'client'). + """ + yield in_memory(writable_storage=str(tmpdir)) + + +@pytest_asyncio.fixture +async def sqlite_with_example_data_adapter(request, tmpdir): + """ + Adapter instance + + Note that startup() and shutdown() are not called, and must be run + either manually (as in the fixture 'a') or via the app (as in the fixture 'client'). + """ + SQLITE_DATABASE_PATH = Path("./tiled_test_db_sqlite.db") + if not SQLITE_DATABASE_PATH.is_file(): + raise pytest.skip(f"Could not find {SQLITE_DATABASE_PATH}") + adapter = from_uri( + f"sqlite+aiosqlite:///{SQLITE_DATABASE_PATH}", + writable_storage=str(tmpdir), + ) + yield adapter + + +@pytest.fixture(params=["sqlite_adapter", "postgresql_adapter"]) +def adapter(request): + """ + Adapter instance + + Note that startup() and shutdown() are not called, and must be run + either manually (as in the fixture 'a') or via the app (as in the fixture 'client'). + """ + yield request.getfixturevalue(request.param) + + +@pytest.fixture( + params=["sqlite_with_example_data_adapter", "postgresql_with_example_data_adapter"] +) +def example_data_adapter(request): + """ + Adapter instance + + Note that startup() and shutdown() are not called, and must be run + either manually (as in the fixture 'a') or via the app (as in the fixture 'client'). + """ + yield request.getfixturevalue(request.param) diff --git a/tiled/_tests/test_catalog.py b/tiled/_tests/test_catalog.py index 53559ee65..edae9f552 100644 --- a/tiled/_tests/test_catalog.py +++ b/tiled/_tests/test_catalog.py @@ -156,24 +156,14 @@ async def test_search(a): assert await d.search(Eq("number", 12)).keys_range(0, 5) == ["c"] -@pytest.mark.slow @pytest.mark.asyncio -async def test_metadata_index_is_used(a): - for i in range(10000): - await a.create_node( - metadata={ - "number": i, - "number_as_string": str(i), - "nested": {"number": i, "number_as_string": str(i), "bool": bool(i)}, - "bool": bool(i), - }, - specs=[], - structure_family="array", - ) - # Check that an index (specifically the 'top_level_metdata' index) is used +async def test_metadata_index_is_used(example_data_adapter): + a = example_data_adapter # for succinctness below + # Check that an index (specifically the 'top_level_metadata' index) is used # by inspecting the content of an 'EXPLAIN ...' query. The exact content # is intended for humans and is not an API, but we can coarsely check # that the index of interest is mentioned. + await a.startup() with record_explanations() as e: results = await a.search(Key("number_as_string") == "3").keys_range(0, 5) assert len(results) == 1 @@ -200,6 +190,7 @@ async def test_metadata_index_is_used(a): ) assert len(results) == 1 assert "top_level_metadata" in str(e) + await a.shutdown() @pytest.mark.asyncio