pytest-neon 2.2.1__tar.gz → 2.3.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 (31) hide show
  1. pytest_neon-2.3.0/CLAUDE.md +120 -0
  2. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/PKG-INFO +150 -76
  3. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/README.md +149 -75
  4. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/pyproject.toml +1 -1
  5. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/src/pytest_neon/__init__.py +1 -1
  6. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/src/pytest_neon/plugin.py +914 -300
  7. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/conftest.py +27 -2
  8. pytest_neon-2.3.0/tests/test_dirty_isolated_fixtures.py +400 -0
  9. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_readwrite_readonly_fixtures.py +129 -2
  10. pytest_neon-2.3.0/tests/test_service_classes.py +119 -0
  11. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/uv.lock +1 -1
  12. pytest_neon-2.2.1/CLAUDE.md +0 -89
  13. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/.config/wt.toml +0 -0
  14. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/.env.example +0 -0
  15. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/.github/workflows/release.yml +0 -0
  16. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/.github/workflows/tests.yml +0 -0
  17. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/.gitignore +0 -0
  18. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/.neon +0 -0
  19. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/LICENSE +0 -0
  20. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/src/pytest_neon/py.typed +0 -0
  21. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_branch_lifecycle.py +0 -0
  22. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_branch_name_prefix.py +0 -0
  23. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_cli_options.py +0 -0
  24. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_default_branch_safety.py +0 -0
  25. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_env_var.py +0 -0
  26. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_fixture_errors.py +0 -0
  27. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_integration.py +0 -0
  28. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_migrations.py +0 -0
  29. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_reset_behavior.py +0 -0
  30. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_skip_behavior.py +0 -0
  31. {pytest_neon-2.2.1 → pytest_neon-2.3.0}/tests/test_xdist_worker_support.py +0 -0
