Compare commits

..
Author SHA1 Message Date
ClaudeandClaude Opus 5.5 1cbdc479cf test_block_step_sync: locate block_stepper without importing it
The module-level import of CelerisLab.lbm.block_stepper loaded
pycuda.driver into the shared test process, unlike the other CPU host
tests (which load mocked copies via AST). find_spec gives the source path
without the import. Test content and assertions unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:39:52 +00:00
ClaudeandClaude Opus 5.5 f3f1e3d588 PeriodicBlockStepper.step: mid-batch launch failure test; async-error docstring note
Review follow-up (non-blocking items): a launch failure on the second of
three steps fails closed without the batch wait, keeps step_count at its
entry value and blocks download; the docstring now states, as for
FixedPatchStepper, that async faults surface at the single wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:31:09 +00:00
ClaudeandClaude Opus 5.5 3648ad70ef PeriodicBlockStepper.step(n): wait on the stream once per call
Same change as the reviewed FixedPatchStepper end-sync (cherry-picked
ad1d82c+512dcc9 above): the n steps are enqueued in order and the host
waits once; step(0) launches nothing; step_count advances after the wait
and keeps its entry value on failure. Launches, buffer roles and bits are
those of n step(1) calls. Mocked-driver host test covers the single
wait, launch/role equality with n step(1), step(0) and failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:25:16 +00:00
ClaudeandClaude Opus 5.5 5399c9c0a6 step(n) docstring: raw-exchange ledger holds the final interval only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:24:36 +00:00
ClaudeandClaude Opus 5.5 54be33e006 FixedPatchStepper.step(n): wait on the stream once per call
Enqueue all n base intervals in order on the one stream and synchronize
once before returning (step(0): no launch, no wait). step_count, the raw
exchange ledger and an open window are published only after that wait;
any failure leaves step_count at its entry value and invalidates both
ledgers. Async device errors are no longer attributed to an interval;
the docstring says to use step(1) loops to localize one. Launch order and
buffers are unchanged. Adds a mocked-driver host test of the contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:24:36 +00:00
18 changed files with 380 additions and 567 deletions

No files matched your search

