freeroute/tests/ses/test_indent.py
Ryan Malloy 12b0a231f0 Implement Specctra SES session-file writer
Ports the write path of io/specctra/SesWriter.java to emit a valid
session from a parsed DsnBoard plus a RoutingResult. Each _write_*
function mirrors a write* method upstream:

- session scope with base_design, placement (resolution + components
  echoed from the DSN), an empty was_is, and routes
- routes carries resolution, a reduced parser scope, library_out with
  the via padstacks, and network_out
- network_out emits (net (wire (path layer width x1 y1 ...)) (via
  padstack x y)) for each routed net; integer coordinates via round-
  half-up to match Java Math.round

write_ses(board) with no result produces a valid no-op session,
proving the DSN-in / SES-out round trip. Only routes > network_out is
required by SesReader and kicad-cli; the rest is echoed for a
well-formed file.

18 tests: indent/quoting rules, no-op session structure, placement and
library_out echo, routed wire/via serialization, integer rounding, and
round-trips re-parsed through the project's own DSN tokenizer/sexp
(64 tests total across the suite, all green; ruff clean).
2026-07-11 16:34:19 -06:00

64 lines
1.7 KiB
Python

"""Tests for the SES indent writer and identifier quoting."""
from __future__ import annotations
from freeroute.ses.indent import Identifier, IndentWriter
def test_nested_scopes_indent_by_two_spaces():
out = IndentWriter()
out.start_scope(new_line=False)
out.write("session x")
out.start_scope()
out.write("routes")
out.end_scope()
out.end_scope()
assert out.getvalue() == "(session x\n (routes\n )\n)"
def test_new_line_uses_current_indent():
out = IndentWriter()
out.start_scope(new_line=False)
out.write("a")
out.new_line()
out.write("(b)")
out.end_scope()
assert out.getvalue() == "(a\n (b)\n)"
def test_plain_name_not_quoted():
ident = Identifier()
assert ident.format("Top") == "Top"
assert ident.format("U1") == "U1"
assert ident.format("F.Cu") == "F.Cu" # '.' is not reserved
def test_reserved_chars_force_quotes():
ident = Identifier()
assert ident.format("NET_A") == '"NET_A"' # underscore reserved
assert ident.format("a-b") == '"a-b"' # hyphen reserved
assert ident.format("with space") == '"with space"'
assert ident.format("Via[0-1]_600:300_um") == '"Via[0-1]_600:300_um"'
def test_leading_digit_forces_quotes():
ident = Identifier()
assert ident.format("2layer") == '"2layer"'
assert ident.format("-3x") == '"-3x"'
def test_non_ascii_forces_quotes():
ident = Identifier()
assert ident.format("Паяльная") == '"Паяльная"'
def test_surrounding_quotes_stripped_before_reprocessing():
ident = Identifier()
# already-quoted plain name comes back unquoted
assert ident.format('"Top"') == "Top"
def test_embedded_quote_removed():
ident = Identifier()
assert ident.format('a"b') == "ab"