@@ -0,0 +1,120 @@
1
+ # Claude Code Instructions for pytest-neon
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
+
7
+ ## Project Overview
8
+
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.
10
+
11
+ ## Key Architecture
12
+
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
16
+ - **Core fixtures**:
17
+ - `neon_branch_readonly` - Session-scoped, uses true read_only endpoint (enforced read-only)
18
+ - `neon_branch_dirty` - Session-scoped, shared across ALL xdist workers (fast, shared state)
19
+ - `neon_branch_isolated` - Function-scoped, per-worker branch with reset after each test (recommended for writes)
20
+ - `neon_branch_readwrite` - Deprecated alias for `neon_branch_isolated`
21
+ - `neon_branch` - Deprecated alias for `neon_branch_isolated`
22
+ - **Shared fixture**: `neon_branch_shared` - Module-scoped, no reset between tests
23
+ - **Convenience fixtures**: `neon_connection`, `neon_connection_psycopg`, `neon_engine` - Optional, require extras
24
+
25
+ ## Branch Hierarchy
26
+
27
+ ```
28
+ Parent Branch (configured or project default)
29
+ └── Migration Branch (session-scoped, read_write endpoint)
30
+ │ ↑ migrations run here ONCE
31
+
32
+ ├── Read-only Endpoint (read_only endpoint ON migration branch)
33
+ │ ↑ neon_branch_readonly uses this (enforced read-only)
34
+
35
+ ├── Dirty Branch (session-scoped child, shared across ALL workers)
36
+ │ ↑ neon_branch_dirty uses this
37
+
38
+ └── Isolated Branch (one per xdist worker, lazily created)
39
+ ↑ neon_branch_isolated uses this, reset after each test
40
+ ```
41
+
42
+ ## Dependencies
43
+
44
+ - Core: `pytest`, `neon-api`, `requests`, `filelock`
45
+ - Optional extras: `psycopg2`, `psycopg`, `sqlalchemy` - for convenience fixtures
46
+
47
+ ## Important Patterns
48
+
49
+ ### Modular Architecture
50
+
51
+ The plugin uses a service-oriented architecture for testability:
52
+
53
+ - **NeonConfig**: Dataclass for configuration extraction from pytest config
54
+ - **NeonBranchManager**: Manages all Neon API operations (branch create/delete, endpoint create, password reset)
55
+ - **XdistCoordinator**: Handles worker synchronization with file locks and JSON caching
56
+ - **EnvironmentManager**: Manages DATABASE_URL environment variable lifecycle
57
+
58
+ ### Fixture Scopes
59
+ - `_neon_config`: `scope="session"` - Configuration extracted from pytest config
60
+ - `_neon_branch_manager`: `scope="session"` - Branch lifecycle manager
61
+ - `_neon_xdist_coordinator`: `scope="session"` - Worker synchronization
62
+ - `_neon_migration_branch`: `scope="session"` - Parent for all test branches, migrations run here
63
+ - `neon_apply_migrations`: `scope="session"` - User overrides to run migrations
64
+ - `_neon_migrations_synchronized`: `scope="session"` - Signals migration completion across workers
65
+ - `_neon_dirty_branch`: `scope="session"` - Internal, shared across ALL workers
66
+ - `_neon_readonly_endpoint`: `scope="session"` - Internal, read_only endpoint on migration branch
67
+ - `_neon_isolated_branch`: `scope="session"` - Internal, one per xdist worker
68
+ - `neon_branch_readonly`: `scope="session"` - User-facing, true read-only access
69
+ - `neon_branch_dirty`: `scope="session"` - User-facing, shared state across workers
70
+ - `neon_branch_isolated`: `scope="function"` - User-facing, reset after each test
71
+ - `neon_branch_readwrite`: `scope="function"` - Deprecated alias for isolated
72
+ - `neon_branch`: `scope="function"` - Deprecated alias for isolated
73
+ - `neon_branch_shared`: `scope="module"` - One branch per test file, no reset
74
+ - Connection fixtures: `scope="function"` (default) - Fresh connection per test
75
+
76
+ ### Environment Variable Handling
77
+ The `EnvironmentManager` class handles `DATABASE_URL` lifecycle:
78
+ - Sets environment variable when fixture starts
79
+ - Saves original value for restoration
80
+ - Restores original value (or removes) when fixture ends
81
+
82
+ ### xdist Worker Synchronization
83
+ The `XdistCoordinator` handles sharing resources across workers:
84
+ - Uses file locks (`filelock`) for coordination
85
+ - Stores shared resource data in JSON files
86
+ - `coordinate_resource()` ensures only one worker creates shared resources
87
+ - `wait_for_signal()` / `send_signal()` for migration synchronization
88
+
89
+ ### Error Messages
90
+ 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.
91
+
92
+ ## Documentation
93
+
94
+ Important help text should be documented in BOTH:
95
+ 1. **README.md** - Full user-facing documentation
96
+ 2. **Module/fixture docstrings** - So `help(pytest_neon)` shows useful info
97
+
98
+ The module docstring in `plugin.py` should include key usage notes (like the SQLAlchemy `pool_pre_ping=True` requirement). Keep docstrings and README in sync.
99
+
100
+ ## Commit Messages
101
+ - Do NOT add Claude attribution or Co-Authored-By lines
102
+ - Keep commits clean and descriptive
103
+
104
+ ## Testing
105
+
106
+ Run tests with:
107
+ ```bash
108
+ uv run pytest tests/ -v
109
+ ```
110
+
111
+ Tests in `tests/` use `pytester` for testing pytest plugins. The plugin itself can be tested without a real Neon connection by mocking `NeonAPI`.
112
+
113
+ ## Publishing
114
+
115
+ **Always use the GitHub Actions release workflow** - do not manually bump versions:
116
+ 1. Go to Actions → Release → Run workflow
117
+ 2. Choose patch/minor/major
118
+ 3. Workflow bumps version, commits, tags, and publishes to PyPI
119
+
120
+ Package name on PyPI: `pytest-neon`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 2.2.1
3
+ Version: 2.3.0
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
@@ -98,7 +98,7 @@ export NEON_PROJECT_ID="your-project-id"
98
98
  2. Write tests:
99
99
 
