"""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()