Files
rpmroute/tests/test_cli.py
T
rsp2k 74d97b601c Check grid and room output on exact shapes and report shorts
The grid and room engines write copper they never check. After a grid or
room run the result is now put through the exact engine's geometry (its
pads, fixed copper and keepouts; wires as boxes or slanted slabs; vias as
their pads) and the report gives the shorts and clearance violations, with
examples, instead of a blanket warning. --strict exits 3 when there are any;
by default the exit status stays 0 so the JAR drop-in contract holds.

The exact engine's static-copper setup moves into insert_static_copper so
both use it; exact runs are byte-identical before and after.
2026-10-07 20:04:04 -06:00

198 lines
6.8 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_are_checked_after_routing(tmp_path, capsys, engine):
assert main([str(FIXTURE), "--engine", engine, "-do", str(tmp_path / "out.ses")]) == 0
assert f"{engine} output checked on exact shapes: 0 short(s), 0 clearance" in capsys.readouterr().err
def test_strict_fails_a_run_whose_copper_is_not_clean(tmp_path, monkeypatch, capsys):
from rpmroute import cli
from rpmroute.route.verify import VerifyReport, Violation
bad = VerifyReport(shorts=1, short_examples=[Violation("A", "B", "F.Cu", 1.0, 2.0)])
monkeypatch.setattr(cli, "verify_result", lambda dsn, result: bad)
out = tmp_path / "out.ses"
assert main([str(FIXTURE), "--engine", "grid", "-do", str(out)]) == 0
assert main([str(FIXTURE), "--engine", "grid", "--strict", "-do", str(out)]) == 3
assert out.exists() # the session is still written
err = capsys.readouterr().err
assert "short: A x B on F.Cu at (1.00, 2.00) mm" in err and "not DRC-clean" in 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")