100
100
  ```python
101
- def test_user_creation(neon_branch_readwrite):
101
+ def test_user_creation(neon_branch_isolated):
102
102
  # DATABASE_URL is automatically set to the test branch
103
103
  import psycopg # Your own install
104
104
 
@@ -119,12 +119,40 @@ pytest
119
119
 
120
120
  **Which fixture should I use?**
121
121
 
122
- - **Use `neon_branch_readonly`** if your test only reads data (SELECT queries). This is the fastest option with no per-test overhead.
123
- - **Use `neon_branch_readwrite`** if your test modifies data (INSERT, UPDATE, DELETE). This resets the branch after each test for isolation.
122
+ | Fixture | Scope | Reset | Best For | Overhead |
123
+ |---------|-------|-------|----------|----------|
124
+ | `neon_branch_readonly` | session | None | Read-only tests (SELECT) - enforced | ~0s/test |
125
+ | `neon_branch_dirty` | session | None | Fast writes, shared state OK | ~0s/test |
126
+ | `neon_branch_isolated` | function | After each test | Write tests needing isolation | ~0.5s/test |
127
+ | `neon_branch_shared` | module | None | Module-level shared state | ~0s/test |
124
128
 
125
- ### `neon_branch_readonly` (recommended, fastest)
129
+ **Quick guide:**
130
+ - **Use `neon_branch_readonly`** if your test only reads data (SELECT queries). This uses a true read-only endpoint that enforces read-only access at the database level. Any write attempt will fail with a database error.
131
+ - **Use `neon_branch_dirty`** if your tests write data but can tolerate shared state across the session. Fast because no reset or branch creation per test.
132
+ - **Use `neon_branch_isolated`** if your test modifies data and needs isolation from other tests. Resets the branch after each test.
126
133
 
127
- **Use this fixture by default** if your tests don't need to write data. It provides the best performance by skipping the branch reset step (~0.5s saved per test), which also reduces API calls and avoids rate limiting issues.
134
+ ### Architecture
135
+
136
+ The plugin creates a branch hierarchy to efficiently support all fixture types:
137
+
138
+ ```
139
+ Parent Branch (configured or project default)
140
+ └── Migration Branch (session-scoped, read_write endpoint)
141
+ │ ↑ migrations run here ONCE
142
+
143
+ ├── Read-only Endpoint (read_only endpoint ON migration branch)
144
+ │ ↑ neon_branch_readonly uses this (enforced read-only)
145
+
146
+ ├── Dirty Branch (session-scoped child, shared across ALL workers)
147
+ │ ↑ neon_branch_dirty uses this
148
+
149
+ └── Isolated Branch (one per xdist worker, lazily created)
150
+ ↑ neon_branch_isolated uses this, reset after each test
151
+ ```
152
+
153
+ ### `neon_branch_readonly` (session-scoped, enforced read-only)
154
+
155
+ **Use this fixture** for tests that only read data. It uses a true `read_only` endpoint on the migration branch, which enforces read-only access at the database level. Any attempt to INSERT, UPDATE, or DELETE will result in a database error.
128
156
 
129
157
  ```python
130
158
  def test_query_users(neon_branch_readonly):
@@ -133,57 +161,115 @@ def test_query_users(neon_branch_readonly):
133
161
  with psycopg.connect(neon_branch_readonly.connection_string) as conn:
134
162
  result = conn.execute("SELECT * FROM users").fetchall()
135
163
  assert len(result) >= 0
