diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b88ff..6ce1b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### Fixed +- Per-run CSV and PAE plot reuse no longer depend on the device number of the files. A shared filesystem such as BeeGFS reports another `st_dev` for the same file on each cluster node, so a recursive run on one node recomputed every table and redrew every plot certified on another. Size, mtime, ctime and inode still detect edits and replaced files. Manifests written by 1.4.4 are recomputed once, as after any software change. + ## 1.4.4 - 2026-10-01 ### Fixed diff --git a/src/alphajudge/cache.py b/src/alphajudge/cache.py index 0622065..43b73de 100644 --- a/src/alphajudge/cache.py +++ b/src/alphajudge/cache.py @@ -49,10 +49,12 @@ def file_fingerprint(path: Path) -> dict[str, int]: ctime supplements mtime, but coarse/preserved timestamps can still hide same-size rewrites. Use content validation when metadata is insufficient. + The device number is not part of it: a shared filesystem (BeeGFS) reports + another st_dev for the same file on each cluster node. """ stat = path.stat() return {"size": stat.st_size, "mtime_ns": stat.st_mtime_ns, "ctime_ns": stat.st_ctime_ns, - "device": stat.st_dev, "inode": stat.st_ino} + "inode": stat.st_ino} def software_identity() -> dict: diff --git a/test/test_run_integrity.py b/test/test_run_integrity.py index 0a80d64..fc7c879 100644 --- a/test/test_run_integrity.py +++ b/test/test_run_integrity.py @@ -9,6 +9,7 @@ import shutil from dataclasses import replace from pathlib import Path +from types import SimpleNamespace import numpy as np import pytest @@ -232,6 +233,24 @@ def test_cache_detects_added_removed_and_replaced_inputs(af2_run): assert cache.request_identity(directory, csv_path) != before +def test_cache_and_plots_are_reused_on_a_node_with_another_device_number(af2_run, record_process, monkeypatch): + directory, _ = af2_run + score(directory, skip_pae_png=False) + calls = record_process() + stat = Path.stat + def on_another_node(self, **kwargs): + fields = {name: getattr(stat(self, **kwargs), name) + for name in dir(os.stat_result) if name.startswith("st_")} + fields["st_dev"] += 1 + return SimpleNamespace(**fields) + def redraw(*args, **kwargs): + pytest.fail("A current PAE plot was redrawn") + monkeypatch.setattr(Path, "stat", on_another_node) + monkeypatch.setattr(AF2Parser, "parse_run", redraw) + score(directory, skip_pae_png=False) + assert calls == [] + + def test_png_toggle_generates_missing_plot_without_rescoring(af2_run, monkeypatch): directory, models = af2_run original = score(directory)