pytest-neon 0.6.0__tar.gz → 2.0.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 (24) hide show
  1. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/CLAUDE.md +7 -2
  2. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/PKG-INFO +67 -22
  3. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/README.md +66 -21
  4. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/pyproject.toml +1 -1
  5. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/src/pytest_neon/__init__.py +1 -1
  6. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/src/pytest_neon/plugin.py +134 -18
  7. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_integration.py +35 -14
  8. pytest_neon-2.0.0/tests/test_readwrite_readonly_fixtures.py +258 -0
  9. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_reset_behavior.py +113 -0
  10. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/uv.lock +10 -1
  11. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/.env.example +0 -0
  12. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/.github/workflows/release.yml +0 -0
  13. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/.github/workflows/tests.yml +0 -0
  14. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/.gitignore +0 -0
  15. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/.neon +0 -0
  16. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/LICENSE +0 -0
  17. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/src/pytest_neon/py.typed +0 -0
  18. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/conftest.py +0 -0
  19. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_branch_lifecycle.py +0 -0
  20. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_cli_options.py +0 -0
  21. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_env_var.py +0 -0
  22. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_fixture_errors.py +0 -0
  23. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_migrations.py +0 -0
  24. {pytest_neon-0.6.0 → pytest_neon-2.0.0}/tests/test_skip_behavior.py +0 -0
@@ -13,7 +13,10 @@ This is a pytest plugin that provides isolated Neon database branches for integr
13
13
  - **Entry point**: `src/pytest_neon/plugin.py` - Contains all fixtures and pytest hooks
14
14
  - **Migration fixture**: `_neon_migration_branch` - Session-scoped, parent for all test branches
15
15
  - **User migration hook**: `neon_apply_migrations` - Session-scoped no-op, users override to run migrations
16
- - **Core fixture**: `neon_branch` - Creates branch (session-scoped), resets after each test (function-scoped wrapper), sets `DATABASE_URL`, yields `NeonBranch` dataclass
16
+ - **Core fixtures**:
17
+ - `neon_branch_readwrite` - Function-scoped, resets after each test (recommended for write tests)
18
+ - `neon_branch_readonly` - Function-scoped, NO reset (recommended for read-only tests, faster)
19
+ - `neon_branch` - Deprecated alias for `neon_branch_readwrite`
17
20
  - **Shared fixture**: `neon_branch_shared` - Module-scoped, no reset between tests
18
21
  - **Convenience fixtures**: `neon_connection`, `neon_connection_psycopg`, `neon_engine` - Optional, require extras
19
22
 
@@ -28,7 +31,9 @@ This is a pytest plugin that provides isolated Neon database branches for integr
28
31
  - `_neon_migration_branch`: `scope="session"` - internal, parent for all test branches, migrations run here
29
32
  - `neon_apply_migrations`: `scope="session"` - user overrides to run migrations
30
33
  - `_neon_branch_for_reset`: `scope="session"` - internal, creates one branch per session from migration branch
31
- - `neon_branch`: `scope="function"` - wraps the above, resets branch after each test
34
+ - `neon_branch_readwrite`: `scope="function"` - resets branch after each test (for write tests)
35
+ - `neon_branch_readonly`: `scope="function"` - NO reset (for read-only tests, faster)
36
+ - `neon_branch`: `scope="function"` - deprecated alias for `neon_branch_readwrite`
32
37
  - `neon_branch_shared`: `scope="module"` - one branch per test file, no reset
33
38
  - Connection fixtures: `scope="function"` (default) - fresh connection per test
34
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 0.6.0
3
+ Version: 2.0.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
@@ -97,7 +97,7 @@ export NEON_PROJECT_ID="your-project-id"
97
97
  2. Write tests:
98
98
 
99
99
  ```python
100
- def test_user_creation(neon_branch):
100
+ def test_user_creation(neon_branch_readwrite):
101
101
  # DATABASE_URL is automatically set to the test branch
102
102
  import psycopg # Your own install
103
103
 
@@ -105,6 +105,7 @@ def test_user_creation(neon_branch):
105
105
  with conn.cursor() as cur:
106
106
  cur.execute("INSERT INTO users (email) VALUES ('test@example.com')")
107
107
  conn.commit()
108
+ # Branch automatically resets after test - next test sees clean state
108
109
  ```
109
110
 
110
111
  3. Run tests:
@@ -115,33 +116,75 @@ pytest
115
116
 
116
117
  ## Fixtures
117
118
 
118
- ### `neon_branch` (default, recommended)
119
+ **Which fixture should I use?**
119
120
 
120
- The primary fixture for database testing. 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.
121
+ - **Use `neon_branch_readonly`** if your test only reads data (SELECT queries). This is the fastest option with no per-test overhead.
122
+ - **Use `neon_branch_readwrite`** if your test modifies data (INSERT, UPDATE, DELETE). This resets the branch after each test for isolation.
121
123
 
122
- Returns a `NeonBranch` dataclass with:
124
+ ### `neon_branch_readonly` (recommended, fastest)
123
125
 
124
- - `branch_id`: The Neon branch ID
125
- - `project_id`: The Neon project ID
126
- - `connection_string`: Full PostgreSQL connection URI
127
- - `host`: The database host
128
- - `parent_id`: The parent branch ID (used for resets)
126
+ **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.
127
+
128
+ ```python
129
+ def test_query_users(neon_branch_readonly):
130
+ # DATABASE_URL is set automatically
131
+ import psycopg
132
+ with psycopg.connect(neon_branch_readonly.connection_string) as conn:
133
+ result = conn.execute("SELECT * FROM users").fetchall()
134
+ assert len(result) >= 0
135
+ # No reset after this test - fast!
136
+ ```
137
+
138
+ **Use this when**:
139
+ - Tests only perform SELECT queries
140
+ - Tests don't modify database state
141
+ - You want maximum performance
142
+
143
+ **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.
144
+
145
+ **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).
146
+
147
+ ### `neon_branch_readwrite` (for write tests)
148
+
149
+ 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.
129
150
 
