Buckets:
| """Tests for scripts/run_sr.py — the Python success-rate eval driver. | |
| NO GPU, no real eval: every test either exercises the driver's pure logic (yaml parsing, the | |
| modes.sh parser, gate math, LPT scheduling, resume/completeness decisions) against fabricated | |
| fixtures, or monkeypatches the one external dependency that would otherwise require hardware | |
| (torch.cuda) / a subprocess (subprocess.Popen, forced to raise so a --dry-run that | |
| accidentally launches something fails loudly instead of silently doing the wrong thing). | |
| scripts/run_sr.py is not a package module (it lives outside src/), so it is loaded by file | |
| path (see _load_run_sr below). | |
| """ | |
| from __future__ import annotations | |
| import dataclasses | |
| import importlib.util | |
| import json | |
| import math | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| import pytest | |
| import torch | |
| REPO = Path(__file__).resolve().parents[1] | |
| def _load_run_sr(): | |
| path = REPO / "scripts" / "run_sr.py" | |
| spec = importlib.util.spec_from_file_location("run_sr", path) | |
| mod = importlib.util.module_from_spec(spec) | |
| # register BEFORE exec: run_sr.py's dataclasses (with from __future__ import annotations) | |
| # resolve their type hints via sys.modules[cls.__module__] at class-body execution time. | |
| sys.modules[spec.name] = mod | |
| spec.loader.exec_module(mod) | |
| return mod | |
| run_sr = _load_run_sr() | |
| # One shared LadderConfig for every test that only READS the ladder. Tests that need a different | |
| # results root build their own with _config(results_root=...). | |
| CONFIG = run_sr.LadderConfig.load() | |
| LADDER = CONFIG.doc | |
| PATHS = CONFIG.paths | |
| SUITES = CONFIG.suites | |
| AXES = CONFIG.scorers.axes | |
| def _config(results_root: Path | None = None) -> "run_sr.LadderConfig": | |
| return run_sr.LadderConfig.load(results_root=results_root) | |
| def _mkcell(name: str, expected_n: int, **kw) -> "run_sr.Cell": | |
| """A test cell whose tag is derived the way production derives it. name only needs to make the | |
| cell DISTINCT from its neighbours, so it rides in as the variant suffix; read the real tag off | |
| cell.tag, never off name.""" | |
| kw.setdefault("rung", "t") | |
| kw.setdefault("suite", "object") | |
| kw.setdefault("axis", "Objects_Layout") | |
| kw.setdefault("mode", "base") | |
| kw.setdefault("policy", "t") | |
| kw.setdefault("bench", CONFIG.scorers.bench(kw["suite"])) | |
| if "comparators" in kw: | |
| kw["comparators"] = tuple(kw["comparators"]) | |
| return run_sr.Cell(variant=name, expected_n=expected_n, **kw) | |
| def _score(cell, successes: int, actual_n: int) -> "run_sr.CellScore": | |
| return run_sr.CellScore(cell=cell, count=run_sr.DirCount(successes, actual_n)) | |
| def _verdict(score, allow_partial: bool) -> "run_sr.GateVerdict": | |
| """The production verdict path: completeness first, then the cell's own gate.""" | |
| return run_sr.CellScorer(CONFIG).verdict(score, allow_partial) | |
| def _iter_declared_cells(): | |
| """(rung_name, cell_doc) for every hand-declared cell (skips R4's auto_generate: full_grid).""" | |
| for rung_name, rung_doc in LADDER["rungs"].items(): | |
| if rung_doc.get("auto_generate"): | |
| continue | |
| for cell_doc in rung_doc["cells"]: | |
| yield rung_name, cell_doc | |
| # ====================================================================================================== | |
| # ladder yaml: parses, every cell names a real suite/axis, expected counts match the task's table | |
| # ====================================================================================================== | |
| def test_ladder_yaml_parses_and_has_all_rungs(): | |
| # R1 (smoke) and R4 (the full 4-suite x 7-axis grid). A wider rung set here | |
| # (R2/R3/R5/E*/F*/G*/H* ...) was the pre-reduction ablation ladder; asserting the CURRENT set | |
| # pins this down instead of letting the test silently pass against a stale, larger yaml. | |
| assert set(LADDER["rungs"]) == {"R1", "R4"} | |
| def test_ladder_paths_block_declares_every_artifact_the_driver_drives(): | |
| """LadderPaths is the single place the driver learns where its external artifacts live, so a | |
| missing entry must fail loudly at load rather than as a KeyError deep inside a launch.""" | |
| with pytest.raises(KeyError, match="paths block is missing"): | |
| run_sr.LadderPaths.from_doc({"paths": {"score_py": "x"}}) | |
| def test_every_declared_cell_names_a_real_suite_and_axis(): | |
| for rung_name, cell_doc in _iter_declared_cells(): | |
| assert cell_doc["suite"] in SUITES, f"{rung_name}: unknown suite {cell_doc['suite']!r}" | |
| assert cell_doc["axis"] in AXES, f"{rung_name}: unknown axis {cell_doc['axis']!r}" | |
| def test_every_declared_cell_mode_is_a_known_modes_sh_recipe(): | |
| for rung_name, cell_doc in _iter_declared_cells(): | |
| mode = cell_doc.get("mode") | |
| if mode is not None: | |
| assert mode in CONFIG.modes.modes, f"{rung_name} {cell_doc['tag']}: unknown mode {mode!r}" | |
| def test_expected_episode_counts_match_the_task_table(): | |
| expected = { | |
| "object": {"Background_Textures": 248, "Camera_Viewpoints": 396, "Language_Instructions": 354, | |
| "Light_Conditions": 296, "Objects_Layout": 403, "Robot_Initial_States": 398, "Sensor_Noise": 422}, | |
| "spatial": {"Background_Textures": 266, "Camera_Viewpoints": 376, "Language_Instructions": 390, | |
| "Light_Conditions": 291, "Objects_Layout": 385, "Robot_Initial_States": 350, "Sensor_Noise": 351}, | |
| "goal": {"Background_Textures": 281, "Camera_Viewpoints": 408, "Language_Instructions": 410, | |
| "Light_Conditions": 278, "Objects_Layout": 425, "Robot_Initial_States": 409, "Sensor_Noise": 379}, | |
| "long": {"Background_Textures": 289, "Camera_Viewpoints": 419, "Language_Instructions": 382, | |
| "Light_Conditions": 273, "Objects_Layout": 312, "Robot_Initial_States": 393, "Sensor_Noise": 449}, | |
| } | |
| assert LADDER["expected_episodes"] == expected | |
| def test_unfiltered_cell_expected_n_matches_expected_episodes_table(): | |
| """A cell without its own instance-id filter must inherit the full suite x axis denominator.""" | |
| for rung_name, cell_doc in _iter_declared_cells(): | |
| if cell_doc.get("filter") not in (None,) and cell_doc["axis"] != "Light_Conditions": | |
| continue # e.g. R1's 20-instance smoke filter legitimately shrinks expected_n | |
| suite, axis = cell_doc["suite"], cell_doc["axis"] | |
| assert cell_doc["expected_n"] == LADDER["expected_episodes"][suite][axis], (rung_name, cell_doc["tag"]) | |
| def test_suites_yaml_axes_match_score_py_axes(): | |
| doc = run_sr.load_yaml(PATHS.suites_yaml) | |
| spaced_to_underscored = [a.replace(" ", "_") for a in doc["axes"]] | |
| assert spaced_to_underscored == AXES | |
| # ====================================================================================================== | |
| # evals/common/modes.sh parser — env composition must match the CONTRACT below (this pins the | |
| # PARSER's correctness; the parser reads modes.sh directly, so it can never itself drift from the | |
| # file -- this guards against a parsing regression instead). | |
| # ====================================================================================================== | |
| MODES_SH_CONTRACT = { | |
| "base": {}, | |
| } | |
| def test_mode_env_matches_modes_sh_contract(mode): | |
| assert CONFIG.modes.env_for(mode) == MODES_SH_CONTRACT[mode] | |
| def test_mode_env_unknown_mode_raises(): | |
| with pytest.raises(ValueError, match="unknown MODE"): | |
| CONFIG.modes.env_for("not_a_real_mode") | |
| def test_mode_env_returns_a_fresh_copy_each_call(): | |
| """A caller that mutates its recipe must not corrupt the registry for the next cell.""" | |
| first = CONFIG.modes.env_for("base") | |
| first["SOMETHING"] = "999" | |
| assert "SOMETHING" not in CONFIG.modes.env_for("base") | |
| # ====================================================================================================== | |
| # cell resolution / env composition | |
| # ====================================================================================================== | |
| def test_compose_cell_env_extra_env_override_wins(): | |
| """A cell's per-cell env: (yaml) / extra_env override must win over the builder's own defaults.""" | |
| cell = _mkcell("t", 10, extra_env={"LOAD_VISION": "1"}) | |
| env = run_sr.EnvBuilder(CONFIG).for_cell(cell) | |
| assert env["LOAD_VISION"] == "1" # extra_env override, not the builder's own default | |
| def test_compose_cell_env_sets_qndf_dir_per_suite(): | |
| builder = run_sr.EnvBuilder(CONFIG) | |
| for cell in CONFIG.cells_for_rung("R4"): | |
| env = builder.for_cell(cell) | |
| assert env["QNDF_DIR"] == str(SUITES[cell.suite].fwm_dir) | |
| def test_resolve_graph_dir_matches_onf_config_paths_graph(): | |
| from onf.config import default_paths | |
| for suite in SUITES: | |
| assert run_sr.resolve_graph_dir(suite) == default_paths().graph(suite) | |
| def test_graph_artifact_status_reports_missing_files_not_ok(tmp_path): | |
| status = run_sr.graph_artifact_status(tmp_path / "no_such_graph_dir") | |
| assert not status.ok | |
| assert status.graph_hash is None | |
| assert status.g_head_mtime is None | |
| assert len(status.missing) == 2 # g_head.npz + g_track.npz | |
| def test_graph_artifact_status_ok_when_both_artifacts_present(tmp_path): | |
| import numpy as np | |
| from onf.graph.core import schema as graph_schema | |
| d = tmp_path / "graph_dir" | |
| d.mkdir() | |
| np.savez(d / graph_schema.HEAD_NPZ, graph_hash=np.array("deadbeef")) | |
| (d / graph_schema.TRACK_NPZ).write_bytes(b"") # presence is all graph_artifact_status checks | |
| status = run_sr.graph_artifact_status(d) | |
| assert status.ok | |
| assert status.missing == () | |
| assert status.graph_hash == "deadbeef" | |
| assert status.g_head_mtime is not None | |
| assert status.as_record()["graph_hash"] == "deadbeef" | |
| # ====================================================================================================== | |
| # 1f: log-directory naming -- shared by both drivers, UTC-stamped, encodes policy/mode/suite/axis so | |
| # two cells that differ only by one of those can never collide/truncate each other's shard logs. | |
| # ====================================================================================================== | |
| def test_make_log_dir_embeds_policy_mode_suite_axis_and_normalizes_axis_spaces(): | |
| d = run_sr.make_log_dir("stablevla", "sentinel", "long", "Sensor Noise", root=Path("/x")) | |
| # the log dir name is exactly the cell tag, so logs and results are findable from each other | |
| assert d.parent == Path("/x/logs") | |
| assert d.name.endswith("_stablevla_sentinel_long_Sensor_Noise") | |
| # UTC timestamp prefix: YYYYmmdd_HHMMSS_... | |
| assert re.match(r"^\d{8}_\d{6}_stablevla_sentinel_long_Sensor_Noise$", d.name), d.name | |
| # ====================================================================================================== | |
| # 1e: run.json provenance -- every result dir gets one, naming policy/mode/suite/axis/expected_n/ | |
| # resolved graph dir/graph_hash/g_head mtime/git commit/the full mode env. | |
| # ====================================================================================================== | |
| def test_write_run_json_round_trips_expected_fields(tmp_path): | |
| out = run_sr.write_run_json( | |
| tmp_path / "result_dir", policy="stablevla", mode="sentinel", suite="long", | |
| axis="Sensor_Noise", expected_n=449, graph_dir="/g", graph_hash="abc123", | |
| g_head_mtime="2026-08-01T00:00:00+00:00", git_commit="deadbeef", mode_env={"SENTINEL": "1"}, | |
| ) | |
| assert out == tmp_path / "result_dir" / "run.json" | |
| payload = json.loads(out.read_text()) | |
| assert payload["policy"] == "stablevla" | |
| assert payload["mode"] == "sentinel" | |
| assert payload["suite"] == "long" | |
| assert payload["axis"] == "Sensor_Noise" | |
| assert payload["expected_n"] == 449 | |
| assert payload["graph_dir"] == "/g" | |
| assert payload["graph_hash"] == "abc123" | |
| assert payload["g_head_mtime"] == "2026-08-01T00:00:00+00:00" | |
| assert payload["git_commit"] == "deadbeef" | |
| assert payload["mode_env"] == {"SENTINEL": "1"} | |
| assert "written_at" in payload | |
| def test_light_conditions_axis_auto_resolves_per_suite_filter(): | |
| cell = CONFIG.cell_from_doc("adhoc", {"suite": "goal", "axis": "Light_Conditions", "mode": "base", | |
| "tag": "t", "expected_n": 278}) | |
| assert cell.filter_file == SUITES["goal"].light_filter | |
| def test_non_light_axis_has_no_filter_by_default(): | |
| cell = CONFIG.cell_from_doc("adhoc", {"suite": "goal", "axis": "Background_Textures", "mode": "base", | |
| "tag": "t", "expected_n": 281}) | |
| assert cell.filter_file is None | |
| def test_cell_category_value_is_axis_with_spaces(): | |
| cell = _mkcell("t", 10, axis="Robot_Initial_States") | |
| assert cell.category_value == "Robot Initial States" | |
| def test_cell_result_dir_layout(): | |
| cell = _mkcell("sr_test", 10, suite="goal", axis="Background_Textures") | |
| d = cell.result_dir(Path("/results")) | |
| assert d == Path("/results/plus_libero_goal/Background_Textures") / cell.tag | |
| # ====================================================================================================== | |
| # 1a: a rung tag must bake in its resolved MODE, and overriding --mode must re-key the tag's mode | |
| # suffix along with it -- NOT leave the tag naming the original mode. That mismatch is the actual | |
| # wrong-number hazard from the task: --rung R4 then --rung R4 --mode base --resume used to write | |
| # into the SAME result directory (tag had no mode in it) and the ledger's tag-only lookup then reported | |
| # the FIRST run's numbers under the SECOND run's mode name. | |
| # ====================================================================================================== | |
| def test_cell_tag_embeds_policy_mode_suite_axis(): | |
| assert run_sr.cell_tag("stablevla", "base", "object", "Robot_Initial_States") \ | |
| == "stablevla_base_object_Robot_Initial_States" | |
| # the optional variant separates a filtered smoke pool from the full axis it subsets | |
| assert run_sr.cell_tag("gr00t", "probe", "long", "Sensor_Noise", "smoke") \ | |
| == "gr00t_probe_long_Sensor_Noise_smoke" | |
| def test_r1_cell_tag_bakes_in_its_declared_mode(): | |
| cells = CONFIG.cells_for_rung("R1") | |
| assert cells[0].mode == "base" | |
| assert "_base_" in cells[0].tag | |
| def test_r4_generated_tags_bake_in_their_axis_default_mode(): | |
| for c in CONFIG.cells_for_rung("R4"): | |
| assert f"_{c.mode}_" in c.tag, c.tag | |
| def test_r1_mode_override_re_keys_the_tag_not_just_the_mode(): | |
| cells = CONFIG.cells_for_rung("R1") | |
| assert cells[0].mode == "base" | |
| overridden = [c.with_mode("probe") for c in cells] | |
| assert overridden[0].mode == "probe" | |
| assert overridden[0].tag != cells[0].tag # THE FIX: tag changes with mode | |
| assert "_probe_" in overridden[0].tag | |
| assert "_base_" not in overridden[0].tag # no stale mode fragment left behind | |
| # everything else about the cell (expected_n, comparators, gate, filter) survives the swap | |
| assert overridden[0].expected_n == cells[0].expected_n | |
| assert overridden[0].comparators == cells[0].comparators | |
| def test_cell_with_mode_round_trips_through_two_overrides(): | |
| """Overriding mode twice (e.g. two separate CLI invocations against the same rung) must never | |
| leave a stale mode fragment baked into the tag from an EARLIER override.""" | |
| cell = run_sr.Cell(rung="t", suite="object", axis="Robot_Initial_States", bench="libero_object", | |
| mode="probe", expected_n=398) | |
| once = cell.with_mode("other") | |
| assert once.tag == "stablevla_other_object_Robot_Initial_States" | |
| twice = once.with_mode("base") | |
| assert twice.tag == "stablevla_base_object_Robot_Initial_States" | |
| def test_cell_is_frozen_so_a_tag_cannot_drift_from_its_mode(): | |
| cell = _mkcell("t", 10) | |
| with pytest.raises(dataclasses.FrozenInstanceError): | |
| cell.mode = "sentinel" | |
| def test_resolve_cells_with_mode_override_produces_mode_bearing_tags(): | |
| """End-to-end through the same path main() uses for --rung ... --mode ....""" | |
| default_cells = _driver(["--rung", "R1", "--gpus", "0"])._resolve_cells() | |
| assert "_base_" in default_cells[0].tag | |
| overridden_cells = _driver(["--rung", "R1", "--gpus", "0", "--mode", "probe"])._resolve_cells() | |
| assert overridden_cells[0].mode == "probe" | |
| assert "_probe_" in overridden_cells[0].tag | |
| assert overridden_cells[0].tag != default_cells[0].tag | |
| def _driver(argv, ledger=None) -> "run_sr.SRDriver": | |
| return run_sr.SRDriver(run_sr.build_argparser().parse_args(argv), ledger=ledger) | |
| # ====================================================================================================== | |
| # comparator / gate math | |
| # ====================================================================================================== | |
| def test_comparator_rate(): | |
| c = run_sr.Comparator(label="base", tag="x", successes=244, n=281) | |
| assert c.rate == pytest.approx(100 * 244 / 281) | |
| def test_comparator_rate_zero_n_is_nan(): | |
| c = run_sr.Comparator(label="base", tag="x", successes=0, n=0) | |
| assert math.isnan(c.rate) | |
| def test_gate_from_dict_rejects_an_unknown_kind(): | |
| """GateKind turns a typo'd yaml gate into a load-time error instead of a silent no-op.""" | |
| with pytest.raises(ValueError): | |
| run_sr.Gate.from_dict({"kind": "not_a_real_gate"}) | |
| def test_gate_max_regress_pp_pass_and_fail(): | |
| comp = run_sr.Comparator(label="base", tag="b", successes=50, n=100) # 50.0% | |
| cell = _mkcell("t", 100, gate=run_sr.Gate(kind=run_sr.GateKind.MAX_REGRESS_PP, vs="base", | |
| max_regress_pp=2.0), comparators=[comp]) | |
| assert _verdict(_score(cell, 49, 100), allow_partial=False).passed | |
| assert _verdict(_score(cell, 70, 100), allow_partial=False).passed | |
| assert not _verdict(_score(cell, 40, 100), allow_partial=False).passed | |
| def test_gate_exact_match(): | |
| comp = run_sr.Comparator(label="base", tag="b", successes=302, n=403) | |
| cell = _mkcell("t", 403, gate=run_sr.Gate(kind=run_sr.GateKind.EXACT_MATCH, vs="base"), | |
| comparators=[comp]) | |
| assert _verdict(_score(cell, 302, 403), allow_partial=False).passed | |
| assert not _verdict(_score(cell, 301, 403), allow_partial=False).passed | |
| def test_gate_min_successes(): | |
| comp = run_sr.Comparator(label="base", tag="b", successes=9, n=11) | |
| cell = _mkcell("t", 11, gate=run_sr.Gate(kind=run_sr.GateKind.MIN_SUCCESSES, vs="base", | |
| min_successes=8), comparators=[comp]) | |
| assert _verdict(_score(cell, 8, 11), allow_partial=False).passed | |
| assert not _verdict(_score(cell, 7, 11), allow_partial=False).passed | |
| def test_gate_missing_comparator_label_skips_not_fails(): | |
| cell = _mkcell("t", 10, gate=run_sr.Gate(kind=run_sr.GateKind.MAX_REGRESS_PP, vs="nonexistent", | |
| max_regress_pp=2.0), comparators=[]) | |
| verdict = _verdict(_score(cell, 5, 10), allow_partial=False) | |
| assert verdict.passed and "SKIP" in verdict.message | |
| def test_gate_missing_comparator_skips_even_a_min_successes_gate(): | |
| """A comparator's absence means the ladder has nothing to compare against yet -- failing the cell | |
| for that would make a fresh checkout's first run red for a reason unrelated to the run.""" | |
| cell = _mkcell("t", 10, gate=run_sr.Gate(kind=run_sr.GateKind.MIN_SUCCESSES, vs="base", | |
| min_successes=999), comparators=[]) | |
| verdict = _verdict(_score(cell, 0, 10), allow_partial=False) | |
| assert verdict.passed and "SKIP" in verdict.message | |
| def test_gate_none_always_passes(): | |
| cell = _mkcell("t", 10, gate=None) | |
| assert _verdict(_score(cell, 1, 10), allow_partial=False).passed | |
| # ====================================================================================================== | |
| # completeness: mp4 count vs expected_n | |
| # ====================================================================================================== | |
| def test_completeness_rule_marks_short_cell_incomplete_and_excludes_by_default(): | |
| comp = run_sr.Comparator(label="base", tag="b", successes=50, n=100) | |
| cell = _mkcell("t", 100, gate=run_sr.Gate(kind=run_sr.GateKind.MAX_REGRESS_PP, vs="base", | |
| max_regress_pp=2.0), comparators=[comp]) | |
| verdict = _verdict(_score(cell, 90, 90), allow_partial=False) | |
| assert not verdict.passed | |
| assert "INCOMPLETE" in verdict.message | |
| def test_completeness_rule_allow_partial_falls_through_to_the_real_gate(): | |
| comp = run_sr.Comparator(label="base", tag="b", successes=50, n=100) | |
| cell = _mkcell("t", 100, gate=run_sr.Gate(kind=run_sr.GateKind.MAX_REGRESS_PP, vs="base", | |
| max_regress_pp=2.0), comparators=[comp]) | |
| assert _verdict(_score(cell, 90, 90), allow_partial=True).passed | |
| def test_score_cell_against_fabricated_results_tree_marks_short_dir_incomplete(tmp_path): | |
| results_root = tmp_path / "results" | |
| cell = _mkcell("sr_test", 10) | |
| d = results_root / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| for i in range(3): | |
| (d / f"rollout_task_episode{i}_success.mp4").write_bytes(b"x") | |
| score = run_sr.CellScorer(_config(results_root)).score(cell) | |
| assert (score.successes, score.actual_n, score.complete) == (3, 3, False) | |
| def test_score_cell_missing_dir_is_zero_not_a_crash(tmp_path): | |
| cell = _mkcell("sr_missing", 10) | |
| score = run_sr.CellScorer(_config(tmp_path / "results")).score(cell) | |
| assert (score.successes, score.actual_n, score.complete) == (0, 0, False) | |
| # ====================================================================================================== | |
| # ledger — append/read round-trip | |
| # ====================================================================================================== | |
| def _rec(**kw) -> "run_sr.LedgerRecord": | |
| kw.setdefault("suite", "object") | |
| kw.setdefault("bench", "libero_object") | |
| kw.setdefault("axis", "Objects_Layout") | |
| kw.setdefault("mode", "base") | |
| return run_sr.LedgerRecord(**kw) | |
| def test_ledger_append_read_round_trip(tmp_path): | |
| ledger = run_sr.Ledger(tmp_path / "ledger.jsonl") | |
| ledger.append(_rec(tag="a", status=run_sr.RunStatus.RUNNING, ts="t0")) | |
| ledger.append(_rec(tag="a", status=run_sr.RunStatus.COMPLETE, ts="t1", actual_n=10)) | |
| records = ledger.read() | |
| assert [(r.tag, r.status, r.actual_n) for r in records] == [ | |
| ("a", run_sr.RunStatus.RUNNING, None), | |
| ("a", run_sr.RunStatus.COMPLETE, 10), | |
| ] | |
| assert ledger.latest("a").actual_n == 10 | |
| assert ledger.latest("a", records=records).actual_n == 10 | |
| assert ledger.latest("nope") is None | |
| def test_ledger_record_stamps_ts_when_the_caller_leaves_it_blank(): | |
| payload = _rec(tag="a", status=run_sr.RunStatus.RUNNING).to_dict() | |
| assert payload["ts"] | |
| assert payload["status"] == "running" # the VALUE, not the enum repr | |
| def test_ledger_record_from_dict_tolerates_an_older_row(tmp_path): | |
| """Rows written before a schema change must still read: unknown keys are dropped, missing | |
| identity fields become "", and an unrecognised status reads as crashed rather than crashing.""" | |
| row = {"tag": "x", "status": "some_status_we_retired", "who_knows": 1, "actual_n": 3} | |
| rec = run_sr.LedgerRecord.from_dict(row) | |
| assert rec.tag == "x" and rec.suite == "" and rec.actual_n == 3 | |
| assert rec.status is run_sr.RunStatus.CRASHED | |
| # ====================================================================================================== | |
| # 1a (ledger end): Ledger.latest must ALSO compare mode when given one, not tag alone -- the other | |
| # half of the --resume wrong-number fix (tags now bake mode in via cell_tag, but the ledger lookup | |
| # itself is defense in depth against a record written before that fix, or any other tag collision). | |
| # ====================================================================================================== | |
| def test_latest_matching_record_requires_matching_mode_when_given(tmp_path): | |
| ledger = run_sr.Ledger(tmp_path / "l.jsonl") | |
| records = [ | |
| _rec(tag="x", mode="base", status=run_sr.RunStatus.COMPLETE, successes=1), | |
| _rec(tag="x", mode="sentinel", status=run_sr.RunStatus.COMPLETE, successes=2), | |
| ] | |
| assert ledger.latest("x", "base", records).successes == 1 | |
| assert ledger.latest("x", "sentinel", records).successes == 2 | |
| # tag-only (mode=None, the default) still returns the LAST matching record, unfiltered by mode -- | |
| # exact prior behaviour, so a caller that doesn't pass mode is unaffected. | |
| assert ledger.latest("x", None, records).successes == 2 | |
| def test_latest_matching_record_mode_mismatch_is_not_a_match(tmp_path): | |
| ledger = run_sr.Ledger(tmp_path / "l.jsonl") | |
| records = [_rec(tag="x", mode="base", status=run_sr.RunStatus.COMPLETE)] | |
| assert ledger.latest("x", "sentinel", records) is None | |
| def test_read_ledger_missing_file_returns_empty(tmp_path): | |
| assert run_sr.Ledger(tmp_path / "nope.jsonl").read() == [] | |
| def test_print_ledger_empty(tmp_path, capsys): | |
| rc = _driver(["--ledger"], ledger=run_sr.Ledger(tmp_path / "empty.jsonl")).run() | |
| assert rc == run_sr.ExitCode.OK | |
| assert "empty" in capsys.readouterr().out | |
| def test_print_ledger_with_records(tmp_path, capsys): | |
| ledger = run_sr.Ledger(tmp_path / "ledger.jsonl") | |
| ledger.append(_rec(ts="t0", tag="x", status=run_sr.RunStatus.COMPLETE, actual_n=5, | |
| expected_n=5, successes=4, rate=80.0, wall_s=12.3, episodes_per_min=5.0, | |
| gpu=0)) | |
| rc = _driver(["--ledger"], ledger=ledger).run() | |
| assert rc == run_sr.ExitCode.OK | |
| out = capsys.readouterr().out | |
| assert "x" in out and "complete" in out | |
| # ====================================================================================================== | |
| # resume: ResumePlanner.plan is a PURE decision; .apply does the actual purge. | |
| # ====================================================================================================== | |
| def _planner(results_root: Path, ledger_path: Path) -> "run_sr.ResumePlanner": | |
| return run_sr.ResumePlanner(_config(results_root), run_sr.Ledger(ledger_path)) | |
| def test_plan_cell_action_no_existing_dir_runs(tmp_path): | |
| cell = _mkcell("x", 5) | |
| plan = _planner(tmp_path, tmp_path / "l.jsonl").plan(cell, resume=False, records=[]) | |
| assert plan.action is run_sr.CellAction.RUN | |
| def test_plan_cell_action_existing_dir_without_resume_errors(tmp_path): | |
| cell = _mkcell("x", 5) | |
| d = tmp_path / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| (d / "rollout_t_episode0_success.mp4").write_bytes(b"x") | |
| plan = _planner(tmp_path, tmp_path / "l.jsonl").plan(cell, resume=False, records=[]) | |
| assert plan.action is run_sr.CellAction.ERROR | |
| def test_plan_cell_action_resume_skips_ledger_confirmed_complete(tmp_path): | |
| cell = _mkcell("x", 5) # _mkcell defaults mode="base" -- the ledger record's mode must match it. | |
| d = tmp_path / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| for i in range(5): | |
| (d / f"rollout_t_episode{i}_success.mp4").write_bytes(b"x") | |
| records = [_rec(tag=cell.tag, mode="base", status=run_sr.RunStatus.COMPLETE)] | |
| plan = _planner(tmp_path, tmp_path / "l.jsonl").plan(cell, resume=True, records=records) | |
| assert plan.action is run_sr.CellAction.SKIP | |
| assert d.exists() # untouched | |
| def test_plan_cell_action_resume_does_not_skip_on_a_different_modes_ledger_record(tmp_path): | |
| """The 1a regression this whole fix targets: a complete ledger record for the SAME tag but a | |
| DIFFERENT mode (e.g. left over from a --rung R4 run, now re-invoked as --rung R4 --mode base) | |
| must NOT be read as confirming completeness for this cell's mode -- that is exactly how the first | |
| run's numbers used to get silently reported under the second run's mode name.""" | |
| cell = _mkcell("x", 5, mode="base") | |
| d = tmp_path / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| for i in range(5): | |
| (d / f"rollout_t_episode{i}_success.mp4").write_bytes(b"x") | |
| records = [_rec(tag=cell.tag, mode="sentinel", status=run_sr.RunStatus.COMPLETE)] # different mode | |
| plan = _planner(tmp_path, tmp_path / "l.jsonl").plan(cell, resume=True, records=records) | |
| assert plan.action is run_sr.CellAction.PURGE # NOT skip | |
| def test_plan_cell_action_resume_purges_partial_dir_despite_stale_complete_ledger(tmp_path): | |
| """The hazard from the task: a ledger record claiming 'complete' must NOT be trusted blindly -- | |
| the mp4 count on disk is re-verified against expected_n every time.""" | |
| cell = _mkcell("x", 5) | |
| d = tmp_path / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| (d / "rollout_t_episode0_success.mp4").write_bytes(b"x") # only 1 of expected 5 | |
| records = [_rec(tag=cell.tag, status=run_sr.RunStatus.COMPLETE)] # stale/wrong ledger claim | |
| planner = _planner(tmp_path, tmp_path / "l.jsonl") | |
| plan = planner.plan(cell, resume=True, records=records) | |
| assert plan.action is run_sr.CellAction.PURGE | |
| assert d.exists() # plan is pure -- nothing deleted yet | |
| planner.apply(plan) | |
| assert not d.exists() | |
| def test_plan_cell_action_resume_without_ledger_record_purges(tmp_path): | |
| cell = _mkcell("x", 5) | |
| d = tmp_path / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| (d / "rollout_t_episode0_success.mp4").write_bytes(b"x") | |
| plan = _planner(tmp_path, tmp_path / "l.jsonl").plan(cell, resume=True, records=[]) | |
| assert plan.action is run_sr.CellAction.PURGE | |
| def test_apply_cell_action_is_a_noop_for_run_and_skip(tmp_path): | |
| cell = _mkcell("x", 5) | |
| d = tmp_path / "plus_libero_object" / "Objects_Layout" / cell.tag | |
| d.mkdir(parents=True) | |
| (d / "rollout_t_episode0_success.mp4").write_bytes(b"x") | |
| planner = _planner(tmp_path, tmp_path / "l.jsonl") | |
| planner.apply(run_sr.CellPlan(cell, run_sr.CellAction.RUN, "")) | |
| planner.apply(run_sr.CellPlan(cell, run_sr.CellAction.SKIP, "")) | |
| assert d.exists() | |
| # ====================================================================================================== | |
| # LPT scheduling | |
| # ====================================================================================================== | |
| def _assign(cells, gpus): | |
| return run_sr.Schedule.build(cells, gpus, CONFIG, []).assignments | |
| def test_lpt_schedule_exact_greedy_assignment(): | |
| cells = [_mkcell("a", 100), _mkcell("b", 90), _mkcell("c", 50), _mkcell("d", 10), _mkcell("e", 10)] | |
| assign = _assign(cells, [0, 1]) | |
| assert [c.variant for c in assign[0]] == ["a", "d", "e"] # 100 + 10 + 10 = 120 | |
| assert [c.variant for c in assign[1]] == ["b", "c"] # 90 + 50 = 140 | |
| totals = {g: sum(c.expected_n for c in cs) for g, cs in assign.items()} | |
| assert totals == {0: 120, 1: 140} | |
| def test_lpt_schedule_processes_largest_first_on_a_single_gpu(): | |
| assign = _assign([_mkcell("small", 1), _mkcell("big", 1000)], [0]) | |
| assert [c.variant for c in assign[0]] == ["big", "small"] | |
| def test_lpt_schedule_covers_every_cell_exactly_once(): | |
| cells = [_mkcell(str(i), i + 1) for i in range(9)] | |
| assign = _assign(cells, [0, 1, 2]) | |
| scheduled = [c.variant for cs in assign.values() for c in cs] | |
| assert sorted(scheduled, key=int) == sorted((c.variant for c in cells), key=int) | |
| def test_schedule_makespan_is_the_busiest_track(): | |
| cells = [_mkcell("a", 600), _mkcell("b", 60)] | |
| sched = run_sr.Schedule.build(cells, [0, 1], CONFIG, []) | |
| epm = CONFIG.defaults.episodes_per_min | |
| assert sched.makespan_min == pytest.approx(600 / epm) | |
| assert {round(v, 6) for v in sched.per_gpu_minutes.values()} == { | |
| round(600 / epm, 6), round(60 / epm, 6)} | |
| # ====================================================================================================== | |
| # rolling episodes/min | |
| # ====================================================================================================== | |
| def test_rolling_episodes_per_min_uses_ledger_history(): | |
| cell = _mkcell("t", 100, suite="goal", axis="Background_Textures", mode="sentinel") | |
| records = [ | |
| _rec(tag="a", suite="goal", axis="Background_Textures", mode="sentinel", | |
| status=run_sr.RunStatus.COMPLETE, episodes_per_min=10.0), | |
| _rec(tag="a", suite="goal", axis="Background_Textures", mode="sentinel", | |
| status=run_sr.RunStatus.COMPLETE, episodes_per_min=20.0), | |
| ] | |
| assert run_sr.Schedule.rolling_rate(cell, CONFIG, records) == 15.0 | |
| def test_rolling_episodes_per_min_falls_back_to_yaml_default_with_no_history(): | |
| cell = _mkcell("t", 100) | |
| assert run_sr.Schedule.rolling_rate(cell, CONFIG, []) == CONFIG.defaults.episodes_per_min | |
| def test_rolling_episodes_per_min_ignores_other_cells(): | |
| cell = _mkcell("t", 100, suite="goal", axis="Background_Textures", mode="sentinel") | |
| records = [_rec(tag="a", suite="long", axis="Camera_Viewpoints", mode="sentinel", | |
| status=run_sr.RunStatus.COMPLETE, episodes_per_min=999.0)] | |
| assert run_sr.Schedule.rolling_rate(cell, CONFIG, records) == CONFIG.defaults.episodes_per_min | |
| def test_rolling_episodes_per_min_ignores_incomplete_runs(): | |
| """An incomplete run's throughput is not a throughput -- it is a partial run divided by a full | |
| wall clock, and using it would under-project every future makespan for that cell.""" | |
| cell = _mkcell("t", 100, suite="goal", axis="Background_Textures", mode="sentinel") | |
| records = [_rec(tag="a", suite="goal", axis="Background_Textures", mode="sentinel", | |
| status=run_sr.RunStatus.INCOMPLETE, episodes_per_min=999.0)] | |
| assert run_sr.Schedule.rolling_rate(cell, CONFIG, records) == CONFIG.defaults.episodes_per_min | |
| # ====================================================================================================== | |
| # --cells parsing | |
| # ====================================================================================================== | |
| def test_parse_cells_arg_builds_cells_in_order_with_explicit_mode(): | |
| cells = CONFIG.cells_from_tokens("goal:Background_Textures,long:Camera_Viewpoints", "sentinel") | |
| assert [(c.suite, c.axis, c.mode) for c in cells] == [ | |
| ("goal", "Background_Textures", "sentinel"), | |
| ("long", "Camera_Viewpoints", "sentinel"), | |
| ] | |
| # this exact (suite, axis) pair isn't hand-declared in either remaining rung (R1/R4 -- R4 is | |
| # auto_generate, skipped by _find_declared_cell), so this falls through to the dynamic | |
| # probe_comparators disk lookup -- exercised here, and asserted non-empty because the | |
| # lp_libero_goal_Background_Textures family is a real, present result dir (see | |
| # test_r4_comparators_resolved_dynamically_from_disk). | |
| assert cells[0].comparator("base") is not None | |
| assert cells[0].tag == "stablevla_sentinel_goal_Background_Textures" | |
| def test_parse_cells_arg_defaults_mode_from_axis_when_omitted(): | |
| cells = CONFIG.cells_from_tokens("goal:Robot_Initial_States", None) | |
| # Read the expectation from the ladder rather than restating it: the whole point of this test is | |
| # that an omitted --mode falls through to axis_default_mode, not that the default is any one value. | |
| assert cells[0].mode == CONFIG.default_mode("Robot_Initial_States") | |
| def test_parse_cells_arg_unknown_axis_raises(): | |
| with pytest.raises(ValueError): | |
| CONFIG.cells_from_tokens("goal:NotAnAxis", "base") | |
| def test_parse_cells_arg_unknown_suite_raises(): | |
| with pytest.raises(ValueError): | |
| CONFIG.cells_from_tokens("notasuite:Background_Textures", "base") | |
| # ====================================================================================================== | |
| # R4 (full grid) auto-generation | |
| # ====================================================================================================== | |
| def test_r4_generates_the_full_28_cell_grid(): | |
| cells = CONFIG.cells_for_rung("R4") | |
| assert len(cells) == 28 | |
| assert {c.suite for c in cells} == {"object", "spatial", "goal", "long"} | |
| assert {c.axis for c in cells} == set(AXES) | |
| for c in cells: | |
| assert c.mode == CONFIG.default_mode(c.axis) | |
| def test_r4_comparators_resolved_dynamically_from_disk(): | |
| comps = CONFIG.probe_comparators("goal", "Background_Textures") | |
| labels = {c.label for c in comps} | |
| assert "base" in labels | |
| base = next(c for c in comps if c.label == "base") | |
| assert base.n == 281 # lp_libero_goal_Background_Textures on disk | |
| def test_r4_comparators_empty_when_results_root_absent(tmp_path): | |
| assert _config(tmp_path / "nowhere").probe_comparators("goal", "Background_Textures") == () | |
| def test_cells_for_rung_unknown_name_raises(): | |
| with pytest.raises(ValueError, match="unknown rung"): | |
| CONFIG.cells_for_rung("NOPE") | |
| # ====================================================================================================== | |
| # GPU / load preflight | |
| # ====================================================================================================== | |
| def _preflight(min_free_gib=12.0, max_loadavg_factor=2.5) -> "run_sr.Preflight": | |
| config = _config() | |
| config.limits = dataclasses.replace( | |
| config.limits, min_free_gib=min_free_gib, max_loadavg_factor=max_loadavg_factor) | |
| return run_sr.Preflight(config) | |
| def test_gpu_report_refuses_busy_gpu(monkeypatch): | |
| monkeypatch.setattr(torch.cuda, "device_count", lambda: 1) | |
| monkeypatch.setattr(torch.cuda, "mem_get_info", lambda i: (5 * 1024**3, 140 * 1024**3)) | |
| statuses = _preflight(min_free_gib=12.0).gpus([0]) | |
| assert len(statuses) == 1 | |
| assert not statuses[0].ok | |
| assert "GiB free" in statuses[0].reason | |
| def test_gpu_report_allows_free_gpu(monkeypatch): | |
| monkeypatch.setattr(torch.cuda, "device_count", lambda: 1) | |
| monkeypatch.setattr(torch.cuda, "mem_get_info", lambda i: (50 * 1024**3, 140 * 1024**3)) | |
| assert _preflight(min_free_gib=12.0).gpus([0])[0].ok | |
| def test_gpu_report_nonexistent_index_is_not_ok(monkeypatch): | |
| monkeypatch.setattr(torch.cuda, "device_count", lambda: 1) | |
| monkeypatch.setattr(torch.cuda, "mem_get_info", lambda i: (50 * 1024**3, 140 * 1024**3)) | |
| assert not _preflight(min_free_gib=12.0).gpus([5])[0].ok | |
| def test_gpu_report_defaults_to_every_visible_device(monkeypatch): | |
| monkeypatch.setattr(torch.cuda, "device_count", lambda: 3) | |
| monkeypatch.setattr(torch.cuda, "mem_get_info", lambda i: (50 * 1024**3, 140 * 1024**3)) | |
| assert [s.index for s in _preflight().gpus()] == [0, 1, 2] | |
| def test_load_report_ok_and_busy(monkeypatch): | |
| monkeypatch.setattr(run_sr.os, "getloadavg", lambda: (1.0, 1.0, 1.0)) | |
| monkeypatch.setattr(run_sr.os, "cpu_count", lambda: 8) | |
| ok = _preflight(max_loadavg_factor=1.5).load() | |
| assert ok.ok and ok.threshold == 12.0 | |
| monkeypatch.setattr(run_sr.os, "getloadavg", lambda: (20.0, 20.0, 20.0)) | |
| assert not _preflight(max_loadavg_factor=1.5).load().ok | |
| def test_check_libero_plus_reports_missing_tree(monkeypatch, tmp_path): | |
| monkeypatch.setenv("LIBERO_HOME", str(tmp_path / "does_not_exist")) | |
| result = _preflight().libero_plus() | |
| assert not result.ok | |
| assert "missing/empty" in result.message | |
| # ====================================================================================================== | |
| # --dry-run: no subprocess, no filesystem mutation, even with --resume against a partial dir | |
| # ====================================================================================================== | |
| def _no_launch(monkeypatch, tmp_path, n_gpus=2): | |
| def _boom(*_a, **_k): | |
| raise AssertionError("subprocess.Popen must not be called during --dry-run") | |
| monkeypatch.setattr(subprocess, "Popen", _boom) | |
| monkeypatch.setattr(torch.cuda, "device_count", lambda: n_gpus) | |
| monkeypatch.setattr(torch.cuda, "mem_get_info", lambda i: (100 * 1024**3, 140 * 1024**3)) | |
| monkeypatch.setattr(run_sr.os, "getloadavg", lambda: (0.1, 0.1, 0.1)) | |
| return run_sr.Ledger(tmp_path / "ledger.jsonl") | |
| def test_dry_run_never_calls_subprocess_popen(monkeypatch, tmp_path): | |
| ledger = _no_launch(monkeypatch, tmp_path) | |
| results_root = tmp_path / "results" | |
| rc = _driver(["--rung", "R1", "--gpus", "0,1", "--dry-run", | |
| "--results-root", str(results_root)], ledger=ledger).run() | |
| assert rc == run_sr.ExitCode.OK | |
| def test_dry_run_with_resume_does_not_delete_a_partial_dir(monkeypatch, tmp_path): | |
| """Regression guard: --dry-run --resume must PREVIEW the purge, never perform it.""" | |
| ledger = _no_launch(monkeypatch, tmp_path, n_gpus=1) | |
| results_root = tmp_path / "results" | |
| cells = _config(results_root).cells_for_rung("R1") | |
| partial_dir = cells[0].result_dir(results_root) | |
| partial_dir.mkdir(parents=True) | |
| (partial_dir / "rollout_t_episode0_success.mp4").write_bytes(b"x") # 1 of expected 11 | |
| rc = _driver(["--rung", "R1", "--gpus", "0", "--dry-run", "--resume", | |
| "--results-root", str(results_root)], ledger=ledger).run() | |
| assert rc == run_sr.ExitCode.OK | |
| assert partial_dir.exists() # NOT purged -- dry-run is read-only | |
| def test_dry_run_refuses_when_every_gpu_is_busy(monkeypatch, tmp_path): | |
| _no_launch(monkeypatch, tmp_path) | |
| monkeypatch.setattr(torch.cuda, "mem_get_info", lambda i: (1 * 1024**3, 140 * 1024**3)) | |
| rc = _driver(["--rung", "R1", "--gpus", "0,1", "--dry-run", | |
| "--results-root", str(tmp_path / "r")], | |
| ledger=run_sr.Ledger(tmp_path / "l.jsonl")).run() | |
| assert rc == run_sr.ExitCode.ERROR | |
| # ====================================================================================================== | |
| # exit codes (main() wiring) | |
| # ====================================================================================================== | |
| R1_TAG = "stablevla_base_object_Robot_Initial_States_smoke" # rung R1's first cell, tag derived by | |
| # cell_tag(policy, mode, suite, axis, variant) | |
| def test_main_exit_code_2_when_gate_not_met(tmp_path): | |
| results_root = tmp_path / "results" | |
| cell_dir = results_root / "plus_libero_object" / "Robot_Initial_States" / R1_TAG | |
| cell_dir.mkdir(parents=True) | |
| # 1/11 successes vs the min_successes=8 gate (comparator base is 9/11) -- must fail | |
| (cell_dir / "rollout_t_episode0_success.mp4").write_bytes(b"x") | |
| for i in range(1, 11): | |
| (cell_dir / f"rollout_t_episode{i}_failure.mp4").write_bytes(b"x") | |
| rc = run_sr.main(["--score", "--rung", "R1", "--results-root", str(results_root)]) | |
| assert rc == 2 | |
| def test_main_exit_code_0_when_gate_met(tmp_path): | |
| results_root = tmp_path / "results" | |
| cell_dir = results_root / "plus_libero_object" / "Robot_Initial_States" / R1_TAG | |
| cell_dir.mkdir(parents=True) | |
| # 9/11 successes -- meets the min_successes=8 gate | |
| for i in range(9): | |
| (cell_dir / f"rollout_t_episode{i}_success.mp4").write_bytes(b"x") | |
| for i in range(9, 11): | |
| (cell_dir / f"rollout_t_episode{i}_failure.mp4").write_bytes(b"x") | |
| rc = run_sr.main(["--score", "--rung", "R1", "--results-root", str(results_root)]) | |
| assert rc == 0 | |
| def test_main_exit_code_1_when_nothing_scored(tmp_path): | |
| rc = run_sr.main(["--score", "--rung", "R1", "--results-root", str(tmp_path / "results")]) | |
| assert rc == 1 | |
| def test_main_requires_rung_or_cells(tmp_path): | |
| rc = run_sr.main(["--results-root", str(tmp_path / "results")]) | |
| assert rc == 1 | |
| def test_main_rejects_rung_and_cells_together(): | |
| with pytest.raises(SystemExit): | |
| run_sr.main(["--rung", "R1", "--cells", "object:Objects_Layout", "--gpus", "0"]) | |
| def test_main_unknown_rung_raises(): | |
| with pytest.raises(SystemExit): | |
| run_sr.main(["--rung", "NOPE", "--gpus", "0"]) | |
| def test_main_requires_gpus_to_launch(tmp_path): | |
| rc = run_sr.main(["--rung", "R1", "--results-root", str(tmp_path / "results")]) | |
| assert rc == 1 | |
| def test_main_ledger_flag_short_circuits_before_touching_ladder_cells(tmp_path, monkeypatch): | |
| ledger_path = tmp_path / "ledger.jsonl" | |
| monkeypatch.setattr(run_sr, "default_ledger_path", lambda: ledger_path) | |
| rc = run_sr.main(["--ledger"]) | |
| assert rc == 0 | |
| def test_parse_gpus_dedups_and_keeps_order(): | |
| parse = run_sr.SRDriver._parse_gpus | |
| assert parse(None) == [] and parse("") == [] | |
| assert parse("0,1") == [0, 1] | |
| assert parse("3,2") == [3, 2] | |
| assert parse(" 1 , 2 ") == [1, 2] | |
| # a repeated index would otherwise open two schedule tracks against the same device | |
| assert parse("0,0,1") == [0, 1] | |
| # ====================================================================================================== | |
| # Port claiming — concurrent run_sr invocations must never share a policy-server port. | |
| # | |
| # Why this has its own test: the failure is SILENT and it mis-scores. Two run_sr processes both start | |
| # their port counter at defaults.base_port, so the second one's sim clients connect to the FIRST one's | |
| # already-running policy server -- a different CHECKPOINT -- and write ordinary-looking mp4s. It was | |
| # caught in practice with a goal Robot-Init cell whose clients reached a long-checkpoint server and | |
| # produced 121 episodes under the wrong policy, with nothing in any log to say so. | |
| # ====================================================================================================== | |
| def test_claim_port_skips_a_bound_port(tmp_path): | |
| import socket as _s | |
| with _s.socket(_s.AF_INET, _s.SOCK_STREAM) as held: | |
| held.setsockopt(_s.SOL_SOCKET, _s.SO_REUSEADDR, 1) | |
| held.bind(("0.0.0.0", 0)) | |
| held.listen(1) | |
| busy = held.getsockname()[1] | |
| got = run_sr.PortAllocator(busy, tmp_path / ".ports").claim() | |
| assert got != busy, "claim handed out a port something is already listening on" | |
| def test_claim_port_reservation_blocks_a_concurrent_process(tmp_path): | |
| """A reservation held by a LIVE pid must not be handed out again. This is the half a bind-probe | |
| cannot cover: a policy server spends minutes loading its checkpoint before it binds, so a second | |
| process probing during that window still sees the port as free and claims it too.""" | |
| base = 15900 | |
| reservations = tmp_path / ".ports" | |
| reservations.mkdir(parents=True) | |
| (reservations / str(base)).write_text(f"{os.getpid()}\n") # us: definitely alive | |
| got = run_sr.PortAllocator(base, reservations).claim() | |
| assert got != base, "a reservation held by a live process was handed out anyway" | |
| def test_claim_port_reclaims_a_reservation_whose_owner_died(tmp_path): | |
| """A run killed mid-flight leaves its reservation files behind; they must not wedge the port | |
| range forever, so an unreachable owner pid is treated as stale and taken over.""" | |
| base = 15910 | |
| reservations = tmp_path / ".ports" | |
| reservations.mkdir(parents=True) | |
| dead_pid = 999_999 # far above /proc/sys/kernel/pid_max on this box | |
| (reservations / str(base)).write_text(f"{dead_pid}\n") | |
| assert run_sr.PortAllocator(base, reservations).claim() == base, \ | |
| "a reservation from a dead process was not reclaimed" | |
| def test_claim_port_release_all_drops_every_marker(tmp_path): | |
| reservations = tmp_path / ".ports" | |
| allocator = run_sr.PortAllocator(16100, reservations) | |
| ports = [allocator.claim() for _ in range(3)] | |
| assert all((reservations / str(p)).exists() for p in ports) | |
| allocator.release_all() | |
| assert not any((reservations / str(p)).exists() for p in ports) | |
| def test_claim_port_gives_out_a_distinct_port_every_call(tmp_path): | |
| allocator = run_sr.PortAllocator(16200, tmp_path / ".ports") | |
| ports = [allocator.claim() for _ in range(5)] | |
| assert len(set(ports)) == 5 | |
| # ====================================================================================================== | |
| # --rung + --cells subsetting | |
| # ====================================================================================================== | |
| def test_rung_plus_cells_selects_a_subset_and_keeps_the_rungs_own_filter_and_n(): | |
| """--rung R --cells suite:axis must SUBSET the rung, not rebuild the cell ad hoc: an ad hoc cell | |
| carries the full-axis expected_n and no instance filter, so a 12-episode smoke would silently | |
| become a full run.""" | |
| cells = _driver(["--rung", "R1", "--cells", "long:Sensor_Noise", "--gpus", "0"])._resolve_cells() | |
| assert [(c.suite, c.axis) for c in cells] == [("long", "Sensor_Noise")] | |
| full = [c for c in CONFIG.cells_for_rung("R1") if (c.suite, c.axis) == ("long", "Sensor_Noise")][0] | |
| assert cells[0].expected_n == full.expected_n | |
| assert cells[0].filter_file == full.filter_file is not None | |
| def test_rung_plus_cells_rejects_a_cell_the_rung_does_not_declare(): | |
| driver = _driver(["--rung", "R1", "--cells", "goal:Camera_Viewpoints", "--gpus", "0"]) | |
| with pytest.raises(SystemExit): | |
| driver._resolve_cells() | |
Xet Storage Details
- Size:
- 48.9 kB
- Xet hash:
- 85b24681eeda8593d8884e194d27a3dd45a8c278ed250cbe52e340a77418fcb3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.