A last stage on the finished result, wire by wire: a run of legs between two points becomes the octilinear shape joining them when its copper is clear, the furthest point first; right-angle corners left are chamfered; straight runs are merged. Ends, vias, points where another wire of the net meets the wire, and legs across own-net pads stay. Every new leg and corner is checked on the exact shapes against other nets, pads, keepouts and the board edge, slanted legs both ways round as the verifier counts a pair. A net whose pads end up joined differently is put back, and the whole smoothing is dropped if the finished check finds more violations than before. --smooth-report adds track length per layer.
201 lines
6.9 KiB
Python
201 lines
6.9 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"],
|
|
"vias": ["--reduce-vias"],
|
|
"via_cost": ["--via-cost", "40"],
|
|
"smooth": ["--smooth"],
|
|
}
|
|
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")
|