130
151
  ```python
131
152
  import os
132
153
 
133
- def test_branch_info(neon_branch):
154
+ def test_insert_user(neon_branch_readwrite):
134
155
  # DATABASE_URL is set automatically
135
- assert os.environ["DATABASE_URL"] == neon_branch.connection_string
156
+ assert os.environ["DATABASE_URL"] == neon_branch_readwrite.connection_string
136
157
 
137
158
  # Use with any driver
138
159
  import psycopg
139
- conn = psycopg.connect(neon_branch.connection_string)
160
+ with psycopg.connect(neon_branch_readwrite.connection_string) as conn:
161
+ conn.execute("INSERT INTO users (name) VALUES ('test')")
162
+ conn.commit()
163
+ # Branch resets after this test - changes won't affect other tests
140
164
  ```
141
165
 
142
- **Performance**: ~1.5s initial setup per session + ~0.5s reset per test. For 10 tests, expect ~6.5s total overhead.
166
+ **Performance**: ~1.5s initial setup per session + ~0.5s reset per test. For 10 write tests, expect ~6.5s total overhead.
143
167
 
144
- ### `neon_branch_shared` (fastest, no isolation)
168
+ ### `NeonBranch` dataclass
169
+
170
+ Both fixtures return a `NeonBranch` dataclass with:
171
+
172
+ - `branch_id`: The Neon branch ID
173
+ - `project_id`: The Neon project ID
174
+ - `connection_string`: Full PostgreSQL connection URI
175
+ - `host`: The database host
176
+ - `parent_id`: The parent branch ID (used for resets)
177
+
178
+ ### `neon_branch` (deprecated)
179
+
180
+ > **Deprecated**: Use `neon_branch_readwrite` or `neon_branch_readonly` instead.
181
+
182
+ This fixture is an alias for `neon_branch_readwrite` and will emit a deprecation warning. Migrate to the explicit fixture names for clarity:
183
+
184
+ - `neon_branch_readwrite`: For tests that modify data (INSERT/UPDATE/DELETE)
185
+ - `neon_branch_readonly`: For tests that only read data (SELECT)
186
+
187
+ ### `neon_branch_shared` (module-scoped, no isolation)
145
188
 
146
189
  Creates one branch per test module and shares it across all tests without resetting. This is the fastest option but tests can see each other's data modifications.
147
190
 
@@ -207,19 +250,21 @@ def test_query(neon_engine):
207
250
 
208
251
  ### Using Your Own SQLAlchemy Engine
209
252
 
210
- If you have a module-level SQLAlchemy engine (common pattern), you **must** use `pool_pre_ping=True`:
253
+ If you have a module-level SQLAlchemy engine (common pattern) and use `neon_branch_readwrite`, you **must** use `pool_pre_ping=True`:
211
254
 
212
255
  ```python
213
256
  # database.py
214
257
  from sqlalchemy import create_engine
215
258
  from config import DATABASE_URL
216
259
 
217
- # pool_pre_ping=True is REQUIRED for pytest-neon
260
+ # pool_pre_ping=True is REQUIRED when using neon_branch_readwrite
218
261
  # It verifies connections are alive before using them
219
262
  engine = create_engine(DATABASE_URL, pool_pre_ping=True)
220
263
  ```
221
264
 
222
- **Why?** After each test, pytest-neon 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.
265
+ **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.
266
+
267
+ **Note**: If you only use `neon_branch_readonly`, `pool_pre_ping` is not required since no resets occur.
223
268
 
224
269
  This is also a best practice for any cloud database (Neon, RDS, etc.) where connections can be terminated externally.
225
270
 
@@ -435,7 +480,7 @@ Branches use copy-on-write storage, so you only pay for data that differs from t
435
480
 
436
481
  ### What Reset Does
437
482
 
438
- The `neon_branch` fixture uses Neon's branch restore API to reset database state after each test:
483
+ The `neon_branch_readwrite` fixture uses Neon's branch restore API to reset database state after each test:
439
484
 
440
485
  - **Data changes are reverted**: All INSERT, UPDATE, DELETE operations are undone
441
486
  - **Schema changes are reverted**: CREATE TABLE, ALTER TABLE, DROP TABLE, etc. are undone
@@ -472,12 +517,12 @@ pip install pytest-neon[psycopg2]
472
517
  pip install pytest-neon[sqlalchemy]
473
518
  ```
474
519
 
475
- Or use the core `neon_branch` fixture with your own driver:
520
+ Or use the core fixtures with your own driver:
476
521
 
477
522
  ```python
478
- def test_example(neon_branch):
523
+ def test_example(neon_branch_readwrite):
479
524
  import my_preferred_driver
480
- conn = my_preferred_driver.connect(neon_branch.connection_string)
525
+ conn = my_preferred_driver.connect(neon_branch_readwrite.connection_string)
481
526
  ```
482
527
 
483
528
  ### "Neon API key not configured"
@@ -52,7 +52,7 @@ export NEON_PROJECT_ID="your-project-id"
52
52
  2. Write tests:
53
53
 
54
54
  ```python
55
- def test_user_creation(neon_branch):
55
+ def test_user_creation(neon_branch_readwrite):
56
56
  # DATABASE_URL is automatically set to the test branch
57
57
  import psycopg # Your own install
58
58
 
@@ -60,6 +60,7 @@ def test_user_creation(neon_branch):
60
60
  with conn.cursor() as cur:
61
61
  cur.execute("INSERT INTO users (email) VALUES ('test@example.com')")
62
62
  conn.commit()
