Files
2026-07-13 12:09:03 +08:00

138 lines
5.1 KiB
Python

"""Tests for ExperimentRunner: ok path, timeout, crash, ablation, determinism."""
from __future__ import annotations
import json
import os
import sys
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
from main import ( # noqa: E402
AblationRunner,
ExperimentResult,
ExperimentRunner,
ExperimentSpec,
_scan_intermediates,
ablate,
)
CODE_ROOT = os.path.dirname(HERE)
SCRIPT_OK = os.path.join(CODE_ROOT, "experiments", "sparsity_experiment.py")
SCRIPT_CRASH = os.path.join(CODE_ROOT, "experiments", "crashing_experiment.py")
def make_spec(**overrides) -> ExperimentSpec:
base = ExperimentSpec(
spec_id=overrides.pop("spec_id", "test"),
hypothesis_id=overrides.pop("hypothesis_id", 1),
script_path=overrides.pop("script_path", SCRIPT_OK),
config=overrides.pop("config", {"k": 8, "steps": 3, "sleep_s": 0.0}),
seed=overrides.pop("seed", 7),
wall_timeout_s=overrides.pop("wall_timeout_s", 15.0),
memory_cap_mb=overrides.pop("memory_cap_mb", 256),
metric_keys=overrides.pop("metric_keys", ["perplexity", "final_loss"]),
)
return base
class TestScanIntermediates(unittest.TestCase):
def test_picks_last_covering_line(self) -> None:
stdout = "noise\n{\"step\":0,\"perplexity\":1.0,\"final_loss\":0.5}\n{\"perplexity\":0.9,\"final_loss\":0.4}\n"
final, intermediates = _scan_intermediates(stdout, ["perplexity", "final_loss"])
self.assertEqual(final["perplexity"], 0.9)
self.assertEqual(len(intermediates), 1)
self.assertEqual(intermediates[0]["step"], 0)
def test_ignores_lines_missing_keys(self) -> None:
stdout = "{\"step\":0,\"only\":1}\n{\"perplexity\":0.7,\"final_loss\":0.3}\n"
final, intermediates = _scan_intermediates(stdout, ["perplexity", "final_loss"])
self.assertEqual(final["perplexity"], 0.7)
self.assertEqual(intermediates, [])
def test_no_required_keys_takes_last_json(self) -> None:
stdout = "{\"a\":1}\n{\"a\":2}\n"
final, _ = _scan_intermediates(stdout, [])
self.assertEqual(final, {"a": 2})
class TestRunnerOk(unittest.TestCase):
def test_success_returns_metrics(self) -> None:
runner = ExperimentRunner()
result = runner.run(make_spec())
self.assertEqual(result.terminal, "ok")
self.assertEqual(result.exit_code, 0)
self.assertIn("perplexity", result.metrics)
self.assertIn("final_loss", result.metrics)
self.assertGreater(result.metrics["perplexity"], 0.0)
def test_intermediate_metrics_captured(self) -> None:
runner = ExperimentRunner()
result = runner.run(make_spec(config={"k": 8, "steps": 4, "sleep_s": 0.0}))
self.assertEqual(len(result.intermediate_metrics), 4)
steps = [m["step"] for m in result.intermediate_metrics]
self.assertEqual(steps, [0, 1, 2, 3])
def test_seed_forwarded(self) -> None:
runner = ExperimentRunner()
result = runner.run(make_spec(seed=42))
self.assertEqual(result.metrics["seed"], 42)
class TestRunnerTimeout(unittest.TestCase):
def test_timeout_terminal(self) -> None:
runner = ExperimentRunner()
spec = make_spec(
spec_id="timeout_case",
config={"k": 8, "steps": 20, "sleep_s": 0.4},
wall_timeout_s=0.5,
)
result = runner.run(spec)
self.assertEqual(result.terminal, "timeout")
class TestRunnerCrash(unittest.TestCase):
def test_non_zero_exit_is_crash(self) -> None:
runner = ExperimentRunner()
spec = make_spec(script_path=SCRIPT_CRASH, metric_keys=["perplexity"])
result = runner.run(spec)
self.assertEqual(result.terminal, "crash")
self.assertNotEqual(result.exit_code, 0)
self.assertIn("simulated failure", result.stderr_tail)
class TestAblate(unittest.TestCase):
def test_ablate_creates_one_spec_per_value(self) -> None:
base = make_spec()
specs = ablate(base, "k", [4, 8, 16])
self.assertEqual(len(specs), 3)
ids = [s.spec_id for s in specs]
self.assertEqual(ids, ["test_k_4", "test_k_8", "test_k_16"])
for spec, value in zip(specs, [4, 8, 16]):
self.assertEqual(spec.config["k"], value)
def test_ablation_sweep_returns_table(self) -> None:
runner = AblationRunner(ExperimentRunner())
table = runner.sweep(make_spec(), "k", [4, 8])
self.assertEqual(table.knob, "k")
self.assertEqual(len(table.rows), 2)
for value, result in table.rows:
self.assertEqual(result.terminal, "ok")
self.assertEqual(result.metrics["k"], value)
class TestDeterminism(unittest.TestCase):
def test_two_runs_same_seed_match(self) -> None:
runner = ExperimentRunner()
a = runner.run(make_spec(seed=11))
b = runner.run(make_spec(seed=11))
self.assertEqual(a.metrics["perplexity"], b.metrics["perplexity"])
self.assertEqual(a.metrics["final_loss"], b.metrics["final_loss"])
if __name__ == "__main__":
unittest.main()