136
- # No reset after this test - fast!
164
+
165
+ # This would fail with a database error:
166
+ # conn.execute("INSERT INTO users (name) VALUES ('test')")
137
167
  ```
138
168
 
139
169
  **Use this when**:
140
170
  - Tests only perform SELECT queries
141
171
  - Tests don't modify database state
142
- - You want maximum performance
172
+ - You want maximum performance with true enforcement
143
173
 
144
- **Warning**: If you accidentally write data using this fixture, subsequent tests will see those modifications. The fixture does not enforce read-only access at the database level.
174
+ **Session-scoped**: The endpoint is created once and shared across all tests and workers.
145
175
 
146
- **Performance**: ~1.5s initial setup per session, **no per-test overhead**. For 10 read-only tests, expect only ~1.5s total overhead (vs ~6.5s with readwrite).
176
+ **Performance**: ~1.5s initial setup per session, **no per-test overhead**. For 10 read-only tests, expect only ~1.5s total overhead.
147
177
 
148
- ### `neon_branch_readwrite` (for write tests)
178
+ ### `neon_branch_dirty` (session-scoped, shared state)
149
179
 
150
- Use this fixture when your tests need to INSERT, UPDATE, or DELETE data. Creates one branch per test session, then resets it to the parent branch's state after each test. This provides test isolation with ~0.5s overhead per test.
180
+ Use this fixture when your tests write data but can tolerate shared state across the session. All tests share the same branch and writes persist (no cleanup between tests). This is faster than `neon_branch_isolated` because there's no reset overhead.
151
181
 
152
182
  ```python
153
- import os
154
-
155
- def test_insert_user(neon_branch_readwrite):
183
+ def test_insert_user(neon_branch_dirty):
156
184
  # DATABASE_URL is set automatically
157
- assert os.environ["DATABASE_URL"] == neon_branch_readwrite.connection_string
185
+ import psycopg
186
+ with psycopg.connect(neon_branch_dirty.connection_string) as conn:
187
+ conn.execute("INSERT INTO users (name) VALUES ('test')")
188
+ conn.commit()
189
+ # Data persists - subsequent tests will see this user
190
+
191
+ def test_count_users(neon_branch_dirty):
192
+ # This test sees data from previous tests
193
+ import psycopg
194
+ with psycopg.connect(neon_branch_dirty.connection_string) as conn:
195
+ result = conn.execute("SELECT COUNT(*) FROM users").fetchone()
196
+ # Count includes users from previous tests
197
+ ```
198
+
199
+ **Use this when**:
200
+ - Most tests can share database state without interference
201
+ - You want maximum performance with minimal API calls
202
+ - You manually manage test data cleanup if needed
203
+ - You're combining it with `neon_branch_isolated` for specific tests that need clean state
204
+
205
+ **Warning**: Data written by one test WILL be visible to subsequent tests AND to other xdist workers. This is truly shared - use `neon_branch_isolated` for tests that require guaranteed clean state.
206
+
207
+ **pytest-xdist note**: ALL workers share the same dirty branch. Concurrent writes from different workers may conflict. This is "dirty" by design - for isolation, use `neon_branch_isolated`.
208
+
209
+ **Performance**: ~1.5s initial setup per session, **no per-test overhead**. For 10 write tests, expect only ~1.5s total overhead.
158
210
 
159
- # Use with any driver
211
+ ### `neon_branch_isolated` (function-scoped, full isolation)
212
+
213
+ Use this fixture when your test modifies data and needs isolation from other tests. Each xdist worker has its own branch, and the branch is reset to the migration state after each test.
214
+
215
+ ```python
216
+ def test_insert_user(neon_branch_isolated):
217
+ # DATABASE_URL is set automatically
160
218
  import psycopg
161
- with psycopg.connect(neon_branch_readwrite.connection_string) as conn:
219
+ with psycopg.connect(neon_branch_isolated.connection_string) as conn:
220
+ # Guaranteed clean state - no data from other tests
221
+ # (only migration/seed data if you defined neon_apply_migrations)
222
+ result = conn.execute("SELECT COUNT(*) FROM users").fetchone()
223
+ initial_count = result[0]
224
+
162
225
  conn.execute("INSERT INTO users (name) VALUES ('test')")
163
226
  conn.commit()
