fix(task-12): refuse non-finite points, non-axis ports and unfillable number formats

Three ways `engineering/pid/lines.py` could emit a silent wrong result or
an opaque crash instead of an honest refusal:

- `format_line_number` only recognised a token that was exactly `{field}`;
  `P-{seq:04d}`, `{size}"-{service}` or `{bogus}x` were copied verbatim,
  braces and all, into the line number. Tokens are now parsed with
  `string.Formatter`: one named known field per token, a format spec is
  honoured (`{seq:04d}` -> `0007`), literal decoration around the field
  survives with it and drops out with it, and conversions, positional
  fields, multi-field tokens, unbalanced braces and specs the value cannot
  take are all refused by token name.
- `route` never checked finiteness: a NaN `end` collapsed the U candidate
  to a valid two-point "straight" path ending at the stub point. Every
  coordinate of start/end/waypoints and the stub are now checked and the
  refusal names the offending `end.x` / `waypoints[1].y` / stub.
- A non-axis port direction (45.0, 90.5, nan) raised `KeyError` from the
  AXIS lookup. `axis_direction()` now normalises with a 1e-6 deg tolerance
  for engine drift and otherwise raises ValueError naming `start_dir` /
  `end_dir` and how to resolve it (rotate to a multiple of 90 or pass
  waypoints).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-15 13:30:02 +03:00
co-authored by Claude Opus 5
parent 6dd25ff0c2
commit 2833e1bc93
2 changed files with 142 additions and 22 deletions
+47
View File
@@ -129,3 +129,50 @@ def test_line_number_format_drops_empty_fields():
assert format_line_number("L-{seq}", seq=7) == "L-7"
with pytest.raises(ValueError, match="unknown"):
format_line_number("{bogus}", seq=1)
def test_line_number_format_fills_specs_and_refuses_what_it_cannot_fill():
# A format spec is honoured, and literal text around a field survives with it.
assert format_line_number("P-{seq:04d}", seq=7) == "P-0007"
assert format_line_number('{size}"-{service}-{seq}', size=4, service="P", seq=1) == '4"-P-1'
# A token whose field is empty drops out with its literal decoration.
assert format_line_number('{size}"-{service}-{seq}', service="P", seq=1) == "P-1"
# Nothing with a brace in it is ever copied verbatim into the number.
with pytest.raises(ValueError, match="unknown"):
format_line_number("{bogus}x-{seq}", seq=7)
with pytest.raises(ValueError, match="one field"):
format_line_number("{area}{unit}-{seq}", area="1", unit="2", seq=7)
with pytest.raises(ValueError, match="conversion"):
format_line_number("{seq!r}", seq=7)
with pytest.raises(ValueError, match="positional"):
format_line_number("{}-{seq}", seq=7)
with pytest.raises(ValueError, match=r"'\{seq'"):
format_line_number("{seq", seq=7)
with pytest.raises(ValueError, match=r"'\{seq:04d\}'"):
format_line_number("{seq:04d}", seq="abc")
@pytest.mark.parametrize("bad", [float("nan"), float("inf"), -float("inf")])
def test_route_refuses_non_finite_coordinates(bad):
with pytest.raises(ValueError, match=r"end\.x"):
route((0, 0), 0.0, (bad, 0), 180.0)
with pytest.raises(ValueError, match=r"end\.x"):
route((0, 0), 0.0, (bad, 0), 180.0, mode="direct")
with pytest.raises(ValueError, match=r"start\.y"):
route((0, bad), 0.0, (50, 0), 180.0)
with pytest.raises(ValueError, match=r"waypoints\[1\]\.x"):
route((0, 0), 0.0, (10, 7), 90.0, mode=[(10, 0), (bad, 0)])
with pytest.raises(ValueError, match="stub"):
route((0, 0), 0.0, (50, 0), 180.0, stub=bad)
def test_route_refuses_non_axis_port_directions_with_a_value_error():
with pytest.raises(ValueError, match=r"start_dir 45\.0 is not an axis direction"):
route((0, 0), 45.0, (50, 50), 225.0)
with pytest.raises(ValueError, match=r"end_dir 90\.5 is not an axis direction"):
route((0, 0), 0.0, (50, 30), 90.5)
with pytest.raises(ValueError, match="start_dir nan"):
route((0, 0), float("nan"), (50, 0), 180.0)
# Equivalent angles and engine float drift still resolve to the axis.
assert route((0, 0), -360.0, (50, 0), 540.0) == [(0.0, 0.0), (50.0, 0.0)]
assert route((0, 0), 1e-9, (50, 0), 180.0 - 1e-9) == [(0.0, 0.0), (50.0, 0.0)]