|
| 1 | +"""Each parallel worker gets its own database, and nothing else moves. |
| 2 | +
|
| 3 | +The gotcha this guards, per TESTING.md's core rule, is a measured one rather |
| 4 | +than a hypothetical. `conftest.py`'s isolation model is "drop every table |
| 5 | +between tests", which is safe exactly as long as one process is doing it. |
| 6 | +Pointed at one database, four `pytest -n` workers do not merely fail - they |
| 7 | +wedge, each blocking on locks held by tables another worker is partway |
| 8 | +through dropping. The run that found this took 1785s before it was killed, |
| 9 | +against 60s for the same suite serially. |
| 10 | +
|
| 11 | +The fix is a database per worker, and it is one line of URL rewriting, which |
| 12 | +is the kind of thing that looks obviously correct and silently stops |
| 13 | +happening. These tests are what make it stay true: the first three pin the |
| 14 | +rewrite, and the last one checks it actually took effect in the process |
| 15 | +running right now - because a rewrite that quietly became a no-op would put |
| 16 | +every worker back on one database and buy back the deadlock. |
| 17 | +""" |
| 18 | + |
| 19 | +from __future__ import annotations |
| 20 | + |
| 21 | +import os |
| 22 | + |
| 23 | +from sqlalchemy.engine import make_url |
| 24 | + |
| 25 | +from app.config import settings |
| 26 | +from tests.conftest import _worker_database_url |
| 27 | + |
| 28 | +SERIAL_URL = "postgresql+psycopg://ourhike:ourhike@localhost:5432/ourhike_test" |
| 29 | + |
| 30 | + |
| 31 | +def test_the_worker_name_lands_on_the_database_and_nowhere_else(): |
| 32 | + """Only the database changes - the server it is on must not. |
| 33 | +
|
| 34 | + Rewriting the wrong component is the failure that would not look like |
| 35 | + one: a URL pointing at a different host fails loudly, but one that |
| 36 | + silently kept the shared database name would pass this file's siblings |
| 37 | + and deadlock the moment somebody ran with `-n`. |
| 38 | + """ |
| 39 | + rewritten = make_url(_worker_database_url(SERIAL_URL, "gw0")) |
| 40 | + original = make_url(SERIAL_URL) |
| 41 | + |
| 42 | + assert rewritten.database == "ourhike_test_gw0" |
| 43 | + assert rewritten.host == original.host |
| 44 | + assert rewritten.port == original.port |
| 45 | + assert rewritten.username == original.username |
| 46 | + assert rewritten.password == original.password |
| 47 | + assert rewritten.drivername == original.drivername |
| 48 | + |
| 49 | + |
| 50 | +def test_two_workers_never_land_on_the_same_database(): |
| 51 | + """The invariant the whole change exists for, stated directly. |
| 52 | +
|
| 53 | + Everything else here is detail; this is the property that makes parallel |
| 54 | + running safe at all, so it is asserted on its own rather than left to be |
| 55 | + inferred from the naming test above. |
| 56 | + """ |
| 57 | + databases = {make_url(_worker_database_url(SERIAL_URL, f"gw{n}")).database for n in range(8)} |
| 58 | + |
| 59 | + assert len(databases) == 8 |
| 60 | + |
| 61 | + |
| 62 | +def test_a_url_carrying_no_database_is_still_rewritten_per_worker(): |
| 63 | + """CI passes DATABASE_URL in, so the input is not always the default. |
| 64 | +
|
| 65 | + A URL whose database is absent must not collapse every worker onto one |
| 66 | + name - that is the deadlock again, arriving through a code path nobody |
| 67 | + ran locally. |
| 68 | + """ |
| 69 | + without = "postgresql+psycopg://ourhike:ourhike@localhost:5432/" |
| 70 | + |
| 71 | + first = make_url(_worker_database_url(without, "gw0")).database |
| 72 | + second = make_url(_worker_database_url(without, "gw1")).database |
| 73 | + |
| 74 | + assert first != second |
| 75 | + |
| 76 | + |
| 77 | +def test_this_process_is_really_on_the_database_its_worker_was_given(): |
| 78 | + """Guards the guard: the rewrite above is wired up, not just correct. |
| 79 | +
|
| 80 | + The three tests above would all pass on a `conftest.py` that computed the |
| 81 | + per-worker URL and then never applied it, which is the shape this is here |
| 82 | + to catch. Serially there is no worker and the URL is the plain one, and |
| 83 | + asserting that is worth as much - it is what says the parallel path stays |
| 84 | + out of the way of an ordinary `pytest` run. |
| 85 | + """ |
| 86 | + worker = os.environ.get("PYTEST_XDIST_WORKER") |
| 87 | + database = make_url(settings.database_url).database or "" |
| 88 | + |
| 89 | + if worker: |
| 90 | + assert database.endswith(f"_{worker}") |
| 91 | + else: |
| 92 | + assert not database.startswith("ourhike_test_gw") |
0 commit comments