Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ PROJECT_NAME ?= $(notdir $(abspath .))
INFRA_SERVICES ?= db_pg
INFRA_INIT_SERVICES ?=
MIGRATION_DB_SERVICE ?= db_pg
STAIRWAY_TEST ?= tests/integration/with_infra/test_stairway.py
STAIRWAY_TEST ?= tests/integration/migrations/test_stairway.py

# -----------------------------
# Internal vars / aliases
Expand Down Expand Up @@ -44,10 +44,12 @@ PYTEST_PATHS_LIGHT := \
tests/sanity \
tests/unit \
tests/integration/no_infra
PYTEST_PATHS_ALL := \
PYTEST_PATHS_APP_INFRA := \
$(PYTEST_PATHS_LIGHT) \
tests/smoke \
tests/integration/with_infra
PYTEST_PATHS_MIGRATIONS := \
tests/integration/migrations

# Pytest args
PYTEST_ARGS_VERBOSE := -s -vv
Expand Down Expand Up @@ -130,14 +132,14 @@ stop-all:
# Migrations
.PHONY: migration
migration: local-env
PROJECT_NAME=$(PROJECT_NAME) \
MIGRATION_DB_SERVICE=$(MIGRATION_DB_SERVICE) \
INFRA_INIT_SERVICES="$(INFRA_INIT_SERVICES)" \
STAIRWAY_TEST=$(STAIRWAY_TEST) \
$(MIGRATION) "$(msg)"

# Tests (with infra)
.PHONY: test-docker
test-docker: docker-env
.PHONY: test-docker test-docker-app test-docker-migrations
test-docker-app: docker-env
rc=0; \
$(DC_TEST_DOCKER) down -v --remove-orphans >/dev/null 2>&1 || true; \
if [ -n "$(strip $(INFRA_SERVICES))" ]; then \
Expand All @@ -150,16 +152,36 @@ test-docker: docker-env
fi; \
$(DC_TEST_DOCKER) run --build --name $(TEST_RUNNER) app \
pytest $(PYTEST_ARGS_VERBOSE) \
$(PYTEST_PATHS_ALL) \
$(PYTEST_PATHS_APP_INFRA) \
$(PYTEST_ARGS_COV_DOCKER) \
|| rc=$$?; \
docker cp $(TEST_RUNNER):/tmp/.coverage ./.coverage.docker 2>/dev/null || true; \
docker rm $(TEST_RUNNER) >/dev/null 2>&1 || true; \
$(DC_TEST_DOCKER) down -v --remove-orphans; \
coverage html --data-file=.coverage.docker -d htmlcov-docker && \
echo "Coverage HTML report: htmlcov-docker/index.html" || true; \
exit $$rc

test-docker-migrations: docker-env
if [ -z "$(strip $(PYTEST_PATHS_MIGRATIONS))" ] || [ -z "$(strip $(MIGRATION_DB_SERVICE))" ]; then \
echo "PYTEST_PATHS_MIGRATIONS or MIGRATION_DB_SERVICE is empty, skipping migrations tests"; \
exit 0; \
fi; \
rc=0; \
$(DC_TEST_DOCKER) down -v --remove-orphans >/dev/null 2>&1 || true; \
$(DC_TEST_DOCKER) up -d --build --wait --wait-timeout 180 $(MIGRATION_DB_SERVICE); \
$(DC_TEST_DOCKER) run --build --no-deps --name $(TEST_RUNNER) app \
pytest $(PYTEST_ARGS_VERBOSE) \
$(PYTEST_PATHS_MIGRATIONS) \
|| rc=$$?; \
docker rm $(TEST_RUNNER) >/dev/null 2>&1 || true; \
$(DC_TEST_DOCKER) down -v --remove-orphans; \
exit $$rc

test-docker:
$(MAKE) test-docker-app
$(MAKE) test-docker-migrations
coverage html --data-file=.coverage.docker -d htmlcov-docker && \
echo "Coverage HTML report: htmlcov-docker/index.html" || true

