Files
rpmroute/tests/test_cli.py
T
rsp2k 33de21ba0e Route with the exact engine by default; warn when an engine does not check
The grid engine was the default and its copper is not checked against exact
shapes: a full reroute of the Labrador lid with no flags came out with 15
tracks_crossing and 87 clearance violations under KiCad's DRC while the
report listed only unrouted connections. The exact engine (what mckicad and
every benchmark already use) is now the default, and grid or room runs end
with a warning that their output needs a DRC.
2026-10-07 18:37:33 -06:00

184 lines
6.0 KiB
Python

"""Tests for the rpmroute CLI (Specctra DSN in, SES out).
Covers the engine selection contract: every engine must route a fixture from the
command line, and every option the selected engine does not implement must fail
loudly rather than being silently dropped.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from rpmroute.cli import ENGINE_OPTIONS, main
FIXTURES = Path(__file__).parent / "dsn" / "fixtures"
FIXTURE = FIXTURES / "simple_2net.dsn"
CHANNEL_PACK = FIXTURES / "channel_pack.dsn"
SHOVE_NEEDED = FIXTURES / "shove_needed.dsn"
DIAGONAL_WIN = FIXTURES / "diagonal_win.dsn"
def test_cli_jar_compatible_flags_write_ses(tmp_path):
out = tmp_path / "out.ses"
rc = main(["-de", str(FIXTURE), "-do", str(out)])
assert rc == 0
text = out.read_text()
assert text.startswith("(session")
assert "(net" in text and "(wire" in text
def test_cli_short_output_alias(tmp_path):
out = tmp_path / "out.ses"
assert main([str(FIXTURE), "-o", str(out)]) == 0
assert out.read_text().startswith("(session")
def test_cli_positional_input_to_stdout(capsys):
rc = main([str(FIXTURE)])
assert rc == 0
assert "(session" in capsys.readouterr().out
def test_cli_missing_input_errors():
with pytest.raises(SystemExit):
main([])
def test_cli_unreadable_input_returns_2(capsys):
rc = main(["/nonexistent/board.dsn"])
assert rc == 2
assert "cannot read" in capsys.readouterr().err
def test_cli_bad_layers_errors(capsys):
with pytest.raises(SystemExit):
main([str(FIXTURE), "--layers", "top,bottom"])
assert "--layers must be comma-separated integers" in capsys.readouterr().err
# --- engine selection -------------------------------------------------------
@pytest.mark.parametrize("engine", sorted(ENGINE_OPTIONS))
def test_cli_every_engine_routes_a_fixture(engine, tmp_path):
out = tmp_path / f"{engine}.ses"
rc = main([str(FIXTURE), "--engine", engine, "-do", str(out)])
assert rc == 0
text = out.read_text()
assert text.startswith("(session")
assert "(wire" in text
def test_cli_default_engine_is_exact(tmp_path):
"""The default is the engine that checks its copper: grid output can short
and say nothing (a user routing with no flags shipped 15 crossings)."""
default = tmp_path / "default.ses"
exact = tmp_path / "exact.ses"
assert main([str(FIXTURE), "-do", str(default)]) == 0
assert main([str(FIXTURE), "--engine", "exact", "-do", str(exact)]) == 0
assert default.read_text() == exact.read_text()
@pytest.mark.parametrize("engine", ["grid", "room"])
def test_unchecked_engines_say_so(tmp_path, capsys, engine):
assert main([str(FIXTURE), "--engine", engine, "-do", str(tmp_path / "out.ses")]) == 0
assert "does not check its copper" in capsys.readouterr().err
def test_the_exact_engine_does_not_warn(tmp_path, capsys):
assert main([str(FIXTURE), "-do", str(tmp_path / "out.ses")]) == 0
assert "WARNING" not in capsys.readouterr().err
def test_cli_room_pack_routes(tmp_path):
out = tmp_path / "pack.ses"
rc = main([str(CHANNEL_PACK), "--engine", "room", "--pack", "-do", str(out)])
assert rc == 0
assert "(wire" in out.read_text()
def test_cli_room_shove_routes(tmp_path):
out = tmp_path / "shove.ses"
rc = main([str(SHOVE_NEEDED), "--engine", "room", "--shove", "-do", str(out)])
assert rc == 0
assert "(wire" in out.read_text()
def test_cli_exact_shove_routes(tmp_path):
out = tmp_path / "shove.ses"
rc = main([str(SHOVE_NEEDED), "--engine", "exact", "--shove", "-do", str(out)])
assert rc == 0
assert "(wire" in out.read_text()
def test_cli_exact_diagonal_routes(tmp_path):
out = tmp_path / "diag.ses"
rc = main([str(DIAGONAL_WIN), "--engine", "exact", "--diagonal", "-do", str(out)])
assert rc == 0
assert "(wire" in out.read_text()
def test_cli_max_passes_is_honoured_not_advisory(tmp_path):
out = tmp_path / "mp.ses"
rc = main([str(FIXTURE), "--engine", "exact", "-mp", "3", "-do", str(out)])
assert rc == 0
assert "(wire" in out.read_text()
# --- unsupported combinations must error, never be silently ignored ---------
@pytest.mark.parametrize(
("argv", "flag"),
[
(["--engine", "grid", "--pack"], "--pack"),
(["--engine", "grid", "--shove"], "--shove"),
(["--engine", "grid", "--diagonal"], "--diagonal"),
(["--engine", "exact", "--pack"], "--pack"),
(["--engine", "room", "--diagonal"], "--diagonal"),
(["--engine", "room", "--no-rip-up"], "--no-rip-up"),
(["--engine", "room", "-mp", "5"], "-mp/--max-passes"),
],
)
def test_cli_unsupported_option_errors(argv, flag, capsys):
with pytest.raises(SystemExit) as excinfo:
main([str(FIXTURE), *argv])
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert flag in err
assert "not supported by --engine" in err
assert "supported by:" in err
def test_cli_unknown_engine_errors(capsys):
with pytest.raises(SystemExit):
main([str(FIXTURE), "--engine", "quantum"])
assert "invalid choice" in capsys.readouterr().err
def test_cli_supported_combinations_are_accepted(tmp_path):
"""Every (engine, flag) pair the matrix claims to support must actually run."""
argv_for = {
"pack": ["--pack"],
"shove": ["--shove"],
"diagonal": ["--diagonal"],
"rip_up": ["--no-rip-up"],
"max_passes": ["-mp", "4"],
"seed": ["--seed", "3"],
"orders": ["--orders", "2"],
"promote": ["--promote-failures", "2"],
"access": ["--access-cost", "5"],
"negotiate": ["--negotiate", "2"],
"rollback": ["--no-rollback"],
"tree": ["--no-tree"],
"window": ["--window-reroute", "1"],
}
for engine, options in ENGINE_OPTIONS.items():
for option in options:
out = tmp_path / f"{engine}-{option}.ses"
rc = main([str(FIXTURE), "--engine", engine, *argv_for[option], "-do", str(out)])
assert rc == 0, f"{engine} + {option} failed"
assert out.read_text().startswith("(session")