B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
390 lines
15 KiB
Java
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.");
|
|
}
|
|
}
|