use lini::testing::{node_rect, route_sample, routes_str};
use lini::{Rule, Severity};
const PCB: &str = include_str!("../samples/pcb.lini");
type Routes = Vec<((String, String), Vec<(f64, f64)>)>;
fn routes(src: &str) -> Routes {
routes_str(src).expect("compiles and routes")
}
fn path<'a>(routes: &'a Routes, from: &str, to: &str) -> &'a [(f64, f64)] {
&routes
.iter()
.find(|((a, b), _)| a == from && b == to)
.unwrap_or_else(|| panic!("no drawn route {from} -> {to}"))
.1
}
fn paths<'a>(routes: &'a Routes, from: &str, to: &str) -> Vec<&'a [(f64, f64)]> {
routes
.iter()
.filter(|((a, b), _)| a == from && b == to)
.map(|(_, p)| p.as_slice())
.collect()
}
fn turns(path: &[(f64, f64)]) -> usize {
let sign = |v: f64| {
if v > 0.0 {
1
} else if v < 0.0 {
-1
} else {
0
}
};
let dir = |a: (f64, f64), b: (f64, f64)| (sign(b.0 - a.0), sign(b.1 - a.1));
path.windows(3)
.filter(|w| dir(w[0], w[1]) != dir(w[1], w[2]))
.count()
}
fn orthogonal(p: &[(f64, f64)]) {
for s in p.windows(2) {
assert!(
s[0].0 == s[1].0 || s[0].1 == s[1].1,
"diagonal segment {s:?} in {p:?}"
);
assert!(s[0] != s[1], "zero-length segment in {p:?}");
}
}
fn report(src: &str) -> Vec<lini::Violation> {
lini::validate_str(src).expect("compiles")
}
fn crossings(src: &str) -> usize {
report(src)
.iter()
.filter(|v| v.rule == Rule::Crossing)
.count()
}
fn impossibles(src: &str) -> usize {
report(src)
.iter()
.filter(|v| v.rule == Rule::Impossible && v.severity == Severity::Warning)
.count()
}
#[test]
fn facing_aligned_centres_draw_one_straight_wire() {
let r = routes(
"{ direction: row; gap: 100; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#b| { width: 60; height: 60 }\n\
a -> b\n",
);
let p = path(&r, "a", "b");
assert_eq!(p.len(), 2, "straight: {p:?}");
assert_eq!(p[0].1, p[1].1, "horizontal: {p:?}");
}
#[test]
fn offset_within_windows_still_draws_straight() {
let r = routes(
"{ direction: row; gap: 100; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#b| { width: 60; height: 60; translate: 0 20 }\n\
a -> b\n",
);
let p = path(&r, "a", "b");
assert_eq!(p.len(), 2, "windows overlap → straight: {p:?}");
assert_eq!(p[0].1, p[1].1);
}
#[test]
fn offset_past_windows_jogs_once_on_the_gap_midline() {
let src = "{ direction: row; gap: 100; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#b| { width: 60; height: 60; translate: 0 60 }\n\
a:right -> b:left\n";
let r = routes(src);
let p = path(&r, "a", "b");
orthogonal(p);
assert_eq!(p.len(), 4, "one dogleg: {p:?}");
let laid = route_sample(src, 10.0);
let a = node_rect(&laid, "a").expect("a placed");
let b = node_rect(&laid, "b").expect("b placed");
let midline = ((a.2 + 10.0) + (b.0 - 10.0)) / 2.0;
assert!(
(p[1].0 - midline).abs() < 1e-9 && (p[2].0 - midline).abs() < 1e-9,
"jog on the gap midline {midline}: {p:?}"
);
}
#[test]
fn offset_past_windows_without_forced_sides_takes_the_one_turn_l() {
let r = routes(
"{ direction: row; gap: 100; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#b| { width: 60; height: 60; translate: 0 60 }\n\
a -> b\n",
);
let p = path(&r, "a", "b");
orthogonal(p);
assert_eq!(turns(p), 1, "one corner beats the dogleg: {p:?}");
}
#[test]
fn a_bundle_draws_parallel_rails_at_pitch_centred_on_the_midline() {
let src = "{ direction: row; gap: 100; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#b| { width: 60; height: 60 }\n\
a -> b\na -> b\na -> b\na -> b\n";
let r = routes(src);
let rails = paths(&r, "a", "b");
assert_eq!(rails.len(), 4);
let mut ys: Vec<f64> = rails
.iter()
.map(|p| {
assert_eq!(p.len(), 2, "each rail straight: {p:?}");
p[0].1
})
.collect();
ys.sort_by(f64::total_cmp);
for w in ys.windows(2) {
assert!((w[1] - w[0] - 10.0).abs() < 1e-9, "rails at pitch: {ys:?}");
}
let laid = route_sample(src, 10.0);
let a = node_rect(&laid, "a").expect("a placed");
let centre = (a.1 + a.3) / 2.0;
let mean = ys.iter().sum::<f64>() / 4.0;
assert!(
(mean - centre).abs() < 1e-9,
"ladder centred on the aligned centres {centre}: {ys:?}"
);
}
#[test]
fn a_wire_over_the_top_hugs_the_keepout_not_the_margin() {
let src = "{ direction: row; gap: 80; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#w| { width: 20; height: 200 }\n\
|box#b| { width: 60; height: 60 }\n\
a -> b\n";
let r = routes(src);
let p = path(&r, "a", "b");
orthogonal(p);
let laid = route_sample(src, 10.0);
let w = node_rect(&laid, "w").expect("w placed");
let hug = w.3 + 10.0;
assert!(
p.windows(2)
.any(|s| s[0].1 == s[1].1 && (s[0].1 - hug).abs() < 1e-9),
"the crossing run hugs the keep-out at {hug}: {p:?}"
);
}
#[test]
fn a_crossing_beats_the_orbit() {
let src = "{ layout: grid; columns: repeat(3, 60); rows: repeat(3, 60); gap: 30; clearance: 10 }\n\
|box#n| { cell: 2 1; width: 60; height: 60 }\n\
|box#a| { cell: 1 2; width: 60; height: 60 }\n\
|box#b| { cell: 3 2; width: 60; height: 60 }\n\
|box#s| { cell: 2 3; width: 60; height: 60 }\n\
n -> s\n\
a -> b\n";
let r = routes(src);
assert_eq!(path(&r, "n", "s").len(), 2, "n→s straight");
assert_eq!(path(&r, "a", "b").len(), 2, "a→b crosses, never orbits");
assert_eq!(crossings(src), 1, "one drawn crossing, reported");
}
#[test]
fn a_forced_side_is_honored_when_reachable() {
let src = "{ direction: row; gap: 60; clearance: 10 }\n\
|box#w| { width: 20; height: 200 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#b| { width: 60; height: 60 }\n\
a:left -> b\n";
let r = routes(src);
let p = path(&r, "a", "b");
let laid = route_sample(src, 10.0);
let a = node_rect(&laid, "a").expect("a placed");
assert_eq!(p[0].0, a.0, "leaves a's left side: {p:?}");
}
#[test]
fn a_walled_forced_side_strays() {
let src = "{ layout: grid; columns: repeat(2, 60); rows: repeat(2, 60); gap: 8; clearance: 10 }\n\
|box#w| { cell: 1 1; width: 60; height: 60 }\n\
|box#a| { cell: 2 1; width: 60; height: 60 }\n\
|box#b| { cell: 2 2; width: 60; height: 60 }\n\
a:left -> b\n";
let r = routes(src);
assert!(
paths(&r, "a", "b").is_empty(),
"no lawful-looking wire for a blocked forced side"
);
assert_eq!(impossibles(src), 1);
}
#[test]
fn fan_siblings_share_their_first_point() {
let src = "{ layout: grid; columns: repeat(2, 60); rows: repeat(2, 60); gap: 40; clearance: 10 }\n\
|box#a| { cell: 1 1; span: 1 2; width: 60; height: 60 }\n\
|box#b| { cell: 2 1; width: 60; height: 60 }\n\
|box#c| { cell: 2 2; width: 60; height: 60 }\n\
a -> b & c\n";
let r = routes(src);
let (pb, pc) = (path(&r, "a", "b"), path(&r, "a", "c"));
assert_eq!(pb[0], pc[0], "one fan, one port: {pb:?} vs {pc:?}");
}
#[test]
fn a_self_loop_wraps_the_top_right_corner() {
let src = "{ clearance: 10 }\n|box#a| { width: 80; height: 40 }\na -> a\n";
let r = routes(src);
let p = path(&r, "a", "a");
orthogonal(p);
let laid = route_sample(src, 10.0);
let a = node_rect(&laid, "a").expect("a placed");
let (cx, cy) = ((a.0 + a.2) / 2.0, (a.1 + a.3) / 2.0);
assert_eq!(
p,
&[
(a.2, cy),
(a.2 + 10.0, cy),
(a.2 + 10.0, a.1 - 10.0),
(cx, a.1 - 10.0),
(cx, a.1)
],
"right → top around the keep-out corner"
);
}
#[test]
fn a_self_loop_forced_onto_one_side_is_reported() {
let src = "{ clearance: 10 }\n|box#a| { width: 80; height: 40 }\na:top -> a:top\n";
assert!(paths(&routes(src), "a", "a").is_empty());
let rep = report(src);
assert!(
rep.iter()
.any(|v| v.rule == Rule::Impossible && v.detail.contains("one side")),
"{rep:?}"
);
}
#[test]
fn a_containment_link_lands_on_the_parents_inner_side() {
let src = "{ clearance: 10 }\n\
|group#p| { padding: 30 } [ |box#c| { width: 40; height: 40 } ]\n\
p -> p.c\n";
let r = routes(src);
let p = path(&r, "p", "p.c");
orthogonal(p);
let laid = route_sample(src, 10.0);
let pr = node_rect(&laid, "p").expect("p placed");
let on_side = p[0].0 == pr.0 || p[0].0 == pr.2 || p[0].1 == pr.1 || p[0].1 == pr.3;
assert!(on_side, "p's end sits on p's own boundary: {p:?} vs {pr:?}");
for pt in p {
assert!(
pt.0 >= pr.0 && pt.0 <= pr.2 && pt.1 >= pr.1 && pt.1 <= pr.3,
"the wire stays inside the parent: {p:?}"
);
}
}
#[test]
fn routing_straight_draws_one_trimmed_oblique_segment() {
let src = "{ layout: grid; columns: repeat(2, 60); rows: repeat(2, 60); gap: 40; \
clearance: 10; routing: straight }\n\
|box#a| { cell: 1 1; width: 60; height: 60 }\n\
|box#b| { cell: 2 2; width: 60; height: 60 }\n\
a -> b\n";
let r = routes(src);
let p = path(&r, "a", "b");
assert_eq!(p.len(), 2, "one segment: {p:?}");
assert!(
p[0].0 != p[1].0 && p[0].1 != p[1].1,
"oblique, no avoidance: {p:?}"
);
let laid = route_sample(src, 10.0);
let a = node_rect(&laid, "a").expect("a placed");
let b = node_rect(&laid, "b").expect("b placed");
let close = |p: (f64, f64), q: (f64, f64)| (p.0 - q.0).abs() < 1e-9 && (p.1 - q.1).abs() < 1e-9;
assert!(close(p[0], (a.2, a.3)), "trimmed to a's corner: {p:?}");
assert!(close(p[1], (b.0, b.1)), "trimmed to b's corner: {p:?}");
assert_eq!(impossibles(src), 0);
assert_eq!(crossings(src), 0);
}
#[test]
fn routing_straight_self_link_draws_the_rectangular_hook() {
let src = "{ clearance: 16; routing: straight }\n|box#a| { width: 80; height: 40 }\na -> a\n";
let r = routes(src);
let p = path(&r, "a", "a");
assert_eq!(p.len(), 4, "the rectangular self-hook: {p:?}");
let laid = route_sample(src, 16.0);
let a = node_rect(&laid, "a").expect("a placed");
assert!(
p.iter().all(|pt| pt.0 >= a.2),
"the hook hangs off the right side: {p:?}"
);
assert_eq!(p[0].0, a.2);
assert_eq!(p[3].0, a.2);
}
#[test]
fn a_bundle_compresses_uniformly_then_strays_exactly_at_capacity() {
let src = |h: u32| {
format!(
"{{ direction: row; gap: 100; clearance: 10 }}\n\
|box#a| {{ width: 60; height: {h} }}\n\
|box#b| {{ width: 60; height: {h} }}\n\
a:right -> b:left\na:right -> b:left\na:right -> b:left\na:right -> b:left\n"
)
};
for h in (30..=60).rev() {
let text = src(h);
let a = node_rect(&route_sample(&text, 10.0), "a").expect("a placed");
let window = (a.3 - a.1) - 20.0;
let rails = routes(&text);
let report = report(&text);
let breaches: Vec<_> = report
.iter()
.filter(|v| v.severity == Severity::Warning && v.rule != Rule::Impossible)
.collect();
assert!(breaches.is_empty(), "h={h}: {breaches:?}");
if window < 15.0 {
assert!(rails.is_empty(), "h={h}: past capacity, all stray");
assert_eq!(impossibles(&text), 4, "h={h}");
continue;
}
assert_eq!(rails.len(), 4, "h={h}: the bundle routes whole");
let mut ys: Vec<f64> = rails
.iter()
.map(|(_, p)| {
assert_eq!(p.len(), 2, "h={h}: rails stay straight: {p:?}");
p[0].1
})
.collect();
ys.sort_by(f64::total_cmp);
let expect = (window / 3.0).min(10.0);
for w in ys.windows(2) {
assert!(
(w[1] - w[0] - expect).abs() < 1e-9,
"h={h}: uniform pitch {expect}, got {ys:?}"
);
}
}
}
#[test]
fn a_full_side_spills_to_other_sides_then_strays() {
let src = |n: usize| {
let mut s = String::from(
"{ layout: grid; columns: 40, 200, 40; rows: repeat(15, 34); gap: 10; clearance: 10 }\n\
|box#t| { cell: 2 8; width: 30; height: 30 }\n",
);
for i in 0..n {
s.push_str(&format!(
"|box#s{i}| {{ cell: 1 {}; width: 30; height: 30 }}\n",
i + 1
));
}
for i in 0..n {
s.push_str(&format!("s{i} -> t\n"));
}
s
};
for n in [1, 3, 6, 12, 13, 14] {
let text = src(n);
let r = routes(&text);
let breaches: Vec<_> = report(&text)
.into_iter()
.filter(|v| v.severity == Severity::Warning && v.rule != Rule::Impossible)
.collect();
assert!(breaches.is_empty(), "n={n}: {breaches:?}");
let drawn = r.len();
assert_eq!(
drawn + impossibles(&text),
n,
"n={n}: every wire drawn or honestly reported"
);
assert_eq!(drawn, n.min(12), "n={n}: capacity is 12 ports");
let laid = route_sample(&text, 10.0);
let t = node_rect(&laid, "t").expect("t placed");
let mut left: Vec<(f64, usize)> = r
.iter()
.filter(|((_, to), p)| to == "t" && p.last().unwrap().0 == t.0)
.map(|((from, _), p)| {
let i: usize = from[1..].parse().expect("source index");
(p.last().unwrap().1, i)
})
.collect();
left.sort_by(|a, b| a.0.total_cmp(&b.0));
let order: Vec<usize> = left.iter().map(|&(_, i)| i).collect();
let mut sorted = order.clone();
sorted.sort_unstable();
assert_eq!(order, sorted, "n={n}: left ports braid");
}
}
#[test]
fn crossings_appear_one_at_a_time_and_never_wrap() {
let src = |rails: &[u32]| {
let cols = rails.len() + 2;
let mut s = format!(
"{{ layout: grid; columns: repeat({cols}, 60); rows: 160, 60, 160; \
gap: 30; clearance: 10 }}\n\
|box#a| {{ cell: 1 2; width: 60; height: 60 }}\n\
|box#b| {{ cell: {cols} 2; width: 60; height: 60 }}\n"
);
for (i, h) in rails.iter().enumerate() {
s.push_str(&format!(
"|box#n{i}| {{ cell: {c} 1; width: 60; height: {h} }}\n\
|box#s{i}| {{ cell: {c} 3; width: 60; height: {h} }}\n\
n{i} -> s{i}\n",
c = i + 2
));
}
s.push_str("a -> b\n");
s
};
for (rails, expected) in [
(vec![], 0),
(vec![40], 1),
(vec![160], 1),
(vec![160, 40], 2),
(vec![160, 160], 2),
(vec![160, 160, 160], 3),
] {
let text = src(&rails);
assert_eq!(
crossings(&text),
expected,
"rails {rails:?} force exactly {expected} crossing(s)"
);
assert_eq!(impossibles(&text), 0, "rails {rails:?}");
let breaches: Vec<_> = report(&text)
.into_iter()
.filter(|v| v.severity == Severity::Warning)
.collect();
assert!(breaches.is_empty(), "rails {rails:?}: {breaches:?}");
let r = routes(&text);
let laid = route_sample(&text, 10.0);
let a = node_rect(&laid, "a").expect("a placed");
for &(x, y) in path(&r, "a", "b") {
assert!(
y >= a.1 - 40.0 && y <= a.3 + 40.0 && x >= a.0,
"rails {rails:?}: the wire wraps: ({x}, {y})"
);
}
}
}
#[test]
fn duplicate_detours_nest_without_crossing() {
let dims = "{ direction: row; gap: 50; clearance: 10 }\n\
|box#a| { width: 60; height: 60 }\n\
|box#wall| { width: 60; height: 140 }\n\
|box#b| { width: 60; height: 60 }\n";
for pair in ["a -> b\nb -> a\n", "a -> b\na -> b\n"] {
let src = format!("{dims}{pair}");
assert_eq!(crossings(&src), 0, "the pair braids: {pair:?}");
assert_eq!(impossibles(&src), 0);
}
}
#[test]
fn links_simple_reports_zero_crossings() {
let src = include_str!("../samples/links_simple.lini");
assert_eq!(crossings(src), 0);
}
#[test]
fn a_bundle_of_s_curves_keeps_pitch_on_both_legs() {
let src = "{ layout: grid; columns: repeat(3); gap: 35; clearance: 12; }\n\
|box#alpha| \"Alpha\" { cell: 1 1; }\n\
|group#north| { cell: 2 1; gap: 16; } [\n\
|caption| \"North\"\n\
|box#nn1| \"N1\"\n\
|box#nn2| \"N2\"\n\
]\n\
|box#beta| \"Beta\" { cell: 3 1; }\n\
|group#west| { cell: 1 2; gap: 16; } [\n\
|caption| \"West\"\n\
|box#ww1| \"W1\"\n\
|box#ww2| \"W2\"\n\
]\n\
|group#east| { cell: 3 2; padding: 16; gap: 16; } [\n\
|caption| \"East\"\n\
|box#ee1| \"E1\"\n\
|box#ee2| \"E2\"\n\
]\n\
|group#south| { cell: 2 3; gap: 16; } [\n\
|caption| \"South\"\n\
|box#ss1| \"S1\"\n\
]\n\
hub -> south.ss1 & west.ww1\n\
north.nn2 -> east.ee1\n\
north.nn2 -> east.ee1\n\
north.nn2 -> east.ee1\n";
let breaches: Vec<_> = report(src)
.into_iter()
.filter(|v| v.severity == Severity::Warning)
.collect();
assert!(breaches.is_empty(), "{breaches:?}");
}
#[test]
fn a_duplicate_pair_keeps_full_pitch_beside_a_keepout() {
let src = include_str!("../samples/links_hard.lini");
let r = routes(src);
let pair = paths(&r, "west.w2", "south.s1");
assert_eq!(pair.len(), 2, "both rails draw");
assert_eq!(pair[0].len(), pair[1].len(), "rails share one shape");
for (i, (a, b)) in pair[0].windows(2).zip(pair[1].windows(2)).enumerate() {
let (da, db) = (
(a[1].0 - a[0].0, a[1].1 - a[0].1),
(b[1].0 - b[0].0, b[1].1 - b[0].1),
);
let gap = if da.0 == 0.0 && db.0 == 0.0 {
(a[0].0 - b[0].0).abs()
} else if da.1 == 0.0 && db.1 == 0.0 {
(a[0].1 - b[0].1).abs()
} else {
continue;
};
assert!(
gap >= 12.0 - 1e-6,
"leg {i} of the pair compressed to {gap}: {:?} vs {:?}",
pair[0],
pair[1]
);
}
}
#[test]
fn identical_input_routes_identically() {
let first = routes(PCB);
for _ in 0..3 {
assert_eq!(routes(PCB), first);
}
}
#[test]
fn uncontended_fan_ports_take_their_side_centres() {
let src = include_str!("../samples/links_medium.lini");
let r = routes(src);
let laid = route_sample(src, 12.0);
for to in ["kitchen.bowl", "kitchen.water"] {
let rect = node_rect(&laid, to).expect("placed");
let centre = (rect.1 + rect.3) / 2.0;
let p = paths(&r, "cat", to)[0];
let port = p.last().unwrap().1;
assert!(
(port - centre).abs() < 1e-9,
"{to}: port {port} != side centre {centre}: {p:?}"
);
}
}
#[test]
fn a_nested_row_under_a_root_drawing_routes_its_wire() {
let r = routes_str(
"{ layout: drawing; }\n\
|sketch#part| { draw: move(0, 0) right(60) down(30) left(60) close(); }\n\
|row#legend| { translate: 0 80; gap: 30; } [\n\
|box#a| \"A\"\n |box#b| \"B\"\n a -> b\n]\n",
)
.expect("compiles");
assert_eq!(r.len(), 1, "the nested wire routes: {r:?}");
assert_eq!(r[0].0, ("legend.a".to_string(), "legend.b".to_string()));
}
#[test]
fn a_nested_row_under_a_root_sequence_routes_its_wire_with_its_own_label() {
let src = "{ layout: sequence; }\n\
|box#a| \"A\"\n|box#b| \"B\"\na -> b \"hello\"\n\
|row#legend| { gap: 20; } [\n\
|box#x| \"X\"\n |box#y| \"Y\"\n x -> y \"wired\"\n]\n";
let r = routes_str(src).expect("compiles");
assert!(
r.iter()
.any(|(ep, _)| ep == &("legend.x".to_string(), "legend.y".to_string())),
"the nested wire routes: {r:?}"
);
let svg = lini::compile_str(src).expect("compile");
let link = svg
.find("data-from=\"legend.x\"")
.expect("the routed link group");
let group = &svg[link..link + svg[link..].find("</g>").unwrap()];
assert!(
group.contains("wired") && !group.contains("hello"),
"the routed wire wears its own label: {group}"
);
}