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.
Files changed (22) hide show
  1. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/CLAUDE.md +16 -7
  2. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/PKG-INFO +48 -1
  3. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/README.md +47 -0
  4. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/pyproject.toml +1 -1
  5. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/src/pytest_neon/__init__.py +1 -1
  6. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/src/pytest_neon/plugin.py +92 -31
  7. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/conftest.py +7 -2
  8. pytest_neon-0.4.0/tests/test_reset_behavior.py +179 -0
  9. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.env.example +0 -0
  10. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.github/workflows/release.yml +0 -0
  11. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.github/workflows/tests.yml +0 -0
  12. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.gitignore +0 -0
  13. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/.neon +0 -0
  14. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/LICENSE +0 -0
  15. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/src/pytest_neon/py.typed +0 -0
  16. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_branch_lifecycle.py +0 -0
  17. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_cli_options.py +0 -0
  18. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_env_var.py +0 -0
  19. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_fixture_errors.py +0 -0
  20. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_integration.py +0 -0
  21. {pytest_neon-0.2.1 → pytest_neon-0.4.0}/tests/test_skip_behavior.py +0 -0
  22. {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 module gets its own branch, with automatic cleanup.
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` only
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
- - `neon_branch`: `scope="module"` - one branch per test file
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 `_temporary_env` context manager sets `DATABASE_URL` during test execution and restores the original value after. This is critical for not polluting other tests.
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
- python -m build
42
- python -m twine upload dist/*
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.2.1
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"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-neon"
7
- version = "0.2.1"
7
+ version = "0.4.0"
8
8
  description = "Pytest plugin for Neon database branch isolation in tests"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,7 +9,7 @@ from pytest_neon.plugin import (
9
9
  neon_engine,
10
10
  )
11
11
 
12
- __version__ = "0.2.1"
12
+ __version__ = "0.4.0"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
@@ -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 TYPE_CHECKING, Any
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, option: str, env_var: str, default: str | None = None
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
- return os.environ.get(env_var, default)
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(config, "neon_project_id", "NEON_PROJECT_ID")
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
- role_name = _get_config_value(config, "neon_role", "NEON_ROLE", "neondb_owner")
118
- keep_branches = config.getoption("neon_keep_branches", default=False)
119
- branch_expiry = config.getoption(
120
- "neon_branch_expiry", default=DEFAULT_BRANCH_EXPIRY_SECONDS
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 == "active" or str(state) == "EndpointState.active":
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
- import warnings
315
-
316
- warnings.warn(
317
- f"Failed to reset branch {_neon_branch_for_reset.branch_id}: {e}",
318
- stacklevel=2,
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=False)
23
- env_var_name = request.config.getoption("neon_env_var", default="DATABASE_URL")
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