164
- # Branch resets after this test - changes won't affect other tests
227
+
228
+ # Verify our insert worked
229
+ result = conn.execute("SELECT COUNT(*) FROM users").fetchone()
230
+ assert result[0] == initial_count + 1
231
+ # Branch resets after this test - next test sees clean state
165
232
  ```
166
233
 
167
- **Performance**: ~1.5s initial setup per session + ~0.5s reset per test. For 10 write tests, expect ~6.5s total overhead.
234
+ **Use this when**:
235
+ - A test modifies database state (INSERT, UPDATE, DELETE)
236
+ - Test isolation is important
237
+ - You're combining it with `neon_branch_dirty` for most tests but need isolation for specific ones
238
+
239
+ **SQLAlchemy Users**: If you create your own engine (not using the `neon_engine` fixture), you MUST use `pool_pre_ping=True`:
240
+
241
+ ```python
242
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
243
+ ```
244
+
245
+ Branch resets terminate server-side connections. Without `pool_pre_ping`, SQLAlchemy may reuse dead pooled connections, causing SSL errors.
246
+
247
+ **pytest-xdist note**: Each worker has its own isolated branch. Resets only affect that worker's branch, so workers don't interfere with each other.
248
+
249
+ **Performance**: ~1.5s initial setup per session per worker + ~0.5s reset per test. For 10 write tests, expect ~6.5s total overhead.
168
250
 
169
251
  ### `NeonBranch` dataclass
170
252
 
171
- Both fixtures return a `NeonBranch` dataclass with:
253
+ All fixtures return a `NeonBranch` dataclass with:
172
254
 
173
255
  - `branch_id`: The Neon branch ID
174
256
  - `project_id`: The Neon project ID
175
257
  - `connection_string`: Full PostgreSQL connection URI
176
258
  - `host`: The database host
177
259
  - `parent_id`: The parent branch ID (used for resets)
260
+ - `endpoint_id`: The endpoint ID (for cleanup)
178
261
 
179
- ### `neon_branch` (deprecated)
262
+ ### `neon_branch_readwrite` (deprecated)
180
263
 
181
- > **Deprecated**: Use `neon_branch_readwrite` or `neon_branch_readonly` instead.
264
+ > **Deprecated**: Use `neon_branch_isolated` instead.
265
+
266
+ This fixture is an alias for `neon_branch_isolated` and will emit a deprecation warning.
267
+
268
+ ### `neon_branch` (deprecated)
182
269
 
183
- This fixture is an alias for `neon_branch_readwrite` and will emit a deprecation warning. Migrate to the explicit fixture names for clarity:
270
+ > **Deprecated**: Use `neon_branch_isolated`, `neon_branch_dirty`, or `neon_branch_readonly` instead.
184
271
 
185
- - `neon_branch_readwrite`: For tests that modify data (INSERT/UPDATE/DELETE)
186
- - `neon_branch_readonly`: For tests that only read data (SELECT)
272
+ This fixture is an alias for `neon_branch_isolated` and will emit a deprecation warning.
187
273
 
188
274
  ### `neon_branch_shared` (module-scoped, no isolation)
189
275
 
@@ -251,21 +337,21 @@ def test_query(neon_engine):
251
337
 
252
338
  ### Using Your Own SQLAlchemy Engine
253
339
 
254
- If you have a module-level SQLAlchemy engine (common pattern) and use `neon_branch_readwrite`, you **must** use `pool_pre_ping=True`:
340
+ If you have a module-level SQLAlchemy engine (common pattern) and use `neon_branch_isolated`, you **must** use `pool_pre_ping=True`:
255
341
 
256
342
  ```python
257
343
  # database.py
258
344
  from sqlalchemy import create_engine
259
345
  from config import DATABASE_URL
260
346
 
261
- # pool_pre_ping=True is REQUIRED when using neon_branch_readwrite
347
+ # pool_pre_ping=True is REQUIRED when using neon_branch_isolated
262
348
  # It verifies connections are alive before using them
263
349
  engine = create_engine(DATABASE_URL, pool_pre_ping=True)
