pytest-neon 2.1.2__py3-none-any.whl → 2.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_neon/__init__.py +1 -1
- pytest_neon/plugin.py +96 -3
- {pytest_neon-2.1.2.dist-info → pytest_neon-2.1.4.dist-info}/METADATA +1 -1
- pytest_neon-2.1.4.dist-info/RECORD +8 -0
- pytest_neon-2.1.2.dist-info/RECORD +0 -8
- {pytest_neon-2.1.2.dist-info → pytest_neon-2.1.4.dist-info}/WHEEL +0 -0
- {pytest_neon-2.1.2.dist-info → pytest_neon-2.1.4.dist-info}/entry_points.txt +0 -0
- {pytest_neon-2.1.2.dist-info → pytest_neon-2.1.4.dist-info}/licenses/LICENSE +0 -0
pytest_neon/__init__.py
CHANGED
pytest_neon/plugin.py
CHANGED
|
@@ -67,6 +67,50 @@ def _get_xdist_worker_id() -> str:
|
|
|
67
67
|
return os.environ.get("PYTEST_XDIST_WORKER", "main")
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def _sanitize_branch_name(name: str) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Sanitize a string for use in Neon branch names.
|
|
73
|
+
|
|
74
|
+
Only allows alphanumeric characters, hyphens, and underscores.
|
|
75
|
+
All other characters (including non-ASCII) are replaced with hyphens.
|
|
76
|
+
"""
|
|
77
|
+
import re
|
|
78
|
+
|
|
79
|
+
# Replace anything that's not alphanumeric, hyphen, or underscore with hyphen
|
|
80
|
+
sanitized = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
|
|
81
|
+
# Collapse multiple hyphens into one
|
|
82
|
+
sanitized = re.sub(r"-+", "-", sanitized)
|
|
83
|
+
# Remove leading/trailing hyphens
|
|
84
|
+
sanitized = sanitized.strip("-")
|
|
85
|
+
return sanitized
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_git_branch_name() -> str | None:
|
|
89
|
+
"""
|
|
90
|
+
Get the current git branch name (sanitized), or None if not in a git repo.
|
|
91
|
+
|
|
92
|
+
Used to include the git branch in Neon branch names, making it easier
|
|
93
|
+
to identify which git branch/PR created orphaned test branches.
|
|
94
|
+
|
|
95
|
+
The branch name is sanitized to replace special characters with hyphens.
|
|
96
|
+
"""
|
|
97
|
+
import subprocess
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
timeout=5,
|
|
105
|
+
)
|
|
106
|
+
if result.returncode == 0:
|
|
107
|
+
branch = result.stdout.strip()
|
|
108
|
+
return _sanitize_branch_name(branch) if branch else None
|
|
109
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
110
|
+
pass
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
70
114
|
def _get_schema_fingerprint(connection_string: str) -> tuple[tuple[Any, ...], ...]:
|
|
71
115
|
"""
|
|
72
116
|
Get a fingerprint of the database schema for change detection.
|
|
@@ -110,6 +154,30 @@ class NeonBranch:
|
|
|
110
154
|
parent_id: str | None = None
|
|
111
155
|
|
|
112
156
|
|
|
157
|
+
def _get_default_branch_id(neon: NeonAPI, project_id: str) -> str | None:
|
|
158
|
+
"""
|
|
159
|
+
Get the default/primary branch ID for a project.
|
|
160
|
+
|
|
161
|
+
This is used as a safety check to ensure we never accidentally
|
|
162
|
+
perform destructive operations (like password reset) on the
|
|
163
|
+
production branch.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The branch ID of the default branch, or None if not found.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
response = neon.branches(project_id=project_id)
|
|
170
|
+
for branch in response.branches:
|
|
171
|
+
# Check both 'default' and 'primary' flags for compatibility
|
|
172
|
+
if getattr(branch, "default", False) or getattr(branch, "primary", False):
|
|
173
|
+
return branch.id
|
|
174
|
+
except Exception:
|
|
175
|
+
# If we can't fetch branches, don't block - the safety check
|
|
176
|
+
# will be skipped but tests can still run
|
|
177
|
+
pass
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
113
181
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
114
182
|
"""Add Neon-specific command line options and ini settings."""
|
|
115
183
|
group = parser.getgroup("neon", "Neon database branching")
|
|
@@ -279,8 +347,21 @@ def _create_neon_branch(
|
|
|
279
347
|
|
|
280
348
|
neon = NeonAPI(api_key=api_key)
|
|
281
349
|
|
|
350
|
+
# Cache the default branch ID for safety checks (only fetch once per session)
|
|
351
|
+
if not hasattr(config, "_neon_default_branch_id"):
|
|
352
|
+
config._neon_default_branch_id = _get_default_branch_id(neon, project_id) # type: ignore[attr-defined]
|
|
353
|
+
|
|
282
354
|
# Generate unique branch name
|
|
283
|
-
|
|
355
|
+
# Format: pytest-[git branch (first 15 chars)]-[random]-[suffix]
|
|
356
|
+
# This helps identify orphaned branches by showing which git branch created them
|
|
357
|
+
random_suffix = os.urandom(2).hex() # 2 bytes = 4 hex chars
|
|
358
|
+
git_branch = _get_git_branch_name()
|
|
359
|
+
if git_branch:
|
|
360
|
+
# Truncate git branch to 15 chars to keep branch names reasonable
|
|
361
|
+
git_prefix = git_branch[:15]
|
|
362
|
+
branch_name = f"pytest-{git_prefix}-{random_suffix}{branch_name_suffix}"
|
|
363
|
+
else:
|
|
364
|
+
branch_name = f"pytest-{random_suffix}{branch_name_suffix}"
|
|
284
365
|
|
|
285
366
|
# Build branch creation payload
|
|
286
367
|
branch_config: dict[str, Any] = {"name": branch_name}
|
|
@@ -341,6 +422,18 @@ def _create_neon_branch(
|
|
|
341
422
|
|
|
342
423
|
host = endpoint.host
|
|
343
424
|
|
|
425
|
+
# SAFETY CHECK: Ensure we never reset password on the default/production branch
|
|
426
|
+
# This should be impossible since we just created this branch, but we check
|
|
427
|
+
# defensively to prevent catastrophic mistakes if there's ever a bug
|
|
428
|
+
default_branch_id = getattr(config, "_neon_default_branch_id", None)
|
|
429
|
+
if default_branch_id and branch.id == default_branch_id:
|
|
430
|
+
raise RuntimeError(
|
|
431
|
+
f"SAFETY CHECK FAILED: Attempted to reset password on default branch "
|
|
432
|
+
f"{branch.id}. This should never happen - the plugin creates new "
|
|
433
|
+
f"branches and should never operate on the default branch. "
|
|
434
|
+
f"Please report this bug at https://github.com/ZainRizvi/pytest-neon/issues"
|
|
435
|
+
)
|
|
436
|
+
|
|
344
437
|
# Reset password to get the password value
|
|
345
438
|
# (newly created branches don't expose password)
|
|
346
439
|
password_response = neon.role_password_reset(
|
|
@@ -502,10 +595,10 @@ def _wait_for_operations(
|
|
|
502
595
|
op_data = response.json().get("operation", {})
|
|
503
596
|
status = op_data.get("status")
|
|
504
597
|
|
|
505
|
-
if status == "
|
|
598
|
+
if status == "failed":
|
|
506
599
|
err = op_data.get("error", "unknown error")
|
|
507
600
|
raise RuntimeError(f"Operation {op_id} failed: {err}")
|
|
508
|
-
if status not in ("finished", "skipped"):
|
|
601
|
+
if status not in ("finished", "skipped", "cancelled"):
|
|
509
602
|
still_pending.append(op_id)
|
|
510
603
|
except requests.RequestException:
|
|
511
604
|
# On network error, assume still pending and retry
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-neon
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pytest_neon/__init__.py,sha256=bWWilGWaSJW2ofj0YsKqdC7r972s0c2Ar5DcE1Tn0Oc,398
|
|
2
|
+
pytest_neon/plugin.py,sha256=A9qv1mv0CXez-SDAWruzVxjRFp0YdSWkU-8j9TWV_LA,41770
|
|
3
|
+
pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pytest_neon-2.1.4.dist-info/METADATA,sha256=dmGCHSNi32pEA5wG6fZhjoFrcirza1kO16RdRSmyEJs,18734
|
|
5
|
+
pytest_neon-2.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
pytest_neon-2.1.4.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
|
|
7
|
+
pytest_neon-2.1.4.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
|
|
8
|
+
pytest_neon-2.1.4.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pytest_neon/__init__.py,sha256=EKni9NpcUiIPy8J8vj-s6oKH6S98JlNs5JYuF151cMA,398
|
|
2
|
-
pytest_neon/plugin.py,sha256=r30wz96rQZerAiAF8wAVmWPksF6lM2dsJyUNj2X9rWo,38038
|
|
3
|
-
pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
pytest_neon-2.1.2.dist-info/METADATA,sha256=QA7xaOCbi_wD8LTwb8gWQpZe8rG8BWcMrETS78HUOks,18734
|
|
5
|
-
pytest_neon-2.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
pytest_neon-2.1.2.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
|
|
7
|
-
pytest_neon-2.1.2.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
|
|
8
|
-
pytest_neon-2.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|