"""Integration tests for autoroute's best-of-N loop. Mocks subprocess + pcbnew so the test verifies the orchestration without actually invoking Freerouting or KiCad. Specifically: - attempts=1 preserves single-attempt behaviour (no `attempts`/ `passSchedule` in response, no per-attempt `_best.ses` snapshotting). - attempts>1 runs N times and imports the highest-scoring SES. - One failing attempt doesn't abort the whole best-of-N run. - The pass schedule wraps when attempts > len(schedule). """ import sys import types from pathlib import Path from unittest.mock import MagicMock, patch import pytest PYTHON_DIR = Path(__file__).parent.parent / "python" sys.path.insert(0, str(PYTHON_DIR)) from commands import freerouting as fr_mod # noqa: E402 from commands.freerouting import FreeroutingCommands # noqa: E402 # Synthetic SES text: a `(net ...` section with N (wire ...) inside -> # scores as N nets, N segments (matching the scorer's expectations). def _make_ses(num_nets, segs_per_net=2): chunks = [] for i in range(num_nets): chunks.append(f'(net "N{i}"\n') for _ in range(segs_per_net): chunks.append(" (wire (path F.Cu 200 0 0 1 1))\n") chunks.append(")\n") return "".join(chunks) def _stub_pcbnew(dsn_to_create=None): """A minimal pcbnew stub for autoroute's needs. ExportSpecctraDSN normally writes a file as a side effect; tests pass a path so the stub creates an empty placeholder file to match the real-world contract (the `if not os.path.isfile(dsn_path):` guard inside autoroute). """ pcb = MagicMock(name="pcbnew_module") def _fake_export(_board, dsn_path, *_args, **_kw): # Write a placeholder DSN so the existence check passes with open(dsn_path, "w") as f: f.write("(pcb \"stub\")\n") return True pcb.ExportSpecctraDSN.side_effect = _fake_export pcb.ImportSpecctraSES.return_value = True return pcb def _make_cmds(board_path, dsn_exists=True): """FreeroutingCommands with a mocked board pointing at board_path.""" cc = FreeroutingCommands.__new__(FreeroutingCommands) board = MagicMock(name="board") board.GetFileName.return_value = str(board_path) board.Save.return_value = None tracks = MagicMock() tracks.__iter__ = lambda s: iter([]) board.GetTracks.return_value = tracks cc.board = board return cc @pytest.fixture() def workdir(tmp_path): # Pretend we have an existing board file (tmp_path / "test.kicad_pcb").write_text("(kicad_pcb)\n") return tmp_path @pytest.fixture() def fake_jar(tmp_path): p = tmp_path / "freerouting.jar" p.write_text("not a real jar") return p def _patch_exec_mode(cc): """Force direct (non-Docker) mode so we don't go down the docker branch.""" cc._resolve_execution_mode = MagicMock(return_value={ "mode": "direct", "use_docker": False, }) @pytest.mark.unit def test_single_attempt_default_keeps_legacy_response_shape(workdir, fake_jar): """attempts=1 (or omitted): no `attempts` list in response.""" cc = _make_cmds(workdir / "test.kicad_pcb") _patch_exec_mode(cc) ses_path = workdir / "test.ses" def fake_run(cmd, **kw): # Write a one-net SES on each subprocess call ses_path.write_text(_make_ses(num_nets=5)) proc = types.SimpleNamespace(returncode=0, stdout="ok", stderr="") return proc fake_pcb = _stub_pcbnew() with patch.object(fr_mod, "subprocess") as sp, \ patch.dict(sys.modules, {"pcbnew": fake_pcb}): sp.run.side_effect = fake_run sp.TimeoutExpired = TimeoutError out = cc.autoroute({ "boardPath": str(workdir / "test.kicad_pcb"), "freeroutingJar": str(fake_jar), }) assert out["success"], out assert "attempts" not in out, "single-attempt response must not include attempts list" assert "best_attempt" not in out assert sp.run.call_count == 1 @pytest.mark.unit def test_best_of_three_picks_highest_scoring_ses(workdir, fake_jar): """attempts=3 with varying SES content: best one wins.""" cc = _make_cmds(workdir / "test.kicad_pcb") _patch_exec_mode(cc) ses_path = workdir / "test.ses" best_path = workdir / "test_best.ses" # Per-attempt SES net counts: 5, 8, 7. Best should be attempt 2 (8 nets). counts = iter([5, 8, 7]) def fake_run(cmd, **kw): n = next(counts) ses_path.write_text(_make_ses(num_nets=n)) return types.SimpleNamespace(returncode=0, stdout=f"n={n}", stderr="") fake_pcb = _stub_pcbnew() with patch.object(fr_mod, "subprocess") as sp, \ patch.dict(sys.modules, {"pcbnew": fake_pcb}): sp.run.side_effect = fake_run sp.TimeoutExpired = TimeoutError out = cc.autoroute({ "boardPath": str(workdir / "test.kicad_pcb"), "freeroutingJar": str(fake_jar), "attempts": 3, }) assert out["success"], out assert "attempts" in out assert len(out["attempts"]) == 3 assert out["best_attempt"] == 2, f"attempt 2 has 8 nets, should win; got {out['best_attempt']}" assert out["best_score"] == 8 * 1000 + (8 * 2) # 8 nets, 16 segments # The final ses_path content should match the winning attempt (8 nets) final_ses = ses_path.read_text() assert final_ses.count("(net ") == 8 # The _best.ses snapshot must also exist assert best_path.exists() @pytest.mark.unit def test_one_failing_attempt_does_not_abort_best_of_n(workdir, fake_jar): """Attempt 2 exits nonzero; best-of-N keeps going and picks among the rest.""" cc = _make_cmds(workdir / "test.kicad_pcb") _patch_exec_mode(cc) ses_path = workdir / "test.ses" call_idx = {"n": 0} def fake_run(cmd, **kw): call_idx["n"] += 1 if call_idx["n"] == 2: # Fail the middle attempt return types.SimpleNamespace(returncode=99, stdout="", stderr="boom") # First and third produce 5 and 9 nets respectively nets = 5 if call_idx["n"] == 1 else 9 ses_path.write_text(_make_ses(num_nets=nets)) return types.SimpleNamespace(returncode=0, stdout="", stderr="") fake_pcb = _stub_pcbnew() with patch.object(fr_mod, "subprocess") as sp, \ patch.dict(sys.modules, {"pcbnew": fake_pcb}): sp.run.side_effect = fake_run sp.TimeoutExpired = TimeoutError out = cc.autoroute({ "boardPath": str(workdir / "test.kicad_pcb"), "freeroutingJar": str(fake_jar), "attempts": 3, }) assert out["success"], out assert sp.run.call_count == 3 assert out["best_attempt"] == 3, "attempt 3 (9 nets) must win over attempt 1 (5 nets)" # The middle attempt is recorded but marked not-ok middle = next(a for a in out["attempts"] if a["attempt"] == 2) assert middle["ok"] is False assert middle["exit_code"] == 99 @pytest.mark.unit def test_pass_schedule_wraps_when_attempts_exceeds_schedule_length(workdir, fake_jar): """Custom 2-entry schedule + 5 attempts → schedule cycles [a, b, a, b, a].""" cc = _make_cmds(workdir / "test.kicad_pcb") _patch_exec_mode(cc) ses_path = workdir / "test.ses" seen_mp = [] def fake_run(cmd, **kw): # extract `-mp N` from the command mp = int(cmd[cmd.index("-mp") + 1]) seen_mp.append(mp) ses_path.write_text(_make_ses(num_nets=3)) return types.SimpleNamespace(returncode=0, stdout="", stderr="") fake_pcb = _stub_pcbnew() with patch.object(fr_mod, "subprocess") as sp, \ patch.dict(sys.modules, {"pcbnew": fake_pcb}): sp.run.side_effect = fake_run sp.TimeoutExpired = TimeoutError out = cc.autoroute({ "boardPath": str(workdir / "test.kicad_pcb"), "freeroutingJar": str(fake_jar), "attempts": 5, "passSchedule": [42, 99], }) assert out["success"], out assert seen_mp == [42, 99, 42, 99, 42] @pytest.mark.unit def test_target_nets_bonus_wins_against_more_nets_without_targets(workdir, fake_jar): """Attempt with all targets beats higher-net attempt missing one target.""" cc = _make_cmds(workdir / "test.kicad_pcb") _patch_exec_mode(cc) ses_path = workdir / "test.ses" # Attempt 1: 8 nets, none named "CRIT" — misses target # Attempt 2: 4 nets including "CRIT" — hits target, gets 50k bonus def fake_run(cmd, **kw): attempt = sum(1 for c in cmd if c == "-mp") # rough counter — patched below ses_path.write_text(fake_run.next_ses) return types.SimpleNamespace(returncode=0, stdout="", stderr="") sess = [ _make_ses(num_nets=8), # 4 nets but one named CRIT '(net "N0"\n (wire (path F.Cu 200 0 0 1 1))\n)\n' '(net "N1"\n (wire (path F.Cu 200 0 0 1 1))\n)\n' '(net "N2"\n (wire (path F.Cu 200 0 0 1 1))\n)\n' '(net "CRIT"\n (wire (path F.Cu 200 0 0 1 1))\n)\n', ] seq = iter(sess) def fake_run(cmd, **kw): fake_run.next_ses = next(seq) ses_path.write_text(fake_run.next_ses) return types.SimpleNamespace(returncode=0, stdout="", stderr="") fake_pcb = _stub_pcbnew() with patch.object(fr_mod, "subprocess") as sp, \ patch.dict(sys.modules, {"pcbnew": fake_pcb}): sp.run.side_effect = fake_run sp.TimeoutExpired = TimeoutError out = cc.autoroute({ "boardPath": str(workdir / "test.kicad_pcb"), "freeroutingJar": str(fake_jar), "attempts": 2, "targetNets": ["CRIT"], }) assert out["success"], out assert out["best_attempt"] == 2, ( "attempt 2 routed the critical target — its 50k bonus must beat " "attempt 1's higher raw net count" ) @pytest.mark.unit def test_invalid_attempts_rejected_cleanly(workdir, fake_jar): cc = _make_cmds(workdir / "test.kicad_pcb") _patch_exec_mode(cc) out = cc.autoroute({ "boardPath": str(workdir / "test.kicad_pcb"), "freeroutingJar": str(fake_jar), "attempts": 0, }) assert out["success"] is False assert "Invalid attempts" in out["message"]