From 7e238155450fea04dac835678eec83d581de7209 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:32:26 +0200 Subject: [PATCH] fix: reports hold the data trees they read generate_readme, generate_site and slots resolve against data/ but took no hold when run on their own, as the documented commands do. A refresh swapping a tree mid-read then gave counts from half of it. Each now holds the trees shared, like verify and the builder. --- scripts/generate_readme.py | 11 ++++++++++- scripts/generate_site.py | 9 ++++++++- scripts/slots.py | 10 ++++++++++ tests/test_audit_regressions.py | 22 ++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index cabf8aed..574ed11a 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -772,9 +772,18 @@ def main() -> None: ) args = parser.parse_args() + import contextlib + + from common import load_data_dir_registry + from refresh_data_dirs import hold_data_dirs + db = load_database(args.db) - readme = generate_readme(db, args.platforms_dir) + # Coverage resolves against data/: a refresh swapping a tree mid-read + # published counts taken from half of it. + with contextlib.ExitStack() as held: + hold_data_dirs(held, load_data_dir_registry(args.platforms_dir)) + readme = generate_readme(db, args.platforms_dir) status = "Generated" if write_if_changed("README.md", readme) else "Unchanged" print(f"{status} ./README.md") diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 03ca6935..d57ea385 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -4395,7 +4395,14 @@ def main() -> None: try: # Two runs over one docs/ delete each other's pages and scratch # files in the sweep, so the second is refused instead. - with artifact_lock(args.docs_dir): + import contextlib + + from common import load_data_dir_registry + from refresh_data_dirs import hold_data_dirs + + # The coverage pages resolve against data/, held while they are read. + with artifact_lock(args.docs_dir), contextlib.ExitStack() as held: + hold_data_dirs(held, load_data_dir_registry(args.platforms_dir)) _generate(args) except ArtifactLockBusy as exc: print(f"ERROR: {exc}", file=sys.stderr) diff --git a/scripts/slots.py b/scripts/slots.py index a810b2a6..1e91c6b8 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -894,6 +894,16 @@ def main() -> int: ) args = parser.parse_args() + import contextlib + + from common import load_data_dir_registry + from refresh_data_dirs import hold_data_dirs + + # Claims resolve against data/: the trees stay put until the process + # ends, which releases the hold. + held = contextlib.ExitStack() + hold_data_dirs(held, load_data_dir_registry(args.platforms_dir)) + db = load_database(args.db) profiles = load_emulator_profiles(args.emulators_dir) if args.platform: diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index b57c1316..0209b03f 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -5265,5 +5265,27 @@ class AHashScanningCoreContradictsNoPlatform(unittest.TestCase): self.assertEqual(claimed, []) +class ReportsHoldTheDataTrees(unittest.TestCase): + """Run on their own, the README, site and slot reports resolved against + data/ without the shared hold, so a concurrent refresh could swap a tree + while they read it and the published counts came from half of it.""" + + def test_each_entry_point_holds_them(self): + import ast + + for script in ("generate_readme.py", "generate_site.py", "slots.py"): + tree = ast.parse((ROOT / "scripts" / script).read_text(encoding="utf-8")) + main = next( + node for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "main" + ) + with self.subTest(script=script): + self.assertTrue(any( + isinstance(call, ast.Call) + and getattr(call.func, "id", None) == "hold_data_dirs" + for call in ast.walk(main) + )) + + if __name__ == "__main__": unittest.main()