216 lines
8.3 KiB
Java
216 lines
8.3 KiB
Java
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");
|
||
}
|
||
}
|