-5
View File
@@ -46,9 +46,4 @@ CUDA_VISIBLE_DEVICES=<n> python tests/validation/run_dfg_re20.py --case both \
The uniform D80 control separates method error from refinement error. `--plan` writes `plan.json` only. A GPU run writes `plan.json` and `result.json` The uniform D80 control separates method error from refinement error. `--plan` writes `plan.json` only. A GPU run writes `plan.json` and `result.json`
(per case: lattice parameters, every sample, window ranges, status, verdict). (per case: lattice parameters, every sample, window ranges, status, verdict).
Receipts: `plan.json` and `result.json` carry `schema` 2 and `provenance` (HEAD, src tree,
runner hash; clean checkout required, refused if it changes during the run). Receipts
written before this (schema 1, e.g. evidence 70f6617) are a bare list of cases. Read both
with `run_dfg_re20.read_result(path)` (`["cases"][0]` replaces the old `[0]`); existing
evidence scripts stay valid for schema-1 receipts and need this reader for new ones.
Host contract tests: `tests/unit/test_dfg_re20.py`. Host contract tests: `tests/unit/test_dfg_re20.py`.
+14 -4
View File
@@ -204,11 +204,21 @@ class PeriodicBlockStepper:
raise raise
def step(self, n=1): def step(self, n=1):
"""Advance n steps: halo refresh, then core pull/collide, per step.""" """Advance n steps: halo refresh, then core pull/collide, per step.
All n steps are enqueued in order on the one stream and the host waits
once before returning (same launches and bits as n step(1) calls).
Asynchronous device errors surface at that single wait, so they are not
attributed to the step that caused them; call step(1) repeatedly to
localize one. On any failure the instance is failed and step_count keeps
its entry value.
"""
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0: if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
raise ValueError("n must be a nonnegative integer") raise ValueError("n must be a nonnegative integer")
with self._lock, self.ctx.activate(): with self._lock, self.ctx.activate():
self._require("ready") self._require("ready")
if n == 0:
return
kw = dict(block=self._block, stream=self._stream) kw = dict(block=self._block, stream=self._stream)
try: try:
self._status = "advancing" self._status = "advancing"
@@ -224,9 +234,9 @@ class PeriodicBlockStepper:
self._step(self._current, self._spare, np.uint32(self.layout.halo), self._step(self._current, self._spare, np.uint32(self.layout.halo),
self._tables.get("flags", np.uintp(0)), np.uintp(0), # no kind self._tables.get("flags", np.uintp(0)), np.uintp(0), # no kind
grid=self._grid, **kw) grid=self._grid, **kw)
self._stream.synchronize() self._current, self._spare = self._spare, self._current # host handles only
self._current, self._spare = self._spare, self._current self._stream.synchronize()
self._step_count += 1 self._step_count += n
self._status = "ready" self._status = "ready"
except BaseException: except BaseException:
self._status = "failed" self._status = "failed"
+21 -7
View File
@@ -822,27 +822,41 @@ class FixedPatchStepper:
grid=((pairs + self._block[0] - 1) // self._block[0], 1), **kw) grid=((pairs + self._block[0] - 1) // self._block[0], 1), **kw)
def step(self, n=1): def step(self, n=1):
"""Advance G→G by n base intervals: level l takes 2**l hops per interval.""" """Advance G→G by n base intervals: level l takes 2**l hops per interval.
All n intervals are enqueued in order on the one stream, and the host waits
once, before returning; step(0) launches and waits for nothing. On return
the device work is complete, step_count and an open window cover all n
intervals, and the raw-exchange ledger holds the final interval only.
Asynchronous device errors surface at that single wait, so they are not
attributed to the interval that caused them; call step(1) repeatedly to
localize one (same launches, same bits). On any failure the instance is
failed, the exchange ledger and window are invalid, and step_count keeps
its value from entry to the failing call.
"""
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0: if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
raise ValueError("n must be a nonnegative integer") raise ValueError("n must be a nonnegative integer")
with self._lock, self.ctx.activate(): with self._lock, self.ctx.activate():
self._require("ready") self._require("ready")
if n == 0:
return
root = self._levels[0] root = self._levels[0]
kw = dict(block=self._block, stream=self._stream) kw = dict(block=self._block, stream=self._stream)
try: try:
self._status = "advancing" self._status = "advancing"
self._exchange_valid = False
for _ in range(n): for _ in range(n):
if self._observe_raw_exchange: if self._observe_raw_exchange: # stream-ordered: holds the last interval only
self._exchange_valid = False
for b in self._level_boundaries: for b in self._level_boundaries:
if b is not None: if b is not None:
cuda.memset_d8_async(b.interval_integral, 0, cuda.memset_d8_async(b.interval_integral, 0,
len(b.tables["cell"]) * 32, self._stream) len(b.tables["cell"]) * 32, self._stream)
self._hop(0, root.current, root.spare, **kw) self._hop(0, root.current, root.spare, **kw)
self._stream.synchronize() root.current, root.spare = root.spare, root.current # host handles only
root.current, root.spare = root.spare, root.current self._stream.synchronize()
self._step_count += 1 self._step_count += n
self._exchange_valid = self._observe_raw_exchange self._exchange_valid = self._observe_raw_exchange
self._status = "ready" self._status = "ready"
except BaseException: except BaseException:
self._window_valid = False self._window_valid = False
@@ -1,66 +0,0 @@
"""Checkpoint save -> load round trip for a scene with passive bodies. Requires GPU.
A fresh Simulation with the same scene, restored from the checkpoint must hold the same populations,
flags, body count and step count, and must stay bitwise identical to the original
after both continue for the same number of steps. Checkpoints with Jets are
refused (documented limitation) and are not covered here.
"""
import os
import tempfile
import unittest
import numpy as np
from ._cases import make_sim
# Three separated cylinders (pinball-like); cut-link receivers stay in bulk fluid.
CIRCLES = (((40, 32), 6), ((56, 22), 6), ((56, 42), 6))
def scene():
sim = make_sim()
sim.initialize()
for center, radius in CIRCLES:
sim.add_body("circle", center=center, radius=radius)
sim.sync_bodies()
return sim
class CheckpointBodiesGPU(unittest.TestCase):
def test_round_trip_is_bitwise_and_continues_identically(self):
sim = scene()
sim.run(60)
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as f:
path = f.name
restored = None
try:
saved = sim.save_checkpoint(path)
# The loader requires the same body count and geometry: rebuild the scene.
restored = scene()
restored.load_checkpoint(saved)
self.assertEqual(restored.bodies.count, len(CIRCLES))
self.assertEqual(restored.stepper.step_count, sim.stepper.step_count)
np.testing.assert_array_equal(restored.get_flags(), sim.get_flags())
a, b = sim.get_ddf(), restored.get_ddf()
self.assertEqual(a.dtype, b.dtype)
self.assertEqual(a.tobytes(), b.tobytes())
sim.run(40)
restored.run(40)
self.assertEqual(restored.stepper.step_count, sim.stepper.step_count)
a, b = sim.get_ddf(), restored.get_ddf()
self.assertTrue(np.isfinite(a).all())
self.assertEqual(a.tobytes(), b.tobytes())
for i in range(len(CIRCLES)):
self.assertTrue(np.all(np.isfinite(restored.read_force(i))))
finally:
if restored is not None:
restored.close()
os.unlink(path)
sim.close()
if __name__ == "__main__":
unittest.main()
+104
View File
@@ -0,0 +1,104 @@
"""Host contract of PeriodicBlockStepper.step(n): one stream wait per call (mocked driver)."""
import ast
from contextlib import nullcontext
import importlib.util
from pathlib import Path
import types
from unittest.mock import Mock
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.blocks import BlockLayout2D
def host_module():
"""block_stepper with pycuda/compiler replaced by mocks; no GPU context."""
# Locate the source without importing it: importing block_stepper loads pycuda.driver,
# which CPU-only import-boundary tests (test_build_contracts) must never see.
path = Path(importlib.util.find_spec("CelerisLab.lbm.block_stepper").origin)
tree = ast.parse(path.read_text())
tree.body = [node for node in tree.body if not (
isinstance(node, ast.Import) and any(a.name == "pycuda.driver" for a in node.names)
or isinstance(node, ast.ImportFrom) and node.module == "cuda")]
module = types.ModuleType("CelerisLab.lbm._test_host_block_stepper")
module.__package__ = "CelerisLab.lbm"
module.__dict__.update(cuda=Mock(), compiler=Mock())
exec(compile(tree, str(path), "exec"), module.__dict__)
return module
def backend():
host, log = host_module(), []
host.cuda.mem_get_info.return_value = (1 << 40, 1 << 40)
host.cuda.mem_alloc.side_effect = lambda size: Mock(size=size) # distinct buffers
module = Mock()
module.get_global.return_value = (Mock(), 20)
module.get_function.side_effect = lambda name: Mock(
side_effect=lambda *a, **k: log.append((name, a[:2])))
host.compiler.load_module.return_value = module
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
run = host.PeriodicBlockStepper(cfg, BlockLayout2D.build(16, 16, 8, 8),
context=Mock(activate=nullcontext, sm_arch="sm_70"))
run._stream.synchronize.side_effect = lambda: log.append(("sync", ()))
run.initialize(np.full((9, 16, 16), 1 / 9))
log.clear()
return run, log
class BlockStepSync(unittest.TestCase):
def test_step_n_waits_once_and_matches_n_single_steps(self):
many, log_many = backend()
many.step(3)
single, log_single = backend()
for _ in range(3):
single.step(1)
names = [name for name, _ in log_many]
self.assertEqual(names.count("sync"), 1)
self.assertEqual(names[-1], "sync")
self.assertEqual(names.count("BlockStep"), 3)
# Same launch order and buffer roles as three step(1) calls.
strip = lambda log: [(n, tuple(id(x) for x in a)) for n, a in log if n != "sync"]
roles = lambda run, log: [(n, tuple({id(run._current): "c", id(run._spare): "s"}.get(x, "-")
for x in ids)) for n, ids in strip(log)]
self.assertEqual([n for n, _ in strip(log_many)], [n for n, _ in strip(log_single)])
self.assertEqual(roles(many, log_many), roles(single, log_single))
self.assertIsNot(many._current, many._spare)
self.assertEqual((many.step_count, single.step_count), (3, 3))
def test_step_zero_launches_and_waits_for_nothing(self):
run, log = backend()
run.step(0)
self.assertEqual((log, run.step_count), ([], 0))
def test_failure_keeps_entry_step_count(self):
run, log = backend()
run.step(1)
run._stream.synchronize.side_effect = RuntimeError("device fault")
with self.assertRaises(RuntimeError):
run.step(2)
self.assertEqual((run.step_count, run._status), (1, "failed"))
def test_launch_failure_mid_batch_fails_closed_without_waiting(self):
run, log = backend()
calls = []
step = run._step
def failing(*args, **kwargs):
calls.append(1)
if len(calls) == 2:
raise RuntimeError("launch failure")
return step(*args, **kwargs)
run._step = failing
with self.assertRaises(RuntimeError):
run.step(3)
self.assertNotIn("sync", [name for name, _ in log])
self.assertEqual((run.step_count, run._status), (0, "failed"))
with self.assertRaises(RuntimeError):
run.download()
if __name__ == "__main__":
unittest.main()
-54
View File
@@ -75,59 +75,5 @@ class Re20Contract(unittest.TestCase):
self.assertEqual((out["status"], out["verdict"]), ("INCOMPLETE", None)) self.assertEqual((out["status"], out["verdict"]), ("INCOMPLETE", None))
class Re20Provenance(unittest.TestCase):
def test_records_current_checkout(self):
record = v.provenance(rr.DEFAULT_SOURCE)
self.assertEqual(record["source_mode"], "current_checkout")
self.assertEqual(len(record["checkout_revision"]), 40)
self.assertEqual(len(record["src_tree"]), 40)
self.assertEqual(len(record["runner_sha256"]), 64)
self.assertFalse(record["checkout_dirty"])
self.assertEqual(v.RESULT_SCHEMA, 2)
def test_reader_accepts_both_schemas_and_rejects_unknown(self):
import json
import tempfile
from pathlib import Path
case = dict(case="refined", status="NOT_STEADY", verdict=None, rows=[])
with tempfile.TemporaryDirectory() as d:
old, new, bad = Path(d, "old.json"), Path(d, "new.json"), Path(d, "bad.json")
old.write_text(json.dumps([case]))
new.write_text(json.dumps(dict(schema=2, provenance=dict(src_tree="a" * 40),
cases=[case])))
bad.write_text(json.dumps(dict(schema=3, cases=[case])))
self.assertEqual(v.read_result(old), dict(schema=1, provenance=None, cases=[case]))
self.assertEqual(v.read_result(new)["cases"][0], case)
self.assertEqual(v.read_result(new)["provenance"]["src_tree"], "a" * 40)
with self.assertRaisesRegex(ValueError, "unknown DFG Re20 result schema"):
v.read_result(bad)
def test_dirty_checkout_is_refused(self):
from unittest.mock import patch
# Three git calls in order: HEAD:src, HEAD, status --porcelain.
with patch("subprocess.check_output",
side_effect=["e" * 40 + "\n", "f" * 40 + "\n", " M src/x.py\n"]) as git:
with self.assertRaisesRegex(RuntimeError, "clean checkout"):
v.provenance(rr.DEFAULT_SOURCE)
self.assertEqual(git.call_count, 3)
self.assertEqual([c.args[0][-1] for c in git.call_args_list[:2]], ["HEAD:src", "HEAD"])
self.assertIn("--porcelain", git.call_args_list[2].args[0])
def test_output_inside_checkout_is_refused_before_any_work(self):
import tempfile
from pathlib import Path
from unittest.mock import patch
source = rr.DEFAULT_SOURCE.resolve()
for output in (source, source / "re20-out", source / "src" / "x"):
argv = ["run_dfg_re20.py", "--plan", "--source", str(source), "--output", str(output)]
with patch("sys.argv", argv), \
patch.object(v, "provenance", side_effect=AssertionError("work started")):
with self.assertRaisesRegex(SystemExit, "outside the source checkout"):
v.main()
self.assertFalse((source / "re20-out").exists())
with tempfile.TemporaryDirectory() as d:
v.reject_output_inside(Path(d) / "out", source) # outside: accepted
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -28
View File
@@ -166,34 +166,7 @@ class Host(unittest.TestCase):
self.assertEqual(a["steps"] * 4, b["steps"]) self.assertEqual(a["steps"] * 4, b["steps"])
def test_source_pin(self): def test_source_pin(self):
"""Historical integrity: the manifest matches its pinned commit, read with git.""" verify(REPO)
from run_jet_circle_cv import verify_historical
from source_provenance import HistoryUnavailable, history_unavailable
try:
verify_historical(REPO)
except HistoryUnavailable as error:
history_unavailable(self, error)
def test_pinned_mode_still_rejects_changed_source(self):
"""Negative control: the frozen-source guard refuses a tree that differs from PIN."""
import json
import run_jet_circle_cv as v
manifest = json.loads((v.HERE / "data/jet_circle_cv/source_manifest.json").read_text())
import hashlib
current = {f: hashlib.sha256((REPO / f).read_bytes()).hexdigest()
for f in manifest["files"] if (REPO / f).exists()}
if current == manifest["files"]:
self.skipTest("working tree equals PIN; no changed source to reject")
with self.assertRaisesRegex(RuntimeError, "Source hash mismatch"):
verify(REPO)
def test_current_checkout_provenance(self):
from run_jet_circle_cv import current_checkout
record = current_checkout(REPO)
self.assertEqual(record["source_mode"], "current_checkout")
self.assertIn("not the frozen", record["source_capture"])
self.assertEqual(len(record["src_tree"]), 40)
self.assertTrue(record["src_sha256"])
if __name__ == "__main__": if __name__ == "__main__":
+21 -43
View File
@@ -13,15 +13,8 @@ from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER
from source_provenance import (HistoryUnavailable, historical_files, historical_sha256,
historical_tree, history_unavailable)
ROOT = HERE.parents[1] ROOT = HERE.parents[1]
A135252 = "a135252ae78b47b57d7e77d19001700b3bedac8b"
def a135252_pins():
return json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
class GuardTests(unittest.TestCase): class GuardTests(unittest.TestCase):
@@ -48,35 +41,20 @@ class GuardTests(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "velocity/profile"): with self.assertRaisesRegex(RuntimeError, "velocity/profile"):
decode_macros(invalid, w, cx, cy) decode_macros(invalid, w, cx, cy)
def materialize(self, root): def test_current_corrected_pins(self):
"""The pinned a135252 tree, read with git (the guard's own qualification).""" self.assertEqual(
try: source_guard(ROOT, "corrected", "a135252")["base_commit"],
historical_files(ROOT, A135252, a135252_pins()["sources"], root) "a135252ae78b47b57d7e77d19001700b3bedac8b",
except HistoryUnavailable as error: )
history_unavailable(self, error)
def test_a135252_pins_qualify_their_own_commit(self):
with TemporaryDirectory() as directory:
root = Path(directory)
self.materialize(root)
with patch("run_uniform_les_flow.subprocess.check_output",
return_value="isolated-fixture\n"):
self.assertEqual(source_guard(root, "corrected", "a135252")["base_commit"], A135252)
def test_current_tree_is_not_the_a135252_qualification(self):
"""Explicit record: the guard refuses today's src wherever it differs from the pin."""
pins = a135252_pins()["sources"]
changed = sorted(n for n, sha in pins.items() if (ROOT / n).exists()
and hashlib.sha256((ROOT / n).read_bytes()).hexdigest() != sha)
if not changed:
self.skipTest("current src equals the a135252 pin")
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(ROOT, "corrected", "a135252")
def test_corrected_and_tamper_guard(self): def test_corrected_and_tamper_guard(self):
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
with TemporaryDirectory() as directory: with TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)
self.materialize(root) for name in pins["sources"]:
dest = root / name
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / name, dest)
with patch( with patch(
"run_uniform_les_flow.subprocess.check_output", "run_uniform_les_flow.subprocess.check_output",
return_value="isolated-fixture\n", return_value="isolated-fixture\n",
@@ -107,25 +85,25 @@ class GuardTests(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"): with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(ROOT, "corrected") # Omission still uses the historical profile. source_guard(ROOT, "corrected") # Omission still uses the historical profile.
def test_manifest_pins_the_a135252_corrected_tree(self): def test_current_manifest_pins_actual_corrected_tree(self):
pins = a135252_pins() pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
self.assertEqual(pins["base_commit"], A135252) tracked = subprocess.check_output(["git", "-C", str(ROOT), "ls-files", "src"], text=True)
self.assertEqual(set(pins["sources"]), set(tracked.splitlines()))
for name, expected in pins["sources"].items():
self.assertEqual(hashlib.sha256((ROOT / name).read_bytes()).hexdigest(), expected)
self.assertEqual(pins["sources"][HEADER], pins["candidate_header_sha256"]) self.assertEqual(pins["sources"][HEADER], pins["candidate_header_sha256"])
self.assertNotEqual(pins["sources"][HEADER], pins["baseline_header_sha256"]) self.assertNotEqual(pins["sources"][HEADER], pins["baseline_header_sha256"])
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"): with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(ROOT, "baseline", "a135252") source_guard(ROOT, "baseline", "a135252")
try:
names = historical_tree(ROOT, A135252)
actual = historical_sha256(ROOT, A135252, pins["sources"])
except HistoryUnavailable as error:
history_unavailable(self, error)
self.assertEqual(set(pins["sources"]), set(names))
self.assertEqual(actual, pins["sources"])
def test_named_baseline_accepts_only_reversed_header(self): def test_named_baseline_accepts_only_reversed_header(self):
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
with TemporaryDirectory() as directory: with TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)
self.materialize(root) for name in pins["sources"]:
dest = root / name
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / name, dest)
subprocess.run(["git", "apply", "--reverse", str(HERE / "les/data/smagorinsky_normalization.patch")], cwd=root, check=True) subprocess.run(["git", "apply", "--reverse", str(HERE / "les/data/smagorinsky_normalization.patch")], cwd=root, check=True)
with patch("run_uniform_les_flow.subprocess.check_output", return_value="baseline-fixture\n"): with patch("run_uniform_les_flow.subprocess.check_output", return_value="baseline-fixture\n"):
source_guard(root, "baseline", "a135252") source_guard(root, "baseline", "a135252")
+3 -21
View File
@@ -62,31 +62,13 @@ def test_periodic_synthetic():
assert not p.assess(rows()[:100])["periodic"] assert not p.assess(rows()[:100])["periodic"]
def test_manifest_matches_its_pinned_commit(): def test_production_unmodified():
"""Historical integrity only: the manifest describes the h.PIN tree (read with git)."""
m = json.loads( m = json.loads(
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text() (ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
) )
assert m["commit"] == h.PIN assert m["commit"] == h.PIN
import unittest for name, digest in m["production_sha256"].items():
from source_provenance import HistoryUnavailable, history_unavailable, verify_manifest assert hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == digest, name
try:
verify_manifest(ROOT, m["commit"], m["production_sha256"])
except HistoryUnavailable as error:
history_unavailable(unittest.TestCase(), error)
def test_current_src_is_not_the_pinned_qualification():
"""Explicit record: today's src differs from h.PIN, so its qualification does not carry over."""
m = json.loads(
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
)
changed = sorted(n for n, d in m["production_sha256"].items()
if not (ROOT / n).exists()
or hashlib.sha256((ROOT / n).read_bytes()).hexdigest() != d)
if not changed:
pytest.skip("current src equals the pinned tree")
assert changed # e.g. src/CelerisLab/lbm/channel.py since 47b05bc
def test_reference(): def test_reference():
+11 -29
View File
@@ -12,33 +12,16 @@ import run_refined_dfg as r
h, periodic = r.helpers(r.DEFAULT_SOURCE) h, periodic = r.helpers(r.DEFAULT_SOURCE)
def historical_manifest(collision="SRT", observation_mode="last_base_step"):
"""The manifest verify_source would select, without hashing the working tree.
Receipt/plan logic tests use it; source integrity is checked once in test_source_pin.
"""
name = ("source_manifest_window.json" if observation_mode == "window" else
"source_manifest.json" if collision == "SRT" else "source_manifest_mrt.json")
return json.loads((r.HERE / "data/refined_dfg" / name).read_text())
def test_source_pin(): def test_source_pin():
historical = historical_manifest("SRT") historical = json.loads((r.HERE / "data/refined_dfg/source_manifest.json").read_text())
assert historical["commit"].startswith("cd3a657") assert historical["commit"].startswith("cd3a657")
mrt_history = historical_manifest("MRT") mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
assert mrt_history["commit"].startswith("3ea131e") assert mrt_history["commit"].startswith("3ea131e")
assert historical_manifest("MRT", "window")["commit"].startswith("a233c90") assert r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")["commit"].startswith("a233c90")
# Negative controls first, so missing history can never hide them: every pinned with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
# mode refuses a working tree that differs from its pin. r.verify_source(r.DEFAULT_SOURCE, "MRT")
for collision, mode in (("MRT", "last_base_step"), ("SRT", "last_base_step"), ("MRT", "window")): with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"): r.verify_source(r.DEFAULT_SOURCE, "SRT")
r.verify_source(r.DEFAULT_SOURCE, collision, mode)
from source_provenance import HistoryUnavailable, history_unavailable, verify_manifest
for manifest in (historical, mrt_history, historical_manifest("MRT", "window")):
try: # historical integrity: each manifest matches the tree of its own commit
verify_manifest(r.DEFAULT_SOURCE, manifest["commit"], manifest["sha256"])
except HistoryUnavailable as error:
history_unavailable(unittest.TestCase(), error)
def test_matched_units_and_planes(): def test_matched_units_and_planes():
@@ -160,13 +143,12 @@ def test_axis_specific_sampling_phase():
def test_full_requires_completed_pilot(): def test_full_requires_completed_pilot():
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"): with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
r.verify_pilot(None, historical_manifest("MRT", "window")) r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"): def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"):
"""Synthetic contract fixture only; never a physical result or run receipt.""" """Synthetic contract fixture only; never a physical result or run receipt."""
manifest = ((historical_manifest("MRT", "window") if jet_scenario == "none" else manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario)
r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario))
if observation_mode == "window" else if observation_mode == "window" else
json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())) json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text()))
selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement) selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement)
@@ -366,7 +348,7 @@ def test_record_actual_checkout_separately():
actual = checkout_provenance(r.DEFAULT_SOURCE) actual = checkout_provenance(r.DEFAULT_SOURCE)
assert len(actual["checkout_revision"]) == 40 assert len(actual["checkout_revision"]) == 40
assert isinstance(actual["checkout_dirty"], bool) assert isinstance(actual["checkout_dirty"], bool)
assert historical_manifest("MRT", "window")["production_origin_commit"].startswith("d930") assert r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")["production_origin_commit"].startswith("d930")
def test_window_plan_preserves_physics(): def test_window_plan_preserves_physics():
@@ -376,7 +358,7 @@ def test_window_plan_preserves_physics():
assert baseline.pop("observation_mode") == "last_base_step" assert baseline.pop("observation_mode") == "last_base_step"
assert window.pop("observation_mode") == "window" assert window.pop("observation_mode") == "window"
assert baseline == window assert baseline == window
assert historical_manifest(collision, "window")["observation_mode"] == "window" assert r.verify_source(r.DEFAULT_SOURCE, collision, "window")["observation_mode"] == "window"
with unittest.TestCase().assertRaises(ValueError): with unittest.TestCase().assertRaises(ValueError):
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown") r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
+170
View File
@@ -0,0 +1,170 @@
"""Host contract of FixedPatchStepper.step(n): one stream wait per call (mocked driver).
All n base intervals are enqueued in order on the backend stream and the host
waits once before step returns. Launch order and buffer arguments must equal n
step(1) calls; completion metadata, raw-exchange and window ledgers, and failure
invalidation are published only after that wait.
"""
from contextlib import nullcontext
from unittest.mock import Mock
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from tests.unit.test_raw_exchange import body
from tests.unit.test_refinement_three_level_multipatch import host_module
BODY_LEVEL, HOPS = 3, 8 # body on L3: 2**3 hops per base interval
def backend(window=False):
"""Four-level host backend; every launch, memset and stream wait goes to one log."""
host = host_module()
log, allocations = [], []
def allocate(size):
allocations.append(Mock(size=size))
return allocations[-1]
module = Mock()
module.get_global.return_value = (Mock(), 20)
module.get_function.side_effect = lambda name: Mock(
side_effect=lambda *a, **k: log.append((name, a, k)))
host.compiler.load_module.return_value = module
host.cuda.mem_alloc.side_effect = allocate
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
host.cuda.memset_d8_async.side_effect = lambda *a: log.append(("memset", a, {}))
run = host.FixedPatchStepper(
LBMConfig(nx=48, ny=48, viscosity=.02), context=Mock(activate=nullcontext, sm_arch="sm_70"),
refinements=[[(6, 6, 26, 26)], [(20, 20, 44, 44)], [(56, 56, 72, 72)]],
level_blocks=[(4, 4)] * 4, level_objects=[(), (), (), (body(),)],
observe_raw_exchange=True, observe_window_exchange=window)
run._stream.synchronize.side_effect = lambda: log.append(("sync", (), {}))
run.initialize(*[np.full(level.shape, 1 / 9, np.float32) for level in run._levels])
log.clear()
return run, log, allocations
def signature(log, allocations):
"""Event names with device-pointer arguments replaced by allocation index."""
index = {id(a): i for i, a in enumerate(allocations)}
return [(name, tuple(index.get(id(a), "-") for a in args)) for name, args, _ in log]
def names(log):
return [name for name, _, _ in log]
class StepSync(unittest.TestCase):
def test_step1_waits_once_after_all_launches(self):
run, log, _ = backend()
run.step(1)
self.assertEqual(names(log).count("sync"), 1)
self.assertEqual(names(log)[-1], "sync")
self.assertEqual(names(log)[0], "memset")
self.assertEqual(names(log).count("BlockStep"), 1 + 2 + 4 + 8)
self.assertEqual(run.step_count, 1)
run.close()
def test_step_n_equals_n_step1_launches_with_one_wait(self):
single, single_log, single_alloc = backend()
for _ in range(3):
single.step(1)
batched, batched_log, batched_alloc = backend()
batched.step(3)
expected = [e for e in signature(single_log, single_alloc) if e[0] != "sync"]
got = signature(batched_log, batched_alloc)
self.assertEqual(got[-1][0], "sync")
self.assertEqual(names(batched_log).count("sync"), 1)
self.assertEqual(got[:-1], expected) # same order, same ping-pong buffers
self.assertEqual(names(batched_log).count("memset"), 3)
self.assertEqual(batched.step_count, single.step_count)
index = [{id(a): i for i, a in enumerate(alloc)} for alloc in (single_alloc, batched_alloc)]
self.assertEqual(index[1][id(batched._levels[0].current)],
index[0][id(single._levels[0].current)])
single.close()
batched.close()
def test_step0_launches_and_waits_for_nothing(self):
run, log, _ = backend()
run.step(2)
log.clear()
run.step(0)
self.assertEqual(log, [])
self.assertEqual(run.step_count, 2)
value = run.read_raw_exchange(0, level=BODY_LEVEL) # step(0) keeps the ledger valid
self.assertEqual((value["time_start"], value["time_end"]), (1, 2))
run.close()
def test_completion_metadata_covers_all_intervals(self):
run, _, _ = backend()
with self.assertRaisesRegex(RuntimeError, "completed"):
run.read_raw_exchange(0, level=BODY_LEVEL)
run.step(3)
value = run.read_raw_exchange(0, level=BODY_LEVEL)
self.assertEqual((value["time_start"], value["time_end"], value["sample_count"]),
(2, 3, HOPS)) # step(n) retains only the final base interval
run.step(2)
self.assertEqual(run.read_raw_exchange(0, level=BODY_LEVEL)["time_end"], 5)
run.close()
def test_window_accounting_across_batched_calls(self):
windows = []
for calls in ((1, 3), (4,), (1, 1, 1, 1)):
run, log, _ = backend(window=True)
run.step(1)
run.start_exchange_window()
log.clear()
for n in calls:
run.step(n)
self.assertEqual(names(log).count("BlockAccumulateExchange"), 2 * 4 * HOPS)
self.assertEqual(names(log).count("sync"), len(calls))
out = run.read_exchange_window(0, level=BODY_LEVEL)
windows.append((out["time_start"], out["time_end"], out["sample_count"]))
run.close()
self.assertEqual(windows, [(1., 5, 4 * HOPS)] * 3)
def test_failed_wait_publishes_nothing(self):
run, _, _ = backend(window=True)
run.step(2)
run.start_exchange_window()
run._stream.synchronize.side_effect = RuntimeError("device failure")
with self.assertRaisesRegex(RuntimeError, "device failure"):
run.step(3)
self.assertEqual(run.step_count, 2) # value at entry to the failing call
self.assertFalse(run._exchange_valid)
self.assertFalse(run._window_valid)
for read in (lambda: run.read_raw_exchange(0, level=BODY_LEVEL),
lambda: run.read_exchange_window(0, level=BODY_LEVEL), run.download,
lambda: run.step(1)):
with self.assertRaisesRegex(RuntimeError, "failed"):
read()
run._stream.synchronize.side_effect = None
run.close()
def test_launch_error_mid_batch_skips_wait_and_fails(self):
run, log, _ = backend()
run.step(1)
log.clear()
hop, calls = run._hop, []
def failing(index, *args, **kw):
if index == 0:
calls.append(1)
if len(calls) == 2:
raise RuntimeError("launch failure")
return hop(index, *args, **kw)
run._hop = failing
with self.assertRaisesRegex(RuntimeError, "launch failure"):
run.step(3)
self.assertNotIn("sync", names(log))
self.assertEqual(run.step_count, 1)
self.assertFalse(run._exchange_valid)
with self.assertRaisesRegex(RuntimeError, "failed"):
run.read_raw_exchange(0, level=BODY_LEVEL)
run.close()
if __name__ == "__main__":
unittest.main()
+10 -49
View File
@@ -8,19 +8,17 @@ import sys
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tests/validation")) sys.path.insert(0, str(ROOT / "tests/validation"))
import pytest from source_provenance import checkout_provenance
from source_provenance import (HistoryUnavailable, checkout_provenance, historical_sha256,
historical_tree, history_unavailable, verify_manifest)
import unittest
import re100_helpers import re100_helpers
import run_jet_circle_cv import run_jet_circle_cv
ORIGIN = "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5" ORIGIN = "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
def test_manifests_match_their_recorded_source_and_preserve_origin(): def test_manifests_cover_current_source_and_preserve_origin():
"""Historical evidence integrity: each manifest describes the tree of its own commit.""" names = subprocess.check_output(
manifests = [] ["git", "-C", str(ROOT), "ls-files", "src"], text=True
).splitlines()
for group, hashes_key, revision_key in ( for group, hashes_key, revision_key in (
("dfg_re100", "production_sha256", "commit"), ("dfg_re100", "production_sha256", "commit"),
("jet_circle_cv", "files", "revision"), ("jet_circle_cv", "files", "revision"),
@@ -30,22 +28,18 @@ def test_manifests_match_their_recorded_source_and_preserve_origin():
ROOT / "tests/validation/data" / group / "source_manifest.json" ROOT / "tests/validation/data" / group / "source_manifest.json"
).read_text() ).read_text()
) )
# Structural checks need no history and always run.
assert manifest[revision_key] == ORIGIN assert manifest[revision_key] == ORIGIN
assert set(manifest[hashes_key]) == set(names)
assert manifest["source_origin_commits"][-1] == ORIGIN assert manifest["source_origin_commits"][-1] == ORIGIN
assert ( assert (
manifest["historical_checkpoints"]["production"] manifest["historical_checkpoints"]["production"]
== "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2" == "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
) )
assert "not inherited from d79" in manifest["experiment"] assert "not inherited from d79" in manifest["experiment"]
manifests.append(manifest[hashes_key]) for name, expected in manifest[hashes_key].items():
try: assert (
names = historical_tree(ROOT, ORIGIN) hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == expected
for digests in manifests: ), name
assert set(digests) == set(names)
verify_manifest(ROOT, ORIGIN, digests)
except HistoryUnavailable as error:
history_unavailable(unittest.TestCase(), error)
assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN
@@ -78,36 +72,3 @@ def test_runtime_checkout_identity_is_separate_from_source_origin(tmp_path):
assert checkout_provenance(tmp_path) == dict( assert checkout_provenance(tmp_path) == dict(
checkout_revision=expected, checkout_dirty=True checkout_revision=expected, checkout_dirty=True
) )
def test_historical_check_rejects_tampering_and_reports_missing_history(tmp_path):
with pytest.raises(HistoryUnavailable):
historical_sha256(tmp_path, ORIGIN, ["src/x.py"]) # not a repository
with pytest.raises(HistoryUnavailable):
historical_sha256(ROOT, "0" * 40, ["src/x.py"]) # commit absent
manifest = json.loads(
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text())
digests = dict(manifest["production_sha256"])
name = sorted(digests)[0]
tampered = dict(digests, **{name: ("0" if digests[name][0] != "0" else "1") + digests[name][1:]})
try:
historical_tree(ROOT, ORIGIN)
except HistoryUnavailable as error:
history_unavailable(unittest.TestCase(), error)
with pytest.raises(RuntimeError, match="Historical manifest mismatch"):
verify_manifest(ROOT, ORIGIN, tampered) # one flipped digest is caught
with pytest.raises(ValueError, match="not part of"):
historical_sha256(ROOT, ORIGIN, ["src/CelerisLab/does_not_exist.py"])
def test_historical_files_reports_unreadable_blob_as_history_unavailable(tmp_path):
"""A blob missing from the clone is HistoryUnavailable, not a raw CalledProcessError."""
import source_provenance
from unittest.mock import patch
missing = subprocess.CompletedProcess(["git", "show"], 128, b"", b"fatal: missing blob")
with patch.object(source_provenance, "historical_sha256", return_value={"src/a.py": "0" * 64}), \
patch.object(source_provenance.subprocess, "run", return_value=missing):
with pytest.raises(HistoryUnavailable, match="blob not in this clone"):
source_provenance.historical_files(ROOT, ORIGIN, ["src/a.py"], tmp_path)
assert not (tmp_path / "src/a.py").exists()
+2 -47
View File
@@ -57,8 +57,6 @@ WINDOW_TSTAR = 10.0 # trailing 5 s, 21 samples
BANDS = dict(cd=(5.57, 5.59), cl=(0.0104, 0.0110), p_diff=(0.1172, 0.1176)) BANDS = dict(cd=(5.57, 5.59), cl=(0.0104, 0.0110), p_diff=(0.1172, 0.1176))
STEADY_RANGE = {k: (hi - lo) / 10 for k, (lo, hi) in BANDS.items()} STEADY_RANGE = {k: (hi - lo) / 10 for k, (lo, hi) in BANDS.items()}
STEADY_SLOPE = {k: (hi - lo) / 50 for k, (lo, hi) in BANDS.items()} STEADY_SLOPE = {k: (hi - lo) / 50 for k, (lo, hi) in BANDS.items()}
# 2: {schema, provenance, cases}; the earlier receipts (schema 1) are a bare list of cases.
RESULT_SCHEMA = 2
DEVIATIONS = ("pressure-NEQ outlet (neq_extrap), not do-nothing", "local Zou-He inlet") DEVIATIONS = ("pressure-NEQ outlet (neq_extrap), not do-nothing", "local Zou-He inlet")
_re100_config = rr.config _re100_config = rr.config
@@ -108,21 +106,6 @@ def summarize(rows):
verdict=inside) verdict=inside)
def read_result(path):
"""Cases and provenance from a result.json of either schema.
Schema 1 (receipts before RESULT_SCHEMA=2, e.g. evidence 70f6617) is a bare list of
cases without provenance; schema 2 is {schema, provenance, cases}. Readers that
indexed [0] on schema 1 must use read_result(path)["cases"][0].
"""
data = json.loads(Path(path).read_text())
if isinstance(data, list):
return dict(schema=1, provenance=None, cases=data)
if not isinstance(data, dict) or data.get("schema") != RESULT_SCHEMA:
raise ValueError(f"unknown DFG Re20 result schema: {data.get('schema') if isinstance(data, dict) else type(data).__name__}")
return dict(schema=RESULT_SCHEMA, provenance=data["provenance"], cases=data["cases"])
def healthy(row): def healthy(row):
return row["max_mach"] <= 0.2 and row["rho_min"] >= 0.5 and row["rho_max"] <= 1.5 return row["max_mach"] <= 0.2 and row["rho_min"] >= 0.5 and row["rho_max"] <= 1.5
@@ -211,27 +194,6 @@ def plan(collision="SRT"):
label="compared against the official reference bands (non-canonical neq_extrap outlet, zou_he_local inlet); not a full benchmark certification") label="compared against the official reference bands (non-canonical neq_extrap outlet, zou_he_local inlet); not a full benchmark certification")
def provenance(source):
"""Current checkout identity: HEAD, dirty flag, src tree and this runner's hash."""
import hashlib
import subprocess
from source_provenance import checkout_provenance
tree = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD:src"],
text=True).strip()
record = dict(source_mode="current_checkout", **checkout_provenance(source), src_tree=tree,
runner_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
if record["checkout_dirty"]: # HEAD would not describe the code that runs
raise RuntimeError("DFG Re20 runs require a clean checkout")
return record
def reject_output_inside(output, source):
"""Receipts inside the checkout would make it dirty and change the recorded source."""
output = Path(output).resolve()
if output == source or source in output.parents:
raise SystemExit(f"--output must be outside the source checkout {source}")
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--case", choices=["refined", "uniform", "both"], default="refined") parser.add_argument("--case", choices=["refined", "uniform", "both"], default="refined")
@@ -242,10 +204,8 @@ def main():
parser.add_argument("--max-wall-seconds", type=float) parser.add_argument("--max-wall-seconds", type=float)
a = parser.parse_args() a = parser.parse_args()
a.source = a.source.resolve() a.source = a.source.resolve()
reject_output_inside(a.output, a.source)
record = dict(plan(a.collision), schema=RESULT_SCHEMA, provenance=provenance(a.source))
a.output.mkdir(parents=True, exist_ok=False) a.output.mkdir(parents=True, exist_ok=False)
(a.output / "plan.json").write_text(json.dumps(record, indent=2, allow_nan=False)) (a.output / "plan.json").write_text(json.dumps(plan(a.collision), indent=2, allow_nan=False))
if a.plan: if a.plan:
return return
if not a.max_wall_seconds or a.max_wall_seconds <= 0 or not os.environ.get("CUDA_VISIBLE_DEVICES"): if not a.max_wall_seconds or a.max_wall_seconds <= 0 or not os.environ.get("CUDA_VISIBLE_DEVICES"):
@@ -255,12 +215,7 @@ def main():
a.deadline = time.monotonic() + a.max_wall_seconds a.deadline = time.monotonic() + a.max_wall_seconds
cases = ["refined", "uniform"] if a.case == "both" else [a.case] cases = ["refined", "uniform"] if a.case == "both" else [a.case]
results = [run_case(a, h, case) for case in cases] results = [run_case(a, h, case) for case in cases]
after = provenance(a.source) (a.output / "result.json").write_text(json.dumps(results, indent=2, allow_nan=False))
if after != record["provenance"]:
raise RuntimeError("Source checkout changed during the run")
(a.output / "result.json").write_text(json.dumps(dict(schema=RESULT_SCHEMA, provenance=after,
cases=results),
indent=2, allow_nan=False))
if __name__ == "__main__": if __name__ == "__main__":
+7 -40
View File
@@ -3,7 +3,6 @@
import argparse import argparse
import hashlib import hashlib
import subprocess
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -221,29 +220,6 @@ def verify(repo):
return manifest return manifest
def verify_historical(repo):
"""The pinned manifest describes the PIN tree (read with git, not the working tree)."""
from source_provenance import verify_manifest
manifest = json.loads((HERE / "data/jet_circle_cv/source_manifest.json").read_text())
if manifest["revision"] != PIN:
raise RuntimeError("Wrong pinned revision")
verify_manifest(repo, PIN, manifest["files"])
return manifest
def current_checkout(repo):
"""Provenance for a run on the current checkout; never labelled as the pinned source."""
files = subprocess.check_output(["git", "-C", str(repo), "ls-files", "src"],
text=True).splitlines()
return dict(source_mode="current_checkout",
source_capture="current checkout, not the frozen pinned qualification",
source_revision=checkout_provenance(repo)["checkout_revision"],
src_tree=subprocess.check_output(["git", "-C", str(repo), "rev-parse",
"HEAD:src"], text=True).strip(),
src_sha256={f: hashlib.sha256((repo / f).read_bytes()).hexdigest()
for f in files})
def run_case(c, ctx, output, provenance, continuation=False): def run_case(c, ctx, output, provenance, continuation=False):
from CelerisLab.body.geometry.circle import CircleGeometry from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject from CelerisLab.body.objects import SimObject
@@ -554,7 +530,8 @@ def run_case(c, ctx, output, provenance, continuation=False):
recorder.flush() recorder.flush()
payload = dict( payload = dict(
case=c, case=c,
**{"source_revision": PIN, **provenance}, source_revision=PIN,
**provenance,
status="PILOT_DIAGNOSTIC_ONLY", status="PILOT_DIAGNOSTIC_ONLY",
windows=rows, windows=rows,
physics_validation_pass=False, physics_validation_pass=False,
@@ -594,9 +571,6 @@ def main():
help="Fixed measurement campaign: Re100, scale1/2, 3200 reference steps", help="Fixed measurement campaign: Re100, scale1/2, 3200 reference steps",
) )
p.add_argument("--plan", action="store_true") p.add_argument("--plan", action="store_true")
p.add_argument("--current-checkout", action="store_true",
help="Run on the current clean checkout and record its provenance "
"instead of requiring the frozen pinned source")
a = p.parse_args() a = p.parse_args()
if a.steps <= 0 or a.steps > 3200: if a.steps <= 0 or a.steps > 3200:
p.error("steps must be 1..3200") p.error("steps must be 1..3200")
@@ -621,8 +595,7 @@ def main():
) )
plan = dict( plan = dict(
case=c, case=c,
required_revision=None if a.current_checkout else PIN, required_revision=PIN,
source_mode="current_checkout" if a.current_checkout else "pinned",
required_free_device_mib=1024, required_free_device_mib=1024,
estimated_peak_device_mib=128 * a.scale**2, estimated_peak_device_mib=128 * a.scale**2,
estimated_output_mib=64 if a.continuation else 2, estimated_output_mib=64 if a.continuation else 2,
@@ -638,16 +611,10 @@ def main():
): ):
p.error("Root must select one verified idle GPU via CUDA_VISIBLE_DEVICES") p.error("Root must select one verified idle GPU via CUDA_VISIBLE_DEVICES")
repo = a.repo.resolve() repo = a.repo.resolve()
if a.current_checkout: source = verify(repo)
provenance = dict(**checkout_provenance(repo), **current_checkout(repo)) provenance = dict(
if provenance["checkout_dirty"]: source_experiment=source["experiment"], **checkout_provenance(repo)
raise RuntimeError("Current-checkout runs require a clean checkout") )
else:
source = verify(repo)
provenance = dict(
source_experiment=source["experiment"], source_mode="pinned",
**checkout_provenance(repo)
)
sys.path.insert(0, str(repo / "src")) sys.path.insert(0, str(repo / "src"))
import CelerisLab import CelerisLab
-71
View File
@@ -13,74 +13,3 @@ def checkout_provenance(repo):
text=True, text=True,
) )
return dict(checkout_revision=revision, checkout_dirty=bool(dirty)) return dict(checkout_revision=revision, checkout_dirty=bool(dirty))
class HistoryUnavailable(RuntimeError):
"""The clone cannot show the recorded commit (no git, shallow clone, installed copy)."""
def historical_sha256(repo, commit, paths):
"""SHA-256 of each path as stored in `commit`, read with git, never from the working tree.
Raises HistoryUnavailable when git or the commit object is missing, so callers can
report an unchecked integrity record explicitly instead of passing or failing silently.
"""
import hashlib
git = ["git", "-C", str(repo)]
try:
subprocess.run([*git, "cat-file", "-e", f"{commit}^{{commit}}"], check=True,
capture_output=True)
except (OSError, subprocess.CalledProcessError) as error:
raise HistoryUnavailable(f"commit {commit} is not available in {repo}") from error
digests = {}
for path in paths:
listed = subprocess.run([*git, "ls-tree", "--name-only", commit, "--", path],
capture_output=True, text=True)
if listed.returncode:
raise HistoryUnavailable(f"tree of {commit} is not available in {repo}")
if listed.stdout.strip() != path:
raise ValueError(f"{path} is not part of {commit}")
shown = subprocess.run([*git, "show", f"{commit}:{path}"], capture_output=True)
if shown.returncode: # e.g. a blobless partial clone without network access
raise HistoryUnavailable(f"content of {path} at {commit} is not available in {repo}")
digests[path] = hashlib.sha256(shown.stdout).hexdigest()
return digests
def verify_manifest(repo, commit, digests):
"""Raise RuntimeError naming the first path whose recorded digest differs at `commit`."""
actual = historical_sha256(repo, commit, digests)
for path, digest in digests.items():
if actual[path] != digest:
raise RuntimeError(f"Historical manifest mismatch at {commit}: {path}")
return actual
def historical_tree(repo, commit, prefix="src"):
"""Tracked file names under `prefix` at `commit` (HistoryUnavailable if missing)."""
historical_sha256(repo, commit, [])
return subprocess.check_output(["git", "-C", str(repo), "ls-tree", "-r", "--name-only",
commit, "--", prefix], text=True).splitlines()
def historical_files(repo, commit, paths, dest):
"""Write each path as stored in `commit` under `dest` (HistoryUnavailable if missing)."""
from pathlib import Path
historical_sha256(repo, commit, paths) # HistoryUnavailable / ValueError before writing
for path in paths:
shown = subprocess.run(["git", "-C", str(repo), "show", f"{commit}:{path}"],
capture_output=True)
if shown.returncode:
raise HistoryUnavailable(f"cannot read {commit}:{path} (blob not in this clone)")
target = Path(dest) / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(shown.stdout)
def history_unavailable(testcase, error):
"""Skip with the reason, or fail when CELERIS_REQUIRE_HISTORY=1 (release gating)."""
import os
message = f"historical integrity NOT checked: {error}"
if os.environ.get("CELERIS_REQUIRE_HISTORY") == "1":
raise AssertionError(message)
testcase.skipTest(message)
@@ -73,10 +73,7 @@ def valid_builds(record, n, depth):
return True return True
def assess(records, expected_revision=None, current=False): def assess(records, expected_revision=None):
"""current=True assesses current-checkout receipts (run_case --current-checkout) with
the same gates; it never accepts them as the frozen a135252 qualification, and the
frozen mode never accepts current-checkout receipts."""
indexed, duplicates = {}, [] indexed, duplicates = {}, []
for record in records: for record in records:
key = tuple(record.get(k) for k in ("re_L", "n", "depth", "explosion")) key = tuple(record.get(k) for k in ("re_L", "n", "depth", "explosion"))
@@ -90,19 +87,11 @@ def assess(records, expected_revision=None, current=False):
sources = [r.get("source", {}) for r in records] sources = [r.get("source", {}) for r in records]
checks["same_source_and_harness"] = bool(sources) and len({ checks["same_source_and_harness"] = bool(sources) and len({
json.dumps(s, sort_keys=True) for s in sources}) == 1 json.dumps(s, sort_keys=True) for s in sources}) == 1
if current: checks["clean_a135252_numerical_source"] = bool(sources) and all(
checks["clean_current_checkout_source"] = bool(sources) and expected_revision is not None and all( s.get("dirty") is False and s.get("source_tree_sha256") == SOURCE_SHA256
s.get("dirty") is False and s.get("source_mode") == "current_checkout" and bool(s.get("revision")) and bool(s.get("harness_sha256"))
and bool(s.get("source_tree_sha256")) and bool(s.get("harness_sha256")) and s.get("build_capture_sha256") == BUILD_CAPTURE_SHA256
and s.get("build_capture_sha256") == BUILD_CAPTURE_SHA256 and (expected_revision is None or s.get("revision") == expected_revision) for s in sources)
and s.get("revision") == expected_revision for s in sources)
else:
checks["clean_a135252_numerical_source"] = bool(sources) and all(
s.get("dirty") is False and s.get("source_tree_sha256") == SOURCE_SHA256
and "source_mode" not in s
and bool(s.get("revision")) and bool(s.get("harness_sha256"))
and s.get("build_capture_sha256") == BUILD_CAPTURE_SHA256
and (expected_revision is None or s.get("revision") == expected_revision) for s in sources)
runtimes = [r.get("runtime_identity") for r in records] runtimes = [r.get("runtime_identity") for r in records]
checks["same_recorded_runtime"] = bool(runtimes) and all( checks["same_recorded_runtime"] = bool(runtimes) and all(
isinstance(v, dict) and all(v.get(k) for k in isinstance(v, dict) and all(v.get(k) for k in
@@ -136,14 +125,8 @@ def assess(records, expected_revision=None, current=False):
and r.get("mach") == U * math.sqrt(3) and r.get("mach") == U * math.sqrt(3)
) )
checks[label + " actual_builds"] = valid_builds(r, n, depth) checks[label + " actual_builds"] = valid_builds(r, n, depth)
if current: checks[label + " reference"] = (r.get("numerical_revision") == NUMERICAL_REVISION
checks[label + " reference"] = (r.get("numerical_revision") is None and r.get("reference_harness") == REFERENCE_HARNESS)
and r.get("source_mode") == "current_checkout"
and r.get("reference_harness") == REFERENCE_HARNESS)
else:
checks[label + " reference"] = (r.get("numerical_revision") == NUMERICAL_REVISION
and "source_mode" not in r
and r.get("reference_harness") == REFERENCE_HARNESS)
checks[label + " numerics"] = r.get("numerics") == NUMERICS and r.get("initialization") == INITIALIZATION checks[label + " numerics"] = r.get("numerics") == NUMERICS and r.get("initialization") == INITIALIZATION
runtime = r.get("runtime_source", {}) runtime = r.get("runtime_source", {})
package = Path(runtime.get("checkout", "")) / "src/CelerisLab" package = Path(runtime.get("checkout", "")) / "src/CelerisLab"
@@ -202,10 +185,7 @@ def assess(records, expected_revision=None, current=False):
valid_case_and_controls=valid, baseline_passed=baseline_passed) valid_case_and_controls=valid, baseline_passed=baseline_passed)
return dict(passed=all(checks.values()), missing=missing, duplicates=duplicates, checks=checks, return dict(passed=all(checks.values()), missing=missing, duplicates=duplicates, checks=checks,
comparisons=comparisons, scheme_results=scheme_results, early_pressure=early, comparisons=comparisons, scheme_results=scheme_results, early_pressure=early,
source_mode="current_checkout" if current else "frozen", numerical_revision=NUMERICAL_REVISION, source_tree_sha256=SOURCE_SHA256,
numerical_revision=None if current else NUMERICAL_REVISION,
source_tree_sha256=(sources[0].get("source_tree_sha256") if current and sources
else None if current else SOURCE_SHA256),
reference_harness=REFERENCE_HARNESS, expected_revision=expected_revision, reference_harness=REFERENCE_HARNESS, expected_revision=expected_revision,
floors=FLOORS, baseline_factor=BASELINE_FACTOR, floors=FLOORS, baseline_factor=BASELINE_FACTOR,
main_statistic="maximum of four post-initial samples at t*=.12,.24,.36,.48", main_statistic="maximum of four post-initial samples at t*=.12,.24,.36,.48",
@@ -219,13 +199,10 @@ if __name__ == "__main__":
p.add_argument("files", type=Path, nargs="+") p.add_argument("files", type=Path, nargs="+")
p.add_argument("--output", type=Path, required=True) p.add_argument("--output", type=Path, required=True)
p.add_argument("--expected-revision", required=True) p.add_argument("--expected-revision", required=True)
p.add_argument("--current-checkout", action="store_true",
help="assess current-checkout receipts; never the frozen qualification")
args = p.parse_args() args = p.parse_args()
if args.output.exists(): if args.output.exists():
p.error("Output already exists; choose a new path to retain prior evidence") p.error("Output already exists; choose a new path to retain prior evidence")
result = assess([json.loads(path.read_text()) for path in args.files], args.expected_revision, result = assess([json.loads(path.read_text()) for path in args.files], args.expected_revision)
current=args.current_checkout)
args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n") args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n")
print(json.dumps(result, indent=2, allow_nan=False)) print(json.dumps(result, indent=2, allow_nan=False))
raise SystemExit(0 if result["passed"] else 1) raise SystemExit(0 if result["passed"] else 1)
+4 -26
View File
@@ -64,16 +64,8 @@ def provenance(repo):
) )
CURRENT_CAPTURE = "current checkout, not the frozen a135252 qualification" def verify_source(repo, revision):
"""Exact clean harness commit, fixed production tree, and reviewed capture helper."""
def verify_source(repo, revision, current=False):
"""Exact clean harness commit, fixed production tree, and reviewed capture helper.
current=True records the clean checkout itself instead of requiring the frozen
numerical tree; such receipts carry source_mode="current_checkout" and are only
assessed with assess(..., current=True), never as the frozen qualification.
"""
import oracle import oracle
from tests.validation.multigrid import check_projected_pairs as gate from tests.validation.multigrid import check_projected_pairs as gate
@@ -85,17 +77,6 @@ def verify_source(repo, revision, current=False):
if (Path(gate.__file__).resolve() != helper if (Path(gate.__file__).resolve() != helper
or hashlib.sha256(helper.read_bytes()).hexdigest() != BUILD_CAPTURE_SHA256): or hashlib.sha256(helper.read_bytes()).hexdigest() != BUILD_CAPTURE_SHA256):
raise RuntimeError("Wrong build-capture helper") raise RuntimeError("Wrong build-capture helper")
if current:
result = provenance(repo)
if result["revision"] != revision or result["dirty"]:
raise ValueError("requires the exact expected clean checkout")
result.update(source_mode="current_checkout", source_capture=CURRENT_CAPTURE,
build_capture_sha256=BUILD_CAPTURE_SHA256)
return result
if subprocess.run(["git", "-C", str(repo), "cat-file", "-e", f"{NUMERICAL_REVISION}^{{commit}}"],
capture_output=True).returncode:
raise RuntimeError(f"frozen numerical revision {NUMERICAL_REVISION} is not available "
"in this clone; the frozen qualification cannot be checked")
if gate.preflight(repo, revision, "candidate") != NUMERICAL_REVISION: if gate.preflight(repo, revision, "candidate") != NUMERICAL_REVISION:
raise RuntimeError("Wrong production revision") raise RuntimeError("Wrong production revision")
result = provenance(repo) result = provenance(repo)
@@ -168,8 +149,6 @@ def main():
p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True) p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True)
p.add_argument("--output", type=Path, required=True) p.add_argument("--output", type=Path, required=True)
p.add_argument("--explosion", choices=["constant", "projected_linear"], default="constant") p.add_argument("--explosion", choices=["constant", "projected_linear"], default="constant")
p.add_argument("--current-checkout", action="store_true",
help="record the clean current checkout instead of the frozen a135252 tree")
p.add_argument("--early-pressure", action="store_true", p.add_argument("--early-pressure", action="store_true",
help="also save 65 scalar pressure samples at t*=k*.001875, k=0..64") help="also save 65 scalar pressure samples at t*=k*.001875, k=0..64")
args = p.parse_args() args = p.parse_args()
@@ -182,7 +161,7 @@ def main():
p.error("--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required") p.error("--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required")
repo = args.repo.resolve() repo = args.repo.resolve()
sys.path[:0] = [str(repo / "src"), str(repo)] sys.path[:0] = [str(repo / "src"), str(repo)]
source = verify_source(repo, args.expected_revision, current=args.current_checkout) source = verify_source(repo, args.expected_revision)
runtime = runtime_source(repo) runtime = runtime_source(repo)
if args.dry_run: if args.dry_run:
print(json.dumps(dict(source=source, runtime_source=runtime, dry_run=True))) print(json.dumps(dict(source=source, runtime_source=runtime, dry_run=True)))
@@ -203,8 +182,7 @@ def main():
report = dict( report = dict(
schema=1, schema=1,
source=source, source=source,
numerical_revision=None if args.current_checkout else NUMERICAL_REVISION, numerical_revision=NUMERICAL_REVISION,
**({"source_mode": "current_checkout"} if args.current_checkout else {}),
reference_harness=REFERENCE_HARNESS, reference_harness=REFERENCE_HARNESS,
actual_build_sidecars=[], actual_build_sidecars=[],
runtime_identity=None, runtime_identity=None,
@@ -291,48 +291,6 @@ class ProjectedPairsTests(unittest.TestCase):
else: else:
self.assertEqual(run_case.verify_source(REPO, expected), source()) self.assertEqual(run_case.verify_source(REPO, expected), source())
def test_frozen_mode_reports_missing_history_not_a_diff(self):
from tests.validation.multigrid import check_projected_pairs as gate
missing = SimpleNamespace(returncode=128)
with patch.object(run_case.subprocess, "run", return_value=missing), \
patch.object(gate, "preflight") as preflight:
with self.assertRaisesRegex(RuntimeError, "not available in this clone"):
run_case.verify_source(REPO, MOCK_REVISION)
preflight.assert_not_called()
def test_current_checkout_source_is_recorded_not_frozen(self):
from tests.validation.multigrid import check_projected_pairs as gate
current = dict(source(), source_tree_sha256="c" * 64)
with patch.object(run_case, "provenance", return_value=dict(current)), \
patch.object(gate, "preflight") as preflight:
result = run_case.verify_source(REPO, MOCK_REVISION, current=True)
preflight.assert_not_called()
self.assertEqual(result["source_mode"], "current_checkout")
self.assertEqual(result["source_capture"], run_case.CURRENT_CAPTURE)
for bad in (dict(current, dirty=True), dict(current, revision=OLD_REVISION)):
with self.subTest(bad=bad), patch.object(run_case, "provenance", return_value=bad):
with self.assertRaisesRegex(ValueError, "exact expected clean checkout"):
run_case.verify_source(REPO, MOCK_REVISION, current=True)
def test_current_and_frozen_receipts_never_cross_assess(self):
frozen = records()
current = deepcopy(frozen)
for r in current:
r["source"] = dict(r["source"], source_tree_sha256="c" * 64,
source_mode="current_checkout",
source_capture=run_case.CURRENT_CAPTURE)
r["numerical_revision"], r["source_mode"] = None, "current_checkout"
ok = assess(current, MOCK_REVISION, current=True)
self.assertTrue(ok["checks"]["clean_current_checkout_source"])
self.assertTrue(all(v for k, v in ok["checks"].items() if k.endswith(" reference")))
self.assertEqual((ok["source_mode"], ok["numerical_revision"]), ("current_checkout", None))
self.assertEqual(ok["source_tree_sha256"], "c" * 64)
self.assertFalse(assess(current, MOCK_REVISION)["checks"]["clean_a135252_numerical_source"])
self.assertFalse(assess(current, MOCK_REVISION)["passed"])
self.assertFalse(assess(frozen, MOCK_REVISION, current=True)["checks"]["clean_current_checkout_source"])
self.assertFalse(assess(current, None, current=True)["checks"]["clean_current_checkout_source"])
self.assertTrue(assess(frozen, MOCK_REVISION)["checks"]["clean_a135252_numerical_source"])
def test_explicit_opt_in_selector_and_dry_run_without_cuda(self): def test_explicit_opt_in_selector_and_dry_run_without_cuda(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "case.json" output = Path(tmp) / "case.json"
@@ -361,7 +319,7 @@ class ProjectedPairsTests(unittest.TestCase):
patch.object(run_case, "runtime_identity", side_effect=AssertionError("CUDA reached")), \ patch.object(run_case, "runtime_identity", side_effect=AssertionError("CUDA reached")), \
redirect_stdout(stdout): redirect_stdout(stdout):
self.assertEqual(run_case.main(), 0) self.assertEqual(run_case.main(), 0)
verify.assert_called_once_with(REPO, MOCK_REVISION, current=False) verify.assert_called_once_with(REPO, MOCK_REVISION)
self.assertTrue(json.loads(stdout.getvalue())["dry_run"]) self.assertTrue(json.loads(stdout.getvalue())["dry_run"])
self.assertFalse(output.exists()) self.assertFalse(output.exists())
@@ -454,7 +412,7 @@ class ProjectedPairsTests(unittest.TestCase):
else: else:
self.assertEqual(run_case.main(), 0) self.assertEqual(run_case.main(), 0)
receipt = json.loads(output.read_text()) receipt = json.loads(output.read_text())
verify.assert_called_once_with(REPO, MOCK_REVISION, current=False) verify.assert_called_once_with(REPO, MOCK_REVISION)
self.assertEqual(selectors, ["projected_linear" if early else "constant"] if depth == 2 else []) self.assertEqual(selectors, ["projected_linear" if early else "constant"] if depth == 2 else [])
self.assertEqual(receipt["source"], source()) self.assertEqual(receipt["source"], source())
self.assertEqual(receipt["runtime_identity"], RUNTIME) self.assertEqual(receipt["runtime_identity"], RUNTIME)