pytest-neon 0.2.1__tar.gz → 0.4.0__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.2.1 → pytest_neon-0.4.0}/CLAUDE.md +16 -7
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/PKG-INFO +48 -1
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/README.md +47 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/pyproject.toml +1 -1
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/src/pytest_neon/__init__.py +1 -1
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/src/pytest_neon/plugin.py +92 -31
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/conftest.py +7 -2
- pytest_neon-0.4.0/tests/test_reset_behavior.py +179 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.env.example +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.github/workflows/release.yml +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.github/workflows/tests.yml +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.gitignore +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.neon +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/LICENSE +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/src/pytest_neon/py.typed +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_branch_lifecycle.py +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_cli_options.py +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_env_var.py +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_fixture_errors.py +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_integration.py +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_skip_behavior.py +0 -0
- {pytest_neon-0.2.1 → pytest_neon-0.4.0}/uv.lock +0 -0
|
@@ -2,27 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Project Overview
|
|
4
4
|
|
|
5
|
-
This is a pytest plugin that provides isolated Neon database branches for integration testing. Each test
|
|
5
|
+
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.
|
|
6
6
|
|
|
7
7
|
## Key Architecture
|
|
8
8
|
|
|
9
9
|
- **Entry point**: `src/pytest_neon/plugin.py` - Contains all fixtures and pytest hooks
|
|
10
|
-
- **Core fixture**: `neon_branch` - Creates branch, sets `DATABASE_URL`, yields `NeonBranch` dataclass
|
|
10
|
+
- **Core fixture**: `neon_branch` - Creates branch (module-scoped), resets after each test (function-scoped wrapper), sets `DATABASE_URL`, yields `NeonBranch` dataclass
|
|
11
|
+
- **Shared fixture**: `neon_branch_shared` - Module-scoped, no reset between tests
|
|
11
12
|
- **Convenience fixtures**: `neon_connection`, `neon_connection_psycopg`, `neon_engine` - Optional, require extras
|
|
12
13
|
|
|
13
14
|
## Dependencies
|
|
14
15
|
|
|
15
|
-
- Core: `pytest`, `neon-api`
|
|
16
|
+
- Core: `pytest`, `neon-api`, `requests`
|
|
16
17
|
- Optional extras: `psycopg2`, `psycopg`, `sqlalchemy` - for convenience fixtures
|
|
17
18
|
|
|
18
19
|
## Important Patterns
|
|
19
20
|
|
|
20
21
|
### Fixture Scopes
|
|
21
|
-
- `
|
|
22
|
+
- `_neon_branch_for_reset`: `scope="module"` - internal, creates one branch per test file
|
|
23
|
+
- `neon_branch`: `scope="function"` - wraps the above, resets branch after each test
|
|
24
|
+
- `neon_branch_shared`: `scope="module"` - one branch per test file, no reset
|
|
22
25
|
- Connection fixtures: `scope="function"` (default) - fresh connection per test
|
|
23
26
|
|
|
24
27
|
### Environment Variable Handling
|
|
25
|
-
The `
|
|
28
|
+
The `_create_neon_branch` function sets `DATABASE_URL` (or configured env var) during the fixture lifecycle and restores the original value in the finally block. This is critical for not polluting other tests.
|
|
26
29
|
|
|
27
30
|
### Error Messages
|
|
28
31
|
Convenience fixtures use `pytest.fail()` with detailed, formatted error messages when dependencies are missing. Keep this pattern - users need clear guidance on how to fix import errors.
|
|
@@ -37,9 +40,15 @@ Tests in `tests/` use `pytester` for testing pytest plugins. The plugin itself c
|
|
|
37
40
|
|
|
38
41
|
## Publishing
|
|
39
42
|
|
|
43
|
+
Use the GitHub Actions release workflow:
|
|
44
|
+
1. Go to Actions → Release → Run workflow
|
|
45
|
+
2. Choose patch/minor/major
|
|
46
|
+
3. Workflow bumps version, commits, tags, and publishes to PyPI
|
|
47
|
+
|
|
48
|
+
Or manually:
|
|
40
49
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
uv build
|
|
51
|
+
uv publish --token $PYPI_TOKEN
|
|
43
52
|
```
|
|
44
53
|
|
|
45
54
|
Package name on PyPI: `pytest-neon`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-neon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Pytest plugin for Neon database branch isolation in tests
|
|
5
5
|
Project-URL: Homepage, https://github.com/zain/pytest-neon
|
|
6
6
|
Project-URL: Repository, https://github.com/zain/pytest-neon
|
|
@@ -120,6 +120,7 @@ Returns a `NeonBranch` dataclass with:
|
|
|
120
120
|
- `project_id`: The Neon project ID
|
|
121
121
|
- `connection_string`: Full PostgreSQL connection URI
|
|
122
122
|
- `host`: The database host
|
|
123
|
+
- `parent_id`: The parent branch ID (used for resets)
|
|
123
124
|
|
|
124
125
|
```python
|
|
125
126
|
import os
|
|
@@ -237,6 +238,30 @@ pytest --neon-branch-expiry=0
|
|
|
237
238
|
pytest --neon-env-var=TEST_DATABASE_URL
|
|
238
239
|
```
|
|
239
240
|
|
|
241
|
+
### pyproject.toml / pytest.ini
|
|
242
|
+
|
|
243
|
+
You can also configure options in your `pyproject.toml`:
|
|
244
|
+
|
|
245
|
+
```toml
|
|
246
|
+
[tool.pytest.ini_options]
|
|
247
|
+
neon_database = "mydb"
|
|
248
|
+
neon_role = "myrole"
|
|
249
|
+
neon_keep_branches = true
|
|
250
|
+
neon_branch_expiry = "300"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Or in `pytest.ini`:
|
|
254
|
+
|
|
255
|
+
```ini
|
|
256
|
+
[pytest]
|
|
257
|
+
neon_database = mydb
|
|
258
|
+
neon_role = myrole
|
|
259
|
+
neon_keep_branches = true
|
|
260
|
+
neon_branch_expiry = 300
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Priority order**: CLI options > environment variables > ini settings > defaults
|
|
264
|
+
|
|
240
265
|
## CI/CD Integration
|
|
241
266
|
|
|
242
267
|
### GitHub Actions
|
|
@@ -276,6 +301,28 @@ jobs:
|
|
|
276
301
|
|
|
277
302
|
Branches use copy-on-write storage, so you only pay for data that differs from the parent branch.
|
|
278
303
|
|
|
304
|
+
### What Reset Does
|
|
305
|
+
|
|
306
|
+
The `neon_branch` fixture uses Neon's branch restore API to reset database state after each test:
|
|
307
|
+
|
|
308
|
+
- **Data changes are reverted**: All INSERT, UPDATE, DELETE operations are undone
|
|
309
|
+
- **Schema changes are reverted**: CREATE TABLE, ALTER TABLE, DROP TABLE, etc. are undone
|
|
310
|
+
- **Sequences are reset**: Auto-increment counters return to parent state
|
|
311
|
+
- **Complete rollback**: The branch is restored to the exact state of the parent at the time the child branch was created
|
|
312
|
+
|
|
313
|
+
This is similar to database transactions but at the branch level.
|
|
314
|
+
|
|
315
|
+
## Limitations
|
|
316
|
+
|
|
317
|
+
### Parallel Test Execution
|
|
318
|
+
|
|
319
|
+
This plugin sets the `DATABASE_URL` environment variable, which is process-global. This means it is **not compatible with pytest-xdist** or other parallel test runners that run tests in the same process.
|
|
320
|
+
|
|
321
|
+
If you need parallel execution, you can:
|
|
322
|
+
- Use `neon_branch.connection_string` directly instead of relying on `DATABASE_URL`
|
|
323
|
+
- Run with `pytest-xdist --dist=loadfile` to keep modules in separate processes
|
|
324
|
+
- Run tests serially (default pytest behavior)
|
|
325
|
+
|
|
279
326
|
## Troubleshooting
|
|
280
327
|
|
|
281
328
|
### "psycopg not installed" or "psycopg2 not installed"
|
|
@@ -78,6 +78,7 @@ Returns a `NeonBranch` dataclass with:
|
|
|
78
78
|
- `project_id`: The Neon project ID
|
|
79
79
|
- `connection_string`: Full PostgreSQL connection URI
|
|
80
80
|
- `host`: The database host
|
|
81
|
+
- `parent_id`: The parent branch ID (used for resets)
|
|
81
82
|
|
|
82
83
|
```python
|
|
83
84
|
import os
|
|
@@ -195,6 +196,30 @@ pytest --neon-branch-expiry=0
|
|
|
195
196
|
pytest --neon-env-var=TEST_DATABASE_URL
|
|
196
197
|
```
|
|
197
198
|
|
|
199
|
+
### pyproject.toml / pytest.ini
|
|
200
|
+
|
|
201
|
+
You can also configure options in your `pyproject.toml`:
|
|
202
|
+
|
|
203
|
+
```toml
|
|
204
|
+
[tool.pytest.ini_options]
|
|
205
|
+
neon_database = "mydb"
|
|
206
|
+
neon_role = "myrole"
|
|
207
|
+
neon_keep_branches = true
|
|
208
|
+
neon_branch_expiry = "300"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Or in `pytest.ini`:
|
|
212
|
+
|
|
213
|
+
```ini
|
|
214
|
+
[pytest]
|
|
215
|
+
neon_database = mydb
|
|
216
|
+
neon_role = myrole
|
|
217
|
+
neon_keep_branches = true
|
|
218
|
+
neon_branch_expiry = 300
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Priority order**: CLI options > environment variables > ini settings > defaults
|
|
222
|
+
|
|
198
223
|
## CI/CD Integration
|
|
199
224
|
|
|
200
225
|
### GitHub Actions
|
|
@@ -234,6 +259,28 @@ jobs:
|
|
|
234
259
|
|
|
235
260
|
Branches use copy-on-write storage, so you only pay for data that differs from the parent branch.
|
|
236
261
|
|
|
262
|
+
### What Reset Does
|
|
263
|
+
|
|
264
|
+
The `neon_branch` fixture uses Neon's branch restore API to reset database state after each test:
|
|
265
|
+
|
|
266
|
+
- **Data changes are reverted**: All INSERT, UPDATE, DELETE operations are undone
|
|
267
|
+
- **Schema changes are reverted**: CREATE TABLE, ALTER TABLE, DROP TABLE, etc. are undone
|
|
268
|
+
- **Sequences are reset**: Auto-increment counters return to parent state
|
|
269
|
+
- **Complete rollback**: The branch is restored to the exact state of the parent at the time the child branch was created
|
|
270
|
+
|
|
271
|
+
This is similar to database transactions but at the branch level.
|
|
272
|
+
|
|
273
|
+
## Limitations
|
|
274
|
+
|
|
275
|
+
### Parallel Test Execution
|
|
276
|
+
|
|
277
|
+
This plugin sets the `DATABASE_URL` environment variable, which is process-global. This means it is **not compatible with pytest-xdist** or other parallel test runners that run tests in the same process.
|
|
278
|
+
|
|
279
|
+
If you need parallel execution, you can:
|
|
280
|
+
- Use `neon_branch.connection_string` directly instead of relying on `DATABASE_URL`
|
|
281
|
+
- Run with `pytest-xdist --dist=loadfile` to keep modules in separate processes
|
|
282
|
+
- Run tests serially (default pytest behavior)
|
|
283
|
+
|
|
237
284
|
## Troubleshooting
|
|
238
285
|
|
|
239
286
|
### "psycopg not installed" or "psycopg2 not installed"
|
|
@@ -7,14 +7,12 @@ import time
|
|
|
7
7
|
from collections.abc import Generator
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import datetime, timedelta, timezone
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
import pytest
|
|
13
13
|
import requests
|
|
14
14
|
from neon_api import NeonAPI
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
pass
|
|
15
|
+
from neon_api.schema import EndpointState
|
|
18
16
|
|
|
19
17
|
# Default branch expiry in seconds (10 minutes)
|
|
20
18
|
DEFAULT_BRANCH_EXPIRY_SECONDS = 600
|
|
@@ -32,9 +30,10 @@ class NeonBranch:
|
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
35
|
-
"""Add Neon-specific command line options."""
|
|
33
|
+
"""Add Neon-specific command line options and ini settings."""
|
|
36
34
|
group = parser.getgroup("neon", "Neon database branching")
|
|
37
35
|
|
|
36
|
+
# CLI options
|
|
38
37
|
group.addoption(
|
|
39
38
|
"--neon-api-key",
|
|
40
39
|
dest="neon_api_key",
|
|
@@ -53,13 +52,11 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
53
52
|
group.addoption(
|
|
54
53
|
"--neon-database",
|
|
55
54
|
dest="neon_database",
|
|
56
|
-
default="neondb",
|
|
57
55
|
help="Database name (default: neondb)",
|
|
58
56
|
)
|
|
59
57
|
group.addoption(
|
|
60
58
|
"--neon-role",
|
|
61
59
|
dest="neon_role",
|
|
62
|
-
default="neondb_owner",
|
|
63
60
|
help="Database role (default: neondb_owner)",
|
|
64
61
|
)
|
|
65
62
|
group.addoption(
|
|
@@ -72,7 +69,6 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
72
69
|
"--neon-branch-expiry",
|
|
73
70
|
dest="neon_branch_expiry",
|
|
74
71
|
type=int,
|
|
75
|
-
default=DEFAULT_BRANCH_EXPIRY_SECONDS,
|
|
76
72
|
help=(
|
|
77
73
|
f"Branch auto-expiry in seconds "
|
|
78
74
|
f"(default: {DEFAULT_BRANCH_EXPIRY_SECONDS}). Set to 0 to disable."
|
|
@@ -81,19 +77,62 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
81
77
|
group.addoption(
|
|
82
78
|
"--neon-env-var",
|
|
83
79
|
dest="neon_env_var",
|
|
84
|
-
default="DATABASE_URL",
|
|
85
80
|
help="Environment variable to set with connection string (default: DATABASE_URL)", # noqa: E501
|
|
86
81
|
)
|
|
87
82
|
|
|
83
|
+
# INI file settings (pytest.ini, pyproject.toml, etc.)
|
|
84
|
+
parser.addini("neon_api_key", "Neon API key", default=None)
|
|
85
|
+
parser.addini("neon_project_id", "Neon project ID", default=None)
|
|
86
|
+
parser.addini("neon_parent_branch", "Parent branch ID", default=None)
|
|
87
|
+
parser.addini("neon_database", "Database name", default="neondb")
|
|
88
|
+
parser.addini("neon_role", "Database role", default="neondb_owner")
|
|
89
|
+
parser.addini(
|
|
90
|
+
"neon_keep_branches",
|
|
91
|
+
"Don't delete branches after tests",
|
|
92
|
+
type="bool",
|
|
93
|
+
default=False,
|
|
94
|
+
)
|
|
95
|
+
parser.addini(
|
|
96
|
+
"neon_branch_expiry",
|
|
97
|
+
"Branch auto-expiry in seconds",
|
|
98
|
+
default=str(DEFAULT_BRANCH_EXPIRY_SECONDS),
|
|
99
|
+
)
|
|
100
|
+
parser.addini(
|
|
101
|
+
"neon_env_var",
|
|
102
|
+
"Environment variable for connection string",
|
|
103
|
+
default="DATABASE_URL",
|
|
104
|
+
)
|
|
105
|
+
|
|
88
106
|
|
|
89
107
|
def _get_config_value(
|
|
90
|
-
config: pytest.Config,
|
|
108
|
+
config: pytest.Config,
|
|
109
|
+
option: str,
|
|
110
|
+
env_var: str,
|
|
111
|
+
ini_name: str | None = None,
|
|
112
|
+
default: str | None = None,
|
|
91
113
|
) -> str | None:
|
|
92
|
-
"""Get config value from CLI option, env var, or default.
|
|
114
|
+
"""Get config value from CLI option, env var, ini setting, or default.
|
|
115
|
+
|
|
116
|
+
Priority order: CLI option > environment variable > ini setting > default
|
|
117
|
+
"""
|
|
118
|
+
# 1. CLI option (highest priority)
|
|
93
119
|
value = config.getoption(option, default=None)
|
|
94
120
|
if value is not None:
|
|
95
121
|
return value
|
|
96
|
-
|
|
122
|
+
|
|
123
|
+
# 2. Environment variable
|
|
124
|
+
env_value = os.environ.get(env_var)
|
|
125
|
+
if env_value is not None:
|
|
126
|
+
return env_value
|
|
127
|
+
|
|
128
|
+
# 3. INI setting (pytest.ini, pyproject.toml, etc.)
|
|
129
|
+
if ini_name is not None:
|
|
130
|
+
ini_value = config.getini(ini_name)
|
|
131
|
+
if ini_value:
|
|
132
|
+
return ini_value
|
|
133
|
+
|
|
134
|
+
# 4. Default
|
|
135
|
+
return default
|
|
97
136
|
|
|
98
137
|
|
|
99
138
|
def _create_neon_branch(
|
|
@@ -106,20 +145,32 @@ def _create_neon_branch(
|
|
|
106
145
|
"""
|
|
107
146
|
config = request.config
|
|
108
147
|
|
|
109
|
-
api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY")
|
|
110
|
-
project_id = _get_config_value(
|
|
148
|
+
api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
|
|
149
|
+
project_id = _get_config_value(
|
|
150
|
+
config, "neon_project_id", "NEON_PROJECT_ID", "neon_project_id"
|
|
151
|
+
)
|
|
111
152
|
parent_branch_id = _get_config_value(
|
|
112
|
-
config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID"
|
|
153
|
+
config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID", "neon_parent_branch"
|
|
113
154
|
)
|
|
114
155
|
database_name = _get_config_value(
|
|
115
|
-
config, "neon_database", "NEON_DATABASE", "neondb"
|
|
156
|
+
config, "neon_database", "NEON_DATABASE", "neon_database", "neondb"
|
|
157
|
+
)
|
|
158
|
+
role_name = _get_config_value(
|
|
159
|
+
config, "neon_role", "NEON_ROLE", "neon_role", "neondb_owner"
|
|
116
160
|
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
|
|
162
|
+
# For boolean/int options, check CLI first, then ini
|
|
163
|
+
keep_branches = config.getoption("neon_keep_branches", default=None)
|
|
164
|
+
if keep_branches is None:
|
|
165
|
+
keep_branches = config.getini("neon_keep_branches")
|
|
166
|
+
|
|
167
|
+
branch_expiry = config.getoption("neon_branch_expiry", default=None)
|
|
168
|
+
if branch_expiry is None:
|
|
169
|
+
branch_expiry = int(config.getini("neon_branch_expiry"))
|
|
170
|
+
|
|
171
|
+
env_var_name = _get_config_value(
|
|
172
|
+
config, "neon_env_var", "", "neon_env_var", "DATABASE_URL"
|
|
121
173
|
)
|
|
122
|
-
env_var_name = config.getoption("neon_env_var", default="DATABASE_URL")
|
|
123
174
|
|
|
124
175
|
if not api_key:
|
|
125
176
|
pytest.skip(
|
|
@@ -168,9 +219,10 @@ def _create_neon_branch(
|
|
|
168
219
|
raise RuntimeError(f"No endpoint created for branch {branch.id}")
|
|
169
220
|
|
|
170
221
|
# Wait for endpoint to be ready (it starts in "init" state)
|
|
171
|
-
# Endpoints typically become active in 1-2 seconds
|
|
222
|
+
# Endpoints typically become active in 1-2 seconds, but we allow up to 60s
|
|
223
|
+
# to handle occasional Neon API slowness or high load scenarios
|
|
172
224
|
max_wait_seconds = 60
|
|
173
|
-
poll_interval = 0.5
|
|
225
|
+
poll_interval = 0.5 # Poll every 500ms for responsive feedback
|
|
174
226
|
waited = 0.0
|
|
175
227
|
|
|
176
228
|
while True:
|
|
@@ -180,7 +232,7 @@ def _create_neon_branch(
|
|
|
180
232
|
endpoint = endpoint_response.endpoint
|
|
181
233
|
state = endpoint.current_state
|
|
182
234
|
|
|
183
|
-
if state ==
|
|
235
|
+
if state == EndpointState.active:
|
|
184
236
|
break
|
|
185
237
|
|
|
186
238
|
if waited >= max_wait_seconds:
|
|
@@ -254,7 +306,7 @@ def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
|
|
|
254
306
|
"Content-Type": "application/json",
|
|
255
307
|
}
|
|
256
308
|
response = requests.post(
|
|
257
|
-
url, headers=headers, json={"source_branch_id": branch.parent_id}
|
|
309
|
+
url, headers=headers, json={"source_branch_id": branch.parent_id}, timeout=30
|
|
258
310
|
)
|
|
259
311
|
response.raise_for_status()
|
|
260
312
|
|
|
@@ -302,7 +354,16 @@ def neon_branch(
|
|
|
302
354
|
conn_string = neon_branch.connection_string
|
|
303
355
|
"""
|
|
304
356
|
config = request.config
|
|
305
|
-
api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY")
|
|
357
|
+
api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
|
|
358
|
+
|
|
359
|
+
# Validate that branch has a parent for reset functionality
|
|
360
|
+
if not _neon_branch_for_reset.parent_id:
|
|
361
|
+
pytest.fail(
|
|
362
|
+
f"\n\nBranch {_neon_branch_for_reset.branch_id} has no parent. "
|
|
363
|
+
f"The neon_branch fixture requires a parent branch for reset.\n\n"
|
|
364
|
+
f"Use neon_branch_shared if you don't need reset, or specify "
|
|
365
|
+
f"a parent branch with --neon-parent-branch or NEON_PARENT_BRANCH_ID."
|
|
366
|
+
)
|
|
306
367
|
|
|
307
368
|
yield _neon_branch_for_reset
|
|
308
369
|
|
|
@@ -311,11 +372,11 @@ def neon_branch(
|
|
|
311
372
|
try:
|
|
312
373
|
_reset_branch_to_parent(branch=_neon_branch_for_reset, api_key=api_key)
|
|
313
374
|
except Exception as e:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
f"
|
|
318
|
-
|
|
375
|
+
pytest.fail(
|
|
376
|
+
f"\n\nFailed to reset branch {_neon_branch_for_reset.branch_id} "
|
|
377
|
+
f"after test. Subsequent tests in this module may see dirty "
|
|
378
|
+
f"database state.\n\nError: {e}\n\n"
|
|
379
|
+
f"To keep the branch for debugging, use --neon-keep-branches"
|
|
319
380
|
)
|
|
320
381
|
|
|
321
382
|
|
|
@@ -19,8 +19,13 @@ from pytest_neon.plugin import NeonBranch
|
|
|
19
19
|
@pytest.fixture(scope="module")
|
|
20
20
|
def neon_branch(request):
|
|
21
21
|
"""Mock neon_branch fixture for testing."""
|
|
22
|
-
keep_branches = request.config.getoption("neon_keep_branches", default=
|
|
23
|
-
|
|
22
|
+
keep_branches = request.config.getoption("neon_keep_branches", default=None)
|
|
23
|
+
if keep_branches is None:
|
|
24
|
+
keep_branches = False
|
|
25
|
+
|
|
26
|
+
env_var_name = request.config.getoption("neon_env_var", default=None)
|
|
27
|
+
if env_var_name is None:
|
|
28
|
+
env_var_name = "DATABASE_URL"
|
|
24
29
|
|
|
25
30
|
branch_info = NeonBranch(
|
|
26
31
|
branch_id="br-mock-123",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Tests for branch reset behavior."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestResetBehavior:
|
|
5
|
+
"""Test that branch reset happens between tests."""
|
|
6
|
+
|
|
7
|
+
def test_reset_called_after_each_test(self, pytester):
|
|
8
|
+
"""Verify reset is called after each test function."""
|
|
9
|
+
pytester.makeconftest(
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import pytest
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
reset_count = [0]
|
|
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="module")
|
|
26
|
+
def _neon_branch_for_reset():
|
|
27
|
+
branch = FakeNeonBranch(
|
|
28
|
+
branch_id="br-test",
|
|
29
|
+
project_id="proj-test",
|
|
30
|
+
connection_string="postgresql://test",
|
|
31
|
+
host="test.neon.tech",
|
|
32
|
+
parent_id="br-parent",
|
|
33
|
+
)
|
|
34
|
+
os.environ["DATABASE_URL"] = branch.connection_string
|
|
35
|
+
try:
|
|
36
|
+
yield branch
|
|
37
|
+
finally:
|
|
38
|
+
os.environ.pop("DATABASE_URL", None)
|
|
39
|
+
|
|
40
|
+
@pytest.fixture(scope="function")
|
|
41
|
+
def neon_branch(_neon_branch_for_reset):
|
|
42
|
+
yield _neon_branch_for_reset
|
|
43
|
+
# Simulate reset
|
|
44
|
+
reset_count[0] += 1
|
|
45
|
+
|
|
46
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
47
|
+
# Verify resets happened
|
|
48
|
+
assert reset_count[0] == 2, f"Expected 2 resets, got {reset_count[0]}"
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
pytester.makepyfile(
|
|
53
|
+
"""
|
|
54
|
+
def test_first(neon_branch):
|
|
55
|
+
assert neon_branch.branch_id == "br-test"
|
|
56
|
+
|
|
57
|
+
def test_second(neon_branch):
|
|
58
|
+
assert neon_branch.branch_id == "br-test"
|
|
59
|
+
"""
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result = pytester.runpytest("-v")
|
|
63
|
+
result.assert_outcomes(passed=2)
|
|
64
|
+
|
|
65
|
+
def test_same_branch_used_across_tests_in_module(self, pytester):
|
|
66
|
+
"""Verify all tests in a module use the same branch instance."""
|
|
67
|
+
pytester.makeconftest(
|
|
68
|
+
"""
|
|
69
|
+
import os
|
|
70
|
+
import pytest
|
|
71
|
+
from dataclasses import dataclass
|
|
72
|
+
|
|
73
|
+
branch_ids_seen = []
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class FakeNeonBranch:
|
|
77
|
+
branch_id: str
|
|
78
|
+
project_id: str
|
|
79
|
+
connection_string: str
|
|
80
|
+
host: str
|
|
81
|
+
parent_id: str
|
|
82
|
+
|
|
83
|
+
@pytest.fixture(scope="module")
|
|
84
|
+
def _neon_branch_for_reset():
|
|
85
|
+
import random
|
|
86
|
+
branch = FakeNeonBranch(
|
|
87
|
+
branch_id=f"br-{random.randint(1000, 9999)}",
|
|
88
|
+
project_id="proj-test",
|
|
89
|
+
connection_string="postgresql://test",
|
|
90
|
+
host="test.neon.tech",
|
|
91
|
+
parent_id="br-parent",
|
|
92
|
+
)
|
|
93
|
+
os.environ["DATABASE_URL"] = branch.connection_string
|
|
94
|
+
try:
|
|
95
|
+
yield branch
|
|
96
|
+
finally:
|
|
97
|
+
os.environ.pop("DATABASE_URL", None)
|
|
98
|
+
|
|
99
|
+
@pytest.fixture(scope="function")
|
|
100
|
+
def neon_branch(_neon_branch_for_reset):
|
|
101
|
+
branch_ids_seen.append(_neon_branch_for_reset.branch_id)
|
|
102
|
+
yield _neon_branch_for_reset
|
|
103
|
+
|
|
104
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
105
|
+
# All tests should see the same branch
|
|
106
|
+
unique = len(set(branch_ids_seen))
|
|
107
|
+
assert unique == 1, f"Expected 1 unique branch, got {unique}"
|
|
108
|
+
"""
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
pytester.makepyfile(
|
|
112
|
+
"""
|
|
113
|
+
def test_first(neon_branch):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def test_second(neon_branch):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def test_third(neon_branch):
|
|
120
|
+
pass
|
|
121
|
+
"""
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
result = pytester.runpytest("-v")
|
|
125
|
+
result.assert_outcomes(passed=3)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestParentIdValidation:
|
|
129
|
+
"""Test that missing parent_id is caught early."""
|
|
130
|
+
|
|
131
|
+
def test_fails_if_no_parent_id(self, pytester):
|
|
132
|
+
"""Verify that neon_branch fails if branch has no parent."""
|
|
133
|
+
pytester.makeconftest(
|
|
134
|
+
"""
|
|
135
|
+
import os
|
|
136
|
+
import pytest
|
|
137
|
+
from dataclasses import dataclass
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class FakeNeonBranch:
|
|
141
|
+
branch_id: str
|
|
142
|
+
project_id: str
|
|
143
|
+
connection_string: str
|
|
144
|
+
host: str
|
|
145
|
+
parent_id: str = None # No parent!
|
|
146
|
+
|
|
147
|
+
@pytest.fixture(scope="module")
|
|
148
|
+
def _neon_branch_for_reset():
|
|
149
|
+
branch = FakeNeonBranch(
|
|
150
|
+
branch_id="br-test",
|
|
151
|
+
project_id="proj-test",
|
|
152
|
+
connection_string="postgresql://test",
|
|
153
|
+
host="test.neon.tech",
|
|
154
|
+
parent_id=None, # No parent
|
|
155
|
+
)
|
|
156
|
+
os.environ["DATABASE_URL"] = branch.connection_string
|
|
157
|
+
try:
|
|
158
|
+
yield branch
|
|
159
|
+
finally:
|
|
160
|
+
os.environ.pop("DATABASE_URL", None)
|
|
161
|
+
|
|
162
|
+
@pytest.fixture(scope="function")
|
|
163
|
+
def neon_branch(_neon_branch_for_reset):
|
|
164
|
+
if not _neon_branch_for_reset.parent_id:
|
|
165
|
+
pytest.fail("Branch has no parent - cannot reset")
|
|
166
|
+
yield _neon_branch_for_reset
|
|
167
|
+
"""
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
pytester.makepyfile(
|
|
171
|
+
"""
|
|
172
|
+
def test_should_fail(neon_branch):
|
|
173
|
+
pass
|
|
174
|
+
"""
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
result = pytester.runpytest("-v")
|
|
178
|
+
result.assert_outcomes(errors=1)
|
|
179
|
+
assert "has no parent" in result.stdout.str()
|
|
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
|