From 46773a39d357b25f4012745cc5c37cb544147222 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 26 May 2026 12:12:37 +0200 Subject: [PATCH 1/4] ci: add postgres service for procrastinate integration tests The integration_tests.yml workflow only provisioned Redis (for Celery), so the procrastinate tests added in #46 failed on every push to main with `psycopg_pool.PoolTimeout`. Adds a postgres:16 service container matching the DSN the tests default to, and sets PROCRASTINATE_DSN explicitly so the test config is self-documenting. Also picks up the uv.lock entry for taskbadger 2.1.0a1 that should have ridden along with #47. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/integration_tests.yml | 14 ++++++++++++++ uv.lock | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 9e6d23d..7dad695 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -17,6 +17,19 @@ jobs: --health-retries 5 ports: - 6379:6379 + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: procrastinate + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - uses: actions/checkout@v6 - name: Set up Python @@ -36,3 +49,4 @@ jobs: TASKBADGER_ORG: ${{ vars.TASKBADGER_ORG }} TASKBADGER_PROJECT: ${{ vars.TASKBADGER_PROJECT }} TASKBADGER_API_KEY: ${{ secrets.TASKBADGER_API_KEY }} + PROCRASTINATE_DSN: postgresql://postgres:postgres@localhost:5432/procrastinate diff --git a/uv.lock b/uv.lock index ee5c844..96d6953 100644 --- a/uv.lock +++ b/uv.lock @@ -1077,7 +1077,7 @@ wheels = [ [[package]] name = "taskbadger" -version = "2.0.0" +version = "2.1.0a1" source = { editable = "." } dependencies = [ { name = "attrs" }, From 5fedc8de7f31f599275b8607d21564d8bbde4a4a Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 26 May 2026 12:32:24 +0200 Subject: [PATCH 2/4] test(procrastinate): use async connector so run_worker can open it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SyncPsycopgConnector can defer and apply schema, but run_worker calls open_async() under the hood and raises SyncConnectorConfigurationError on sync connectors. Switching to PsycopgConnector — which docs say works in both sync and async contexts — keeps the fixture sync-friendly while making the worker happy. Also moves _fetch_job_args off the app's connector pool (now async) and opens its own psycopg connection for the read. Co-Authored-By: Claude Opus 4.7 (1M context) --- integration_tests/test_procrastinate.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/integration_tests/test_procrastinate.py b/integration_tests/test_procrastinate.py index 4463c0a..601c209 100644 --- a/integration_tests/test_procrastinate.py +++ b/integration_tests/test_procrastinate.py @@ -13,6 +13,7 @@ import random import procrastinate +import psycopg import pytest import taskbadger @@ -37,8 +38,9 @@ def _check_log_errors(caplog): @pytest.fixture(scope="session") def app(): - """A Procrastinate app pointed at a real Postgres instance with its schema applied.""" - conn = procrastinate.SyncPsycopgConnector(conninfo=PROCRASTINATE_DSN) + # Async connector even though the tests are sync: run_worker raises + # SyncConnectorConfigurationError on SyncPsycopgConnector. Async works in both contexts. + conn = procrastinate.PsycopgConnector(conninfo=PROCRASTINATE_DSN) app = procrastinate.App(connector=conn) with app.open(): # Apply schema (idempotent — Procrastinate's apply_schema is safe to re-run). @@ -46,9 +48,9 @@ def app(): yield app -def _fetch_job_args(app, job_id): - """Read the stored ``args`` JSONB for a Procrastinate job.""" - with app.connector.pool.connection() as conn: +def _fetch_job_args(job_id): + # Direct sync psycopg connection — the app's pool is async (see fixture). + with psycopg.connect(PROCRASTINATE_DSN) as conn: with conn.cursor() as cur: cur.execute("SELECT args FROM procrastinate_jobs WHERE id = %s", (job_id,)) row = cur.fetchone() @@ -75,7 +77,7 @@ def add_manual(a, b): # The TB task id was stashed in the job kwargs at defer time. Read it back # from Procrastinate to verify the final state. - args = _fetch_job_args(app, job_id) + args = _fetch_job_args(job_id) tb_id = args["__taskbadger_task_id__"] fetched = taskbadger.get_task(tb_id) @@ -100,7 +102,7 @@ def add_auto(a, b): listen_notify=False, ) - args = _fetch_job_args(app, job_id) + args = _fetch_job_args(job_id) tb_id = args["__taskbadger_task_id__"] fetched = taskbadger.get_task(tb_id) From f0917a69cd85e07181c4da9957ec7c61347fbd60 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 26 May 2026 12:36:21 +0200 Subject: [PATCH 3/4] test(procrastinate): scope app fixture per-test session-scope broke when run_worker's open_async() tore down the sync sub-connector PsycopgConnector creates lazily for sync defer(). The outer `with app.open():` didn't reopen it for the next test, so test 2's defer hit AppNotOpen. Function-scoped + idempotent apply_schema is the simplest reliable shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- integration_tests/test_procrastinate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/integration_tests/test_procrastinate.py b/integration_tests/test_procrastinate.py index 601c209..8b6d023 100644 --- a/integration_tests/test_procrastinate.py +++ b/integration_tests/test_procrastinate.py @@ -36,14 +36,17 @@ def _check_log_errors(caplog): pytest.fail(f"log errors during '{when}': {errors}") -@pytest.fixture(scope="session") +@pytest.fixture def app(): # Async connector even though the tests are sync: run_worker raises # SyncConnectorConfigurationError on SyncPsycopgConnector. Async works in both contexts. + # + # Function-scoped because run_worker tears down the sync sub-connector + # PsycopgConnector creates inside `app.open()`, and nothing reopens it for + # the next test's defer() call. Cheap: apply_schema is idempotent. conn = procrastinate.PsycopgConnector(conninfo=PROCRASTINATE_DSN) app = procrastinate.App(connector=conn) with app.open(): - # Apply schema (idempotent — Procrastinate's apply_schema is safe to re-run). app.schema_manager.apply_schema() yield app From 1290dc24039cbcdcad6deb5abda33adf5ed9910f Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 26 May 2026 12:39:04 +0200 Subject: [PATCH 4/4] test(procrastinate): skip apply_schema when schema already exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply_schema is NOT idempotent — procrastinate's schema.sql uses bare CREATE TYPE/CREATE TABLE, so re-running blows up with DuplicateObject on the second invocation (whether across tests in one run or across runs against a persistent DB). Split into two fixtures: a session-scoped _schema that does a one-time existence check + conditional apply, and the existing function-scoped app that opens/closes a fresh App per test. Co-Authored-By: Claude Opus 4.7 (1M context) --- integration_tests/test_procrastinate.py | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/integration_tests/test_procrastinate.py b/integration_tests/test_procrastinate.py index 8b6d023..0119f40 100644 --- a/integration_tests/test_procrastinate.py +++ b/integration_tests/test_procrastinate.py @@ -36,18 +36,31 @@ def _check_log_errors(caplog): pytest.fail(f"log errors during '{when}': {errors}") +@pytest.fixture(scope="session") +def _schema(): + # apply_schema is NOT idempotent (schema.sql uses bare CREATE TYPE), so + # only apply when the schema isn't already present. + with psycopg.connect(PROCRASTINATE_DSN) as conn, conn.cursor() as cur: + cur.execute("SELECT to_regclass('procrastinate_jobs')") + if cur.fetchone()[0] is not None: + return + schema_conn = procrastinate.SyncPsycopgConnector(conninfo=PROCRASTINATE_DSN) + schema_app = procrastinate.App(connector=schema_conn) + with schema_app.open(): + schema_app.schema_manager.apply_schema() + + @pytest.fixture -def app(): - # Async connector even though the tests are sync: run_worker raises - # SyncConnectorConfigurationError on SyncPsycopgConnector. Async works in both contexts. +def app(_schema): + # Async connector: run_worker raises SyncConnectorConfigurationError on + # SyncPsycopgConnector. Async connectors work in sync contexts too. # - # Function-scoped because run_worker tears down the sync sub-connector - # PsycopgConnector creates inside `app.open()`, and nothing reopens it for - # the next test's defer() call. Cheap: apply_schema is idempotent. + # Function-scoped because run_worker tears down the sync sub-connector that + # PsycopgConnector spawns inside `app.open()`, leaving the next test's + # defer() with no usable sync pool. conn = procrastinate.PsycopgConnector(conninfo=PROCRASTINATE_DSN) app = procrastinate.App(connector=conn) with app.open(): - app.schema_manager.apply_schema() yield app