63
+ # Branch automatically resets after test - next test sees clean state
63
64
  ```
64
65
 
65
66
  3. Run tests:
@@ -70,33 +71,75 @@ pytest
70
71
 
71
72
  ## Fixtures
72
73
 
73
- ### `neon_branch` (default, recommended)
74
+ **Which fixture should I use?**
74
75
 
75
- The primary fixture for database testing. 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.
76
+ - **Use `neon_branch_readonly`** if your test only reads data (SELECT queries). This is the fastest option with no per-test overhead.
77
+ - **Use `neon_branch_readwrite`** if your test modifies data (INSERT, UPDATE, DELETE). This resets the branch after each test for isolation.
76
78
 
77
- Returns a `NeonBranch` dataclass with:
79
+ ### `neon_branch_readonly` (recommended, fastest)
78
80
 
79
- - `branch_id`: The Neon branch ID
80
- - `project_id`: The Neon project ID
81
- - `connection_string`: Full PostgreSQL connection URI
82
- - `host`: The database host
83
- - `parent_id`: The parent branch ID (used for resets)
81
+ **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.
82
+
83
+ ```python
84
+ def test_query_users(neon_branch_readonly):
85
+ # DATABASE_URL is set automatically
86
+ import psycopg
87
+ with psycopg.connect(neon_branch_readonly.connection_string) as conn:
88
+ result = conn.execute("SELECT * FROM users").fetchall()
89
+ assert len(result) >= 0
90
+ # No reset after this test - fast!
91
+ ```
92
+
93
+ **Use this when**:
94
+ - Tests only perform SELECT queries
95
+ - Tests don't modify database state
96
+ - You want maximum performance
97
+
98
+ **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.
99
+
100
+ **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).
101
+
102
+ ### `neon_branch_readwrite` (for write tests)
103
+
104
+ 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.
84
105
 
85
106
  ```python
86
107
  import os
87
108
 
88
- def test_branch_info(neon_branch):
109
+ def test_insert_user(neon_branch_readwrite):
89
110
  # DATABASE_URL is set automatically
90
- assert os.environ["DATABASE_URL"] == neon_branch.connection_string
111
+ assert os.environ["DATABASE_URL"] == neon_branch_readwrite.connection_string
91
112
 
92
113
  # Use with any driver
93
114
  import psycopg
94
- conn = psycopg.connect(neon_branch.connection_string)
115
+ with psycopg.connect(neon_branch_readwrite.connection_string) as conn:
116
+ conn.execute("INSERT INTO users (name) VALUES ('test')")
117
+ conn.commit()
118
+ # Branch resets after this test - changes won't affect other tests
95
119
  ```
96
120
 
97
- **Performance**: ~1.5s initial setup per session + ~0.5s reset per test. For 10 tests, expect ~6.5s total overhead.
121
+ **Performance**: ~1.5s initial setup per session + ~0.5s reset per test. For 10 write tests, expect ~6.5s total overhead.
98
122
 
99
- ### `neon_branch_shared` (fastest, no isolation)
123
+ ### `NeonBranch` dataclass
124
+
125
+ Both fixtures return a `NeonBranch` dataclass with:
126
+
127
+ - `branch_id`: The Neon branch ID
128
+ - `project_id`: The Neon project ID
129
+ - `connection_string`: Full PostgreSQL connection URI
130
+ - `host`: The database host
131
+ - `parent_id`: The parent branch ID (used for resets)
132
+
133
+ ### `neon_branch` (deprecated)
134
+
135
+ > **Deprecated**: Use `neon_branch_readwrite` or `neon_branch_readonly` instead.
136
+
137
+ This fixture is an alias for `neon_branch_readwrite` and will emit a deprecation warning. Migrate to the explicit fixture names for clarity:
138
+
139
+ - `neon_branch_readwrite`: For tests that modify data (INSERT/UPDATE/DELETE)
140
+ - `neon_branch_readonly`: For tests that only read data (SELECT)
141
+
142
+ ### `neon_branch_shared` (module-scoped, no isolation)
100
143
 
101
144
  Creates one branch per test module and shares it across all tests without resetting. This is the fastest option but tests can see each other's data modifications.
102
145
 
@@ -162,19 +205,21 @@ def test_query(neon_engine):
162
205
 
163
206
  ### Using Your Own SQLAlchemy Engine
164
207
 
165
- If you have a module-level SQLAlchemy engine (common pattern), you **must** use `pool_pre_ping=True`:
208
+ If you have a module-level SQLAlchemy engine (common pattern) and use `neon_branch_readwrite`, you **must** use `pool_pre_ping=True`:
166
209
 
167
210
  ```python
168
211
  # database.py
169
212
  from sqlalchemy import create_engine
170
213
  from config import DATABASE_URL
171
214
 
172
- # pool_pre_ping=True is REQUIRED for pytest-neon
215
+ # pool_pre_ping=True is REQUIRED when using neon_branch_readwrite
173
216
  # It verifies connections are alive before using them
174
217
  engine = create_engine(DATABASE_URL, pool_pre_ping=True)
175
218
  ```
176
219
 
177
- **Why?** After each test, pytest-neon 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.
220
+ **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.
221
+
222
+ **Note**: If you only use `neon_branch_readonly`, `pool_pre_ping` is not required since no resets occur.
178
223
 
179
224
  This is also a best practice for any cloud database (Neon, RDS, etc.) where connections can be terminated externally.
180
225
 
@@ -390,7 +435,7 @@ Branches use copy-on-write storage, so you only pay for data that differs from t
390
435
 
391
436
  ### What Reset Does
392
437
 
393
- The `neon_branch` fixture uses Neon's branch restore API to reset database state after each test:
438
+ The `neon_branch_readwrite` fixture uses Neon's branch restore API to reset database state after each test:
394
439
 
395
440
  - **Data changes are reverted**: All INSERT, UPDATE, DELETE operations are undone
396
441
  - **Schema changes are reverted**: CREATE TABLE, ALTER TABLE, DROP TABLE, etc. are undone
@@ -427,12 +472,12 @@ pip install pytest-neon[psycopg2]
427
472
  pip install pytest-neon[sqlalchemy]
428
473
  ```
429
474
 
430
- Or use the core `neon_branch` fixture with your own driver:
475
+ Or use the core fixtures with your own driver:
431
476
 
432
477
  ```python
433
- def test_example(neon_branch):
478
+ def test_example(neon_branch_readwrite):
434
479
  import my_preferred_driver
435
- conn = my_preferred_driver.connect(neon_branch.connection_string)
480
+ conn = my_preferred_driver.connect(neon_branch_readwrite.connection_string)
436
481
  ```
437
482
 
438
483
  ### "Neon API key not configured"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-neon"
7
- version = "0.6.0"
7
+ version = "2.0.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.6.0"
12
+ __version__ = "2.0.0"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
@@ -5,15 +5,17 @@ instant branching feature. Each test gets a clean database state via
5
5
  branch reset after each test.