.PHONY: prune
prune:
$(DOCKER_PRUNE)
Expand Down
6 changes: 3 additions & 3 deletions scripts/makefile/migration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ if [ "$#" -ne 1 ] || [ -z "$1" ]; then
fi
slug="$1"

: "${PROJECT_NAME:?must be set in Makefile}"
MIGRATION_PROJECT="$(basename "$PWD")-migration"
: "${MIGRATION_DB_SERVICE:?must be set in Makefile (transactional db service for alembic)}"

trap 'docker compose -p "$PROJECT_NAME" stop "$MIGRATION_DB_SERVICE" >/dev/null' EXIT
trap 'docker compose -p "$MIGRATION_PROJECT" down -v --remove-orphans >/dev/null' EXIT

docker compose -p "$PROJECT_NAME" up -d --build --wait --wait-timeout 180 "$MIGRATION_DB_SERVICE"
docker compose -p "$MIGRATION_PROJECT" up -d --build --wait --wait-timeout 180 "$MIGRATION_DB_SERVICE"
alembic upgrade head
alembic revision --autogenerate -m "$slug"

Expand Down
18 changes: 18 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os
from typing import Final

import pytest

ALLOW_DESTRUCTIVE_TEST_CLEANUP: Final[str] = "ALLOW_DESTRUCTIVE_TEST_CLEANUP"
ALLOW_DESTRUCTIVE_TEST_CLEANUP_EXPECTED_VALUE: Final[str] = "1"


@pytest.fixture(scope="session")
def allow_destructive() -> None:
"""Use on fixtures that require potentially dangerous cleanup."""
if os.getenv(ALLOW_DESTRUCTIVE_TEST_CLEANUP) != ALLOW_DESTRUCTIVE_TEST_CLEANUP_EXPECTED_VALUE:
raise pytest.UsageError(
"Destructive cleanup is disabled: "
f"{ALLOW_DESTRUCTIVE_TEST_CLEANUP} must be set to {ALLOW_DESTRUCTIVE_TEST_CLEANUP_EXPECTED_VALUE}. "
"This guard prevents accidental cleanup of non-test data."
)
Empty file.
18 changes: 3 additions & 15 deletions tests/integration/with_infra/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
from collections.abc import AsyncIterator, Sequence
from typing import Final, cast

Expand All @@ -16,19 +15,6 @@
from app.outbound.persistence_sqla.registry import mapper_registry

LIFESPAN_MANAGER_STARTUP_TIMEOUT_S: Final[int] = 30
ALLOW_DESTRUCTIVE_TEST_CLEANUP: Final[str] = "ALLOW_DESTRUCTIVE_TEST_CLEANUP"
ALLOW_DESTRUCTIVE_TEST_CLEANUP_EXPECTED_VALUE: Final[str] = "1"


@pytest.fixture(scope="session")
def allow_destructive() -> None:
"""Use on fixtures that require potentially dangerous cleanup."""
if os.getenv(ALLOW_DESTRUCTIVE_TEST_CLEANUP) != ALLOW_DESTRUCTIVE_TEST_CLEANUP_EXPECTED_VALUE:
raise pytest.UsageError(
"Destructive cleanup is disabled: "
f"{ALLOW_DESTRUCTIVE_TEST_CLEANUP} must be set to {ALLOW_DESTRUCTIVE_TEST_CLEANUP_EXPECTED_VALUE}. "
"This guard prevents accidental cleanup of non-test data."
)


@pytest.fixture
Expand Down Expand Up @@ -79,7 +65,9 @@ async def it_db_clean(
it_sessionmaker: async_sessionmaker[AsyncSession],
) -> None:
table_names = [table.name for table in mapper_registry.metadata.sorted_tables if table.name != "alembic_version"]
assert table_names, "it_db_clean: no tables found in mapper_registry.metadata (fixture is a no-op)"
if not table_names:
return

sql = "TRUNCATE " + ", ".join(f'"{name}"' for name in table_names) + " RESTART IDENTITY CASCADE;"

async with it_sessionmaker() as session:
Expand Down