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.
198 lines
6.8 KiB
Python
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")
|