6
6
 
7
7
  Main fixtures:
8
- neon_branch: Primary fixture - one branch per session, reset after each test
9
- neon_branch_shared: Shared branch without reset (fastest, no isolation)
8
+ neon_branch_readwrite: Read-write access with reset after each test (recommended)
9
+ neon_branch_readonly: Read-only access, no reset (fastest for read-only tests)
10
+ neon_branch: Deprecated alias for neon_branch_readwrite
11
+ neon_branch_shared: Shared branch without reset (module-scoped)
10
12
  neon_connection: psycopg2 connection (requires psycopg2 extra)
11
13
  neon_connection_psycopg: psycopg v3 connection (requires psycopg extra)
12
14
  neon_engine: SQLAlchemy engine (requires sqlalchemy extra)
13
15
 
14
16
  SQLAlchemy Users:
15
17
  If you create your own SQLAlchemy engine (not using neon_engine fixture),
16
- you MUST use pool_pre_ping=True:
18
+ you MUST use pool_pre_ping=True when using neon_branch_readwrite:
17
19
 
18
20
  engine = create_engine(DATABASE_URL, pool_pre_ping=True)
19
21
 
@@ -21,6 +23,9 @@ SQLAlchemy Users:
21
23
  Without pool_pre_ping, SQLAlchemy may try to reuse dead pooled connections,
22
24
  causing "SSL connection has been closed unexpectedly" errors.
23
25
 
26
+ Note: pool_pre_ping is not required for neon_branch_readonly since no
27
+ resets occur.
28
+
24
29
  Configuration:
25
30
  Set NEON_API_KEY and NEON_PROJECT_ID environment variables, or use
26
31
  --neon-api-key and --neon-project-id CLI options.
@@ -33,6 +38,7 @@ from __future__ import annotations
33
38
  import contextlib
34
39
  import os
35
40
  import time
41
+ import warnings
36
42
  from collections.abc import Generator
37
43
  from dataclasses import dataclass
38
44
  from datetime import datetime, timedelta, timezone
@@ -373,8 +379,19 @@ def _create_neon_branch(
373
379
  )
374
380
 
375
381
 
376
- def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
377
- """Reset a branch to its parent's state using the Neon API."""
382
+ def _reset_branch_to_parent(
383
+ branch: NeonBranch, api_key: str, max_retries: int = 3
384
+ ) -> None:
385
+ """Reset a branch to its parent's state using the Neon API.
386
+
387
+ Uses exponential backoff retry logic to handle transient API errors
388
+ that can occur during parallel test execution.
389
+
390
+ Args:
391
+ branch: The branch to reset
392
+ api_key: Neon API key
393
+ max_retries: Maximum number of retry attempts (default: 3)
394
+ """
378
395
  if not branch.parent_id:
379
396
  raise RuntimeError(f"Branch {branch.branch_id} has no parent - cannot reset")
380
397
 
@@ -383,10 +400,27 @@ def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
383
400
  "Authorization": f"Bearer {api_key}",
384
401
  "Content-Type": "application/json",
385
402
  }
386
- response = requests.post(
387
- url, headers=headers, json={"source_branch_id": branch.parent_id}, timeout=30
388
- )
389
- response.raise_for_status()
403
+
404
+ last_error: Exception | None = None
405
+ for attempt in range(max_retries + 1):
406
+ try:
407
+ response = requests.post(
408
+ url,
409
+ headers=headers,
410
+ json={"source_branch_id": branch.parent_id},
411
+ timeout=30,
412
+ )
413
+ response.raise_for_status()
414
+ return # Success
415
+ except requests.RequestException as e:
416
+ last_error = e
417
+ if attempt < max_retries:
418
+ # Exponential backoff: 1s, 2s, 4s
419
+ wait_time = 2**attempt
420
+ time.sleep(wait_time)
421
+
422
+ # All retries exhausted
423
+ raise last_error # type: ignore[misc]
390
424
 
391
425
 
392
426
  @pytest.fixture(scope="session")
