mirror of
https://github.com/pre-commit/pre-commit-hooks.git
synced 2026-04-06 12:06:53 +00:00
Refactor catch_dotenv hook and tests for improved readability and consistency
This commit is contained in:
parent
989ac68f29
commit
c7f0dae9a4
3 changed files with 259 additions and 98 deletions
|
|
@ -1,18 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from pre_commit_hooks.catch_dotenv import main, ensure_env_in_gitignore, GITIGNORE_BANNER, DEFAULT_ENV_FILE, DEFAULT_EXAMPLE_ENV_FILE, DEFAULT_GITIGNORE_FILE
|
||||
from pre_commit_hooks.catch_dotenv import DEFAULT_ENV_FILE
|
||||
from pre_commit_hooks.catch_dotenv import DEFAULT_EXAMPLE_ENV_FILE
|
||||
from pre_commit_hooks.catch_dotenv import DEFAULT_GITIGNORE_FILE
|
||||
from pre_commit_hooks.catch_dotenv import ensure_env_in_gitignore
|
||||
from pre_commit_hooks.catch_dotenv import GITIGNORE_BANNER
|
||||
from pre_commit_hooks.catch_dotenv import main
|
||||
|
||||
# Tests cover hook behavior: detection gating, .gitignore normalization, example
|
||||
# file generation parsing edge cases, idempotency, and preservation of existing
|
||||
# content. Each test isolates a single behavioral contract.
|
||||
# Tests cover hook behavior: detection gating, .gitignore normalization,
|
||||
# example file generation parsing edge cases, idempotency, and preservation of
|
||||
# existing content. Each test isolates a single behavioral contract.
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
@ -25,13 +31,18 @@ def env_file(tmp_path: Path) -> Path:
|
|||
# __file__ => <repo_root>/tests/catch_dotenv_test.py
|
||||
# parents[0] = <repo_root>/tests, parents[1] = <repo_root>
|
||||
# Source file stored as test.env in repo (cannot commit a real .env in CI)
|
||||
resource_env = Path(__file__).resolve().parents[1] / 'testing' / 'resources' / 'test.env'
|
||||
resource_env = (
|
||||
Path(__file__).resolve().parents[1] /
|
||||
'testing' / 'resources' / 'test.env'
|
||||
)
|
||||
dest = tmp_path / DEFAULT_ENV_FILE
|
||||
shutil.copyfile(resource_env, dest)
|
||||
return dest
|
||||
|
||||
|
||||
def run_hook(tmp_path: Path, staged: list[str], create_example: bool = False) -> int:
|
||||
def run_hook(
|
||||
tmp_path: Path, staged: list[str], create_example: bool = False,
|
||||
) -> int:
|
||||
cwd = os.getcwd()
|
||||
os.chdir(tmp_path)
|
||||
try:
|
||||
|
|
@ -43,13 +54,15 @@ def run_hook(tmp_path: Path, staged: list[str], create_example: bool = False) ->
|
|||
os.chdir(cwd)
|
||||
|
||||
|
||||
def test_no_env_file(tmp_path: Path, env_file: Path):
|
||||
def test_no_env_file(tmp_path: Path, env_file: Path) -> None:
|
||||
"""Hook should no-op (return 0) if .env not staged even if it exists."""
|
||||
(tmp_path / 'foo.txt').write_text('x')
|
||||
assert run_hook(tmp_path, ['foo.txt']) == 0
|
||||
|
||||
|
||||
def test_blocks_env_and_updates_gitignore(tmp_path: Path, env_file: Path):
|
||||
def test_blocks_env_and_updates_gitignore(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Staging .env triggers block (exit 1) and appends banner + env entry."""
|
||||
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
||||
assert ret == 1
|
||||
|
|
@ -58,12 +71,12 @@ def test_blocks_env_and_updates_gitignore(tmp_path: Path, env_file: Path):
|
|||
assert gi[-1] == DEFAULT_ENV_FILE
|
||||
|
||||
|
||||
def test_env_present_but_not_staged(tmp_path: Path, env_file: Path):
|
||||
def test_env_present_but_not_staged(tmp_path: Path, env_file: Path) -> None:
|
||||
"""Existing .env on disk but not staged should not block commit."""
|
||||
assert run_hook(tmp_path, ['unrelated.txt']) == 0
|
||||
|
||||
|
||||
def test_idempotent_gitignore(tmp_path: Path, env_file: Path):
|
||||
def test_idempotent_gitignore(tmp_path: Path, env_file: Path) -> None:
|
||||
"""Re-running after initial normalization leaves .gitignore unchanged."""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
g.write_text(f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n")
|
||||
|
|
@ -75,10 +88,14 @@ def test_idempotent_gitignore(tmp_path: Path, env_file: Path):
|
|||
assert g.read_text() == content1 # unchanged
|
||||
|
||||
|
||||
def test_gitignore_with_existing_content_preserved(tmp_path: Path, env_file: Path):
|
||||
def test_gitignore_with_existing_content_preserved(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Existing entries stay intact; banner/env appended at end cleanly."""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
g.write_text('node_modules/\n# comment line\n') # no trailing newline section markers
|
||||
g.write_text(
|
||||
'node_modules/\n# comment line\n',
|
||||
) # no trailing newline section markers
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
||||
lines = g.read_text().splitlines()
|
||||
# original content should still be at top
|
||||
|
|
@ -88,10 +105,15 @@ def test_gitignore_with_existing_content_preserved(tmp_path: Path, env_file: Pat
|
|||
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
|
||||
|
||||
def test_gitignore_duplicates_are_collapsed(tmp_path: Path, env_file: Path):
|
||||
def test_gitignore_duplicates_are_collapsed(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Multiple prior duplicate banner/env lines collapse to single pair."""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
g.write_text(f"other\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n\n")
|
||||
g.write_text(
|
||||
f"other\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n"
|
||||
f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n\n",
|
||||
)
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
||||
lines = g.read_text().splitlines()
|
||||
assert lines.count(GITIGNORE_BANNER) == 1
|
||||
|
|
@ -99,7 +121,7 @@ def test_gitignore_duplicates_are_collapsed(tmp_path: Path, env_file: Path):
|
|||
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
|
||||
|
||||
def test_create_example(tmp_path: Path, env_file: Path):
|
||||
def test_create_example(tmp_path: Path, env_file: Path) -> None:
|
||||
"""Example file includes discovered keys; values stripped to KEY=."""
|
||||
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
||||
assert ret == 1
|
||||
|
|
@ -108,12 +130,20 @@ def test_create_example(tmp_path: Path, env_file: Path):
|
|||
# All key lines should be KEY=
|
||||
assert all(re.match(r'^[A-Za-z_][A-Za-z0-9_]*=$', ln) for ln in key_lines)
|
||||
# Spot check a few known keys from resource file
|
||||
for k in ['BACKEND_CONTAINER_PORT=', 'ACCESS_TOKEN_SECRET=', 'SUPABASE_SERVICE_KEY=']:
|
||||
for k in [
|
||||
'OPENAI_API_KEY=',
|
||||
'ACCESS_TOKEN_SECRET=',
|
||||
'SUPABASE_SERVICE_KEY=',
|
||||
]:
|
||||
assert k in key_lines
|
||||
|
||||
|
||||
def test_create_example_duplicate_key_variant_ignored(tmp_path: Path, env_file: Path):
|
||||
"""Appending whitespace duplicate of existing key should not duplicate in example."""
|
||||
def test_create_example_duplicate_key_variant_ignored(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Appending whitespace duplicate of existing key should not duplicate
|
||||
in example.
|
||||
"""
|
||||
with open(env_file, 'a', encoding='utf-8') as f:
|
||||
f.write('BACKEND_CONTAINER_PORT =999 # duplicate variant\n')
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
||||
|
|
@ -122,7 +152,9 @@ def test_create_example_duplicate_key_variant_ignored(tmp_path: Path, env_file:
|
|||
assert key_lines.count('BACKEND_CONTAINER_PORT=') == 1
|
||||
|
||||
|
||||
def test_gitignore_without_trailing_newline(tmp_path: Path, env_file: Path):
|
||||
def test_gitignore_without_trailing_newline(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Normalization works when original .gitignore lacks trailing newline."""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
g.write_text('existing_line') # no newline at EOF
|
||||
|
|
@ -132,11 +164,20 @@ def test_gitignore_without_trailing_newline(tmp_path: Path, env_file: Path):
|
|||
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
|
||||
|
||||
def test_ensure_env_in_gitignore_normalizes(tmp_path: Path, env_file: Path):
|
||||
"""Direct API call collapses duplicates and produces canonical tail layout."""
|
||||
def test_ensure_env_in_gitignore_normalizes(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Direct API call collapses duplicates and produces canonical tail
|
||||
layout.
|
||||
"""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
g.write_text(f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n")
|
||||
modified = ensure_env_in_gitignore(DEFAULT_ENV_FILE, str(g), GITIGNORE_BANNER)
|
||||
g.write_text(
|
||||
f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n"
|
||||
f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n",
|
||||
)
|
||||
modified = ensure_env_in_gitignore(
|
||||
DEFAULT_ENV_FILE, str(g), GITIGNORE_BANNER,
|
||||
)
|
||||
assert modified is True
|
||||
lines = g.read_text().splitlines()
|
||||
# final two lines should be banner + env
|
||||
|
|
@ -146,37 +187,55 @@ def test_ensure_env_in_gitignore_normalizes(tmp_path: Path, env_file: Path):
|
|||
assert lines.count(DEFAULT_ENV_FILE) == 1
|
||||
|
||||
|
||||
def test_source_env_file_not_modified(tmp_path: Path, env_file: Path):
|
||||
def test_source_env_file_not_modified(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Hook must not alter original .env (comments and formatting stay)."""
|
||||
original = env_file.read_text()
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
||||
assert env_file.read_text() == original
|
||||
|
||||
|
||||
def test_large_resource_env_parsing(tmp_path: Path, env_file: Path):
|
||||
"""Generate example from resource env; assert broad key coverage & format."""
|
||||
def test_large_resource_env_parsing(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Generate example from resource env; assert broad key coverage &
|
||||
format.
|
||||
"""
|
||||
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
||||
assert ret == 1
|
||||
example_lines = (tmp_path / DEFAULT_EXAMPLE_ENV_FILE).read_text().splitlines()
|
||||
example_lines = (
|
||||
(tmp_path / DEFAULT_EXAMPLE_ENV_FILE).read_text().splitlines()
|
||||
)
|
||||
key_lines = [ln for ln in example_lines if ln and not ln.startswith('#')]
|
||||
assert len(key_lines) > 20
|
||||
assert all(re.match(r'^[A-Za-z_][A-Za-z0-9_]*=$', ln) for ln in key_lines)
|
||||
for k in ['BACKEND_CONTAINER_PORT=', 'SUPABASE_SERVICE_KEY=', 'ACCESS_TOKEN_SECRET=']:
|
||||
for k in [
|
||||
'BACKEND_CONTAINER_PORT=',
|
||||
'SUPABASE_SERVICE_KEY=',
|
||||
'ACCESS_TOKEN_SECRET=',
|
||||
]:
|
||||
assert k in key_lines
|
||||
|
||||
|
||||
def test_failure_message_content(tmp_path: Path, env_file: Path, capsys):
|
||||
def test_failure_message_content(
|
||||
tmp_path: Path,
|
||||
env_file: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Hook stdout message should contain key phrases when blocking commit."""
|
||||
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
||||
assert ret == 1
|
||||
out = capsys.readouterr().out.strip()
|
||||
assert "Blocked committing" in out
|
||||
assert 'Blocked committing' in out
|
||||
assert DEFAULT_GITIGNORE_FILE in out # updated path appears
|
||||
assert "Generated .env.example." in out
|
||||
assert "Remove .env" in out
|
||||
assert 'Generated .env.example.' in out
|
||||
assert 'Remove .env' in out
|
||||
|
||||
|
||||
def test_create_example_when_env_missing(tmp_path: Path, env_file: Path):
|
||||
def test_create_example_when_env_missing(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""--create-example with no .env staged or present should no-op (exit 0).
|
||||
|
||||
Uses env_file fixture (requirement: all tests use fixture) then removes the
|
||||
|
|
@ -188,18 +247,28 @@ def test_create_example_when_env_missing(tmp_path: Path, env_file: Path):
|
|||
assert not (tmp_path / DEFAULT_EXAMPLE_ENV_FILE).exists()
|
||||
|
||||
|
||||
def test_gitignore_is_directory_error(tmp_path: Path, env_file: Path, capsys):
|
||||
"""If .gitignore path is a directory, hook should print error and still block."""
|
||||
def test_gitignore_is_directory_error(
|
||||
tmp_path: Path,
|
||||
env_file: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""If .gitignore path is a directory, hook should print error and still
|
||||
block.
|
||||
"""
|
||||
gitignore_dir = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
gitignore_dir.mkdir()
|
||||
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
||||
assert ret == 1 # still blocks commit
|
||||
captured = capsys.readouterr()
|
||||
assert "ERROR:" in captured.err # error now printed to stderr
|
||||
assert 'ERROR:' in captured.err # error now printed to stderr
|
||||
|
||||
|
||||
def test_env_example_overwrites_existing(tmp_path: Path, env_file: Path):
|
||||
"""Pre-existing example file with junk should be overwritten with header & keys."""
|
||||
def test_env_example_overwrites_existing(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Pre-existing example file with junk should be overwritten with header
|
||||
& keys.
|
||||
"""
|
||||
example = tmp_path / DEFAULT_EXAMPLE_ENV_FILE
|
||||
example.write_text('junk=1\nSHOULD_NOT_REMAIN=2\n')
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
||||
|
|
@ -210,12 +279,17 @@ def test_env_example_overwrites_existing(tmp_path: Path, env_file: Path):
|
|||
assert 'SHOULD_NOT_REMAIN=2' not in content
|
||||
|
||||
|
||||
def test_large_gitignore_normalization_performance(tmp_path: Path, env_file: Path):
|
||||
def test_large_gitignore_normalization_performance(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Very large .gitignore remains normalized quickly (functional smoke)."""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
# Generate many lines with scattered duplicates of banner/env
|
||||
lines = [f"file_{i}" for i in range(3000)] + [GITIGNORE_BANNER, DEFAULT_ENV_FILE] * 3
|
||||
g.write_text("\n".join(lines) + "\n")
|
||||
lines = (
|
||||
[f"file_{i}" for i in range(3000)] +
|
||||
[GITIGNORE_BANNER, DEFAULT_ENV_FILE] * 3
|
||||
)
|
||||
g.write_text('\n'.join(lines) + '\n')
|
||||
start = time.time()
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
||||
elapsed = time.time() - start
|
||||
|
|
@ -223,12 +297,17 @@ def test_large_gitignore_normalization_performance(tmp_path: Path, env_file: Pat
|
|||
assert result_lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
assert result_lines.count(GITIGNORE_BANNER) == 1
|
||||
assert result_lines.count(DEFAULT_ENV_FILE) == 1
|
||||
# Soft performance expectation: should finish fast (< 0.5s on typical dev machine)
|
||||
# Soft performance expectation: should finish fast
|
||||
# (< 0.5s on typical dev machine)
|
||||
assert elapsed < 0.5
|
||||
|
||||
|
||||
def test_concurrent_gitignore_writes(tmp_path: Path, env_file: Path):
|
||||
"""Concurrent ensure_env_in_gitignore calls result in canonical final state."""
|
||||
def test_concurrent_gitignore_writes(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Concurrent ensure_env_in_gitignore calls result in canonical final
|
||||
state.
|
||||
"""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
# Seed with messy duplicates
|
||||
g.write_text(f"other\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n")
|
||||
|
|
@ -247,8 +326,12 @@ def test_concurrent_gitignore_writes(tmp_path: Path, env_file: Path):
|
|||
assert lines.count(DEFAULT_ENV_FILE) == 1
|
||||
|
||||
|
||||
def test_mixed_staged_files(tmp_path: Path, env_file: Path):
|
||||
"""Staging .env with other files still blocks and only normalizes gitignore once."""
|
||||
def test_mixed_staged_files(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Staging .env with other files still blocks and only normalizes
|
||||
gitignore once.
|
||||
"""
|
||||
other = tmp_path / 'README.md'
|
||||
other.write_text('hi')
|
||||
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE, 'README.md'])
|
||||
|
|
@ -257,18 +340,29 @@ def test_mixed_staged_files(tmp_path: Path, env_file: Path):
|
|||
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
|
||||
|
||||
def test_already_ignored_env_with_variations(tmp_path: Path, env_file: Path):
|
||||
"""Pre-existing ignore lines with spacing normalize to single canonical pair."""
|
||||
def test_already_ignored_env_with_variations(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Pre-existing ignore lines with spacing normalize to single
|
||||
canonical pair.
|
||||
"""
|
||||
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
||||
g.write_text(f" {DEFAULT_ENV_FILE} \n{GITIGNORE_BANNER}\n {DEFAULT_ENV_FILE}\n")
|
||||
g.write_text(
|
||||
f" {DEFAULT_ENV_FILE} \n{GITIGNORE_BANNER}\n"
|
||||
f" {DEFAULT_ENV_FILE}\n",
|
||||
)
|
||||
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
||||
lines = g.read_text().splitlines()
|
||||
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
assert lines.count(DEFAULT_ENV_FILE) == 1
|
||||
|
||||
|
||||
def test_subdirectory_invocation(tmp_path: Path, env_file: Path):
|
||||
"""Running from a subdirectory now writes .gitignore relative to CWD (simplified behavior)."""
|
||||
def test_subdirectory_invocation(
|
||||
tmp_path: Path, env_file: Path,
|
||||
) -> None:
|
||||
"""Running from a subdirectory now writes .gitignore relative to CWD
|
||||
(simplified behavior).
|
||||
"""
|
||||
sub = tmp_path / 'subdir'
|
||||
sub.mkdir()
|
||||
# simulate repository root marker
|
||||
|
|
@ -277,7 +371,9 @@ def test_subdirectory_invocation(tmp_path: Path, env_file: Path):
|
|||
cwd = os.getcwd()
|
||||
os.chdir(sub)
|
||||
try:
|
||||
ret = main(['../' + DEFAULT_ENV_FILE]) # staged path relative to subdir
|
||||
ret = main(
|
||||
['../' + DEFAULT_ENV_FILE],
|
||||
) # staged path relative to subdir
|
||||
gi = (sub / DEFAULT_GITIGNORE_FILE).read_text().splitlines()
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
|
|
@ -285,31 +381,49 @@ def test_subdirectory_invocation(tmp_path: Path, env_file: Path):
|
|||
assert gi[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
||||
|
||||
|
||||
def test_atomic_write_failure_gitignore(monkeypatch, tmp_path: Path, env_file: Path, capsys):
|
||||
"""Simulate os.replace failure during gitignore write to exercise error path."""
|
||||
def boom(*a, **k):
|
||||
def test_atomic_write_failure_gitignore(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
env_file: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Simulate os.replace failure during gitignore write to exercise error
|
||||
path.
|
||||
"""
|
||||
def boom(*_a: object, **_k: object) -> None:
|
||||
raise OSError('replace-fail')
|
||||
monkeypatch.setattr('pre_commit_hooks.catch_dotenv.os.replace', boom)
|
||||
modified = ensure_env_in_gitignore(DEFAULT_ENV_FILE, str(tmp_path / DEFAULT_GITIGNORE_FILE), GITIGNORE_BANNER)
|
||||
modified = ensure_env_in_gitignore(
|
||||
DEFAULT_ENV_FILE,
|
||||
str(tmp_path / DEFAULT_GITIGNORE_FILE),
|
||||
GITIGNORE_BANNER,
|
||||
)
|
||||
assert modified is False
|
||||
captured = capsys.readouterr()
|
||||
assert 'ERROR: unable to write' in captured.err
|
||||
|
||||
|
||||
def test_atomic_write_failure_example(monkeypatch, tmp_path: Path, env_file: Path, capsys):
|
||||
def test_atomic_write_failure_example(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
env_file: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Simulate os.replace failure when writing example env file."""
|
||||
def boom(*a, **k):
|
||||
def boom(*_a: object, **_k: object) -> None:
|
||||
raise OSError('replace-fail')
|
||||
monkeypatch.setattr('pre_commit_hooks.catch_dotenv.os.replace', boom)
|
||||
ok = False
|
||||
# create_example_env requires source .env to exist; env_file fixture provides it in tmp_path root
|
||||
# create_example_env requires source .env to exist; env_file fixture
|
||||
# provides it in tmp_path root
|
||||
cwd = os.getcwd()
|
||||
os.chdir(tmp_path)
|
||||
try:
|
||||
ok = main([DEFAULT_ENV_FILE, '--create-example']) == 1
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
# hook still blocks; but example creation failed -> message should not claim Example file generated
|
||||
# hook still blocks; but example creation failed -> message should
|
||||
# not claim Example file generated
|
||||
assert ok is True
|
||||
captured = capsys.readouterr()
|
||||
out = captured.out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue