java-topology/defects/puppet/unit/PuppetGraphTest.java

216 lines
8.3 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
/**
* PuppetGraphTest
*
* Models one CWE-407 defect in puppetlabs/puppet:
*
* PUP-001 (LOW, error path) — lib/puppet/graph/simple_graph.rb:214
* paths_in_cycle() BFS: frame[1].member?(frame[0]) where frame[1] is a
* growing Array. Each membership test is O(path_length); paths grow as
* BFS expands; in a fully connected cycle of length N this produces
* O(N^3) total comparisons.
*
* Fix: carry a parallel Set alongside the Array in each BFS frame.
* frame[2].include?(frame[0]) is O(1) average. Array is retained for
* ordered path output.
*
* All measurements are instrumented operation counts, not wall-clock timing.
*/
public class PuppetGraphTest {
// -----------------------------------------------------------------------
// PUP-001 modelling helpers
// Models BFS over a simple directed cycle: 0 -> 1 -> 2 -> ... -> (N-1) -> 0
// BFS explores from vertex 0; paths grow until cycle is detected.
//
// Defective: frame path is ArrayList; membership test iterates the whole list.
// Fixed: parallel HashSet; membership test is O(1).
// -----------------------------------------------------------------------
/**
* Simulates defective paths_in_cycle BFS on a simple cycle of length N.
* Returns total membership-test comparisons performed.
*
* BFS frame = [vertex, path_as_ArrayList]
* Cycle detection: path.contains(vertex) -- O(path.size()) per call
*/
static long pup001Defective(int cycleLen) {
// Use pair representation: ArrayList<int[]> where int[0]=vertex, index into paths list
// But model directly with operation counting
// BFS: frame = (vertex, path ArrayList)
ArrayList<Object[]> stack = new ArrayList<>();
ArrayList<Integer> emptyPath = new ArrayList<>();
stack.add(new Object[]{0, emptyPath});
long comparisons = 0;
int steps = 0;
int maxSteps = cycleLen * cycleLen * 4; // safety bound
while (!stack.isEmpty() && steps < maxSteps) {
steps++;
Object[] frame = stack.remove(0); // shift (BFS)
int vertex = (Integer) frame[0];
@SuppressWarnings("unchecked")
ArrayList<Integer> path = (ArrayList<Integer>) frame[1];
// Defective: path.contains(vertex) -- O(path.size()) comparisons
boolean inPath = false;
for (Integer p : path) {
comparisons++;
if (p == vertex) {
inPath = true;
break;
}
}
if (inPath) {
// cycle found — stop this branch
} else {
// extend path and push next vertex
ArrayList<Integer> newPath = new ArrayList<>(path);
newPath.add(vertex);
int next = (vertex + 1) % cycleLen;
stack.add(new Object[]{next, newPath});
}
}
return comparisons;
}
/**
* Simulates fixed paths_in_cycle BFS on a simple cycle of length N.
* Returns total membership-test operations (each O(1) hash lookup = 1 op).
*
* BFS frame = [vertex, path_ArrayList, path_HashSet]
* Cycle detection: pathSet.contains(vertex) -- O(1) per call
*/
static long pup001Fixed(int cycleLen) {
ArrayList<Object[]> stack = new ArrayList<>();
ArrayList<Integer> emptyPath = new ArrayList<>();
HashSet<Integer> emptySet = new HashSet<>();
stack.add(new Object[]{0, emptyPath, emptySet});
long lookups = 0;
int steps = 0;
int maxSteps = cycleLen * cycleLen * 4;
while (!stack.isEmpty() && steps < maxSteps) {
steps++;
Object[] frame = stack.remove(0);
int vertex = (Integer) frame[0];
@SuppressWarnings("unchecked")
ArrayList<Integer> path = (ArrayList<Integer>) frame[1];
@SuppressWarnings("unchecked")
HashSet<Integer> pathSet = (HashSet<Integer>) frame[2];
// Fixed: pathSet.contains(vertex) -- O(1) lookup
lookups++;
boolean inPath = pathSet.contains(vertex);
if (inPath) {
// cycle found — stop this branch
} else {
ArrayList<Integer> newPath = new ArrayList<>(path);
newPath.add(vertex);
@SuppressWarnings("unchecked")
HashSet<Integer> newSet = (HashSet<Integer>) pathSet.clone();
newSet.add(vertex);
int next = (vertex + 1) % cycleLen;
stack.add(new Object[]{next, newPath, newSet});
}
}
return lookups;
}
// -----------------------------------------------------------------------
// Test 1 — PUP-001: defect costs more than fix at cycle_len=20
// -----------------------------------------------------------------------
static void test1_pup001_pathMembershipSet() {
int N = 20;
long defectCost = pup001Defective(N);
long fixedCost = pup001Fixed(N);
double ratio = (double) defectCost / Math.max(1, fixedCost);
System.out.printf("test1 PUP-001: cycle_len=%d defect=%d fixed=%d ratio=%.1fx%n",
N, defectCost, fixedCost, ratio);
assert defectCost > fixedCost
: "defect must be more expensive than fix at cycle_len=" + N
+ " (defect=" + defectCost + ", fixed=" + fixedCost + ")";
assert ratio >= 8.0
: "expected ratio >= 8x at cycle_len=" + N + ", got " + ratio;
}
// -----------------------------------------------------------------------
// Test 2 — PUP-001: scaling — doubling cycle length grows defect faster
// than the fix (super-linear vs near-linear)
// -----------------------------------------------------------------------
static void test2_pup001_scalingGrowth() {
int N1 = 15;
int N2 = 30; // doubled
long d1 = pup001Defective(N1);
long d2 = pup001Defective(N2);
long f1 = pup001Fixed(N1);
long f2 = pup001Fixed(N2);
double defectGrowth = (double) d2 / Math.max(1, d1);
double fixedGrowth = (double) f2 / Math.max(1, f1);
System.out.printf("test2 PUP-001: N=%d→%d defect=%d→%d (%.2fx) fixed=%d→%d (%.2fx)%n",
N1, N2, d1, d2, defectGrowth, f1, f2, fixedGrowth);
assert defectGrowth > 2.0
: "defect should grow super-linearly on 2x cycle_len, got " + defectGrowth;
assert fixedGrowth <= 3.0
: "fixed should grow at most linearly (×2 ± slack) on 2x cycle_len, got " + fixedGrowth;
assert defectGrowth > fixedGrowth
: "defect growth " + defectGrowth + " should exceed fixed growth " + fixedGrowth;
}
// -----------------------------------------------------------------------
// Test 3 — PUP-001: ratio at cycle_len=25 exceeds 5x
// -----------------------------------------------------------------------
static void test3_pup001_ratioAt25() {
int N = 25;
long defectOps = pup001Defective(N);
long fixedOps = pup001Fixed(N);
double ratio = (double) defectOps / Math.max(1, fixedOps);
System.out.printf("test3 PUP-001: cycle_len=%d defect=%d fixed=%d ratio=%.1fx%n",
N, defectOps, fixedOps, ratio);
assert ratio > 5.0
: "expected ratio > 5x at cycle_len=25, got " + ratio;
}
// -----------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("=== PuppetGraphTest ===");
System.out.println("Modelling CWE-407: PUP-001 (paths_in_cycle BFS Array#member? O(N^3))");
System.out.println();
test1_pup001_pathMembershipSet();
System.out.println(" PASS test1_pup001_pathMembershipSet");
test2_pup001_scalingGrowth();
System.out.println(" PASS test2_pup001_scalingGrowth");
test3_pup001_ratioAt25();
System.out.println(" PASS test3_pup001_ratioAt25");
System.out.println();
System.out.println("3/3 PASS");
}
}