264
350
  ```
265
351
 
266
- **Why?** After each test, `neon_branch_readwrite` resets the branch which terminates server-side connections. Without `pool_pre_ping`, SQLAlchemy may try to reuse a dead pooled connection, causing `SSL connection has been closed unexpectedly` errors.
352
+ **Why?** After each test, `neon_branch_isolated` resets the branch which terminates server-side connections. Without `pool_pre_ping`, SQLAlchemy may try to reuse a dead pooled connection, causing `SSL connection has been closed unexpectedly` errors.
267
353
 
268
- **Note**: If you only use `neon_branch_readonly`, `pool_pre_ping` is not required since no resets occur.
354
+ **Note**: If you only use `neon_branch_readonly` or `neon_branch_dirty`, `pool_pre_ping` is not required since no resets occur.
269
355
 
270
356
  This is also a best practice for any cloud database (Neon, RDS, etc.) where connections can be terminated externally.
271
357
 
@@ -273,41 +359,18 @@ This is also a best practice for any cloud database (Neon, RDS, etc.) where conn
273
359
 
274
360
  pytest-neon supports running migrations once before tests, with all test resets preserving the migrated state.
275
361
 
276
- ### Smart Migration Detection
277
-
278
- The plugin automatically detects whether migrations actually modified the database schema. This optimization:
279
-
280
- - **Saves Neon costs**: No extra branch created when migrations don't change anything
281
- - **Saves branch slots**: Neon projects have branch limits; this avoids wasting them
282
- - **Zero configuration**: Works automatically with any migration tool
283
-
284
- **When a second branch is created:**
285
- - Only when `neon_apply_migrations` is overridden AND the schema actually changes
286
-
287
- **When only one branch is used:**
288
- - If you don't override `neon_apply_migrations` (no migrations defined)
289
- - If your migrations are already applied (schema unchanged)
290
-
291
- The detection works by comparing a fingerprint of `information_schema.columns` before and after migrations run.
292
-
293
362
  ### How It Works
294
363
 
295
- When migrations actually modify the schema, the plugin uses a two-branch architecture:
364
+ The plugin always creates a migration branch from your configured parent:
296
365
 
297
366
  ```
298
367
  Parent Branch (your configured parent)
299
368
  └── Migration Branch (session-scoped)
300
369
  │ ↑ migrations run here ONCE
301
- └── Test Branch (session-scoped)
302
- resets to migration branch after each test
303
- ```
304
-
305
- When no schema changes occur, the plugin uses a single-branch architecture:
306
-
307
- ```
308
- Parent Branch (your configured parent)
309
- └── Migration/Test Branch (session-scoped)
310
- ↑ resets to parent after each test
370
+
371
+ ├── Read-only Endpoint (for neon_branch_readonly)
372
+ ├── Dirty Branch (for neon_branch_dirty)
373
+ └── Isolated Branches (for neon_branch_isolated)
311
374
  ```
312
375
 
313
376
  This means:
@@ -470,18 +533,19 @@ jobs:
470
533
 
471
534
  ## How It Works
472
535
 
473
- 1. At the start of the test session, the plugin creates a new Neon branch from your parent branch
474
- 2. `DATABASE_URL` is set to point to the new branch
475
- 3. Tests run against this isolated branch with full access to your schema and data
476
- 4. After each test, the branch is reset to its parent state (~0.5s)
477
- 5. After all tests complete, the branch is deleted
478
- 6. As a safety net, branches auto-expire after 10 minutes even if cleanup fails
536
+ 1. At the start of the test session, the plugin creates a migration branch from your parent branch
537
+ 2. If defined, migrations run once on the migration branch
538
+ 3. Test branches (dirty, isolated) are created as children of the migration branch
539
+ 4. `DATABASE_URL` is set to point to the appropriate branch for each fixture
540
+ 5. For `neon_branch_isolated`, the branch is reset after each test (~0.5s)
541
+ 6. After all tests complete, branches are deleted
542
+ 7. As a safety net, branches auto-expire after 10 minutes even if cleanup fails
479
543
 
480
544
  Branches use copy-on-write storage, so you only pay for data that differs from the parent branch.
481
545
 
482
546
  ### What Reset Does
483
547
 
484
- The `neon_branch_readwrite` fixture uses Neon's branch restore API to reset database state after each test:
548
+ The `neon_branch_isolated` fixture uses Neon's branch restore API to reset database state after each test:
485
549
 
486
550
  - **Data changes are reverted**: All INSERT, UPDATE, DELETE operations are undone
487
551
  - **Schema changes are reverted**: CREATE TABLE, ALTER TABLE, DROP TABLE, etc. are undone
@@ -499,15 +563,16 @@ pytest-[git-branch]-[random]-[suffix]
499
563
  ```
