pytest-neon 2.0.0__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/PKG-INFO +18 -8
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/README.md +17 -7
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/pyproject.toml +1 -1
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/src/pytest_neon/__init__.py +1 -1
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/src/pytest_neon/plugin.py +106 -6
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_reset_behavior.py +49 -0
- pytest_neon-2.1.1/tests/test_xdist_worker_support.py +189 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/uv.lock +1 -1
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/.env.example +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/.github/workflows/release.yml +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/.github/workflows/tests.yml +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/.gitignore +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/.neon +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/CLAUDE.md +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/LICENSE +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/src/pytest_neon/py.typed +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/conftest.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_branch_lifecycle.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_cli_options.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_env_var.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_fixture_errors.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_integration.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_migrations.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_readwrite_readonly_fixtures.py +0 -0
- {pytest_neon-2.0.0 → pytest_neon-2.1.1}/tests/test_skip_behavior.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-neon
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Pytest plugin for Neon database branch isolation in tests
|
|
5
5
|
Project-URL: Homepage, https://github.com/ZainRizvi/pytest-neon
|
|
6
6
|
Project-URL: Repository, https://github.com/ZainRizvi/pytest-neon
|
|
@@ -489,16 +489,26 @@ The `neon_branch_readwrite` fixture uses Neon's branch restore API to reset data
|
|
|
489
489
|
|
|
490
490
|
This is similar to database transactions but at the branch level.
|
|
491
491
|
|
|
492
|
-
##
|
|
492
|
+
## Parallel Test Execution (pytest-xdist)
|
|
493
493
|
|
|
494
|
-
|
|
494
|
+
This plugin supports parallel test execution with [pytest-xdist](https://pytest-xdist.readthedocs.io/). Each xdist worker automatically gets its own isolated branch.
|
|
495
495
|
|
|
496
|
-
|
|
496
|
+
```bash
|
|
497
|
+
# Run tests in parallel with 4 workers
|
|
498
|
+
pip install pytest-xdist
|
|
499
|
+
pytest -n 4
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**How it works:**
|
|
503
|
+
- Each xdist worker (gw0, gw1, gw2, etc.) creates its own branch
|
|
504
|
+
- Branches are named with the worker ID suffix (e.g., `-test-gw0`, `-test-gw1`)
|
|
505
|
+
- Workers run tests in parallel without database state interference
|
|
506
|
+
- All branches are cleaned up after the test session
|
|
497
507
|
|
|
498
|
-
|
|
499
|
-
-
|
|
500
|
-
-
|
|
501
|
-
-
|
|
508
|
+
**Cost implications:**
|
|
509
|
+
- Running with `-n 4` creates 4 branches (one per worker) plus the migration branch
|
|
510
|
+
- Choose your parallelism level based on your Neon plan's branch limits
|
|
511
|
+
- Each worker's branch is reset after each test using the fast reset operation (~0.5s)
|
|
502
512
|
|
|
503
513
|
## Troubleshooting
|
|
504
514
|
|
|
@@ -444,16 +444,26 @@ The `neon_branch_readwrite` fixture uses Neon's branch restore API to reset data
|
|
|
444
444
|
|
|
445
445
|
This is similar to database transactions but at the branch level.
|
|
446
446
|
|
|
447
|
-
##
|
|
447
|
+
## Parallel Test Execution (pytest-xdist)
|
|
448
448
|
|
|
449
|
-
|
|
449
|
+
This plugin supports parallel test execution with [pytest-xdist](https://pytest-xdist.readthedocs.io/). Each xdist worker automatically gets its own isolated branch.
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
```bash
|
|
452
|
+
# Run tests in parallel with 4 workers
|
|
453
|
+
pip install pytest-xdist
|
|
454
|
+
pytest -n 4
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**How it works:**
|
|
458
|
+
- Each xdist worker (gw0, gw1, gw2, etc.) creates its own branch
|
|
459
|
+
- Branches are named with the worker ID suffix (e.g., `-test-gw0`, `-test-gw1`)
|
|
460
|
+
- Workers run tests in parallel without database state interference
|
|
461
|
+
- All branches are cleaned up after the test session
|
|
452
462
|
|
|
453
|
-
|
|
454
|
-
-
|
|
455
|
-
-
|
|
456
|
-
-
|
|
463
|
+
**Cost implications:**
|
|
464
|
+
- Running with `-n 4` creates 4 branches (one per worker) plus the migration branch
|
|
465
|
+
- Choose your parallelism level based on your Neon plan's branch limits
|
|
466
|
+
- Each worker's branch is reset after each test using the fast reset operation (~0.5s)
|
|
457
467
|
|
|
458
468
|
## Troubleshooting
|
|
459
469
|
|
|
@@ -56,6 +56,17 @@ DEFAULT_BRANCH_EXPIRY_SECONDS = 600
|
|
|
56
56
|
_MIGRATIONS_NOT_DEFINED = object()
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def _get_xdist_worker_id() -> str:
|
|
60
|
+
"""
|
|
61
|
+
Get the pytest-xdist worker ID, or "main" if not running under xdist.
|
|
62
|
+
|
|
63
|
+
When running tests in parallel with pytest-xdist, each worker process
|
|
64
|
+
gets a unique ID (gw0, gw1, gw2, etc.). This is used to create separate
|
|
65
|
+
branches per worker to avoid database state pollution between parallel tests.
|
|
66
|
+
"""
|
|
67
|
+
return os.environ.get("PYTEST_XDIST_WORKER", "main")
|
|
68
|
+
|
|
69
|
+
|
|
59
70
|
def _get_schema_fingerprint(connection_string: str) -> tuple[tuple[Any, ...], ...]:
|
|
60
71
|
"""
|
|
61
72
|
Get a fingerprint of the database schema for change detection.
|
|
@@ -385,7 +396,8 @@ def _reset_branch_to_parent(
|
|
|
385
396
|
"""Reset a branch to its parent's state using the Neon API.
|
|
386
397
|
|
|
387
398
|
Uses exponential backoff retry logic to handle transient API errors
|
|
388
|
-
that can occur during parallel test execution.
|
|
399
|
+
that can occur during parallel test execution. After initiating the
|
|
400
|
+
restore, polls the operation status until it completes.
|
|
389
401
|
|
|
390
402
|
Args:
|
|
391
403
|
branch: The branch to reset
|
|
@@ -395,7 +407,10 @@ def _reset_branch_to_parent(
|
|
|
395
407
|
if not branch.parent_id:
|
|
396
408
|
raise RuntimeError(f"Branch {branch.branch_id} has no parent - cannot reset")
|
|
397
409
|
|
|
398
|
-
|
|
410
|
+
base_url = "https://console.neon.tech/api/v2"
|
|
411
|
+
project_id = branch.project_id
|
|
412
|
+
branch_id = branch.branch_id
|
|
413
|
+
restore_url = f"{base_url}/projects/{project_id}/branches/{branch_id}/restore"
|
|
399
414
|
headers = {
|
|
400
415
|
"Authorization": f"Bearer {api_key}",
|
|
401
416
|
"Content-Type": "application/json",
|
|
@@ -405,12 +420,27 @@ def _reset_branch_to_parent(
|
|
|
405
420
|
for attempt in range(max_retries + 1):
|
|
406
421
|
try:
|
|
407
422
|
response = requests.post(
|
|
408
|
-
|
|
423
|
+
restore_url,
|
|
409
424
|
headers=headers,
|
|
410
425
|
json={"source_branch_id": branch.parent_id},
|
|
411
426
|
timeout=30,
|
|
412
427
|
)
|
|
413
428
|
response.raise_for_status()
|
|
429
|
+
|
|
430
|
+
# The restore API returns operations that run asynchronously.
|
|
431
|
+
# We must wait for operations to complete before the next test
|
|
432
|
+
# starts, otherwise connections may fail during the restore.
|
|
433
|
+
data = response.json()
|
|
434
|
+
operations = data.get("operations", [])
|
|
435
|
+
|
|
436
|
+
if operations:
|
|
437
|
+
_wait_for_operations(
|
|
438
|
+
project_id=branch.project_id,
|
|
439
|
+
operations=operations,
|
|
440
|
+
headers=headers,
|
|
441
|
+
base_url=base_url,
|
|
442
|
+
)
|
|
443
|
+
|
|
414
444
|
return # Success
|
|
415
445
|
except requests.RequestException as e:
|
|
416
446
|
last_error = e
|
|
@@ -423,6 +453,64 @@ def _reset_branch_to_parent(
|
|
|
423
453
|
raise last_error # type: ignore[misc]
|
|
424
454
|
|
|
425
455
|
|
|
456
|
+
def _wait_for_operations(
|
|
457
|
+
project_id: str,
|
|
458
|
+
operations: list[dict[str, Any]],
|
|
459
|
+
headers: dict[str, str],
|
|
460
|
+
base_url: str,
|
|
461
|
+
max_wait_seconds: float = 60,
|
|
462
|
+
poll_interval: float = 0.5,
|
|
463
|
+
) -> None:
|
|
464
|
+
"""Wait for Neon operations to complete.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
project_id: The Neon project ID
|
|
468
|
+
operations: List of operation dicts from the API response
|
|
469
|
+
headers: HTTP headers including auth
|
|
470
|
+
base_url: Base URL for Neon API
|
|
471
|
+
max_wait_seconds: Maximum time to wait (default: 60s)
|
|
472
|
+
poll_interval: Time between polls (default: 0.5s)
|
|
473
|
+
"""
|
|
474
|
+
# Get operation IDs that aren't already finished
|
|
475
|
+
pending_op_ids = [
|
|
476
|
+
op["id"] for op in operations if op.get("status") not in ("finished", "skipped")
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
if not pending_op_ids:
|
|
480
|
+
return # All operations already complete
|
|
481
|
+
|
|
482
|
+
waited = 0.0
|
|
483
|
+
while pending_op_ids and waited < max_wait_seconds:
|
|
484
|
+
time.sleep(poll_interval)
|
|
485
|
+
waited += poll_interval
|
|
486
|
+
|
|
487
|
+
# Check status of each pending operation
|
|
488
|
+
still_pending = []
|
|
489
|
+
for op_id in pending_op_ids:
|
|
490
|
+
op_url = f"{base_url}/projects/{project_id}/operations/{op_id}"
|
|
491
|
+
try:
|
|
492
|
+
response = requests.get(op_url, headers=headers, timeout=10)
|
|
493
|
+
response.raise_for_status()
|
|
494
|
+
op_data = response.json().get("operation", {})
|
|
495
|
+
status = op_data.get("status")
|
|
496
|
+
|
|
497
|
+
if status == "error":
|
|
498
|
+
err = op_data.get("error", "unknown error")
|
|
499
|
+
raise RuntimeError(f"Operation {op_id} failed: {err}")
|
|
500
|
+
if status not in ("finished", "skipped"):
|
|
501
|
+
still_pending.append(op_id)
|
|
502
|
+
except requests.RequestException:
|
|
503
|
+
# On network error, assume still pending and retry
|
|
504
|
+
still_pending.append(op_id)
|
|
505
|
+
|
|
506
|
+
pending_op_ids = still_pending
|
|
507
|
+
|
|
508
|
+
if pending_op_ids:
|
|
509
|
+
raise RuntimeError(
|
|
510
|
+
f"Timeout waiting for operations to complete: {pending_op_ids}"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
426
514
|
@pytest.fixture(scope="session")
|
|
427
515
|
def _neon_migration_branch(
|
|
428
516
|
request: pytest.FixtureRequest,
|
|
@@ -527,6 +615,12 @@ def _neon_branch_for_reset(
|
|
|
527
615
|
session, avoiding issues with Python's module caching (e.g., SQLAlchemy
|
|
528
616
|
engines created at import time would otherwise point to stale branches).
|
|
529
617
|
|
|
618
|
+
Parallel Test Support (pytest-xdist):
|
|
619
|
+
When running tests in parallel with pytest-xdist, each worker gets its
|
|
620
|
+
own branch. This prevents database state pollution between tests running
|
|
621
|
+
concurrently on different workers. The worker ID is included in the
|
|
622
|
+
branch name suffix (e.g., "-test-gw0", "-test-gw1").
|
|
623
|
+
|
|
530
624
|
Smart Migration Detection:
|
|
531
625
|
This fixture implements a cost-optimization strategy:
|
|
532
626
|
|
|
@@ -558,15 +652,21 @@ def _neon_branch_for_reset(
|
|
|
558
652
|
# Assume migrations changed something to be safe
|
|
559
653
|
schema_changed = True
|
|
560
654
|
|
|
655
|
+
# Get worker ID for parallel test support
|
|
656
|
+
# Each xdist worker gets its own branch to avoid state pollution
|
|
657
|
+
worker_id = _get_xdist_worker_id()
|
|
658
|
+
branch_suffix = f"-test-{worker_id}"
|
|
659
|
+
|
|
561
660
|
# Only create a child branch if migrations actually modified the schema
|
|
562
|
-
if
|
|
661
|
+
# OR if we're running under xdist (each worker needs its own branch)
|
|
662
|
+
if schema_changed or worker_id != "main":
|
|
563
663
|
yield from _create_neon_branch(
|
|
564
664
|
request,
|
|
565
665
|
parent_branch_id_override=_neon_migration_branch.branch_id,
|
|
566
|
-
branch_name_suffix=
|
|
666
|
+
branch_name_suffix=branch_suffix,
|
|
567
667
|
)
|
|
568
668
|
else:
|
|
569
|
-
# No schema changes - reuse the migration branch directly
|
|
669
|
+
# No schema changes and not parallel - reuse the migration branch directly
|
|
570
670
|
# This saves creating an unnecessary branch
|
|
571
671
|
yield _neon_migration_branch
|
|
572
672
|
|
|
@@ -21,6 +21,8 @@ class TestResetRetryBehavior:
|
|
|
21
21
|
# Mock requests.post to fail twice, then succeed
|
|
22
22
|
mock_response = mocker.Mock()
|
|
23
23
|
mock_response.raise_for_status = mocker.Mock()
|
|
24
|
+
# Return empty operations list (already complete)
|
|
25
|
+
mock_response.json.return_value = {"operations": []}
|
|
24
26
|
|
|
25
27
|
import requests
|
|
26
28
|
|
|
@@ -103,6 +105,8 @@ class TestResetRetryBehavior:
|
|
|
103
105
|
|
|
104
106
|
mock_response = mocker.Mock()
|
|
105
107
|
mock_response.raise_for_status = mocker.Mock()
|
|
108
|
+
# Return empty operations list (already complete)
|
|
109
|
+
mock_response.json.return_value = {"operations": []}
|
|
106
110
|
mock_post = mocker.patch(
|
|
107
111
|
"pytest_neon.plugin.requests.post", return_value=mock_response
|
|
108
112
|
)
|
|
@@ -113,6 +117,51 @@ class TestResetRetryBehavior:
|
|
|
113
117
|
assert mock_post.call_count == 1
|
|
114
118
|
assert mock_sleep.call_count == 0
|
|
115
119
|
|
|
120
|
+
def test_reset_waits_for_operations_to_complete(self, mocker):
|
|
121
|
+
"""Verify reset polls operation status until complete."""
|
|
122
|
+
branch = NeonBranch(
|
|
123
|
+
branch_id="br-test",
|
|
124
|
+
project_id="proj-test",
|
|
125
|
+
connection_string="postgresql://test",
|
|
126
|
+
host="test.neon.tech",
|
|
127
|
+
parent_id="br-parent",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Mock POST response with pending operation
|
|
131
|
+
mock_post_response = mocker.Mock()
|
|
132
|
+
mock_post_response.raise_for_status = mocker.Mock()
|
|
133
|
+
mock_post_response.json.return_value = {
|
|
134
|
+
"operations": [{"id": "op-123", "status": "running"}]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Mock GET responses: first running, then finished
|
|
138
|
+
get_call_count = [0]
|
|
139
|
+
|
|
140
|
+
def mock_get(*args, **kwargs):
|
|
141
|
+
get_call_count[0] += 1
|
|
142
|
+
mock_get_response = mocker.Mock()
|
|
143
|
+
mock_get_response.raise_for_status = mocker.Mock()
|
|
144
|
+
if get_call_count[0] < 3:
|
|
145
|
+
mock_get_response.json.return_value = {
|
|
146
|
+
"operation": {"id": "op-123", "status": "running"}
|
|
147
|
+
}
|
|
148
|
+
else:
|
|
149
|
+
mock_get_response.json.return_value = {
|
|
150
|
+
"operation": {"id": "op-123", "status": "finished"}
|
|
151
|
+
}
|
|
152
|
+
return mock_get_response
|
|
153
|
+
|
|
154
|
+
mocker.patch(
|
|
155
|
+
"pytest_neon.plugin.requests.post", return_value=mock_post_response
|
|
156
|
+
)
|
|
157
|
+
mocker.patch("pytest_neon.plugin.requests.get", side_effect=mock_get)
|
|
158
|
+
mocker.patch("pytest_neon.plugin.time.sleep")
|
|
159
|
+
|
|
160
|
+
_reset_branch_to_parent(branch, "fake-api-key")
|
|
161
|
+
|
|
162
|
+
# Should have polled until finished
|
|
163
|
+
assert get_call_count[0] == 3
|
|
164
|
+
|
|
116
165
|
|
|
117
166
|
class TestResetBehavior:
|
|
118
167
|
"""Test that branch reset happens between tests."""
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Tests for pytest-xdist parallel worker support."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestXdistBranchIsolation:
|
|
5
|
+
"""Test that parallel workers get separate branches."""
|
|
6
|
+
|
|
7
|
+
def test_xdist_worker_creates_branch_even_without_migrations(
|
|
8
|
+
self, pytester, monkeypatch
|
|
9
|
+
):
|
|
10
|
+
"""Even without schema changes, xdist workers should get their own branch."""
|
|
11
|
+
monkeypatch.setenv("PYTEST_XDIST_WORKER", "gw0")
|
|
12
|
+
|
|
13
|
+
pytester.makeconftest(
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import pytest
|
|
17
|
+
from pytest_neon.plugin import (
|
|
18
|
+
NeonBranch,
|
|
19
|
+
_get_xdist_worker_id,
|
|
20
|
+
_MIGRATIONS_NOT_DEFINED,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
branch_creation_calls = []
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(scope="session")
|
|
26
|
+
def _neon_migration_branch(request):
|
|
27
|
+
# Store empty fingerprint to simulate no psycopg available
|
|
28
|
+
request.config._neon_pre_migration_fingerprint = ()
|
|
29
|
+
|
|
30
|
+
return NeonBranch(
|
|
31
|
+
branch_id="br-migration-123",
|
|
32
|
+
project_id="proj-mock",
|
|
33
|
+
connection_string="postgresql://mock:mock@migration.neon.tech/mockdb",
|
|
34
|
+
host="migration.neon.tech",
|
|
35
|
+
parent_id="br-parent-000",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(scope="session")
|
|
39
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
40
|
+
# Return sentinel to simulate NO migrations defined
|
|
41
|
+
return _MIGRATIONS_NOT_DEFINED
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="session")
|
|
44
|
+
def _neon_branch_for_reset(
|
|
45
|
+
request, _neon_migration_branch, neon_apply_migrations
|
|
46
|
+
):
|
|
47
|
+
# Replicate the real logic
|
|
48
|
+
sentinel = _MIGRATIONS_NOT_DEFINED
|
|
49
|
+
migrations_defined = neon_apply_migrations is not sentinel
|
|
50
|
+
fingerprint_key = "_neon_pre_migration_fingerprint"
|
|
51
|
+
pre_fp = getattr(request.config, fingerprint_key, ())
|
|
52
|
+
schema_changed = False
|
|
53
|
+
|
|
54
|
+
if migrations_defined and pre_fp:
|
|
55
|
+
schema_changed = False # Simplified
|
|
56
|
+
elif migrations_defined and not pre_fp:
|
|
57
|
+
schema_changed = True
|
|
58
|
+
|
|
59
|
+
worker_id = _get_xdist_worker_id()
|
|
60
|
+
suffix = f"-test-{worker_id}"
|
|
61
|
+
|
|
62
|
+
# Key: create branch even without schema changes when xdist
|
|
63
|
+
if schema_changed or worker_id != "main":
|
|
64
|
+
branch_creation_calls.append(f"created-branch{suffix}")
|
|
65
|
+
conn = f"postgresql://mock:mock@t{suffix}.neon.tech/db"
|
|
66
|
+
branch_info = NeonBranch(
|
|
67
|
+
branch_id=f"br-test{suffix}",
|
|
68
|
+
project_id="proj-mock",
|
|
69
|
+
connection_string=conn,
|
|
70
|
+
host=f"t{suffix}.neon.tech",
|
|
71
|
+
parent_id=_neon_migration_branch.branch_id,
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
branch_creation_calls.append("reused-migration-branch")
|
|
75
|
+
branch_info = _neon_migration_branch
|
|
76
|
+
|
|
77
|
+
os.environ["DATABASE_URL"] = branch_info.connection_string
|
|
78
|
+
try:
|
|
79
|
+
yield branch_info
|
|
80
|
+
finally:
|
|
81
|
+
os.environ.pop("DATABASE_URL", None)
|
|
82
|
+
|
|
83
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
84
|
+
def verify_branch_created():
|
|
85
|
+
yield
|
|
86
|
+
# Under xdist, should create a new branch
|
|
87
|
+
assert branch_creation_calls == ["created-branch-test-gw0"]
|
|
88
|
+
"""
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
pytester.makepyfile(
|
|
92
|
+
"""
|
|
93
|
+
def test_xdist_creates_branch(_neon_branch_for_reset):
|
|
94
|
+
# Just trigger the fixture
|
|
95
|
+
assert _neon_branch_for_reset is not None
|
|
96
|
+
"""
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
result = pytester.runpytest("-v")
|
|
100
|
+
result.assert_outcomes(passed=1)
|
|
101
|
+
|
|
102
|
+
def test_non_xdist_reuses_migration_branch_without_migrations(
|
|
103
|
+
self, pytester, monkeypatch
|
|
104
|
+
):
|
|
105
|
+
"""Without xdist and without migrations, should reuse migration branch."""
|
|
106
|
+
monkeypatch.delenv("PYTEST_XDIST_WORKER", raising=False)
|
|
107
|
+
|
|
108
|
+
pytester.makeconftest(
|
|
109
|
+
"""
|
|
110
|
+
import os
|
|
111
|
+
import pytest
|
|
112
|
+
from pytest_neon.plugin import (
|
|
113
|
+
NeonBranch,
|
|
114
|
+
_get_xdist_worker_id,
|
|
115
|
+
_MIGRATIONS_NOT_DEFINED,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
branch_creation_calls = []
|
|
119
|
+
|
|
120
|
+
@pytest.fixture(scope="session")
|
|
121
|
+
def _neon_migration_branch(request):
|
|
122
|
+
request.config._neon_pre_migration_fingerprint = ()
|
|
123
|
+
return NeonBranch(
|
|
124
|
+
branch_id="br-migration-123",
|
|
125
|
+
project_id="proj-mock",
|
|
126
|
+
connection_string="postgresql://mock:mock@migration.neon.tech/mockdb",
|
|
127
|
+
host="migration.neon.tech",
|
|
128
|
+
parent_id="br-parent-000",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@pytest.fixture(scope="session")
|
|
132
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
133
|
+
return _MIGRATIONS_NOT_DEFINED
|
|
134
|
+
|
|
135
|
+
@pytest.fixture(scope="session")
|
|
136
|
+
def _neon_branch_for_reset(
|
|
137
|
+
request, _neon_migration_branch, neon_apply_migrations
|
|
138
|
+
):
|
|
139
|
+
sentinel = _MIGRATIONS_NOT_DEFINED
|
|
140
|
+
migrations_defined = neon_apply_migrations is not sentinel
|
|
141
|
+
fingerprint_key = "_neon_pre_migration_fingerprint"
|
|
142
|
+
pre_fp = getattr(request.config, fingerprint_key, ())
|
|
143
|
+
schema_changed = False
|
|
144
|
+
|
|
145
|
+
if migrations_defined and pre_fp:
|
|
146
|
+
schema_changed = False
|
|
147
|
+
elif migrations_defined and not pre_fp:
|
|
148
|
+
schema_changed = True
|
|
149
|
+
|
|
150
|
+
worker_id = _get_xdist_worker_id()
|
|
151
|
+
suffix = f"-test-{worker_id}"
|
|
152
|
+
|
|
153
|
+
if schema_changed or worker_id != "main":
|
|
154
|
+
branch_creation_calls.append(f"created-branch{suffix}")
|
|
155
|
+
conn = f"postgresql://mock:mock@t{suffix}.neon.tech/db"
|
|
156
|
+
branch_info = NeonBranch(
|
|
157
|
+
branch_id=f"br-test{suffix}",
|
|
158
|
+
project_id="proj-mock",
|
|
159
|
+
connection_string=conn,
|
|
160
|
+
host=f"t{suffix}.neon.tech",
|
|
161
|
+
parent_id=_neon_migration_branch.branch_id,
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
branch_creation_calls.append("reused-migration-branch")
|
|
165
|
+
branch_info = _neon_migration_branch
|
|
166
|
+
|
|
167
|
+
os.environ["DATABASE_URL"] = branch_info.connection_string
|
|
168
|
+
try:
|
|
169
|
+
yield branch_info
|
|
170
|
+
finally:
|
|
171
|
+
os.environ.pop("DATABASE_URL", None)
|
|
172
|
+
|
|
173
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
174
|
+
def verify_branch_reused():
|
|
175
|
+
yield
|
|
176
|
+
# Without xdist, should reuse migration branch
|
|
177
|
+
assert branch_creation_calls == ["reused-migration-branch"]
|
|
178
|
+
"""
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
pytester.makepyfile(
|
|
182
|
+
"""
|
|
183
|
+
def test_no_xdist_reuses_branch(_neon_branch_for_reset):
|
|
184
|
+
assert _neon_branch_for_reset is not None
|
|
185
|
+
"""
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
result = pytester.runpytest("-v")
|
|
189
|
+
result.assert_outcomes(passed=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|