package unit; import java.util.*; /** * KicadFromToTest — Java model of the CWE-407 defect in KiCad's * pcbnew/connectivity/from_to_cache.cpp :: uniquePathBetweenNodes(). * * Defective path: visited check uses ArrayList.contains() — O(|path|) element probes per call. * Fixed path: visited check uses HashSet.contains() — O(1) element probes per call. * * Instrumentation: * elementProbes — total element comparisons inside isVertexVisited (the quadratic work) * membershipCalls — total number of isVertexVisited invocations * probesPerCall — elementProbes / membershipCalls = average scan depth * * For defective: probesPerCall = average path length scanned ≈ O(V). * For fixed: probesPerCall = 1.0 exactly (one hash probe per call). * Ratio = probesPerCall_defective / probesPerCall_fixed ≥ average path length. * * Graph model: adjacency list, nodes as Integer, V=300, B=2 (sparse). */ public class KicadFromToTest { // ------------------------------------------------------------------ // // Instrumentation counters // // ------------------------------------------------------------------ // static long elementProbes = 0; // element-level comparisons inside isVertexVisited static long membershipCalls = 0; // total calls to isVertexVisited static void resetCounters() { elementProbes = 0; membershipCalls = 0; } static double probesPerCall() { if (membershipCalls == 0) return 0.0; return (double) elementProbes / membershipCalls; } // ------------------------------------------------------------------ // // Graph builder // // ------------------------------------------------------------------ // /** * Build a graph that maximizes BFS path lengths to stress the O(|path|) check. * * Structure: a spine of V nodes (0→1→2→…→V-1) with B-1 short-circuit branches * of length 3 from every 5th spine node. This creates alternate paths that keep * the BFS queue populated with long paths, exposing the O(V^2) membership work. * * For the linear-chain case (B=1) every path has to follow the spine, * forcing avg path length ≈ V/2 and ratio ≈ V/2. */ static Map> buildGraph(int V, int B, long seed) { Random rng = new Random(seed); Map> adj = new HashMap<>(); for (int i = 0; i < V; i++) adj.put(i, new ArrayList<>()); // Spine: 0-1-2-...-V-1 (undirected) for (int i = 0; i < V - 1; i++) { adj.get(i).add(i + 1); adj.get(i + 1).add(i); } // Extra random edges (avoid making graph too dense / short-circuiting paths) // Use only long-range edges (skip at least V/4 nodes) to keep paths long int extraEdges = (V * (B - 1)) / 2; for (int e = 0; e < extraEdges; e++) { int a = rng.nextInt(V); int delta = V / 4 + rng.nextInt(V / 4); int b = (a + delta) % V; adj.get(a).add(b); adj.get(b).add(a); } return adj; } // ------------------------------------------------------------------ // // DEFECTIVE BFS — isVertexVisited is O(|path|) element probes // // ------------------------------------------------------------------ // static boolean isVertexVisited_Defective(int v, List path) { membershipCalls++; for (int u : path) { elementProbes++; // one probe per element examined — O(|path|) on miss if (u == v) return true; } return false; } static List uniquePathBetweenNodes_Defective( Map> adj, int src, int dst) { Deque> Q = new ArrayDeque<>(); List init = new ArrayList<>(); init.add(src); Q.add(init); while (!Q.isEmpty()) { List path = Q.pollFirst(); int last = path.get(path.size() - 1); if (last == dst) return path; for (int ci : adj.get(last)) { boolean visited = isVertexVisited_Defective(ci, path); // O(|path|) if (!visited) { for (List p : Q) { if (isVertexVisited_Defective(ci, p)) { // O(|p|) visited = true; break; } } } if (!visited) { List newPath = new ArrayList<>(path); newPath.add(ci); Q.add(newPath); } } } return null; } // ------------------------------------------------------------------ // // FIXED BFS — isVertexVisited is O(1) via HashSet // // ------------------------------------------------------------------ // static boolean isVertexVisited_Fixed(int v, Set visited) { membershipCalls++; elementProbes++; // exactly one hash probe — O(1) return visited.contains(v); } static class Path { final List nodes; final Set visited; Path() { nodes = new ArrayList<>(); visited = new HashSet<>(); } Path(Path other) { nodes = new ArrayList<>(other.nodes); visited = new HashSet<>(other.visited); } void add(int node) { nodes.add(node); visited.add(node); } int last() { return nodes.get(nodes.size() - 1); } } static List uniquePathBetweenNodes_Fixed( Map> adj, int src, int dst) { Deque Q = new ArrayDeque<>(); Path init = new Path(); init.add(src); Q.add(init); while (!Q.isEmpty()) { Path path = Q.pollFirst(); int last = path.last(); if (last == dst) return path.nodes; for (int ci : adj.get(last)) { boolean visited = isVertexVisited_Fixed(ci, path.visited); // O(1) if (!visited) { for (Path p : Q) { if (isVertexVisited_Fixed(ci, p.visited)) { // O(1) visited = true; break; } } } if (!visited) { Path newPath = new Path(path); newPath.add(ci); Q.add(newPath); } } } return null; } // ------------------------------------------------------------------ // // Constants // // ------------------------------------------------------------------ // // V=200, B=1 (pure spine): every path follows the chain, avg path length ~V/3. // Forces probes/call ≫ 10 for defective vs exactly 1 for fixed → ratio ≥ 10x. static final int V = 200; static final int B = 1; static final long SEED = 42L; static final int PAIRS = 10; static int[][] buildPairs(int V, int pairs, long seed) { Random rng = new Random(seed + 1); int[][] result = new int[pairs][2]; for (int i = 0; i < pairs; i++) { int a, b; do { a = rng.nextInt(V); b = rng.nextInt(V); } while (a == b); result[i][0] = a; result[i][1] = b; } return result; } static void pass(String name) { System.out.println("PASS " + name); } static void fail(String name, String reason) { System.out.println("FAIL " + name + " — " + reason); throw new AssertionError(name + ": " + reason); } // ------------------------------------------------------------------ // // Test methods // // ------------------------------------------------------------------ // /** * Defective BFS: average element probes per isVertexVisited call must * exceed 10 (i.e., paths are long enough to manifest O(V) scan cost). */ static void testDefectiveIsQuadratic() { Map> adj = buildGraph(V, B, SEED); int[][] pairs = buildPairs(V, PAIRS, SEED); resetCounters(); for (int[] pair : pairs) uniquePathBetweenNodes_Defective(adj, pair[0], pair[1]); double ppc = probesPerCall(); System.out.printf(" defective probes/call = %.1f (probes=%d calls=%d)%n", ppc, elementProbes, membershipCalls); if (ppc <= 10.0) fail("testDefectiveIsQuadratic", String.format("expected probes/call > 10, got %.1f — paths may be too short", ppc)); pass("testDefectiveIsQuadratic"); } /** * Fixed BFS: average element probes per isVertexVisited call must equal 1.0 * (each call is exactly one HashSet.contains() probe — O(1)). */ static void testFixedIsLinear() { Map> adj = buildGraph(V, B, SEED); int[][] pairs = buildPairs(V, PAIRS, SEED); resetCounters(); for (int[] pair : pairs) uniquePathBetweenNodes_Fixed(adj, pair[0], pair[1]); double ppc = probesPerCall(); System.out.printf(" fixed probes/call = %.1f (probes=%d calls=%d)%n", ppc, elementProbes, membershipCalls); // Fixed must be exactly 1.0: elementProbes == membershipCalls if (elementProbes != membershipCalls) fail("testFixedIsLinear", "expected elementProbes == membershipCalls (each call = 1 probe), " + "got probes=" + elementProbes + " calls=" + membershipCalls); pass("testFixedIsLinear"); } /** * Ratio of probes/call: defective vs fixed must exceed 10x. * * defective probes/call ≈ average path length at each membership check. * fixed probes/call = 1.0 exactly. * Ratio = average path scan depth — must be ≥ 10 for test to be meaningful. */ static void testRatioAtScale() { Map> adj = buildGraph(V, B, SEED); int[][] pairs = buildPairs(V, PAIRS, SEED); resetCounters(); for (int[] pair : pairs) uniquePathBetweenNodes_Defective(adj, pair[0], pair[1]); double defPPC = probesPerCall(); long defProbes = elementProbes, defCalls = membershipCalls; resetCounters(); for (int[] pair : pairs) uniquePathBetweenNodes_Fixed(adj, pair[0], pair[1]); double fixPPC = probesPerCall(); long fixProbes = elementProbes, fixCalls = membershipCalls; double ratio = defPPC / fixPPC; System.out.printf(" defective probes/call = %.1f fixed probes/call = %.1f ratio = %.1fx%n", defPPC, fixPPC, ratio); if (ratio < 10.0) fail("testRatioAtScale", String.format("expected probes/call ratio > 10x, got %.1fx " + "(defective=%.1f, fixed=%.1f)", ratio, defPPC, fixPPC)); pass("testRatioAtScale"); } /** * Correctness: defective BFS returns a valid simple path for every pair. */ static void testCorrectnessDefective() { Map> adj = buildGraph(V, B, SEED); int[][] pairs = buildPairs(V, PAIRS, SEED); for (int[] pair : pairs) { List path = uniquePathBetweenNodes_Defective(adj, pair[0], pair[1]); if (path == null) fail("testCorrectnessDefective", "no path found from " + pair[0] + " to " + pair[1]); if (path.get(0) != pair[0]) fail("testCorrectnessDefective", "path does not start at src=" + pair[0]); if (path.get(path.size() - 1) != pair[1]) fail("testCorrectnessDefective", "path does not end at dst=" + pair[1]); for (int i = 0; i < path.size() - 1; i++) { int a = path.get(i), b = path.get(i + 1); if (!adj.get(a).contains(b)) fail("testCorrectnessDefective", "invalid edge " + a + "→" + b); } Set seen = new HashSet<>(path); if (seen.size() != path.size()) fail("testCorrectnessDefective", "path contains repeated nodes"); } pass("testCorrectnessDefective"); } /** * Correctness: fixed BFS agrees with defective on reachability and returns * a valid simple path for every pair. */ static void testCorrectnessFixed() { Map> adj = buildGraph(V, B, SEED); int[][] pairs = buildPairs(V, PAIRS, SEED); for (int[] pair : pairs) { List defPath = uniquePathBetweenNodes_Defective(adj, pair[0], pair[1]); List fixPath = uniquePathBetweenNodes_Fixed(adj, pair[0], pair[1]); boolean defFound = (defPath != null); boolean fixFound = (fixPath != null); if (defFound != fixFound) fail("testCorrectnessFixed", "reachability disagreement " + pair[0] + "→" + pair[1] + ": defective=" + defFound + " fixed=" + fixFound); if (fixPath == null) continue; if (fixPath.get(0) != pair[0]) fail("testCorrectnessFixed", "path does not start at src=" + pair[0]); if (fixPath.get(fixPath.size() - 1) != pair[1]) fail("testCorrectnessFixed", "path does not end at dst=" + pair[1]); for (int i = 0; i < fixPath.size() - 1; i++) { int a = fixPath.get(i), b = fixPath.get(i + 1); if (!adj.get(a).contains(b)) fail("testCorrectnessFixed", "invalid edge " + a + "→" + b); } Set seen = new HashSet<>(fixPath); if (seen.size() != fixPath.size()) fail("testCorrectnessFixed", "fixed path contains repeated nodes"); } pass("testCorrectnessFixed"); } // ------------------------------------------------------------------ // // Main // // ------------------------------------------------------------------ // public static void main(String[] args) { System.out.println("=== KicadFromToTest — KICAD-001 CWE-407 ==="); System.out.println(" V=" + V + " B=" + B + " pairs=" + PAIRS); System.out.println(); testDefectiveIsQuadratic(); testFixedIsLinear(); testRatioAtScale(); testCorrectnessDefective(); testCorrectnessFixed(); System.out.println(); System.out.println("All tests passed."); } }