500
564
 
501
565
  **Examples:**
502
- - `pytest-main-a1b2-migrated` - Migration branch from `main`
503
- - `pytest-feature-auth-c3d4-test-main` - Test branch from `feature/auth`
504
- - `pytest-a1b2-migrated` - When not in a git repo
566
+ - `pytest-main-a1b2-migration` - Migration branch from `main`
567
+ - `pytest-feature-auth-c3d4-dirty` - Dirty branch from `feature/auth`
568
+ - `pytest-main-a1b2-isolated-gw0` - Isolated branch for xdist worker 0
569
+ - `pytest-a1b2-migration` - When not in a git repo
505
570
 
506
571
  The git branch name is sanitized (only `a-z`, `0-9`, `-`, `_` allowed) and truncated to 15 characters. This makes it easy to identify orphaned branches in the Neon console.
507
572
 
508
573
  ## Parallel Test Execution (pytest-xdist)
509
574
 
510
- This plugin supports parallel test execution with [pytest-xdist](https://pytest-xdist.readthedocs.io/). Each xdist worker automatically gets its own isolated branch.
575
+ This plugin supports parallel test execution with [pytest-xdist](https://pytest-xdist.readthedocs.io/).
511
576
 
512
577
  ```bash
513
578
  # Run tests in parallel with 4 workers
@@ -516,15 +581,24 @@ pytest -n 4
516
581
  ```
517
582
 
518
583
  **How it works:**
519
- - Each xdist worker (gw0, gw1, gw2, etc.) creates its own branch
520
- - Branches are named with the worker ID suffix (e.g., `-test-gw0`, `-test-gw1`)
521
- - Workers run tests in parallel without database state interference
584
+
585
+ | Fixture | xdist Behavior |
586
+ |---------|----------------|
587
+ | `neon_branch_readonly` | Shared across ALL workers (read-only endpoint) |
588
+ | `neon_branch_dirty` | Shared across ALL workers (concurrent writes possible) |
589
+ | `neon_branch_isolated` | One branch per worker (e.g., `-isolated-gw0`, `-isolated-gw1`) |
590
+
591
+ **Key points:**
592
+ - The migration branch is created once and shared across all workers
593
+ - `neon_branch_readonly` and `neon_branch_dirty` share resources across workers
594
+ - `neon_branch_isolated` creates one branch per worker for isolation
595
+ - Workers run tests in parallel without database state interference (when using isolated)
522
596
  - All branches are cleaned up after the test session
523
597
 
524
598
  **Cost implications:**
525
- - Running with `-n 4` creates 4 branches (one per worker) plus the migration branch
599
+ - Running with `-n 4` creates: 1 migration branch + 1 dirty branch + 4 isolated branches (if all workers use isolated)
526
600
  - Choose your parallelism level based on your Neon plan's branch limits
527
- - Each worker's branch is reset after each test using the fast reset operation (~0.5s)
601
+ - Each worker's isolated branch is reset after each test using the fast reset operation (~0.5s)
528
602
 
529
603
  ## Troubleshooting
530
604
 
@@ -546,9 +620,9 @@ pip install pytest-neon[sqlalchemy]
546
620
  Or use the core fixtures with your own driver:
547
621
 
548
622
  ```python
549
- def test_example(neon_branch_readwrite):
623
+ def test_example(neon_branch_isolated):
550
624
  import my_preferred_driver
551
- conn = my_preferred_driver.connect(neon_branch_readwrite.connection_string)
625
+ conn = my_preferred_driver.connect(neon_branch_isolated.connection_string)
552
626
  ```
553
627
 
554
628
  ### "Neon API key not configured"