freeroute/tests/test_cli.py
Ryan Malloy bf868b7b05 Harden packaging for a public release
Add the GPLv3 text (the package declared the licence but never shipped it),
a py.typed marker, and an sdist include/exclude allowlist so the distribution
carries only src/, docs/, README, LICENSE and pyproject. reference/ is a clone
of the GPL FreeRouting Java tree kept purely as a porting reference and must
never be redistributed inside this package; tests, caches and build output are
excluded too.

Switch to the PEP 639 SPDX licence expression with license-files, widen the
classifiers, and add an -o alias for -do. Rewrite the README around the engine
matrix: what each track and pass actually does, and what this is not (no
FreeRouting density parity on dense boards, and --diagonal is a recovery and
shortening pass, not a diagonal-native search).
2026-07-13 18:17:13 -06:00

164 lines
5.2 KiB
Python

"""Tests for the freeroute 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 freeroute.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_grid(tmp_path):
"""The default must stay grid — the drop-in JAR replacement behaviour."""
default = tmp_path / "default.ses"
grid = tmp_path / "grid.ses"
assert main([str(FIXTURE), "-do", str(default)]) == 0
assert main([str(FIXTURE), "--engine", "grid", "-do", str(grid)]) == 0
assert default.read_text() == grid.read_text()
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"],
}
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")