diff --git a/crates/oa-core/tests/experimental_port_policy_falsifiers.rs b/crates/oa-core/tests/experimental_port_policy_falsifiers.rs new file mode 100644 index 0000000..ef6d197 --- /dev/null +++ b/crates/oa-core/tests/experimental_port_policy_falsifiers.rs @@ -0,0 +1,404 @@ +//! Prospective acceptance falsifiers, intentionally expected to fail at 7e25b3d. +//! Do not change the policy or relax these criteria before preserving first-run +//! receipts. Source-derived predictions are diagnostic text, not test outcomes. +use oa_core::{projection::*, routing::*, *}; +use std::{fs::OpenOptions, io::Write, path::PathBuf}; + +const POLICIES: [ExperimentalPortPolicy; 3] = [ + ExperimentalPortPolicy::Current, + ExperimentalPortPolicy::SameSidesAdaptSpacing, + ExperimentalPortPolicy::CapacityAware, +]; +const SIDES: [PortAnchorSide; 4] = [ + PortAnchorSide::Left, + PortAnchorSide::Right, + PortAnchorSide::Top, + PortAnchorSide::Bottom, +]; +fn id(n: u128) -> Id { + Id::from_u128(0x956a2026_0912_4000_8000_000000000000 + n) +} +fn pos(x: f32, y: f32) -> Position { + Position { x, y } +} +fn size(width: f32, height: f32) -> Size { + Size { width, height } +} +fn element(p: &mut Project, n: u128, kind: Kind, name: &str) -> Id { + let mut e = p.make_element(kind, Layer::Sa, name, None).unwrap(); + e.id = id(n); + p.elements.insert(e.id, e); + id(n) +} +fn standard(p: &mut Project, n: u128, owner: Id, name: &str) -> Id { + let port = make_port( + p, + id(n), + owner, + name, + PortContract::Component { + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }, + ) + .unwrap(); + p.elements.insert(port.id, port); + id(n) +} +fn place(p: &mut Project, d: Id, n: u128, e: Id, at: Position, dimensions: Size) -> Id { + let generated = p.place(d, e, at); + let nodes = &mut p.diagrams.get_mut(&d).unwrap().nodes; + let mut node = nodes.remove(&generated).unwrap(); + node.id = id(n); + node.size = dimensions; + nodes.insert(node.id, node); + id(n) +} +struct Fixture { + p: Project, + d: Id, + owner: Id, + ports: Vec, +} +fn right_peer(count: usize, height: f32, obstacle: bool) -> Fixture { + let mut p = Project::empty(); + p.id = id(1); + let a = element(&mut p, 10, Kind::System, "Owner"); + let b = element(&mut p, 11, Kind::SystemActor, "Far right peer"); + let pb = standard(&mut p, 20, b, "Peer interface"); + let mut ports = Vec::new(); + for i in 0..count { + let pa = standard(&mut p, 100 + i as u128, a, &format!("Interface {i}")); + let generated = p.relate( + RelationKind::BoundaryExchange, + &format!("Exchange {i}"), + pa, + pb, + ); + let mut relation = p.relations.remove(&generated).unwrap(); + relation.id = id(1000 + i as u128); + p.relations.insert(relation.id, relation); + ports.push(pa); + } + let generated = p.ensure_diagram(Layer::Sa); + let mut diagram = p.diagrams.remove(&generated).unwrap(); + diagram.id = id(30); + let d = diagram.id; + p.diagrams.insert(d, diagram); + let owner = place(&mut p, d, 40, a, pos(0., 0.), size(224., height)); + place(&mut p, d, 41, b, pos(1908., 336.), size(184., 132.)); + if obstacle { + let c = element(&mut p, 12, Kind::SystemActor, "Unrelated top obstacle"); + place(&mut p, d, 42, c, pos(0., -150.), size(224., 132.)); + } + validate_candidate(&p).unwrap(); + assert_eq!(language::parse(&language::format(&p)).unwrap(), p); + Fixture { p, d, owner, ports } +} +fn project(f: &Fixture, policy: ExperimentalPortPolicy) -> ProjectedView { + let view = project_diagram_with_experimental_port_policy(&f.p, &f.p.diagrams[&f.d], policy); + assert!(view.nodes.iter().all(|node| node.visual_parent.is_none())); + assert_eq!(view.edges.len(), f.ports.len()); + view +} +fn owner<'a>(f: &Fixture, view: &'a ProjectedView) -> &'a ProjectedNode { + view.nodes.iter().find(|n| n.id == f.owner).unwrap() +} +fn port(view: &ProjectedView, id: Id) -> &ProjectedPort { + view.ports.iter().find(|p| p.element == id).unwrap() +} +fn side_counts(f: &Fixture, view: &ProjectedView) -> [usize; 4] { + SIDES.map(|side| { + view.ports + .iter() + .filter(|p| p.owner == f.owner && p.side == side) + .count() + }) +} +fn gap(f: &Fixture, view: &ProjectedView) -> f32 { + let mut minimum = f32::INFINITY; + for side in SIDES { + let mut offsets: Vec<_> = view + .ports + .iter() + .filter(|p| p.owner == f.owner && p.side == side) + .map(|p| match side { + PortAnchorSide::Left | PortAnchorSide::Right => p.position.y, + _ => p.position.x, + }) + .collect(); + offsets.sort_by(f32::total_cmp); + for pair in offsets.windows(2) { + minimum = minimum.min(pair[1] - pair[0]); + } + } + minimum +} +// Independent segment/slab test, not the router's private Rect::crosses helper. +// All fixture nodes are unrelated flat obstacles, so ancestor/header exceptions +// do not apply. A node's own outward stub is allowed through its own clearance. +fn stub_hits(port: &ProjectedPort, obstacle: &ProjectedNode) -> bool { + if port.owner == obstacle.id { + return false; + } + let normal = port.side.normal(); + let mut lo = 0.0_f32; + let mut hi = 1.0_f32; + for (start, delta, min, max) in [ + ( + port.position.x, + normal.x * PORT_STUB, + obstacle.position.x - CLEARANCE, + obstacle.position.x + obstacle.size.width + CLEARANCE, + ), + ( + port.position.y, + normal.y * PORT_STUB, + obstacle.position.y - CLEARANCE, + obstacle.position.y + obstacle.size.height + CLEARANCE, + ), + ] { + if delta == 0. { + if start <= min || start >= max { + return false; + } + } else { + let a = (min - start) / delta; + let b = (max - start) / delta; + lo = lo.max(a.min(b)); + hi = hi.min(a.max(b)); + } + } + lo < hi +} +fn blocked_stubs(view: &ProjectedView) -> Vec<(Id, Id)> { + view.ports + .iter() + .flat_map(|p| { + view.nodes + .iter() + .filter(move |n| stub_hits(p, n)) + .map(move |n| (p.element, n.id)) + }) + .collect() +} +fn measure(f: &Fixture, view: &ProjectedView, label: &str) -> RoutedView { + let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], view); + let mut length = 0.0_f32; + let mut bends = 0; + for edge in &view.edges { + let route = &routed.edges[&edge.id]; + assert_eq!( + route.points.first(), + Some(&port(view, edge.source_port.unwrap()).position) + ); + assert_eq!( + route.points.last(), + Some(&port(view, edge.target_port.unwrap()).position) + ); + for pair in route.points.windows(2) { + assert!(pair[0].x == pair[1].x || pair[0].y == pair[1].y); + length += (pair[0].x - pair[1].x).abs() + (pair[0].y - pair[1].y).abs(); + } + for triple in route.points.windows(3) { + let a = pos(triple[1].x - triple[0].x, triple[1].y - triple[0].y); + let b = pos(triple[2].x - triple[1].x, triple[2].y - triple[1].y); + if a.x * b.y != a.y * b.x { + bends += 1; + } + } + println!( + "{label} edge={} points={:?} warnings={:?}", + edge.id, route.points, route.warnings + ); + } + let warnings: usize = routed.edges.values().map(|e| e.warnings.len()).sum(); + println!( + "{label} bounds={:?}/{:?} LRTB={:?} gap={} warnings={warnings} length={length} bends={bends} blocked_stubs={:?}", + owner(f, view).position, + owner(f, view).size, + side_counts(f, view), + gap(f, view), + blocked_stubs(view) + ); + for id in &f.ports { + println!( + "{label} port={id} side={:?} position={:?}", + port(view, *id).side, + port(view, *id).position + ); + } + // A caller may retain exact model, projection and route geometry. Never + // overwrite a previous run. Ordinary export still chooses Current. + if let Some(directory) = std::env::var_os("OA_PORT_POLICY_RECEIPTS") { + let directory = PathBuf::from(directory); + std::fs::create_dir_all(&directory).unwrap(); + for (suffix, content) in [ + ("arcadia", language::format(&f.p)), + ( + "projection.json", + serde_json::to_string_pretty(view).unwrap(), + ), + ("routing.txt", format!("{routed:#?}")), + ] { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(directory.join(format!("{label}.{suffix}"))) + .unwrap(); + file.write_all(content.as_bytes()).unwrap(); + } + } + routed +} + +#[test] +fn a_point_one_height_increase_does_not_needlessly_flip_twelve_of_twenty_ports() { + let mut f = right_peer(20, 259.9, false); + let mut violations = Vec::new(); + for policy in POLICIES { + let mut views = Vec::new(); + for height in [259.9, 260., 260.1] { + f.p.diagrams + .get_mut(&f.d) + .unwrap() + .nodes + .get_mut(&f.owner) + .unwrap() + .size + .height = height; + let before = f.p.clone(); + let view = project(&f, policy); + measure(&f, &view, &format!("threshold-{policy:?}-{height}")); + assert_eq!(f.p, before, "Projection/routing must not author geometry"); + if policy != ExperimentalPortPolicy::Current { + assert_eq!(owner(&f, &view).size, size(224., height)); + } + views.push(view); + } + let flips: Vec<_> = views + .windows(2) + .map(|pair| { + f.ports + .iter() + .filter(|id| port(&pair[0], **id).side != port(&pair[1], **id).side) + .count() + }) + .collect(); + println!( + "threshold {policy:?}: adjacent side changes={flips:?}; source prediction capacity=[12,0], controls=[0,0]" + ); + if policy == ExperimentalPortPolicy::CapacityAware { + // The prior assignment is still feasible at the larger rectangle. + // Nominal independent capacities: floor((edge - 36) / 28) + 1. + let new = owner(&f, &views[1]); + let capacities = [ + new.size.height, + new.size.height, + new.size.width, + new.size.width, + ] + .map(|length| ((length - 36.) / 28.).floor() as usize + 1); + assert!( + side_counts(&f, &views[0]) + .into_iter() + .zip(capacities) + .all(|(n, c)| n <= c) + ); + assert!(gap(&f, &views[0]) >= 27.99); + // A single relocation can use the new preferred-side slot. This + // fixture-specific bound does not prohibit justified side changes. + if flips.iter().any(|n| *n > 1) { + violations.push(format!( + "CapacityAware changes {flips:?} sides while zero/one move suffices" + )); + } + } else if flips != [0, 0] { + violations.push(format!( + "{policy:?} control unexpectedly changes {flips:?} sides" + )); + } + } + assert!( + violations.is_empty(), + "Prospective resize-stability criterion failed: {violations:?}" + ); +} + +#[test] +fn capacity_spillover_does_not_add_a_blocked_mandatory_stub_above_the_owner() { + let f = right_peer(8, 160., true); + let original = f.p.clone(); + let mut violations = Vec::new(); + for policy in POLICIES { + let view = project(&f, policy); + let routed = measure(&f, &view, &format!("obstacle-{policy:?}")); + let actual = owner(&f, &view); + assert_eq!(actual.position, pos(0., 0.)); + assert_eq!( + actual.size, + size( + 224., + if policy == ExperimentalPortPolicy::Current { + 232. + } else { + 160. + } + ) + ); + let obstacle = view.nodes.iter().find(|n| n.id == id(42)).unwrap(); + assert_eq!( + (obstacle.position, obstacle.size), + (pos(0., -150.), size(224., 132.)) + ); + let blocked = blocked_stubs(&view); + let no_clear: Vec<_> = routed + .edges + .values() + .filter(|e| e.warnings.contains(&RouteWarning::NoClearPath)) + .map(|e| e.id) + .collect(); + println!( + "obstacle {policy:?}: independently blocked={blocked:?}, NoClearPath={no_clear:?}; source prediction capacity Top=(112,0), stub=(112,-18), obstacle inflation y=[-162,-6]" + ); + if policy == ExperimentalPortPolicy::CapacityAware { + // Record whether each physically blocked source also reports the + // predicted route warning; do not use warning text as geometry proof. + for (port_id, obstacle_id) in &blocked { + let edges: Vec<_> = view + .edges + .iter() + .filter(|e| e.source_port == Some(*port_id) || e.target_port == Some(*port_id)) + .map(|e| e.id) + .collect(); + println!( + "blocked port={port_id} obstacle={obstacle_id} affected_edges={edges:?} all_NoClearPath={}", + edges.iter().all(|e| no_clear.contains(e)) + ); + } + if !blocked.is_empty() || routed.edges.values().any(|e| !e.warnings.is_empty()) { + violations.push(format!( + "CapacityAware introduces blocked stubs {blocked:?} or routed warnings" + )); + } + } else { + if !f + .ports + .iter() + .all(|p| port(&view, *p).side == PortAnchorSide::Right) + { + violations.push(format!("{policy:?} control no longer keeps Right exits")); + } + if !blocked.is_empty() || routed.edges.values().any(|e| !e.warnings.is_empty()) { + violations.push(format!( + "{policy:?} control has blocked stubs {blocked:?} or routed warnings" + )); + } + } + assert_eq!(f.p, original); + } + assert!( + violations.is_empty(), + "Prospective exit-clearance criterion failed: {violations:?}" + ); +}