kicad-mcp/tests/test_freeroute_backend.py
Ryan Malloy f837b76547
Some checks are pending
CI / Build Package (push) Blocked by required conditions
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
Prefer native freeroute CLI over the FreeRouting JAR for routing
Add find_freeroute_cli() + _run_freeroute_cli(): run_freerouting now tries the
Java-free freeroute autorouter first (freeroute's CLI is -de/-do compatible)
and falls back to the JAR for boards freeroute cannot yet route. freeroute is
invoked as a subprocess, not imported, keeping its GPL license cleanly separated
from mckicad (MIT). Raises a clear error only when neither backend is available.

End-to-end verified: DSN -> freeroute -> SES -> native applier = 141 segments on
the Arduino_Mega board, fully headless and Java-free.
2026-07-12 13:49:55 -06:00

70 lines
2.5 KiB
Python

"""The router prefers the native freeroute CLI and falls back to the JAR."""
from __future__ import annotations
import os
from unittest.mock import MagicMock, patch
import pytest
from mckicad.utils.freerouting import FreeRoutingEngine, FreeRoutingError
def _jar_run_writes_ses(cmd, **_kwargs):
"""Simulate the FreeRouting JAR: write a .ses into the -do output directory."""
outdir = cmd[cmd.index("-do") + 1]
with open(os.path.join(outdir, "board.ses"), "w") as f:
f.write("(session board)")
return MagicMock(returncode=0, stdout="", stderr="")
def test_prefers_freeroute_when_available(tmp_path):
eng = FreeRoutingEngine(freerouting_jar_path="/fake/freerouting.jar")
with (
patch("mckicad.utils.freerouting.find_freeroute_cli", return_value="/usr/bin/freeroute"),
patch.object(eng, "_run_freeroute_cli", return_value=(True, "/out/board.ses")) as fr,
patch("mckicad.utils.freerouting.subprocess.run") as jar_run,
):
ok, ses = eng.run_freerouting("/tmp/board.dsn", "/out")
assert ok and ses == "/out/board.ses"
fr.assert_called_once()
jar_run.assert_not_called() # the JAR is never invoked when freeroute succeeds
def test_falls_back_to_jar_when_freeroute_fails(tmp_path):
jar = tmp_path / "freerouting.jar"
jar.write_text("x")
eng = FreeRoutingEngine(freerouting_jar_path=str(jar))
with (
patch("mckicad.utils.freerouting.find_freeroute_cli", return_value="/usr/bin/freeroute"),
patch.object(eng, "_run_freeroute_cli", return_value=(False, None)),
patch("mckicad.utils.freerouting.subprocess.run", side_effect=_jar_run_writes_ses),
):
ok, ses = eng.run_freerouting("/tmp/board.dsn", str(tmp_path))
assert ok and ses.endswith("board.ses")
def test_uses_jar_when_freeroute_absent(tmp_path):
jar = tmp_path / "freerouting.jar"
jar.write_text("x")
eng = FreeRoutingEngine(freerouting_jar_path=str(jar))
with (
patch("mckicad.utils.freerouting.find_freeroute_cli", return_value=None),
patch("mckicad.utils.freerouting.subprocess.run", side_effect=_jar_run_writes_ses),
):
ok, ses = eng.run_freerouting("/tmp/board.dsn", str(tmp_path))
assert ok
def test_raises_when_no_router_available(tmp_path):
eng = FreeRoutingEngine()
with (
patch("mckicad.utils.freerouting.find_freeroute_cli", return_value=None),
patch.object(eng, "find_freerouting_jar", return_value=None),
pytest.raises(FreeRoutingError),
):
eng.run_freerouting("/tmp/board.dsn", str(tmp_path))