A pair walked in both directions is a 2-cycle in the edge set; the longest-path relaxation loops through it and inflates every downstream depth toward the node-count cap, dragging other relays hard against the ends (a one-hop relay landed at x=0.09 instead of its natural 0.33). Rank balanced_x over the graph with each such pair merged to one representative — the acyclic flow the picture really is; the pair, drawn as a single vertical, rightly shares an x anyway. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
247 lines
9.7 KiB
Python
247 lines
9.7 KiB
Python
"""Unit tests for the shared route-graph widget (the diverge/converge braille renderer)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from meshterm.ui.pathgraph import (
|
|
DST_NODE,
|
|
SRC_NODE,
|
|
PathLayer,
|
|
_balanced_x,
|
|
_coalesce_prefixes,
|
|
_route,
|
|
render_path_graph,
|
|
)
|
|
|
|
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
|
|
WHITE = (255, 255, 255)
|
|
GREEN = (74, 222, 128)
|
|
YELLOW = (250, 204, 21)
|
|
GREY = (120, 120, 120)
|
|
|
|
|
|
def _glyph(node: str) -> tuple[str, str]:
|
|
# Deliberately off-palette marker colours, so colour assertions about *edges*
|
|
# can never accidentally match a marker's escape sequence.
|
|
if node in (SRC_NODE, DST_NODE):
|
|
return ("★", "#123456")
|
|
return ("●", "#654321")
|
|
|
|
|
|
def _render(layers, label_of=None): # noqa: ANN001
|
|
return render_path_graph(
|
|
layers,
|
|
60,
|
|
glyph_of=_glyph,
|
|
label_of=label_of or (lambda node: "you" if node in (SRC_NODE, DST_NODE) else node[:2]),
|
|
label_rgb_of=lambda _node: WHITE,
|
|
)
|
|
|
|
|
|
def _sgr(rgb: tuple[int, int, int]) -> str:
|
|
r, g, b = rgb
|
|
return f"38;2;{r};{g};{b}m"
|
|
|
|
|
|
def test_no_layers_render_nothing() -> None:
|
|
assert render_path_graph(
|
|
[], 60, glyph_of=_glyph, label_of=lambda n: None, label_rgb_of=lambda n: WHITE
|
|
) == []
|
|
|
|
|
|
def test_identical_paths_collapse_to_the_top_layer() -> None:
|
|
"""The same route in two layers draws once, in the higher priority's colour."""
|
|
layers = [
|
|
PathLayer(("aa", "bb"), YELLOW, 2),
|
|
PathLayer(("aa", "bb"), WHITE, 4),
|
|
]
|
|
joined = "\n".join(_render(layers))
|
|
assert _sgr(WHITE) in joined
|
|
assert _sgr(YELLOW) not in joined
|
|
|
|
|
|
def test_distinct_layers_draw_in_their_own_colours() -> None:
|
|
"""Three different routes keep three edge colours on one canvas."""
|
|
layers = [
|
|
PathLayer(("aa", "bb"), YELLOW, 2),
|
|
PathLayer(("cc",), GREEN, 3),
|
|
PathLayer(("dd", "ee", "ff"), WHITE, 4),
|
|
]
|
|
joined = "\n".join(_render(layers))
|
|
for rgb in (YELLOW, GREEN, WHITE):
|
|
assert _sgr(rgb) in joined
|
|
|
|
|
|
def test_labels_follow_the_callback_only_self_named() -> None:
|
|
"""A ``None`` label leaves the marker bare — a caller's unnamed-relay mode."""
|
|
layers = [PathLayer(("ab", "cd"), WHITE, 4)]
|
|
lines = _render(
|
|
layers,
|
|
label_of=lambda node: "Base" if node in (SRC_NODE, DST_NODE) else None,
|
|
)
|
|
plain = _ANSI.sub("", "\n".join(lines))
|
|
assert "Base" in plain
|
|
assert "ab" not in plain and "cd" not in plain
|
|
|
|
|
|
def test_endpoint_labels_may_leave_the_marker_row() -> None:
|
|
"""Endpoints place like relays: a long name lands whole, even off the centre row."""
|
|
layers = [PathLayer(tuple(f"{i:02x}" for i in range(6)), WHITE, 4)]
|
|
lines = _render(
|
|
layers,
|
|
label_of=lambda node: "VeryLongStationName"
|
|
if node in (SRC_NODE, DST_NODE)
|
|
else None,
|
|
)
|
|
plain = _ANSI.sub("", "\n".join(lines))
|
|
# The name fits the 60-cell canvas, so it is kept whole — no fixed label budget clips it.
|
|
assert "VeryLongStationName" in plain
|
|
assert "…" not in plain
|
|
|
|
|
|
def test_prefix_dupe_relay_folds_onto_its_only_wide_match() -> None:
|
|
"""A 1-byte hop beside the same node's wide id, across layers, becomes one node."""
|
|
layers = [
|
|
PathLayer(("e9043958308d", "be"), WHITE, 3),
|
|
PathLayer(("3d63c6429436", "be1d1c1dbc4b"), GREY, 2),
|
|
]
|
|
out = _coalesce_prefixes(layers)
|
|
assert out[0].hops == ("e9043958308d", "be1d1c1dbc4b") # "be" rewritten to the wide id
|
|
assert out[1].hops == ("3d63c6429436", "be1d1c1dbc4b")
|
|
|
|
|
|
def test_ambiguous_prefix_relay_is_left_alone() -> None:
|
|
"""A short hop that prefixes two distinct present nodes is not guessed onto either."""
|
|
layers = [
|
|
PathLayer(("be",), WHITE, 3),
|
|
PathLayer(("be1d1c1dbc4b",), GREY, 2),
|
|
PathLayer(("bedd2bff0011",), GREY, 2),
|
|
]
|
|
out = _coalesce_prefixes(layers)
|
|
assert out[0].hops == ("be",) # opens two present nodes — kept as its honest short self
|
|
|
|
|
|
def test_a_shared_relay_draws_a_single_marker() -> None:
|
|
"""A relay two paths pass through is one vertex, labelled once — never doubled."""
|
|
layers = [PathLayer(("aa", "bb"), WHITE, 3), PathLayer(("cc", "bb"), YELLOW, 2)]
|
|
plain = _ANSI.sub("", "\n".join(_render(layers)))
|
|
assert plain.count("bb") == 1 # the shared relay is seated once
|
|
assert plain.count("aa") == 1 and plain.count("cc") == 1
|
|
|
|
|
|
def test_every_label_shows_on_a_busy_graph() -> None:
|
|
"""Four distinct routes lay out as separated lanes — every relay label lands, none dropped."""
|
|
names = {
|
|
"aa": "Alpha", "bb": "Bravo", "cc": "Charlie", "dd": "Delta",
|
|
"ee": "Echo", "ff": "Foxtrot", "gg": "Golf",
|
|
}
|
|
layers = [
|
|
PathLayer(("aa", "bb"), WHITE, 4),
|
|
PathLayer(("cc", "dd"), GREY, 2),
|
|
PathLayer(("ee", "ff"), GREY, 2),
|
|
PathLayer(("gg",), GREY, 2),
|
|
]
|
|
plain = _ANSI.sub("", "\n".join(_render(layers, label_of=lambda n: names.get(n[:2]))))
|
|
for name in names.values():
|
|
assert name in plain, f"{name} was dropped from the graph"
|
|
|
|
|
|
def test_edge_pinned_relay_labels_are_not_dropped() -> None:
|
|
"""A relay a balanced rank pins near a canvas edge still shows its whole name.
|
|
|
|
A long chain hung off ``cc`` drives its balanced rank hard against the origin and pushes
|
|
the shared ``bb`` hard against us, so both land near an edge where a *centred* long label
|
|
overhangs the canvas. Those anchors must slide inward — like an endpoint's — rather than
|
|
place nothing and leave the marker silently unlabelled.
|
|
"""
|
|
names = {"aa": "UpperStation", "bb": "RightEdgeStation", "cc": "LeftEdgeStation"}
|
|
tail = tuple(f"g{i}" for i in range(1, 13))
|
|
layers = [
|
|
PathLayer(("bb",), WHITE, 3),
|
|
PathLayer(("aa", "bb"), GREY, 2),
|
|
PathLayer(("cc", *tail, "bb"), GREY, 1),
|
|
]
|
|
plain = _ANSI.sub("", "\n".join(_render(layers, label_of=lambda n: names.get(n))))
|
|
for name in names.values():
|
|
assert name in plain, f"{name} was dropped near a canvas edge"
|
|
|
|
|
|
def test_a_subsumed_route_adds_no_duplicate_markers() -> None:
|
|
"""A route whose hops are all carried by a stronger one draws through them, not doubled."""
|
|
layers = [
|
|
PathLayer(("aa", "bb", "cc"), WHITE, 4), # the backbone owns aa, bb, cc
|
|
PathLayer(("aa", "cc"), GREY, 2), # a shortcut over the same relays — no new node
|
|
]
|
|
plain = _ANSI.sub("", "\n".join(_render(layers)))
|
|
for hop in ("aa", "bb", "cc"):
|
|
assert plain.count(hop) == 1 # each relay seated exactly once
|
|
|
|
|
|
def test_route_seats_each_node_on_a_level_platform() -> None:
|
|
"""A lane-changing edge leaves and enters level, so neither node is a pointed apex."""
|
|
pts = _route("u", "v", {"u": (0, 1), "v": (40, 9)}, bidir=False)
|
|
assert pts[0] == (0, 1) and pts[-1] == (40, 9) # the two markers are the ends
|
|
assert pts[0][1] == pts[1][1] # level out of u — u sits flat
|
|
assert pts[-1][1] == pts[-2][1] # level into v — v sits flat
|
|
|
|
|
|
def test_route_same_lane_is_one_level_run() -> None:
|
|
"""Two nodes on the same lane join with a single straight horizontal segment."""
|
|
assert _route("u", "v", {"u": (0, 5), "v": (40, 5)}, bidir=False) == [(0, 5), (40, 5)]
|
|
|
|
|
|
def test_route_orients_left_to_right() -> None:
|
|
"""A right-to-left node pair is flipped, so the trapezium always reads as forward flow."""
|
|
forward = _route("u", "v", {"u": (0, 1), "v": (40, 9)}, bidir=False)
|
|
reverse = _route("u", "v", {"u": (40, 9), "v": (0, 1)}, bidir=False)
|
|
assert reverse == forward # same picture whichever way the pair is handed in
|
|
|
|
|
|
def test_route_bidirectional_is_a_single_straight_segment() -> None:
|
|
"""A two-way pair is the sole straight (near-vertical) edge — never a trapezium."""
|
|
assert _route("u", "v", {"u": (20, 1), "v": (24, 30)}, bidir=True) == [(20, 1), (24, 30)]
|
|
|
|
|
|
def test_bidir_pair_does_not_jam_a_sibling_relay_against_the_origin() -> None:
|
|
"""A both-ways pair's 2-cycle must not inflate the rank and squash another relay left.
|
|
|
|
``aa`` and ``bb`` are walked in both directions (the vertical-drawn pair); ``cc`` is a
|
|
plain relay one hop off the origin feeding ``bb``. Ranked over the raw edge set the cycle
|
|
would drive ``cc`` toward ``0``; collapsing the pair leaves ``cc`` at its natural third of
|
|
the way across, and the pair shares one x (they draw as a single vertical).
|
|
"""
|
|
layers = [
|
|
PathLayer(("bb",), WHITE, 3),
|
|
PathLayer(("aa", "bb"), GREY, 2),
|
|
PathLayer(("bb", "aa"), GREY, 2), # the reverse — makes aa<->bb a 2-cycle
|
|
PathLayer(("cc", "bb"), GREY, 1),
|
|
]
|
|
seqs = [(SRC_NODE, *layer.hops, DST_NODE) for layer in layers]
|
|
ordered = list(dict.fromkeys(n for seq in seqs for n in seq))
|
|
edges = {pair for seq in seqs for pair in zip(seq, seq[1:])}
|
|
x = _balanced_x(ordered, edges)
|
|
assert x[SRC_NODE] == 0.0 and x[DST_NODE] == 1.0
|
|
assert x["cc"] == pytest.approx(1 / 3) # its natural spot, not jammed toward 0
|
|
assert x["aa"] == x["bb"] # the pair shares one x — it draws as a single vertical
|
|
assert x["cc"] < x["aa"] # and still sits left of the pair it feeds
|
|
|
|
|
|
def test_labels_ellipsize_only_when_wider_than_the_canvas() -> None:
|
|
"""A name shortens only when it physically cannot fit — wider than the whole canvas."""
|
|
name = "x" * 30
|
|
lines = render_path_graph(
|
|
[PathLayer(("aa",), WHITE, 4)],
|
|
20,
|
|
glyph_of=_glyph,
|
|
label_of=lambda node: name if node in (SRC_NODE, DST_NODE) else None,
|
|
label_rgb_of=lambda _node: WHITE,
|
|
)
|
|
plain = _ANSI.sub("", "\n".join(lines))
|
|
assert name not in plain # the full 30-cell name cannot fit a 20-cell canvas
|
|
assert "…" in plain # so it is ellipsized to what fits
|
|
assert all(len(_ANSI.sub("", ln)) <= 20 for ln in lines) # never overruns the width
|