|
4 | 4 |
|
5 | 5 | import importlib |
6 | 6 | import json |
| 7 | +import os |
7 | 8 | import subprocess |
8 | 9 | import sys |
9 | 10 | import tempfile |
| 11 | +import textwrap |
10 | 12 | import unittest |
11 | 13 | from pathlib import Path |
12 | 14 |
|
13 | 15 | REPO = Path(__file__).resolve().parents[1] |
14 | 16 | SCRIPTS = REPO / "scripts" |
15 | 17 | SYNC_SCRIPT = SCRIPTS / "synchronize_mip_package.py" |
| 18 | +SYNC_WORKFLOW = REPO / ".github/workflows/reusable-synchronize-mip-package.yml" |
16 | 19 |
|
17 | 20 |
|
18 | 21 | def _load_module(name: str): |
@@ -118,7 +121,10 @@ def test_audioeffects_dispatch_matches_lockfile(self) -> None: |
118 | 121 | self.assertEqual(result.returncode, 0, result.stderr) |
119 | 122 | self.assertTrue((mip / "micropython" / "audioeffects" / "manifest.py").is_file()) |
120 | 123 |
|
121 | | - def test_stale_audioif_caller_is_rejected_when_lockfile_names_audiocomponents(self) -> None: |
| 124 | + def test_script_in_isolation_rejects_source_repository_name_that_disagrees_with_lockfile(self) -> None: |
| 125 | + # Defence in depth only. The publication workflow never presents this |
| 126 | + # mismatch: Record this release refuses a repository move before the |
| 127 | + # sync loop runs synchronize_mip_package.py. |
122 | 128 | with tempfile.TemporaryDirectory() as tmp: |
123 | 129 | root = Path(tmp) |
124 | 130 | source = root / "source" |
@@ -190,6 +196,82 @@ def test_lockfile_repository_reads_named_entry(self) -> None: |
190 | 196 | ) |
191 | 197 |
|
192 | 198 |
|
| 199 | +def record_release_python() -> str: |
| 200 | + text = SYNC_WORKFLOW.read_text(encoding="utf-8") |
| 201 | + start = text.index("- name: Record this release in the lockfile") |
| 202 | + block = text[start:] |
| 203 | + begin = block.index("python3 - <<'PY'\n") + len("python3 - <<'PY'\n") |
| 204 | + end = block.index("\n PY\n", begin) |
| 205 | + return textwrap.dedent(block[begin:end]) |
| 206 | + |
| 207 | + |
| 208 | +def run_record_step( |
| 209 | + lockfile: Path, |
| 210 | + *, |
| 211 | + profile: str, |
| 212 | + repository: str, |
| 213 | + ref: str, |
| 214 | +) -> subprocess.CompletedProcess[str]: |
| 215 | + with tempfile.TemporaryDirectory() as tmp: |
| 216 | + script = Path(tmp) / "record_release.py" |
| 217 | + script.write_text(record_release_python(), encoding="utf-8") |
| 218 | + env = os.environ.copy() |
| 219 | + env.update( |
| 220 | + { |
| 221 | + "LOCKFILE": str(lockfile), |
| 222 | + "PUBLICATION_PROFILE": profile, |
| 223 | + "SOURCE_REPOSITORY": repository, |
| 224 | + "SOURCE_REF": ref, |
| 225 | + } |
| 226 | + ) |
| 227 | + return subprocess.run( |
| 228 | + [sys.executable, str(script)], |
| 229 | + capture_output=True, |
| 230 | + text=True, |
| 231 | + env=env, |
| 232 | + check=False, |
| 233 | + ) |
| 234 | + |
| 235 | + |
| 236 | +class RecordLockfileReleaseTests(unittest.TestCase): |
| 237 | + def test_matching_repository_updates_ref_only(self) -> None: |
| 238 | + with tempfile.TemporaryDirectory() as tmp: |
| 239 | + mip = Path(tmp) |
| 240 | + write_lockfile(mip, {"audioinstruments": "PyDevices/audiocomponents"}) |
| 241 | + lockfile = mip / "pydevices-lock.json" |
| 242 | + before = json.loads(lockfile.read_text(encoding="utf-8")) |
| 243 | + result = run_record_step( |
| 244 | + lockfile, |
| 245 | + profile="audioinstruments", |
| 246 | + repository="PyDevices/audiocomponents", |
| 247 | + ref="v0.3.0", |
| 248 | + ) |
| 249 | + self.assertEqual(result.returncode, 0, result.stderr) |
| 250 | + after = json.loads(lockfile.read_text(encoding="utf-8")) |
| 251 | + self.assertEqual(after["audioinstruments"]["repository"], "PyDevices/audiocomponents") |
| 252 | + self.assertEqual(after["audioinstruments"]["ref"], "v0.3.0") |
| 253 | + self.assertEqual(before["audioinstruments"]["repository"], after["audioinstruments"]["repository"]) |
| 254 | + |
| 255 | + def test_mismatched_repository_fails_without_writing(self) -> None: |
| 256 | + with tempfile.TemporaryDirectory() as tmp: |
| 257 | + mip = Path(tmp) |
| 258 | + write_lockfile(mip, {"audioinstruments": "PyDevices/audiocomponents"}) |
| 259 | + lockfile = mip / "pydevices-lock.json" |
| 260 | + before = lockfile.read_text(encoding="utf-8") |
| 261 | + result = run_record_step( |
| 262 | + lockfile, |
| 263 | + profile="audioinstruments", |
| 264 | + repository="PyDevices/audioif", |
| 265 | + ref="v0.3.0", |
| 266 | + ) |
| 267 | + self.assertNotEqual(result.returncode, 0) |
| 268 | + self.assertIn("PyDevices/audiocomponents", result.stderr) |
| 269 | + self.assertIn("PyDevices/audioif", result.stderr) |
| 270 | + self.assertIn("pydevices-lock.json", result.stderr) |
| 271 | + self.assertIn("PyDevices branch", result.stderr) |
| 272 | + self.assertEqual(lockfile.read_text(encoding="utf-8"), before) |
| 273 | + |
| 274 | + |
193 | 275 | class SharedDescriptionTests(unittest.TestCase): |
194 | 276 | def test_pydevices_manifest_uses_shared_description(self) -> None: |
195 | 277 | text = sync.render_pydevices_manifest("pydevices", "1.2.3", ()) |
|
0 commit comments