java-topology/defects/kicad/unit/KicadFromToTest.java
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

390 lines
15 KiB
Java

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<Integer, List<Integer>> buildGraph(int V, int B, long seed) {
Random rng = new Random(seed);
Map<Integer, List<Integer>> 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<Integer> 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<Integer> uniquePathBetweenNodes_Defective(
Map<Integer, List<Integer>> adj, int src, int dst) {
Deque<List<Integer>> Q = new ArrayDeque<>();
List<Integer> init = new ArrayList<>();
init.add(src);
Q.add(init);
while (!Q.isEmpty()) {
List<Integer> 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<Integer> p : Q) {
if (isVertexVisited_Defective(ci, p)) { // O(|p|)
visited = true;
break;
}
}
}
if (!visited) {
List<Integer> 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<Integer> visited) {
membershipCalls++;
elementProbes++; // exactly one hash probe — O(1)
return visited.contains(v);
}
static class Path {
final List<Integer> nodes;
final Set<Integer> 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<Integer> uniquePathBetweenNodes_Fixed(
Map<Integer, List<Integer>> adj, int src, int dst) {
Deque<Path> 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<Integer, List<Integer>> 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<Integer, List<Integer>> 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<Integer, List<Integer>> 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<Integer, List<Integer>> adj = buildGraph(V, B, SEED);
int[][] pairs = buildPairs(V, PAIRS, SEED);
for (int[] pair : pairs) {
List<Integer> 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<Integer> 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<Integer, List<Integer>> adj = buildGraph(V, B, SEED);
int[][] pairs = buildPairs(V, PAIRS, SEED);
for (int[] pair : pairs) {
List<Integer> defPath = uniquePathBetweenNodes_Defective(adj, pair[0], pair[1]);
List<Integer> 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<Integer> 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.");
}
}