- test_dfg_re20: the dirty-checkout test now supplies all three git responses (HEAD:src, HEAD, status) and asserts the call order, so it reaches the clean-checkout rejection instead of StopIteration. - run_dfg_re20: --output inside the source checkout is refused before provenance, plan writing or any CUDA work (receipts would dirty the recorded source); tested with the checkout root and nested paths. - source_provenance.historical_files: validates all paths first and reports an unreadable blob as HistoryUnavailable, not a raw CalledProcessError; tested with a failing git show. No physics thresholds changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
6.5 KiB
Python
134 lines
6.5 KiB
Python
"""CPU-only contract tests for run_dfg_re20. Not CUDA or DFG acceptance evidence."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
|
import run_dfg_re20 as v # noqa: E402
|
|
import run_refined_dfg as rr # noqa: E402
|
|
|
|
h, _ = rr.helpers(rr.DEFAULT_SOURCE)
|
|
|
|
|
|
def rows(values, tstar_end=v.TSTAR_MAX):
|
|
n = int(round(tstar_end / v.SAMPLE_TSTAR))
|
|
return [dict(tstar=(i + 1) * v.SAMPLE_TSTAR, **values(i)) for i in range(n)]
|
|
|
|
|
|
class Re20Contract(unittest.TestCase):
|
|
def test_lattice_parameters_and_bounds(self):
|
|
root, uniform = v.lattice_parameters(40), v.lattice_parameters(80)
|
|
self.assertAlmostEqual(root["tau"], 0.56, places=12)
|
|
self.assertAlmostEqual(uniform["tau"], 0.62, places=12)
|
|
self.assertEqual((root["steps"], root["interval"]), (240000, 2000))
|
|
self.assertEqual((uniform["steps"], uniform["interval"]), (480000, 4000))
|
|
self.assertAlmostEqual(v.plan()["refined_fine_tau"], 0.62, places=12)
|
|
|
|
def test_config_only_changes_viscosity(self):
|
|
base = v._re100_config(h, 40, "SRT", "none")
|
|
cfg, body, p = v.config(h, 40, "SRT")
|
|
self.assertAlmostEqual(cfg["physics"]["viscosity"], 0.01 * 40 / 20, places=15)
|
|
self.assertEqual((p["re"], p["viscosity"]), (20.0, cfg["physics"]["viscosity"]))
|
|
base[0]["physics"]["viscosity"] = cfg["physics"]["viscosity"]
|
|
self.assertEqual(base[0], cfg)
|
|
self.assertEqual(base[1], body)
|
|
with self.assertRaisesRegex(ValueError, "no Jets"):
|
|
v.config(h, 40, "MRT", "zero")
|
|
|
|
def test_units(self):
|
|
s = v.sample([2.0, -0.5], 4, 40, 3.0)
|
|
den = rr.SPEED**2 * 40 / 2
|
|
self.assertAlmostEqual(s["cd"], 2.0 / 4 / den)
|
|
self.assertAlmostEqual(s["cl"], -0.5 / 4 / den)
|
|
self.assertAlmostEqual(s["p_diff"], 3.0 * 0.04)
|
|
self.assertEqual(s["dp_scaled"], 3.0)
|
|
|
|
def test_steady_and_verdict(self):
|
|
inside = dict(cd=5.58, cl=0.0106, p_diff=0.1174)
|
|
out = v.summarize(rows(lambda i: inside))
|
|
self.assertEqual((out["status"], out["steady"], out["window_samples"]),
|
|
("WITHIN_INTERVALS", True, 21))
|
|
self.assertTrue(out["cd_positive"])
|
|
self.assertAlmostEqual(out["edge_distance"]["cl"], 0.0002)
|
|
self.assertTrue(all(out["verdict"].values()))
|
|
low = v.summarize(rows(lambda i: dict(inside, cl=0.0100)))
|
|
self.assertEqual(low["status"], "OUTSIDE_INTERVALS")
|
|
self.assertFalse(low["verdict"]["cl"])
|
|
|
|
def test_slow_drift_gives_no_verdict(self):
|
|
# Range 2e-5 passes (limit 6e-5); slope * window 2e-5 exceeds 1.2e-5.
|
|
out = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 1e-6 * i, p_diff=0.1174)))
|
|
self.assertEqual((out["status"], out["verdict"]), ("NOT_STEADY", None))
|
|
self.assertLessEqual(out["ranges"]["cl"], out["range_limits"]["cl"])
|
|
self.assertGreater(out["drifts"]["cl"], out["slope_limits"]["cl"])
|
|
|
|
def test_noise_range(self):
|
|
quiet = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 2e-5 * (i % 2), p_diff=0.1174)))
|
|
self.assertTrue(quiet["steady"]) # within the range limit, no trend
|
|
noisy = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 1e-4 * (i % 2), p_diff=0.1174)))
|
|
self.assertEqual(noisy["status"], "NOT_STEADY")
|
|
self.assertGreater(noisy["ranges"]["cl"], noisy["range_limits"]["cl"])
|
|
|
|
def test_incomplete_run_gives_no_verdict(self):
|
|
out = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106, p_diff=0.1174), tstar_end=30))
|
|
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__":
|
|
unittest.main()
|