pytest-neon 0.4.0__tar.gz → 0.5.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-0.4.0 → pytest_neon-0.5.1}/.github/workflows/release.yml +7 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/CLAUDE.md +9 -1
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/PKG-INFO +88 -4
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/README.md +84 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/pyproject.toml +4 -4
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/src/pytest_neon/__init__.py +1 -1
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/src/pytest_neon/plugin.py +105 -8
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_integration.py +75 -0
- pytest_neon-0.5.1/tests/test_migrations.py +77 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/.env.example +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/.github/workflows/tests.yml +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/.gitignore +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/.neon +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/LICENSE +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/src/pytest_neon/py.typed +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/conftest.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_branch_lifecycle.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_cli_options.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_env_var.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_fixture_errors.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_reset_behavior.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/tests/test_skip_behavior.py +0 -0
- {pytest_neon-0.4.0 → pytest_neon-0.5.1}/uv.lock +0 -0
|
@@ -80,6 +80,13 @@ jobs:
|
|
|
80
80
|
git tag "v$VERSION"
|
|
81
81
|
git push origin main --tags
|
|
82
82
|
|
|
83
|
+
- name: Create GitHub Release
|
|
84
|
+
env:
|
|
85
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
86
|
+
VERSION: ${{ steps.bump.outputs.version }}
|
|
87
|
+
run: |
|
|
88
|
+
gh release create "v$VERSION" --generate-notes
|
|
89
|
+
|
|
83
90
|
- name: Build and publish
|
|
84
91
|
env:
|
|
85
92
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Claude Code Instructions for pytest-neon
|
|
2
2
|
|
|
3
|
+
## Understanding the Plugin
|
|
4
|
+
|
|
5
|
+
Read `README.md` for complete documentation on how to use this plugin, including fixtures, configuration options, and migration support.
|
|
6
|
+
|
|
3
7
|
## Project Overview
|
|
4
8
|
|
|
5
9
|
This is a pytest plugin that provides isolated Neon database branches for integration testing. Each test gets isolated database state via branch reset after each test.
|
|
@@ -7,6 +11,8 @@ This is a pytest plugin that provides isolated Neon database branches for integr
|
|
|
7
11
|
## Key Architecture
|
|
8
12
|
|
|
9
13
|
- **Entry point**: `src/pytest_neon/plugin.py` - Contains all fixtures and pytest hooks
|
|
14
|
+
- **Migration fixture**: `_neon_migration_branch` - Session-scoped, parent for all test branches
|
|
15
|
+
- **User migration hook**: `neon_apply_migrations` - Session-scoped no-op, users override to run migrations
|
|
10
16
|
- **Core fixture**: `neon_branch` - Creates branch (module-scoped), resets after each test (function-scoped wrapper), sets `DATABASE_URL`, yields `NeonBranch` dataclass
|
|
11
17
|
- **Shared fixture**: `neon_branch_shared` - Module-scoped, no reset between tests
|
|
12
18
|
- **Convenience fixtures**: `neon_connection`, `neon_connection_psycopg`, `neon_engine` - Optional, require extras
|
|
@@ -19,7 +25,9 @@ This is a pytest plugin that provides isolated Neon database branches for integr
|
|
|
19
25
|
## Important Patterns
|
|
20
26
|
|
|
21
27
|
### Fixture Scopes
|
|
22
|
-
- `
|
|
28
|
+
- `_neon_migration_branch`: `scope="session"` - internal, parent for all test branches, migrations run here
|
|
29
|
+
- `neon_apply_migrations`: `scope="session"` - user overrides to run migrations
|
|
30
|
+
- `_neon_branch_for_reset`: `scope="module"` - internal, creates one branch per test file from migration branch
|
|
23
31
|
- `neon_branch`: `scope="function"` - wraps the above, resets branch after each test
|
|
24
32
|
- `neon_branch_shared`: `scope="module"` - one branch per test file, no reset
|
|
25
33
|
- Connection fixtures: `scope="function"` (default) - fresh connection per test
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-neon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Pytest plugin for Neon database branch isolation in tests
|
|
5
|
-
Project-URL: Homepage, https://github.com/
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Issues, https://github.com/
|
|
5
|
+
Project-URL: Homepage, https://github.com/ZainRizvi/pytest-neon
|
|
6
|
+
Project-URL: Repository, https://github.com/ZainRizvi/pytest-neon
|
|
7
|
+
Project-URL: Issues, https://github.com/ZainRizvi/pytest-neon/issues
|
|
8
8
|
Author: Zain Rizvi
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -200,6 +200,90 @@ def test_query(neon_engine):
|
|
|
200
200
|
assert result.scalar() == 1
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
## Migrations
|
|
204
|
+
|
|
205
|
+
pytest-neon supports running migrations once before tests, with all test resets preserving the migrated state.
|
|
206
|
+
|
|
207
|
+
### How It Works
|
|
208
|
+
|
|
209
|
+
When you override the `neon_apply_migrations` fixture, the plugin uses a two-branch architecture:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
Parent Branch (your configured parent)
|
|
213
|
+
└── Migration Branch (session-scoped)
|
|
214
|
+
│ ↑ migrations run here ONCE
|
|
215
|
+
└── Test Branch (module-scoped)
|
|
216
|
+
↑ resets to migration branch after each test
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This means:
|
|
220
|
+
- Migrations run **once per test session** (not per test or per module)
|
|
221
|
+
- Each test reset restores to the **post-migration state**
|
|
222
|
+
- Tests always see your migrated schema
|
|
223
|
+
|
|
224
|
+
### Setup
|
|
225
|
+
|
|
226
|
+
Override the `neon_apply_migrations` fixture in your `conftest.py`:
|
|
227
|
+
|
|
228
|
+
**Alembic:**
|
|
229
|
+
```python
|
|
230
|
+
# conftest.py
|
|
231
|
+
import subprocess
|
|
232
|
+
import pytest
|
|
233
|
+
|
|
234
|
+
@pytest.fixture(scope="session")
|
|
235
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
236
|
+
"""Run Alembic migrations before tests."""
|
|
237
|
+
# DATABASE_URL is already set to the migration branch
|
|
238
|
+
subprocess.run(["alembic", "upgrade", "head"], check=True)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Django:**
|
|
242
|
+
```python
|
|
243
|
+
# conftest.py
|
|
244
|
+
import pytest
|
|
245
|
+
|
|
246
|
+
@pytest.fixture(scope="session")
|
|
247
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
248
|
+
"""Run Django migrations before tests."""
|
|
249
|
+
from django.core.management import call_command
|
|
250
|
+
call_command("migrate", "--noinput")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Raw SQL:**
|
|
254
|
+
```python
|
|
255
|
+
# conftest.py
|
|
256
|
+
import pytest
|
|
257
|
+
|
|
258
|
+
@pytest.fixture(scope="session")
|
|
259
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
260
|
+
"""Apply schema from SQL file."""
|
|
261
|
+
import psycopg
|
|
262
|
+
with psycopg.connect(_neon_migration_branch.connection_string) as conn:
|
|
263
|
+
with open("schema.sql") as f:
|
|
264
|
+
conn.execute(f.read())
|
|
265
|
+
conn.commit()
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Custom migration tool:**
|
|
269
|
+
```python
|
|
270
|
+
# conftest.py
|
|
271
|
+
import pytest
|
|
272
|
+
|
|
273
|
+
@pytest.fixture(scope="session")
|
|
274
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
275
|
+
"""Run custom migrations."""
|
|
276
|
+
from myapp.migrations import run_migrations
|
|
277
|
+
run_migrations(_neon_migration_branch.connection_string)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Important Notes
|
|
281
|
+
|
|
282
|
+
- The `_neon_migration_branch` parameter gives you access to the `NeonBranch` object with `connection_string`, `branch_id`, etc.
|
|
283
|
+
- `DATABASE_URL` (or your configured env var) is automatically set when the fixture runs
|
|
284
|
+
- If you don't override `neon_apply_migrations`, no migrations run (the fixture is a no-op by default)
|
|
285
|
+
- Migrations run before any test branches are created, so all tests see the same migrated schema
|
|
286
|
+
|
|
203
287
|
## Configuration
|
|
204
288
|
|
|
205
289
|
### Environment Variables
|
|
@@ -158,6 +158,90 @@ def test_query(neon_engine):
|
|
|
158
158
|
assert result.scalar() == 1
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
+
## Migrations
|
|
162
|
+
|
|
163
|
+
pytest-neon supports running migrations once before tests, with all test resets preserving the migrated state.
|
|
164
|
+
|
|
165
|
+
### How It Works
|
|
166
|
+
|
|
167
|
+
When you override the `neon_apply_migrations` fixture, the plugin uses a two-branch architecture:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
Parent Branch (your configured parent)
|
|
171
|
+
└── Migration Branch (session-scoped)
|
|
172
|
+
│ ↑ migrations run here ONCE
|
|
173
|
+
└── Test Branch (module-scoped)
|
|
174
|
+
↑ resets to migration branch after each test
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This means:
|
|
178
|
+
- Migrations run **once per test session** (not per test or per module)
|
|
179
|
+
- Each test reset restores to the **post-migration state**
|
|
180
|
+
- Tests always see your migrated schema
|
|
181
|
+
|
|
182
|
+
### Setup
|
|
183
|
+
|
|
184
|
+
Override the `neon_apply_migrations` fixture in your `conftest.py`:
|
|
185
|
+
|
|
186
|
+
**Alembic:**
|
|
187
|
+
```python
|
|
188
|
+
# conftest.py
|
|
189
|
+
import subprocess
|
|
190
|
+
import pytest
|
|
191
|
+
|
|
192
|
+
@pytest.fixture(scope="session")
|
|
193
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
194
|
+
"""Run Alembic migrations before tests."""
|
|
195
|
+
# DATABASE_URL is already set to the migration branch
|
|
196
|
+
subprocess.run(["alembic", "upgrade", "head"], check=True)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Django:**
|
|
200
|
+
```python
|
|
201
|
+
# conftest.py
|
|
202
|
+
import pytest
|
|
203
|
+
|
|
204
|
+
@pytest.fixture(scope="session")
|
|
205
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
206
|
+
"""Run Django migrations before tests."""
|
|
207
|
+
from django.core.management import call_command
|
|
208
|
+
call_command("migrate", "--noinput")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Raw SQL:**
|
|
212
|
+
```python
|
|
213
|
+
# conftest.py
|
|
214
|
+
import pytest
|
|
215
|
+
|
|
216
|
+
@pytest.fixture(scope="session")
|
|
217
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
218
|
+
"""Apply schema from SQL file."""
|
|
219
|
+
import psycopg
|
|
220
|
+
with psycopg.connect(_neon_migration_branch.connection_string) as conn:
|
|
221
|
+
with open("schema.sql") as f:
|
|
222
|
+
conn.execute(f.read())
|
|
223
|
+
conn.commit()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Custom migration tool:**
|
|
227
|
+
```python
|
|
228
|
+
# conftest.py
|
|
229
|
+
import pytest
|
|
230
|
+
|
|
231
|
+
@pytest.fixture(scope="session")
|
|
232
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
233
|
+
"""Run custom migrations."""
|
|
234
|
+
from myapp.migrations import run_migrations
|
|
235
|
+
run_migrations(_neon_migration_branch.connection_string)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Important Notes
|
|
239
|
+
|
|
240
|
+
- The `_neon_migration_branch` parameter gives you access to the `NeonBranch` object with `connection_string`, `branch_id`, etc.
|
|
241
|
+
- `DATABASE_URL` (or your configured env var) is automatically set when the fixture runs
|
|
242
|
+
- If you don't override `neon_apply_migrations`, no migrations run (the fixture is a no-op by default)
|
|
243
|
+
- Migrations run before any test branches are created, so all tests see the same migrated schema
|
|
244
|
+
|
|
161
245
|
## Configuration
|
|
162
246
|
|
|
163
247
|
### Environment Variables
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-neon"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
description = "Pytest plugin for Neon database branch isolation in tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -45,9 +45,9 @@ dev = [
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
[project.urls]
|
|
48
|
-
Homepage = "https://github.com/
|
|
49
|
-
Repository = "https://github.com/
|
|
50
|
-
Issues = "https://github.com/
|
|
48
|
+
Homepage = "https://github.com/ZainRizvi/pytest-neon"
|
|
49
|
+
Repository = "https://github.com/ZainRizvi/pytest-neon"
|
|
50
|
+
Issues = "https://github.com/ZainRizvi/pytest-neon/issues"
|
|
51
51
|
|
|
52
52
|
[project.entry-points.pytest11]
|
|
53
53
|
neon = "pytest_neon.plugin"
|
|
@@ -137,11 +137,20 @@ def _get_config_value(
|
|
|
137
137
|
|
|
138
138
|
def _create_neon_branch(
|
|
139
139
|
request: pytest.FixtureRequest,
|
|
140
|
+
parent_branch_id_override: str | None = None,
|
|
141
|
+
branch_expiry_override: int | None = None,
|
|
142
|
+
branch_name_suffix: str = "",
|
|
140
143
|
) -> Generator[NeonBranch, None, None]:
|
|
141
144
|
"""
|
|
142
145
|
Internal helper that creates and manages a Neon branch lifecycle.
|
|
143
146
|
|
|
144
147
|
This is the core implementation used by branch fixtures.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
request: Pytest fixture request
|
|
151
|
+
parent_branch_id_override: If provided, use this as parent instead of config
|
|
152
|
+
branch_expiry_override: If provided, use this expiry instead of config
|
|
153
|
+
branch_name_suffix: Optional suffix for branch name (e.g., "-migrated", "-test")
|
|
145
154
|
"""
|
|
146
155
|
config = request.config
|
|
147
156
|
|
|
@@ -149,7 +158,8 @@ def _create_neon_branch(
|
|
|
149
158
|
project_id = _get_config_value(
|
|
150
159
|
config, "neon_project_id", "NEON_PROJECT_ID", "neon_project_id"
|
|
151
160
|
)
|
|
152
|
-
|
|
161
|
+
# Use override if provided, otherwise read from config
|
|
162
|
+
parent_branch_id = parent_branch_id_override or _get_config_value(
|
|
153
163
|
config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID", "neon_parent_branch"
|
|
154
164
|
)
|
|
155
165
|
database_name = _get_config_value(
|
|
@@ -164,9 +174,13 @@ def _create_neon_branch(
|
|
|
164
174
|
if keep_branches is None:
|
|
165
175
|
keep_branches = config.getini("neon_keep_branches")
|
|
166
176
|
|
|
167
|
-
|
|
168
|
-
if
|
|
169
|
-
branch_expiry =
|
|
177
|
+
# Use override if provided, otherwise read from config
|
|
178
|
+
if branch_expiry_override is not None:
|
|
179
|
+
branch_expiry = branch_expiry_override
|
|
180
|
+
else:
|
|
181
|
+
branch_expiry = config.getoption("neon_branch_expiry", default=None)
|
|
182
|
+
if branch_expiry is None:
|
|
183
|
+
branch_expiry = int(config.getini("neon_branch_expiry"))
|
|
170
184
|
|
|
171
185
|
env_var_name = _get_config_value(
|
|
172
186
|
config, "neon_env_var", "", "neon_env_var", "DATABASE_URL"
|
|
@@ -185,7 +199,7 @@ def _create_neon_branch(
|
|
|
185
199
|
neon = NeonAPI(api_key=api_key)
|
|
186
200
|
|
|
187
201
|
# Generate unique branch name
|
|
188
|
-
branch_name = f"pytest-{os.urandom(4).hex()}"
|
|
202
|
+
branch_name = f"pytest-{os.urandom(4).hex()}{branch_name_suffix}"
|
|
189
203
|
|
|
190
204
|
# Build branch creation payload
|
|
191
205
|
branch_config: dict[str, Any] = {"name": branch_name}
|
|
@@ -311,12 +325,86 @@ def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
|
|
|
311
325
|
response.raise_for_status()
|
|
312
326
|
|
|
313
327
|
|
|
328
|
+
@pytest.fixture(scope="session")
|
|
329
|
+
def _neon_migration_branch(
|
|
330
|
+
request: pytest.FixtureRequest,
|
|
331
|
+
) -> Generator[NeonBranch, None, None]:
|
|
332
|
+
"""
|
|
333
|
+
Session-scoped branch where migrations are applied.
|
|
334
|
+
|
|
335
|
+
This branch is created from the configured parent and serves as
|
|
336
|
+
the parent for all test branches. Migrations run once per session
|
|
337
|
+
on this branch.
|
|
338
|
+
|
|
339
|
+
Note: The migration branch cannot have an expiry because Neon doesn't
|
|
340
|
+
allow creating child branches from branches with expiration dates.
|
|
341
|
+
Cleanup relies on the fixture teardown at session end.
|
|
342
|
+
"""
|
|
343
|
+
# No expiry - Neon doesn't allow children from branches with expiry
|
|
344
|
+
yield from _create_neon_branch(
|
|
345
|
+
request,
|
|
346
|
+
branch_expiry_override=0,
|
|
347
|
+
branch_name_suffix="-migrated",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@pytest.fixture(scope="session")
|
|
352
|
+
def neon_apply_migrations(_neon_migration_branch: NeonBranch) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Override this fixture to run migrations on the test database.
|
|
355
|
+
|
|
356
|
+
The migration branch is already created and DATABASE_URL is set.
|
|
357
|
+
Migrations run once per test session, before any tests execute.
|
|
358
|
+
|
|
359
|
+
Example in conftest.py:
|
|
360
|
+
|
|
361
|
+
@pytest.fixture(scope="session")
|
|
362
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
363
|
+
import subprocess
|
|
364
|
+
subprocess.run(["alembic", "upgrade", "head"], check=True)
|
|
365
|
+
|
|
366
|
+
Or with Django:
|
|
367
|
+
|
|
368
|
+
@pytest.fixture(scope="session")
|
|
369
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
370
|
+
from django.core.management import call_command
|
|
371
|
+
call_command("migrate", "--noinput")
|
|
372
|
+
|
|
373
|
+
Or with raw SQL:
|
|
374
|
+
|
|
375
|
+
@pytest.fixture(scope="session")
|
|
376
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
377
|
+
import psycopg
|
|
378
|
+
with psycopg.connect(_neon_migration_branch.connection_string) as conn:
|
|
379
|
+
with open("schema.sql") as f:
|
|
380
|
+
conn.execute(f.read())
|
|
381
|
+
conn.commit()
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
_neon_migration_branch: The migration branch with connection details.
|
|
385
|
+
Use _neon_migration_branch.connection_string to connect directly,
|
|
386
|
+
or rely on DATABASE_URL which is already set.
|
|
387
|
+
"""
|
|
388
|
+
pass # No-op by default - users override this fixture to run migrations
|
|
389
|
+
|
|
390
|
+
|
|
314
391
|
@pytest.fixture(scope="module")
|
|
315
392
|
def _neon_branch_for_reset(
|
|
316
393
|
request: pytest.FixtureRequest,
|
|
394
|
+
_neon_migration_branch: NeonBranch,
|
|
395
|
+
neon_apply_migrations: None, # Ensures migrations run first
|
|
317
396
|
) -> Generator[NeonBranch, None, None]:
|
|
318
|
-
"""
|
|
319
|
-
|
|
397
|
+
"""
|
|
398
|
+
Internal fixture that creates a test branch from the migration branch.
|
|
399
|
+
|
|
400
|
+
The test branch is created as a child of the migration branch, so resets
|
|
401
|
+
restore to post-migration state rather than the original parent state.
|
|
402
|
+
"""
|
|
403
|
+
yield from _create_neon_branch(
|
|
404
|
+
request,
|
|
405
|
+
parent_branch_id_override=_neon_migration_branch.branch_id,
|
|
406
|
+
branch_name_suffix="-test",
|
|
407
|
+
)
|
|
320
408
|
|
|
321
409
|
|
|
322
410
|
@pytest.fixture(scope="function")
|
|
@@ -383,6 +471,8 @@ def neon_branch(
|
|
|
383
471
|
@pytest.fixture(scope="module")
|
|
384
472
|
def neon_branch_shared(
|
|
385
473
|
request: pytest.FixtureRequest,
|
|
474
|
+
_neon_migration_branch: NeonBranch,
|
|
475
|
+
neon_apply_migrations: None, # Ensures migrations run first
|
|
386
476
|
) -> Generator[NeonBranch, None, None]:
|
|
387
477
|
"""
|
|
388
478
|
Provide a shared Neon database branch for all tests in a module.
|
|
@@ -391,6 +481,9 @@ def neon_branch_shared(
|
|
|
391
481
|
tests without resetting. This is the fastest option but tests can see
|
|
392
482
|
each other's data modifications.
|
|
393
483
|
|
|
484
|
+
If you override the `neon_apply_migrations` fixture, migrations will run
|
|
485
|
+
once before the first test, and this branch will include the migrated schema.
|
|
486
|
+
|
|
394
487
|
Use this when:
|
|
395
488
|
- Tests are read-only or don't interfere with each other
|
|
396
489
|
- You manually clean up test data within each test
|
|
@@ -408,7 +501,11 @@ def neon_branch_shared(
|
|
|
408
501
|
# Fast: no reset between tests, but be careful about data leakage
|
|
409
502
|
conn_string = neon_branch_shared.connection_string
|
|
410
503
|
"""
|
|
411
|
-
yield from _create_neon_branch(
|
|
504
|
+
yield from _create_neon_branch(
|
|
505
|
+
request,
|
|
506
|
+
parent_branch_id_override=_neon_migration_branch.branch_id,
|
|
507
|
+
branch_name_suffix="-shared",
|
|
508
|
+
)
|
|
412
509
|
|
|
413
510
|
|
|
414
511
|
@pytest.fixture
|
|
@@ -81,6 +81,81 @@ class TestRealBranchCreation:
|
|
|
81
81
|
assert os.environ.get("DATABASE_URL") == neon_branch.connection_string
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
class TestMigrations:
|
|
85
|
+
"""Test migration support with real Neon branches."""
|
|
86
|
+
|
|
87
|
+
def test_migrations_persist_across_resets(self, pytester, tmp_path):
|
|
88
|
+
"""Test that migrations run once and persist across test resets."""
|
|
89
|
+
# Write a conftest that tracks migration and verifies table exists
|
|
90
|
+
conftest = """
|
|
91
|
+
import os
|
|
92
|
+
import pytest
|
|
93
|
+
|
|
94
|
+
# Track if migrations ran
|
|
95
|
+
migrations_ran = [False]
|
|
96
|
+
|
|
97
|
+
@pytest.fixture(scope="session")
|
|
98
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
99
|
+
\"\"\"Create a test table via migration.\"\"\"
|
|
100
|
+
try:
|
|
101
|
+
import psycopg
|
|
102
|
+
except ImportError:
|
|
103
|
+
pytest.skip("psycopg not installed")
|
|
104
|
+
|
|
105
|
+
with psycopg.connect(_neon_migration_branch.connection_string) as conn:
|
|
106
|
+
with conn.cursor() as cur:
|
|
107
|
+
cur.execute(\"\"\"
|
|
108
|
+
CREATE TABLE IF NOT EXISTS migration_test (
|
|
109
|
+
id SERIAL PRIMARY KEY,
|
|
110
|
+
value TEXT NOT NULL
|
|
111
|
+
)
|
|
112
|
+
\"\"\")
|
|
113
|
+
conn.commit()
|
|
114
|
+
migrations_ran[0] = True
|
|
115
|
+
|
|
116
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
117
|
+
assert migrations_ran[0], "Migrations should have run"
|
|
118
|
+
"""
|
|
119
|
+
pytester.makeconftest(conftest)
|
|
120
|
+
|
|
121
|
+
# Write tests that verify the migrated table exists and data resets
|
|
122
|
+
pytester.makepyfile(
|
|
123
|
+
"""
|
|
124
|
+
import psycopg
|
|
125
|
+
|
|
126
|
+
def test_first_insert(neon_branch):
|
|
127
|
+
\"\"\"Insert data - table should exist from migration.\"\"\"
|
|
128
|
+
with psycopg.connect(neon_branch.connection_string) as conn:
|
|
129
|
+
with conn.cursor() as cur:
|
|
130
|
+
cur.execute("INSERT INTO migration_test (value) VALUES ('first')")
|
|
131
|
+
conn.commit()
|
|
132
|
+
|
|
133
|
+
with conn.cursor() as cur:
|
|
134
|
+
cur.execute("SELECT COUNT(*) FROM migration_test")
|
|
135
|
+
count = cur.fetchone()[0]
|
|
136
|
+
assert count == 1
|
|
137
|
+
|
|
138
|
+
def test_second_insert_after_reset(neon_branch):
|
|
139
|
+
\"\"\"After reset, table exists but previous data is gone.\"\"\"
|
|
140
|
+
with psycopg.connect(neon_branch.connection_string) as conn:
|
|
141
|
+
# Table should still exist (from migration)
|
|
142
|
+
with conn.cursor() as cur:
|
|
143
|
+
cur.execute("SELECT COUNT(*) FROM migration_test")
|
|
144
|
+
count = cur.fetchone()[0]
|
|
145
|
+
# Data from first test should be gone after reset
|
|
146
|
+
assert count == 0
|
|
147
|
+
|
|
148
|
+
# Insert new data
|
|
149
|
+
with conn.cursor() as cur:
|
|
150
|
+
cur.execute("INSERT INTO migration_test (value) VALUES ('second')")
|
|
151
|
+
conn.commit()
|
|
152
|
+
"""
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
result = pytester.runpytest("-v")
|
|
156
|
+
result.assert_outcomes(passed=2)
|
|
157
|
+
|
|
158
|
+
|
|
84
159
|
class TestRealDatabaseConnectivity:
|
|
85
160
|
"""Test actual database connectivity."""
|
|
86
161
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tests for migration support."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestMigrationFixtureOrder:
|
|
5
|
+
"""Test that migrations run before test branches are created."""
|
|
6
|
+
|
|
7
|
+
def test_migrations_run_before_test_branch_created(self, pytester):
|
|
8
|
+
"""Verify neon_apply_migrations is called before test branch exists."""
|
|
9
|
+
pytester.makeconftest(
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import pytest
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
execution_order = []
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class FakeNeonBranch:
|
|
19
|
+
branch_id: str
|
|
20
|
+
project_id: str
|
|
21
|
+
connection_string: str
|
|
22
|
+
host: str
|
|
23
|
+
parent_id: str
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(scope="session")
|
|
26
|
+
def _neon_migration_branch():
|
|
27
|
+
execution_order.append("migration_branch_created")
|
|
28
|
+
branch = FakeNeonBranch(
|
|
29
|
+
branch_id="br-migration",
|
|
30
|
+
project_id="proj-test",
|
|
31
|
+
connection_string="postgresql://migration",
|
|
32
|
+
host="test.neon.tech",
|
|
33
|
+
parent_id="br-parent",
|
|
34
|
+
)
|
|
35
|
+
os.environ["DATABASE_URL"] = branch.connection_string
|
|
36
|
+
yield branch
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(scope="session")
|
|
39
|
+
def neon_apply_migrations(_neon_migration_branch):
|
|
40
|
+
execution_order.append("migrations_applied")
|
|
41
|
+
# User would run migrations here
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="module")
|
|
44
|
+
def _neon_branch_for_reset(_neon_migration_branch, neon_apply_migrations):
|
|
45
|
+
execution_order.append("test_branch_created")
|
|
46
|
+
branch = FakeNeonBranch(
|
|
47
|
+
branch_id="br-test",
|
|
48
|
+
project_id="proj-test",
|
|
49
|
+
connection_string="postgresql://test",
|
|
50
|
+
host="test.neon.tech",
|
|
51
|
+
parent_id=_neon_migration_branch.branch_id,
|
|
52
|
+
)
|
|
53
|
+
yield branch
|
|
54
|
+
|
|
55
|
+
@pytest.fixture(scope="function")
|
|
56
|
+
def neon_branch(_neon_branch_for_reset):
|
|
57
|
+
yield _neon_branch_for_reset
|
|
58
|
+
|
|
59
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
60
|
+
# Verify order: migration branch -> migrations -> test branch
|
|
61
|
+
assert execution_order == [
|
|
62
|
+
"migration_branch_created",
|
|
63
|
+
"migrations_applied",
|
|
64
|
+
"test_branch_created",
|
|
65
|
+
], f"Wrong order: {execution_order}"
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
pytester.makepyfile(
|
|
70
|
+
"""
|
|
71
|
+
def test_uses_branch(neon_branch):
|
|
72
|
+
assert neon_branch.parent_id == "br-migration"
|
|
73
|
+
"""
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
result = pytester.runpytest("-v")
|
|
77
|
+
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
|