- Fix _try_ltspice_generation() to use spicelib.simulators.ltspice_simulator.LTspice instead of the abstract Simulator base class (which always returned unavailable) - Use LTspice.create_netlist() instead of Simulator.run() for correct netlist generation - Add --ltspice-exe CLI option to specify LTspice binary path - Add --bom flag for component BOM CSV output (works on any parse completeness) - Add --bom-wiring flag for wiring BOM CSV from mapped output - Add real 1002A.asc demo circuit and pre-generated .net as test fixtures - Add @pytest.mark.ltspice marker for tests requiring LTspice binary - Bump version to 2026.2.14
203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
"""Tests for the Click CLI."""
|
|
|
|
from pathlib import Path
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from spice2wireviz.cli import main
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
|
|
|
class TestCLIBasic:
|
|
def test_version(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["--version"])
|
|
assert result.exit_code == 0
|
|
assert "2026.2.14" in result.output
|
|
|
|
def test_list_subcircuits(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [str(FIXTURES / "multi_board.net"), "--list-subcircuits"])
|
|
assert result.exit_code == 0
|
|
assert "power_supply" in result.output
|
|
assert "amplifier" in result.output
|
|
assert "io_board" in result.output
|
|
|
|
def test_list_components(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [str(FIXTURES / "multi_board.net"), "--list-components"])
|
|
assert result.exit_code == 0
|
|
assert "J_CHASSIS" in result.output
|
|
assert "TP_VCC" in result.output
|
|
|
|
def test_nonexistent_file(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["/nonexistent/file.net"])
|
|
assert result.exit_code != 0
|
|
|
|
|
|
class TestCLISingleModule:
|
|
def test_single_mode_explicit(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "simple_board.net"), "-m", "single", "-s", "amplifier_board"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "connectors:" in result.output
|
|
assert "amplifier_board" in result.output
|
|
|
|
def test_single_mode_auto_detect(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main, [str(FIXTURES / "simple_board.net"), "-s", "amplifier_board"]
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "connectors:" in result.output
|
|
|
|
def test_single_mode_only_subcircuit(self):
|
|
"""When netlist has one subcircuit and no instances, auto-detect single mode."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [str(FIXTURES / "simple_board.net")])
|
|
assert result.exit_code == 0
|
|
assert "connectors:" in result.output
|
|
|
|
def test_dry_run(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "simple_board.net"), "-s", "amplifier_board", "--dry-run"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "Mode: single" in result.output
|
|
assert "Connectors:" in result.output
|
|
|
|
|
|
class TestCLIInterModule:
|
|
def test_inter_mode_explicit(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main, [str(FIXTURES / "multi_board.net"), "-m", "inter"]
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "connectors:" in result.output
|
|
assert "cables:" in result.output
|
|
|
|
def test_inter_mode_auto_detect(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [str(FIXTURES / "multi_board.net")])
|
|
assert result.exit_code == 0
|
|
assert "connectors:" in result.output
|
|
|
|
def test_output_to_file(self, tmp_path):
|
|
out = tmp_path / "output.yml"
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main, [str(FIXTURES / "multi_board.net"), "-o", str(out)]
|
|
)
|
|
assert result.exit_code == 0
|
|
assert out.exists()
|
|
content = out.read_text()
|
|
assert "connectors:" in content
|
|
|
|
|
|
class TestCLIFiltering:
|
|
def test_no_ground(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main, [str(FIXTURES / "multi_board.net"), "--no-ground"]
|
|
)
|
|
assert result.exit_code == 0
|
|
# GND should not appear in pinlabels
|
|
assert "- GND" not in result.output
|
|
|
|
def test_include_prefixes(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "multi_board.net"), "--include-prefixes", "J"],
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
def test_exclude_refs(self):
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "multi_board.net"), "--exclude-refs", "X1,TP_VCC"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "X1:" not in result.output
|
|
|
|
|
|
class TestCLIAscInput:
|
|
"""Tests for .asc file input through the CLI."""
|
|
|
|
def test_asc_with_companion_works(self):
|
|
"""simple_board.asc has companion .net — full pipeline should work."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "simple_board.asc"), "-s", "amplifier_board"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "connectors:" in result.output
|
|
assert "amplifier_board" in result.output
|
|
|
|
def test_asc_companion_resolution_in_stderr(self):
|
|
"""CLI should report which .net was resolved to stderr."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "simple_board.asc"), "-s", "amplifier_board"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "simple_board.net" in result.stderr
|
|
|
|
def test_asc_without_companion_errors(self):
|
|
"""standalone.asc has no companion .net — diagram generation should fail."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [str(FIXTURES / "standalone.asc")])
|
|
assert result.exit_code != 0
|
|
assert "connectivity" in result.output.lower() or "connectivity" in (
|
|
getattr(result, "stderr", "") or ""
|
|
).lower()
|
|
|
|
def test_asc_without_companion_shows_remediation(self):
|
|
"""Error message should tell the user how to fix it."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, [str(FIXTURES / "standalone.asc")])
|
|
assert result.exit_code != 0
|
|
stderr = result.stderr
|
|
assert "--generate-netlist" in stderr or "--list-components" in stderr
|
|
|
|
def test_asc_list_components_on_metadata_only(self):
|
|
"""--list-components should work even without connectivity."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "standalone.asc"), "--list-components"],
|
|
)
|
|
# Should succeed (exit 0) — inspection doesn't need connectivity
|
|
assert result.exit_code == 0
|
|
|
|
def test_asc_list_subcircuits_with_companion(self):
|
|
"""--list-subcircuits through .asc with companion works."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "simple_board.asc"), "--list-subcircuits"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "amplifier_board" in result.output
|
|
|
|
def test_asc_dry_run_with_companion(self):
|
|
"""--dry-run through .asc with companion produces summary."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
main,
|
|
[str(FIXTURES / "simple_board.asc"), "-s", "amplifier_board", "--dry-run"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "Mode: single" in result.output
|