Files
libretro/tests/test_artifact_lock.py
T

198 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Concurrency guard on the pack output directory.
Two runs building the same dist/ leave readers looking at half-written
ZIPs, which surfaces as BadZipFile far from its cause.
"""
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import ArtifactLockBusy, artifact_lock, hold_artifact_lock # noqa: E402
class ArtifactLockTest(unittest.TestCase):
def setUp(self):
self.dir = tempfile.mkdtemp()
def test_writer_excludes_writer(self):
with artifact_lock(self.dir):
with self.assertRaises(ArtifactLockBusy):
with artifact_lock(self.dir):
pass
def test_writer_excludes_reader(self):
with artifact_lock(self.dir):
with self.assertRaises(ArtifactLockBusy):
with artifact_lock(self.dir, exclusive=False):
pass
def test_readers_share(self):
with artifact_lock(self.dir, exclusive=False):
with artifact_lock(self.dir, exclusive=False):
pass
def test_lock_is_released_on_exit(self):
with artifact_lock(self.dir):
pass
with artifact_lock(self.dir):
pass
def test_lock_is_released_on_error(self):
with self.assertRaises(ValueError):
with artifact_lock(self.dir):
raise ValueError("boom")
with artifact_lock(self.dir):
pass
def test_separate_directories_do_not_contend(self):
other = tempfile.mkdtemp()
with artifact_lock(self.dir):
with artifact_lock(other):
pass
def test_lock_survives_a_missing_directory(self):
missing = os.path.join(self.dir, "not-created-yet")
with artifact_lock(missing):
self.assertTrue(os.path.isdir(missing))
class PackLockCliTest(unittest.TestCase):
"""generate_pack and pipeline must refuse a directory held elsewhere."""
def setUp(self):
self.dir = tempfile.mkdtemp()
def _run(self, argv: list[str]) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, *argv],
capture_output=True,
text=True,
cwd=str(REPO_ROOT),
timeout=300,
)
def test_generate_pack_refuses_locked_output(self):
with artifact_lock(self.dir):
proc = self._run(
[
"scripts/generate_pack.py",
"--platform",
"misterfpga",
"--output-dir",
self.dir,
"--offline",
]
)
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertIn("is in use by another run", proc.stdout)
def test_every_writing_mode_refuses_a_locked_output(self):
"""Two of seven modes took the lock; five wrote straight into it.
A mode that writes packs or manifests into a directory another run
holds produces the half-written artifact the lock exists to prevent.
"""
modes = [
(["--emulator", "handy", "--offline"], "emulator"),
(["--system", "atari-lynx", "--offline"], "system"),
(["--manifest-targets"], "manifest-targets"),
(["--platform", "misterfpga", "--manifest", "--offline"], "manifest"),
(
["--platform", "misterfpga", "--from-md5",
"d8f1206299c48946e6ec5ef96d014eaa", "--offline"],
"from-md5",
),
]
with artifact_lock(self.dir):
for argv, label in modes:
with self.subTest(mode=label):
proc = self._run(
["scripts/generate_pack.py", *argv,
"--output-dir", self.dir]
)
self.assertNotEqual(
proc.returncode, 0,
f"{label} wrote into a directory held by another run",
)
self.assertIn("in use", (proc.stdout + proc.stderr).lower())
def test_verify_packs_refuses_a_writer(self):
with artifact_lock(self.dir):
proc = self._run(
[
"scripts/generate_pack.py",
"--platform",
"misterfpga",
"--verify-packs",
"--output-dir",
self.dir,
]
)
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertIn("is in use by another run", proc.stdout)
def test_pipeline_refuses_locked_output(self):
with artifact_lock(self.dir):
proc = self._run(
[
"scripts/pipeline.py",
"--offline",
"--skip-docs",
"--output-dir",
self.dir,
]
)
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertIn("is in use by another run", proc.stdout)
class HeldLockTest(unittest.TestCase):
"""pipeline.py took the lock step by step: between the pack build and the
integrity check dist/ was free, another run's purge emptied it, and the
check reported every platform SKIP and passed."""
def setUp(self):
self.dir = tempfile.mkdtemp()
def _child_locks(self, env: dict) -> subprocess.CompletedProcess:
code = (
"import sys; sys.path.insert(0, 'scripts')\n"
"from common import artifact_lock\n"
f"with artifact_lock({self.dir!r}):\n pass\n"
)
return subprocess.run(
[sys.executable, "-c", code], cwd=str(REPO_ROOT), env=env,
capture_output=True, text=True, timeout=60, check=False,
)
def test_a_spawned_step_inherits_the_hold(self):
with hold_artifact_lock(self.dir):
self.assertEqual(self._child_locks(dict(os.environ)).returncode, 0)
def test_any_other_run_is_refused_for_the_whole_hold(self):
with hold_artifact_lock(self.dir):
env = {k: v for k, v in os.environ.items() if not k.startswith("RETROBIOS_")}
proc = self._child_locks(env)
self.assertNotEqual(proc.returncode, 0)
self.assertIn("ArtifactLockBusy", proc.stderr)
self.assertNotIn("RETROBIOS_HELD_ARTIFACT_DIR", os.environ)
self.assertEqual(self._child_locks(dict(os.environ)).returncode, 0)
def test_the_pipeline_holds_its_output_for_the_run(self):
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
self.assertIn("hold_artifact_lock(args.output_dir)", source)
if __name__ == "__main__":
unittest.main()