Skip to content

Commit a9c72cb

Browse files
committed
test: cover country package updater scripts
1 parent 20847f1 commit a9c72cb

1 file changed

Lines changed: 385 additions & 0 deletions

File tree

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
"""Unit tests for country package updater scripts."""
2+
3+
from __future__ import annotations
4+
5+
import importlib.util
6+
import os
7+
import subprocess
8+
from pathlib import Path
9+
from types import ModuleType
10+
11+
import pytest
12+
13+
from fixtures.test_modal_scripts import REPO_ROOT, SCRIPTS_DIR
14+
15+
16+
SCRIPT = SCRIPTS_DIR / "update-country-package.sh"
17+
CHANGELOG_SCRIPT = SCRIPTS_DIR / "check-country-package-updates.py"
18+
19+
20+
@pytest.fixture
21+
def changelog_module() -> ModuleType:
22+
spec = importlib.util.spec_from_file_location(
23+
"check_country_package_updates", CHANGELOG_SCRIPT
24+
)
25+
assert spec is not None
26+
assert spec.loader is not None
27+
module = importlib.util.module_from_spec(spec)
28+
spec.loader.exec_module(module)
29+
return module
30+
31+
32+
@pytest.fixture
33+
def fake_repo(tmp_path: Path) -> Path:
34+
project = tmp_path / "simulation"
35+
modal_dir = project / "src" / "modal"
36+
modal_dir.mkdir(parents=True)
37+
38+
(project / "pyproject.toml").write_text(
39+
"\n".join(
40+
[
41+
"[project]",
42+
'dependencies = ["policyengine-us==1.0.0", "policyengine-uk==2.0.0"]',
43+
]
44+
),
45+
encoding="utf-8",
46+
)
47+
(project / "uv.lock").write_text(
48+
"\n".join(
49+
[
50+
"[[package]]",
51+
'name = "policyengine-us"',
52+
'version = "1.0.0"',
53+
"",
54+
"[[package]]",
55+
'name = "policyengine-uk"',
56+
'version = "2.0.0"',
57+
]
58+
),
59+
encoding="utf-8",
60+
)
61+
(modal_dir / "app.py").write_text(
62+
"\n".join(
63+
[
64+
"import os",
65+
'US_VERSION = os.environ.get("POLICYENGINE_US_VERSION", "1.0.0")',
66+
'UK_VERSION = os.environ.get("POLICYENGINE_UK_VERSION", "2.0.0")',
67+
]
68+
),
69+
encoding="utf-8",
70+
)
71+
72+
helper_dir = tmp_path / ".github" / "scripts"
73+
helper_dir.mkdir(parents=True)
74+
(helper_dir / "check-country-package-updates.py").write_text(
75+
'#!/usr/bin/env python3\nprint("### Added\\n- Example upstream change")\n',
76+
encoding="utf-8",
77+
)
78+
79+
return tmp_path
80+
81+
82+
@pytest.fixture
83+
def fake_bin(tmp_path: Path) -> Path:
84+
path = tmp_path / "bin"
85+
path.mkdir()
86+
return path
87+
88+
89+
def write_executable(path: Path, content: str) -> None:
90+
path.write_text(content, encoding="utf-8")
91+
path.chmod(0o755)
92+
93+
94+
def install_fake_git(
95+
fake_bin: Path,
96+
*,
97+
root: Path,
98+
log: Path,
99+
remote_branch_exists: bool = False,
100+
diff_has_changes: bool = False,
101+
) -> None:
102+
write_executable(
103+
fake_bin / "git",
104+
f"""#!/usr/bin/env bash
105+
set -euo pipefail
106+
printf 'git %s\\n' "$*" >> "{log}"
107+
108+
if [[ "$1" == "rev-parse" && "$2" == "--show-toplevel" ]]; then
109+
echo "{root}"
110+
exit 0
111+
fi
112+
113+
if [[ "$1" == "ls-remote" ]]; then
114+
if [[ "{int(remote_branch_exists)}" == "1" ]]; then
115+
exit 0
116+
fi
117+
exit 2
118+
fi
119+
120+
if [[ "$1" == "diff" ]]; then
121+
if [[ "{int(diff_has_changes)}" == "1" ]]; then
122+
exit 1
123+
fi
124+
exit 0
125+
fi
126+
127+
exit 0
128+
""",
129+
)
130+
131+
132+
def install_fake_gh(fake_bin: Path, *, log: Path, open_pr: str = "") -> None:
133+
write_executable(
134+
fake_bin / "gh",
135+
f"""#!/usr/bin/env bash
136+
set -euo pipefail
137+
printf 'gh %s\\n' "$*" >> "{log}"
138+
139+
if [[ "$1" == "pr" && "$2" == "list" ]]; then
140+
printf '%s\\n' "{open_pr}"
141+
exit 0
142+
fi
143+
144+
if [[ "$1" == "pr" && "$2" == "create" ]]; then
145+
exit 0
146+
fi
147+
148+
exit 0
149+
""",
150+
)
151+
152+
153+
def install_fake_uv(fake_bin: Path, *, log: Path) -> None:
154+
write_executable(
155+
fake_bin / "uv",
156+
f"""#!/usr/bin/env bash
157+
set -euo pipefail
158+
printf 'uv %s\\n' "$*" >> "{log}"
159+
exit 0
160+
""",
161+
)
162+
163+
164+
def updater_env(fake_bin: Path, fake_repo: Path, **extra: str) -> dict[str, str]:
165+
env = os.environ.copy()
166+
env.update(
167+
{
168+
"PATH": f"{fake_bin}{os.pathsep}{env['PATH']}",
169+
"PROJECT_DIR": "simulation",
170+
}
171+
)
172+
env.update(extra)
173+
return env
174+
175+
176+
def run_updater(*args: str, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
177+
return subprocess.run(
178+
["bash", str(SCRIPT), *args],
179+
cwd=REPO_ROOT,
180+
env=env,
181+
capture_output=True,
182+
text=True,
183+
)
184+
185+
186+
def test_update_country_package_script_has_valid_bash_syntax() -> None:
187+
result = subprocess.run(
188+
["bash", "-n", str(SCRIPT)],
189+
capture_output=True,
190+
text=True,
191+
)
192+
193+
assert result.returncode == 0, result.stderr
194+
195+
196+
def test_update_country_package_rejects_unknown_package(
197+
fake_bin: Path, fake_repo: Path, tmp_path: Path
198+
) -> None:
199+
git_log = tmp_path / "git.log"
200+
install_fake_git(fake_bin, root=fake_repo, log=git_log)
201+
202+
result = run_updater(
203+
"policyengine-ca",
204+
env=updater_env(fake_bin, fake_repo, LATEST_OVERRIDE="1.1.0"),
205+
)
206+
207+
assert result.returncode != 0
208+
assert "Unsupported package 'policyengine-ca'" in result.stderr
209+
210+
211+
def test_update_country_package_dry_run_reports_planned_changes_without_editing(
212+
fake_bin: Path, fake_repo: Path, tmp_path: Path
213+
) -> None:
214+
git_log = tmp_path / "git.log"
215+
install_fake_git(fake_bin, root=fake_repo, log=git_log)
216+
pyproject = fake_repo / "simulation" / "pyproject.toml"
217+
original_pyproject = pyproject.read_text(encoding="utf-8")
218+
219+
result = run_updater(
220+
"policyengine-us",
221+
"--dry-run",
222+
env=updater_env(fake_bin, fake_repo, LATEST_OVERRIDE="1.1.0"),
223+
)
224+
225+
assert result.returncode == 0, result.stderr
226+
assert "Update available: 1.0.0 -> 1.1.0" in result.stdout
227+
assert "Dry run: would create auto/update-policyengine-us-1.1.0" in result.stdout
228+
assert "simulation/pyproject.toml" in result.stdout
229+
assert "simulation/uv.lock" in result.stdout
230+
assert "simulation/src/modal/app.py" in result.stdout
231+
assert pyproject.read_text(encoding="utf-8") == original_pyproject
232+
233+
234+
def test_update_country_package_dry_run_reports_existing_branch_recovery(
235+
fake_bin: Path, fake_repo: Path, tmp_path: Path
236+
) -> None:
237+
git_log = tmp_path / "git.log"
238+
install_fake_git(
239+
fake_bin,
240+
root=fake_repo,
241+
log=git_log,
242+
remote_branch_exists=True,
243+
)
244+
245+
result = run_updater(
246+
"policyengine-us",
247+
"--dry-run",
248+
env=updater_env(fake_bin, fake_repo, LATEST_OVERRIDE="1.1.0"),
249+
)
250+
251+
assert result.returncode == 0, result.stderr
252+
assert (
253+
"remote branch 'auto/update-policyengine-us-1.1.0' already exists; "
254+
"would ensure a PR exists for it."
255+
) in result.stdout
256+
257+
258+
def test_update_country_package_skips_when_open_pr_exists(
259+
fake_bin: Path, fake_repo: Path, tmp_path: Path
260+
) -> None:
261+
git_log = tmp_path / "git.log"
262+
gh_log = tmp_path / "gh.log"
263+
install_fake_git(fake_bin, root=fake_repo, log=git_log)
264+
install_fake_gh(fake_bin, log=gh_log, open_pr="123")
265+
266+
result = run_updater(
267+
"policyengine-us",
268+
env=updater_env(fake_bin, fake_repo, LATEST_OVERRIDE="1.1.0"),
269+
)
270+
271+
assert result.returncode == 0, result.stderr
272+
assert (
273+
"PR #123 already exists for auto/update-policyengine-us-1.1.0" in result.stdout
274+
)
275+
assert "pr create" not in gh_log.read_text(encoding="utf-8")
276+
277+
278+
def test_update_country_package_opens_pr_for_existing_branch_without_open_pr(
279+
fake_bin: Path, fake_repo: Path, tmp_path: Path
280+
) -> None:
281+
git_log = tmp_path / "git.log"
282+
gh_log = tmp_path / "gh.log"
283+
install_fake_git(
284+
fake_bin,
285+
root=fake_repo,
286+
log=git_log,
287+
remote_branch_exists=True,
288+
)
289+
install_fake_gh(fake_bin, log=gh_log)
290+
291+
result = run_updater(
292+
"policyengine-us",
293+
env=updater_env(fake_bin, fake_repo, LATEST_OVERRIDE="1.1.0"),
294+
)
295+
296+
assert result.returncode == 0, result.stderr
297+
assert "already exists without an open PR. Creating PR." in result.stdout
298+
gh_calls = gh_log.read_text(encoding="utf-8")
299+
assert "pr list" in gh_calls
300+
assert "pr create" in gh_calls
301+
assert "--head auto/update-policyengine-us-1.1.0" in gh_calls
302+
303+
304+
def test_update_country_package_updates_files_and_opens_pr(
305+
fake_bin: Path, fake_repo: Path, tmp_path: Path
306+
) -> None:
307+
git_log = tmp_path / "git.log"
308+
gh_log = tmp_path / "gh.log"
309+
uv_log = tmp_path / "uv.log"
310+
install_fake_git(fake_bin, root=fake_repo, log=git_log, diff_has_changes=True)
311+
install_fake_gh(fake_bin, log=gh_log)
312+
install_fake_uv(fake_bin, log=uv_log)
313+
314+
result = run_updater(
315+
"policyengine-us",
316+
env=updater_env(fake_bin, fake_repo, LATEST_OVERRIDE="1.1.0"),
317+
)
318+
319+
assert result.returncode == 0, result.stderr
320+
assert "PR created for policyengine-us 1.0.0 -> 1.1.0" in result.stdout
321+
322+
pyproject_text = (fake_repo / "simulation" / "pyproject.toml").read_text(
323+
encoding="utf-8"
324+
)
325+
modal_text = (fake_repo / "simulation" / "src" / "modal" / "app.py").read_text(
326+
encoding="utf-8"
327+
)
328+
assert "policyengine-us==1.1.0" in pyproject_text
329+
assert (
330+
'US_VERSION = os.environ.get("POLICYENGINE_US_VERSION", "1.1.0")' in modal_text
331+
)
332+
assert "lock --upgrade-package policyengine-us" in uv_log.read_text(
333+
encoding="utf-8"
334+
)
335+
assert "checkout -b auto/update-policyengine-us-1.1.0" in git_log.read_text(
336+
encoding="utf-8"
337+
)
338+
assert "pr create" in gh_log.read_text(encoding="utf-8")
339+
340+
341+
def test_parse_changelog_collects_versioned_category_items(
342+
changelog_module: ModuleType,
343+
) -> None:
344+
text = """
345+
# Changelog
346+
347+
## 1.2.2
348+
### Added
349+
- New variable
350+
351+
### Fixed
352+
- Important bug fix
353+
354+
## [1.2.1]
355+
### Changed
356+
- Existing calculation changed
357+
358+
## 1.2.0
359+
### Added
360+
- Old change
361+
"""
362+
363+
parsed = changelog_module.parse_changelog(text)
364+
changes = changelog_module.get_changes_between(parsed, "1.2.0", "1.2.2")
365+
formatted = changelog_module.format_changes(changes)
366+
367+
assert "### Added\n- New variable" in formatted
368+
assert "### Changed\n- Existing calculation changed" in formatted
369+
assert "### Fixed\n- Important bug fix" in formatted
370+
assert "Old change" not in formatted
371+
372+
373+
def test_parse_version_requires_three_numeric_parts(
374+
changelog_module: ModuleType,
375+
) -> None:
376+
assert changelog_module.parse_version("1.2.3") == (1, 2, 3)
377+
378+
with pytest.raises(ValueError, match="Expected a semantic version"):
379+
changelog_module.parse_version("1.2")
380+
381+
382+
def test_fetch_changelog_returns_none_for_unknown_package(
383+
changelog_module: ModuleType,
384+
) -> None:
385+
assert changelog_module.fetch_changelog("policyengine-ca") is None

0 commit comments

Comments
 (0)