@@ -538,16 +572,21 @@ def _neon_branch_for_reset(
538
572
 
539
573
 
540
574
  @pytest.fixture(scope="function")
541
- def neon_branch(
575
+ def neon_branch_readwrite(
542
576
  request: pytest.FixtureRequest,
543
577
  _neon_branch_for_reset: NeonBranch,
544
578
  ) -> Generator[NeonBranch, None, None]:
545
579
  """
546
- Provide an isolated Neon database branch for each test.
580
+ Provide a read-write Neon database branch with reset after each test.
581
+
582
+ This is the recommended fixture for tests that modify database state.
583
+ It creates one branch per test session, then resets it to the parent
584
+ branch's state after each test. This provides test isolation with
585
+ ~0.5s overhead per test.
547
586
 
548
- This is the primary fixture for database testing. It creates one branch per
549
- test session, then resets it to the parent branch's state after each test.
550
- This provides test isolation with ~0.5s overhead per test.
587
+ Use this fixture when your tests INSERT, UPDATE, or DELETE data.
588
+ For read-only tests, use ``neon_branch_readonly`` instead for better
589
+ performance (no reset overhead).
551
590
 
552
591
  The branch is automatically deleted after all tests complete, unless
553
592
  --neon-keep-branches is specified. Branches also auto-expire after
@@ -575,11 +614,16 @@ def neon_branch(
575
614
 
576
615
  Example::
577
616
 
578
- def test_database_operation(neon_branch):
617
+ def test_insert_user(neon_branch_readwrite):
579
618
  # DATABASE_URL is automatically set
580
619
  conn_string = os.environ["DATABASE_URL"]
581
620
  # or use directly
582
- conn_string = neon_branch.connection_string
621
+ conn_string = neon_branch_readwrite.connection_string
622
+
623
+ # Insert data - branch will reset after this test
624
+ with psycopg.connect(conn_string) as conn:
625
+ conn.execute("INSERT INTO users (name) VALUES ('test')")
626
+ conn.commit()
583
627
  """
584
628
  config = request.config
585
629
  api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
@@ -588,8 +632,9 @@ def neon_branch(
588
632
  if not _neon_branch_for_reset.parent_id:
589
633
  pytest.fail(
590
634
  f"\n\nBranch {_neon_branch_for_reset.branch_id} has no parent. "
591
- f"The neon_branch fixture requires a parent branch for reset.\n\n"
592
- f"Use neon_branch_shared if you don't need reset, or specify "
635
+ f"The neon_branch_readwrite fixture requires a parent branch for "
636
+ f"reset.\n\n"
637
+ f"Use neon_branch_readonly if you don't need reset, or specify "
593
638
  f"a parent branch with --neon-parent-branch or NEON_PARENT_BRANCH_ID."
594
639
  )
595
640
 
@@ -608,6 +653,77 @@ def neon_branch(
608
653
  )
609
654
 
610
655
 
656
+ @pytest.fixture(scope="function")
657
+ def neon_branch_readonly(
658
+ _neon_branch_for_reset: NeonBranch,
659
+ ) -> NeonBranch:
660
+ """
661
+ Provide a read-only Neon database branch without reset.
662
+
663
+ This is the recommended fixture for tests that only read data (SELECT queries).
664
+ No branch reset occurs after each test, making it faster than
665
+ ``neon_branch_readwrite`` (~0.5s saved per test).
666
+
667
+ Use this fixture when your tests only perform SELECT queries and don't
668
+ modify database state. For tests that INSERT, UPDATE, or DELETE data,
669
+ use ``neon_branch_readwrite`` instead to ensure test isolation.
670
+
671
+ Warning:
672
+ If you accidentally write data using this fixture, subsequent tests
673
+ will see those modifications. The fixture does not enforce read-only
674
+ access at the database level - it simply skips the reset step.
675
+
676
+ The connection string is automatically set in the DATABASE_URL environment
677
+ variable (configurable via --neon-env-var).
678
+
679
+ Requires either:
680
+ - NEON_API_KEY and NEON_PROJECT_ID environment variables, or
681
+ - --neon-api-key and --neon-project-id command line options
682
+
683
+ Yields:
684
+ NeonBranch: Object with branch_id, project_id, connection_string, and host.
685
+
686
+ Example::
687
+
688
+ def test_query_users(neon_branch_readonly):
689
+ # DATABASE_URL is automatically set
690
+ conn_string = os.environ["DATABASE_URL"]
691
+
692
+ # Read-only query - no reset needed after this test
693
+ with psycopg.connect(conn_string) as conn:
694
+ result = conn.execute("SELECT * FROM users").fetchall()
695
+ assert len(result) > 0
696
+ """
697
+ return _neon_branch_for_reset
698
+
699
+
700
+ @pytest.fixture(scope="function")
701
+ def neon_branch(
702
+ request: pytest.FixtureRequest,
703
+ neon_branch_readwrite: NeonBranch,
704
+ ) -> Generator[NeonBranch, None, None]:
705
+ """
706
+ Deprecated: Use ``neon_branch_readwrite`` or ``neon_branch_readonly`` instead.
707
+
708
+ This fixture is an alias for ``neon_branch_readwrite`` and will be removed
709
+ in a future version. Please migrate to the explicit fixture names:
710
+
711
+ - ``neon_branch_readwrite``: For tests that modify data (INSERT/UPDATE/DELETE)
712
+ - ``neon_branch_readonly``: For tests that only read data (SELECT)
713
+
714
+ .. deprecated:: 1.1.0
715
+ Use ``neon_branch_readwrite`` for read-write access with reset,
716
+ or ``neon_branch_readonly`` for read-only access without reset.
717
+ """
718
+ warnings.warn(
719
+ "neon_branch is deprecated. Use neon_branch_readwrite (for tests that "
720
+ "modify data) or neon_branch_readonly (for read-only tests) instead.",
721
+ DeprecationWarning,
722
+ stacklevel=2,
723
+ )
724
+ yield neon_branch_readwrite
725
+
726
+
611
727
  @pytest.fixture(scope="module")
612
728
  def neon_branch_shared(
613
729
  request: pytest.FixtureRequest,
@@ -69,16 +69,22 @@ pytestmark = pytest.mark.skipif(
69
69
  class TestRealBranchCreation:
70
70
  """Test actual branch creation against Neon API."""
71
71
 
72
- def test_branch_is_created_and_accessible(self, neon_branch):
72
+ def test_branch_is_created_and_accessible(self, neon_branch_readwrite):
73
73
  """Test that a real branch is created and has valid connection info."""
74
- assert neon_branch.branch_id.startswith("br-")
75
- assert neon_branch.project_id == PROJECT_ID
76
- assert "neon.tech" in neon_branch.host
77
- assert neon_branch.connection_string.startswith("postgresql://")
74
+ assert neon_branch_readwrite.branch_id.startswith("br-")
75
+ assert neon_branch_readwrite.project_id == PROJECT_ID
76
+ assert "neon.tech" in neon_branch_readwrite.host
77
+ assert neon_branch_readwrite.connection_string.startswith("postgresql://")
78
78
 
79
- def test_database_url_is_set(self, neon_branch):
79
+ def test_database_url_is_set(self, neon_branch_readwrite):
80
80
  """Test that DATABASE_URL environment variable is set."""
81
- assert os.environ.get("DATABASE_URL") == neon_branch.connection_string
81
+ assert os.environ.get("DATABASE_URL") == neon_branch_readwrite.connection_string
82
+
83
+ def test_readonly_fixture_works(self, neon_branch_readonly):
84
+ """Test that readonly fixture provides valid connection info."""
85
+ assert neon_branch_readonly.branch_id.startswith("br-")
86
+ assert neon_branch_readonly.project_id == PROJECT_ID
87
+ assert neon_branch_readonly.connection_string.startswith("postgresql://")
82
88
 
83
89
 
84
90
  class TestMigrations:
@@ -159,7 +165,7 @@ def test_second_insert_after_reset(neon_branch):
159
165
  class TestRealDatabaseConnectivity:
160
166
  """Test actual database connectivity."""
161
167
 
162
- def test_can_connect_and_query(self, neon_branch):
168
+ def test_can_connect_and_query(self, neon_branch_readwrite):
163
169
  """Test that we can actually connect to the created branch."""
164
170
  try:
165
171
  import psycopg
@@ -167,14 +173,14 @@ class TestRealDatabaseConnectivity:
167
173
  pytest.skip("psycopg not installed - run: pip install pytest-neon[psycopg]")
168
174
 
169
175
  with (
170
- psycopg.connect(neon_branch.connection_string) as conn,
176
+ psycopg.connect(neon_branch_readwrite.connection_string) as conn,
171
177
  conn.cursor() as cur,
172
178
  ):
173
179
  cur.execute("SELECT 1 AS result")
174
180
  result = cur.fetchone()
175
181
  assert result[0] == 1
176
182
 
177
- def test_can_create_and_query_table(self, neon_branch):
183
+ def test_can_create_and_query_table(self, neon_branch_readwrite):
178
184
  """Test that we can create tables and insert data."""
179
185
  try:
180
186
  import psycopg
@@ -182,7 +188,7 @@ class TestRealDatabaseConnectivity:
182
188
  pytest.skip("psycopg not installed - run: pip install pytest-neon[psycopg]")
183
189
 
184
190
  with (
185
- psycopg.connect(neon_branch.connection_string) as conn,
191
+ psycopg.connect(neon_branch_readwrite.connection_string) as conn,
186
192
  conn.cursor() as cur,
187
193
  ):
188
194
  # Create a test table
@@ -207,6 +213,21 @@ class TestRealDatabaseConnectivity:
207
213
  result = cur.fetchone()
208
214
  assert result[0] == "test_value"
209
215
 
216
+ def test_readonly_can_query(self, neon_branch_readonly):
217
+ """Test that readonly fixture can execute queries."""
218
+ try:
219
+ import psycopg
220
+ except ImportError:
221
+ pytest.skip("psycopg not installed - run: pip install pytest-neon[psycopg]")
222
+
223
+ with (
224
+ psycopg.connect(neon_branch_readonly.connection_string) as conn,
225
+ conn.cursor() as cur,
226
+ ):
227
+ cur.execute("SELECT 1 AS result")
228
+ result = cur.fetchone()
229
+ assert result[0] == 1
230
+
210
231
 
211
232
  class TestSQLAlchemyPooledConnections:
212
233
  """Test SQLAlchemy connection pooling behavior with branch resets.
@@ -245,7 +266,7 @@ engine = create_engine(DATABASE_URL, pool_pre_ping=True) if DATABASE_URL else No
245
266
  test_sqlalchemy_reset="""
246
267
  from sqlalchemy import text
247
268
 
248
- def test_first_query(neon_branch):
269
+ def test_first_query(neon_branch_readwrite):
249
270
  '''First test - creates pooled connection.'''
250
271
  from database import engine
251
272
  with engine.connect() as conn:
@@ -253,14 +274,14 @@ def test_first_query(neon_branch):
253
274
  assert result.scalar() == 1
254
275
  # Connection returned to pool
255
276
 
256
- def test_second_query_after_reset(neon_branch):
277
+ def test_second_query_after_reset(neon_branch_readwrite):
257
278
  '''Second test - branch was reset, but pool_pre_ping handles it.'''
258
279
  from database import engine
259
280
  with engine.connect() as conn:
260
281
  result = conn.execute(text("SELECT 2"))
261
282
  assert result.scalar() == 2
262
283
 
263
- def test_third_query_after_another_reset(neon_branch):
284
+ def test_third_query_after_another_reset(neon_branch_readwrite):
264
285
  '''Third test - still works thanks to pool_pre_ping.'''
265
286
  from database import engine
266
287
  with engine.connect() as conn:
@@ -0,0 +1,258 @@
1
+ """Tests for neon_branch_readwrite and neon_branch_readonly fixtures."""
2
+
3
+
4
+ class TestReadwriteFixture:
5
+ """Test neon_branch_readwrite fixture behavior."""
6
+
7
+ def test_readwrite_resets_after_each_test(self, pytester):
8
+ """Verify that neon_branch_readwrite resets after each test."""
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="session")
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_readwrite(_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_readwrite):
55
+ assert neon_branch_readwrite.branch_id == "br-test"
56
+
57
+ def test_second(neon_branch_readwrite):
58
+ assert neon_branch_readwrite.branch_id == "br-test"
59
+ """
60
+ )
61
+
62
+ result = pytester.runpytest("-v")
63
+ result.assert_outcomes(passed=2)
64
+
65
+
66
+ class TestReadonlyFixture:
67
+ """Test neon_branch_readonly fixture behavior."""
68
+
69
+ def test_readonly_does_not_reset(self, pytester):
70
+ """Verify that neon_branch_readonly does NOT reset after tests."""
71
+ pytester.makeconftest(
72
+ """
73
+ import os
74
+ import pytest
75
+ from dataclasses import dataclass
76
+
77
+ reset_count = [0]
78
+
79
+ @dataclass
80
+ class FakeNeonBranch:
81
+ branch_id: str
82
+ project_id: str
83
+ connection_string: str
84
+ host: str
85
+ parent_id: str
86
+
87
+ @pytest.fixture(scope="session")
88
+ def _neon_branch_for_reset():
89
+ branch = FakeNeonBranch(
90
+ branch_id="br-test",
91
+ project_id="proj-test",
92
+ connection_string="postgresql://test",
93
+ host="test.neon.tech",
94
+ parent_id="br-parent",
95
+ )
96
+ os.environ["DATABASE_URL"] = branch.connection_string
97
+ try:
98
+ yield branch
99
+ finally:
100
+ os.environ.pop("DATABASE_URL", None)
101
+
102
+ @pytest.fixture(scope="function")
103
+ def neon_branch_readonly(_neon_branch_for_reset):
104
+ # No reset - just return the branch
105
+ return _neon_branch_for_reset
106
+
107
+ def pytest_sessionfinish(session, exitstatus):
108
+ # Verify NO resets happened
109
+ assert reset_count[0] == 0, f"Expected 0 resets, got {reset_count[0]}"
110
+ """
111
+ )
112
+
113
+ pytester.makepyfile(
114
+ """
115
+ def test_first(neon_branch_readonly):
116
+ assert neon_branch_readonly.branch_id == "br-test"
117
+
118
+ def test_second(neon_branch_readonly):
119
+ assert neon_branch_readonly.branch_id == "br-test"
120
+ """
121
+ )
122
+
123
+ result = pytester.runpytest("-v")
124
+ result.assert_outcomes(passed=2)
125
+
126
+
127
+ class TestDeprecatedNeonBranch:
128
+ """Test that neon_branch emits deprecation warning."""
129
+
130
+ def test_neon_branch_emits_deprecation_warning(self, pytester):
131
+ """Verify that using neon_branch emits a deprecation warning."""
132
+ pytester.makeconftest(
133
+ """
134
+ import os
135
+ import pytest
136
+ from dataclasses import dataclass
137
+
138
+ @dataclass
139
+ class FakeNeonBranch:
140
+ branch_id: str
141
+ project_id: str
142
+ connection_string: str
143
+ host: str
144
+ parent_id: str
145
+
146
+ @pytest.fixture(scope="session")
147
+ def _neon_branch_for_reset():
148
+ branch = FakeNeonBranch(
149
+ branch_id="br-test",
150
+ project_id="proj-test",
151
+ connection_string="postgresql://test",
152
+ host="test.neon.tech",
153
+ parent_id="br-parent",
154
+ )
155
+ os.environ["DATABASE_URL"] = branch.connection_string
156
+ try:
157
+ yield branch
158
+ finally:
159
+ os.environ.pop("DATABASE_URL", None)
160
+
161
+ @pytest.fixture(scope="function")
162
+ def neon_branch_readwrite(_neon_branch_for_reset):
163
+ yield _neon_branch_for_reset
164
+
165
+ @pytest.fixture(scope="function")
166
+ def neon_branch(neon_branch_readwrite):
167
+ import warnings
168
+ warnings.warn(
169
+ "neon_branch is deprecated. Use neon_branch_readwrite or "
170
+ "neon_branch_readonly instead.",
171
+ DeprecationWarning,
172
+ stacklevel=2,
173
+ )
174
+ yield neon_branch_readwrite
175
+ """
176
+ )
177
+
178
+ pytester.makepyfile(
179
+ """
180
+ def test_deprecated(neon_branch):
181
+ assert neon_branch.branch_id == "br-test"
182
+ """
183
+ )
184
+
185
+ result = pytester.runpytest("-v", "-W", "error::DeprecationWarning")
186
+ # Should error during fixture setup (deprecation warning treated as error)
187
+ result.assert_outcomes(errors=1)
188
+
189
+
190
+ class TestFixtureUseTogether:
191
+ """Test using both fixtures in the same test session."""
192
+
193
+ def test_readwrite_and_readonly_can_coexist(self, pytester):
194
+ """Verify both fixtures can be used in the same test module."""
195
+ pytester.makeconftest(
196
+ """
197
+ import os
198
+ import pytest
199
+ from dataclasses import dataclass
200
+
201
+ reset_count = [0]
202
+
203
+ @dataclass
204
+ class FakeNeonBranch:
205
+ branch_id: str
206
+ project_id: str
207
+ connection_string: str
208
+ host: str
209
+ parent_id: str
210
+
211
+ @pytest.fixture(scope="session")
212
+ def _neon_branch_for_reset():
213
+ branch = FakeNeonBranch(
214
+ branch_id="br-test",
215
+ project_id="proj-test",
216
+ connection_string="postgresql://test",
217
+ host="test.neon.tech",
218
+ parent_id="br-parent",
219
+ )
220
+ os.environ["DATABASE_URL"] = branch.connection_string
221
+ try:
222
+ yield branch
223
+ finally:
224
+ os.environ.pop("DATABASE_URL", None)
225
+
226
+ @pytest.fixture(scope="function")
227
+ def neon_branch_readwrite(_neon_branch_for_reset):
228
+ yield _neon_branch_for_reset
229
+ reset_count[0] += 1
230
+
231
+ @pytest.fixture(scope="function")
232
+ def neon_branch_readonly(_neon_branch_for_reset):
233
+ return _neon_branch_for_reset
234
+
235
+ def pytest_sessionfinish(session, exitstatus):
236
+ # Only readwrite tests should trigger reset
237
+ assert reset_count[0] == 1, f"Expected 1 reset, got {reset_count[0]}"
238
+ """
239
+ )
240
+
241
+ pytester.makepyfile(
242
+ """
243
+ def test_readonly_first(neon_branch_readonly):
244
+ '''Read-only test - no reset after.'''
245
+ assert neon_branch_readonly.branch_id == "br-test"
246
+
247
+ def test_readonly_second(neon_branch_readonly):
248
+ '''Another read-only test - still no reset.'''
249
+ assert neon_branch_readonly.branch_id == "br-test"
250
+
251
+ def test_readwrite(neon_branch_readwrite):
252
+ '''Read-write test - reset after this one.'''
253
+ assert neon_branch_readwrite.branch_id == "br-test"
254
+ """
255
+ )
256
+
257
+ result = pytester.runpytest("-v")
258
+ result.assert_outcomes(passed=3)
@@ -1,5 +1,118 @@
1
1
  """Tests for branch reset behavior."""
2
2
 
3
+ import pytest
4
+
5
+ from pytest_neon.plugin import NeonBranch, _reset_branch_to_parent
6
+
7
+
8
+ class TestResetRetryBehavior:
9
+ """Test that branch reset retries on transient failures."""
10
+
11
+ def test_reset_succeeds_after_transient_failures(self, mocker):
12
+ """Verify reset retries and succeeds after transient API errors."""
13
+ branch = NeonBranch(
14
+ branch_id="br-test",
15
+ project_id="proj-test",
16
+ connection_string="postgresql://test",
17
+ host="test.neon.tech",
18
+ parent_id="br-parent",
19
+ )
20
+
21
+ # Mock requests.post to fail twice, then succeed
22
+ mock_response = mocker.Mock()
23
+ mock_response.raise_for_status = mocker.Mock()
24
+
25
+ import requests
26
+
27
+ call_count = [0]
28
+
29
+ def mock_post(*args, **kwargs):
30
+ call_count[0] += 1
31
+ if call_count[0] < 3:
32
+ raise requests.RequestException("API rate limited")
33
+ return mock_response
34
+
35
+ mocker.patch("pytest_neon.plugin.requests.post", side_effect=mock_post)
36
+ mocker.patch("pytest_neon.plugin.time.sleep") # Don't actually sleep
37
+
38
+ # Should succeed after retries
39
+ _reset_branch_to_parent(branch, "fake-api-key")
40
+
41
+ assert call_count[0] == 3 # 2 failures + 1 success
42
+
43
+ def test_reset_fails_after_max_retries(self, mocker):
44
+ """Verify reset raises after exhausting all retries."""
45
+ branch = NeonBranch(
46
+ branch_id="br-test",
47
+ project_id="proj-test",
48
+ connection_string="postgresql://test",
49
+ host="test.neon.tech",
50
+ parent_id="br-parent",
51
+ )
52
+
53
+ # Mock requests.post to always fail
54
+ import requests
55
+
56
+ mocker.patch(
57
+ "pytest_neon.plugin.requests.post",
58
+ side_effect=requests.RequestException("API error"),
59
+ )
60
+ mock_sleep = mocker.patch("pytest_neon.plugin.time.sleep")
61
+
62
+ # Should raise after max retries
63
+ with pytest.raises(requests.RequestException, match="API error"):
64
+ _reset_branch_to_parent(branch, "fake-api-key")
65
+
66
+ # Should have slept between retries (3 retries = 3 sleeps)
67
+ assert mock_sleep.call_count == 3
68
+
69
+ def test_reset_uses_exponential_backoff(self, mocker):
70
+ """Verify reset uses exponential backoff between retries."""
71
+ branch = NeonBranch(
72
+ branch_id="br-test",
73
+ project_id="proj-test",
74
+ connection_string="postgresql://test",
75
+ host="test.neon.tech",
76
+ parent_id="br-parent",
77
+ )
78
+
79
+ import requests
80
+
81
+ mocker.patch(
82
+ "pytest_neon.plugin.requests.post",
83
+ side_effect=requests.RequestException("API error"),
84
+ )
85
+ mock_sleep = mocker.patch("pytest_neon.plugin.time.sleep")
86
+
87
+ with pytest.raises(requests.RequestException):
88
+ _reset_branch_to_parent(branch, "fake-api-key")
89
+
90
+ # Check exponential backoff: 1s, 2s, 4s
91
+ sleep_calls = [call[0][0] for call in mock_sleep.call_args_list]
92
+ assert sleep_calls == [1, 2, 4]
93
+
94
+ def test_reset_no_retry_on_success(self, mocker):
95
+ """Verify reset doesn't retry when successful."""
96
+ branch = NeonBranch(
97
+ branch_id="br-test",
98
+ project_id="proj-test",
99
+ connection_string="postgresql://test",
100
+ host="test.neon.tech",
101
+ parent_id="br-parent",
102
+ )
103
+
104
+ mock_response = mocker.Mock()
105
+ mock_response.raise_for_status = mocker.Mock()
106
+ mock_post = mocker.patch(
107
+ "pytest_neon.plugin.requests.post", return_value=mock_response
108
+ )
109
+ mock_sleep = mocker.patch("pytest_neon.plugin.time.sleep")
110
+
111
+ _reset_branch_to_parent(branch, "fake-api-key")
112
+
113
+ assert mock_post.call_count == 1
114
+ assert mock_sleep.call_count == 0
115
+
3
116
 
4
117
  class TestResetBehavior:
5
118
  """Test that branch reset happens between tests."""
@@ -1200,20 +1200,25 @@ wheels = [
1200
1200
 
1201
1201
  [[package]]
1202
1202
  name = "pytest-neon"
1203
- version = "0.1.0"
1203
+ version = "1.0.0"
1204
1204
  source = { editable = "." }
1205
1205
  dependencies = [
1206
1206
  { name = "neon-api" },
1207
1207
  { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
1208
1208
  { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
1209
+ { name = "requests" },
1209
1210
  ]
1210
1211
 
1211
1212
  [package.optional-dependencies]
1212
1213
  dev = [
1213
1214
  { name = "mypy" },
1215
+ { name = "psycopg", version = "3.2.13", source = { registry = "https://pypi.org/simple" }, extra = ["binary"], marker = "python_full_version < '3.10'" },
1216
+ { name = "psycopg", version = "3.3.2", source = { registry = "https://pypi.org/simple" }, extra = ["binary"], marker = "python_full_version >= '3.10'" },
1217
+ { name = "psycopg2-binary" },
1214
1218
  { name = "pytest-cov" },
1215
1219
  { name = "pytest-mock" },
1216
1220
  { name = "ruff" },
1221
+ { name = "sqlalchemy" },
1217
1222
  ]
1218
1223
  psycopg = [
1219
1224
  { name = "psycopg", version = "3.2.13", source = { registry = "https://pypi.org/simple" }, extra = ["binary"], marker = "python_full_version < '3.10'" },
@@ -1230,12 +1235,16 @@ sqlalchemy = [
1230
1235
  requires-dist = [
1231
1236
  { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" },
1232
1237
  { name = "neon-api", specifier = ">=0.1.0" },
1238
+ { name = "psycopg", extras = ["binary"], marker = "extra == 'dev'", specifier = ">=3.1" },
1233
1239
  { name = "psycopg", extras = ["binary"], marker = "extra == 'psycopg'", specifier = ">=3.1" },
1240
+ { name = "psycopg2-binary", marker = "extra == 'dev'", specifier = ">=2.9" },
1234
1241
  { name = "psycopg2-binary", marker = "extra == 'psycopg2'", specifier = ">=2.9" },
1235
1242
  { name = "pytest", specifier = ">=7.0" },
1236
1243
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" },
1237
1244
  { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.0" },
1245
+ { name = "requests", specifier = ">=2.20" },
1238
1246
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" },
1247
+ { name = "sqlalchemy", marker = "extra == 'dev'", specifier = ">=2.0" },
1239
1248
  { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=2.0" },
1240
1249
  ]
1241
1250
  provides-extras = ["psycopg2", "psycopg", "sqlalchemy", "dev"]
File without changes
File without changes
File without changes
File without changes