B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
373 lines
16 KiB
Java
373 lines
16 KiB
Java
package unit;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.HashMap;
|
||
import java.util.IdentityHashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* MavenGraphBuilderTest
|
||
*
|
||
* Models two CWE-407 defects in Apache Maven:
|
||
*
|
||
* maven-0004: DefaultGraphBuilder (trimProjectsToRequest / trimSelectedProjects /
|
||
* includeAlsoMakeTransitively)
|
||
* Defective: result.sort(comparing(sortedProjects::indexOf)) — each indexOf call
|
||
* is O(N) → sort comparator is O(N) → total sort is O(N² log N).
|
||
* Fixed: build an index Map once in O(N); each comparator call is O(1)
|
||
* → sort is O(N log N).
|
||
*
|
||
* maven-0005: BuildPlanLogger
|
||
* Defective: stream sorted with Comparator.comparingInt(plan.sortedNodes()::indexOf)
|
||
* — sortedNodes() called once per comparison; indexOf is O(N) per call
|
||
* → O(N²) total for sorting N steps.
|
||
* Fixed: build an IdentityHashMap<BuildStep, Integer> index once in O(N);
|
||
* comparator uses indexMap.getOrDefault(step, MAX_VALUE) → O(1) per call.
|
||
*
|
||
* Operation counts are instrumented explicitly — no wall-clock timing — to isolate
|
||
* the algorithmic difference.
|
||
*/
|
||
public class MavenGraphBuilderTest {
|
||
|
||
// =========================================================================
|
||
// Models for maven-0004: sortedProjects.indexOf() inside sort comparator
|
||
// =========================================================================
|
||
|
||
/**
|
||
* Simulates a single sort using indexOf-based comparator.
|
||
*
|
||
* During Java's TimSort, a list of size N is sorted using O(N log N) comparisons.
|
||
* Each comparison calls indexOf on a list of size N → O(N) per comparison.
|
||
* Total: O(N² log N) indexOf probes.
|
||
*
|
||
* We instrument by counting the number of indexOf-equivalent scans that would
|
||
* be needed. We run an actual sort and count how many comparator invocations
|
||
* occur, then multiply by the average scan length (N/2 on average for a random
|
||
* hit, N for a miss — we use N to represent worst-case indexOf cost).
|
||
*
|
||
* @param n size of the list to sort
|
||
* @return total element comparisons (simulated indexOf cost)
|
||
*/
|
||
static long defectiveSortIndexOfCost(int n) {
|
||
// Build sortedProjects list (reference ordering)
|
||
List<Integer> sortedProjects = new ArrayList<>(n);
|
||
for (int i = 0; i < n; i++) sortedProjects.add(i);
|
||
|
||
// Build result list in reverse order (worst case for sort)
|
||
List<Integer> result = new ArrayList<>(n);
|
||
for (int i = n - 1; i >= 0; i--) result.add(i);
|
||
|
||
// Defective comparator: O(N) indexOf per invocation
|
||
long[] comparisons = {0L};
|
||
result.sort((a, b) -> {
|
||
// Each indexOf call scans up to N elements
|
||
int ia = 0;
|
||
for (int i = 0; i < sortedProjects.size(); i++) {
|
||
comparisons[0]++;
|
||
if (sortedProjects.get(i).equals(a)) { ia = i; break; }
|
||
}
|
||
int ib = 0;
|
||
for (int i = 0; i < sortedProjects.size(); i++) {
|
||
comparisons[0]++;
|
||
if (sortedProjects.get(i).equals(b)) { ib = i; break; }
|
||
}
|
||
return Integer.compare(ia, ib);
|
||
});
|
||
return comparisons[0];
|
||
}
|
||
|
||
/**
|
||
* Simulates the fixed sort using a pre-built index map.
|
||
*
|
||
* Build an orderMap in O(N) once; comparator does O(1) map.get().
|
||
* We count map lookups (each is O(1)).
|
||
*
|
||
* @param n size of the list to sort
|
||
* @return total map lookups (one pair per comparator invocation)
|
||
*/
|
||
static long fixedSortIndexMapCost(int n) {
|
||
// Build sortedProjects list
|
||
List<Integer> sortedProjects = new ArrayList<>(n);
|
||
for (int i = 0; i < n; i++) sortedProjects.add(i);
|
||
|
||
// CWE-407 fix: build orderMap in O(N)
|
||
Map<Integer, Integer> orderMap = new HashMap<>(n * 2);
|
||
for (int i = 0; i < n; i++) {
|
||
orderMap.put(sortedProjects.get(i), i);
|
||
}
|
||
|
||
// Build result list in reverse order
|
||
List<Integer> result = new ArrayList<>(n);
|
||
for (int i = n - 1; i >= 0; i--) result.add(i);
|
||
|
||
long[] lookups = {0L};
|
||
result.sort((a, b) -> {
|
||
lookups[0] += 2; // two O(1) map.get() calls per comparison
|
||
return Integer.compare(orderMap.get(a), orderMap.get(b));
|
||
});
|
||
return lookups[0];
|
||
}
|
||
|
||
// =========================================================================
|
||
// Models for maven-0005: BuildPlanLogger sortedNodes().indexOf() in stream sort
|
||
// =========================================================================
|
||
|
||
/**
|
||
* Simulates the defective BuildPlanLogger sort:
|
||
* .sorted(Comparator.comparingInt(plan.sortedNodes()::indexOf))
|
||
*
|
||
* sortedNodes() returns a list of N BuildStep objects.
|
||
* indexOf is O(N) per comparison.
|
||
* Sorting M steps using this comparator costs O(M log M) comparisons × O(N) each.
|
||
* When M ≈ N this is O(N² log N).
|
||
*
|
||
* We model steps as Integers and sortedNodes as a List<Integer>.
|
||
* We count individual element comparisons inside the indexOf simulation.
|
||
*
|
||
* @param nodeCount total nodes in the plan (N)
|
||
* @param stepCount steps being sorted for one project (M, typically ≤ N)
|
||
* @return total element comparisons (simulated indexOf cost)
|
||
*/
|
||
static long defectiveBuildPlanLoggerCost(int nodeCount, int stepCount) {
|
||
// sortedNodes: list of node ids 0..nodeCount-1
|
||
List<Integer> sortedNodes = new ArrayList<>(nodeCount);
|
||
for (int i = 0; i < nodeCount; i++) sortedNodes.add(i);
|
||
|
||
// steps for this project: every other node, in reverse order (stress sort)
|
||
List<Integer> steps = new ArrayList<>(stepCount);
|
||
for (int i = stepCount - 1; i >= 0; i--) steps.add(i * (nodeCount / stepCount));
|
||
|
||
long[] comparisons = {0L};
|
||
steps.sort((a, b) -> {
|
||
// Defect: O(N) indexOf per call — simulated here with explicit scan
|
||
int ia = 0;
|
||
for (int i = 0; i < sortedNodes.size(); i++) {
|
||
comparisons[0]++;
|
||
if (sortedNodes.get(i).equals(a)) { ia = i; break; }
|
||
}
|
||
int ib = 0;
|
||
for (int i = 0; i < sortedNodes.size(); i++) {
|
||
comparisons[0]++;
|
||
if (sortedNodes.get(i).equals(b)) { ib = i; break; }
|
||
}
|
||
return Integer.compare(ia, ib);
|
||
});
|
||
return comparisons[0];
|
||
}
|
||
|
||
/**
|
||
* Simulates the fixed BuildPlanLogger sort:
|
||
* build indexMap once, sort with indexMap.getOrDefault(step, MAX_VALUE).
|
||
*
|
||
* @param nodeCount total nodes in the plan (N)
|
||
* @param stepCount steps being sorted (M)
|
||
* @return total map lookups (two per comparator invocation)
|
||
*/
|
||
static long fixedBuildPlanLoggerCost(int nodeCount, int stepCount) {
|
||
// sortedNodes
|
||
List<Integer> sortedNodes = new ArrayList<>(nodeCount);
|
||
for (int i = 0; i < nodeCount; i++) sortedNodes.add(i);
|
||
|
||
// CWE-407 fix: build indexMap using IdentityHashMap equivalent (HashMap here
|
||
// since Integer objects are pooled for small values; semantics are identical)
|
||
Map<Integer, Integer> indexMap = new IdentityHashMap<>(nodeCount * 2);
|
||
for (int i = 0; i < nodeCount; i++) {
|
||
indexMap.put(sortedNodes.get(i), i);
|
||
}
|
||
|
||
// steps (same as defective version)
|
||
List<Integer> steps = new ArrayList<>(stepCount);
|
||
for (int i = stepCount - 1; i >= 0; i--) steps.add(i * (nodeCount / stepCount));
|
||
|
||
long[] lookups = {0L};
|
||
steps.sort((a, b) -> {
|
||
lookups[0] += 2; // two O(1) getOrDefault() calls per comparison
|
||
int ia = indexMap.getOrDefault(a, Integer.MAX_VALUE);
|
||
int ib = indexMap.getOrDefault(b, Integer.MAX_VALUE);
|
||
return Integer.compare(ia, ib);
|
||
});
|
||
return lookups[0];
|
||
}
|
||
|
||
// =========================================================================
|
||
// Test 1 — maven-0004: defect vs fixed operation count at N=100
|
||
// =========================================================================
|
||
|
||
static void test1_sortIndexOfVsIndexMap() {
|
||
int n = 100;
|
||
long defectOps = defectiveSortIndexOfCost(n);
|
||
long fixedOps = fixedSortIndexMapCost(n);
|
||
|
||
System.out.printf(
|
||
"test1: n=%d defect_comparisons=%d fixed_lookups=%d%n",
|
||
n, defectOps, fixedOps);
|
||
|
||
// Defect must do significantly more work: O(N log N) × O(N) vs O(N log N) × O(1)
|
||
assert defectOps > fixedOps
|
||
: "defect must do more work than fix at n=" + n;
|
||
// Conservative lower bound: at least N comparator calls, each doing 2 indexOf
|
||
// scans of at least 1 element each → total ≥ N*(N-1) (triangular sum lower bound)
|
||
long lowerBound = (long) n * (n - 1);
|
||
assert defectOps >= lowerBound
|
||
: "defect comparisons=" + defectOps + " expected >= N*(N-1)=" + lowerBound;
|
||
}
|
||
|
||
// =========================================================================
|
||
// Test 2 — maven-0004: doubling N grows defect super-quadratically,
|
||
// fixed sub-quadratically
|
||
// =========================================================================
|
||
|
||
static void test2_sortScalingDefectVsFixed() {
|
||
int n1 = 60;
|
||
int n2 = 120;
|
||
|
||
long d1 = defectiveSortIndexOfCost(n1);
|
||
long d2 = defectiveSortIndexOfCost(n2);
|
||
long f1 = fixedSortIndexMapCost(n1);
|
||
long f2 = fixedSortIndexMapCost(n2);
|
||
|
||
double defectGrowth = (double) d2 / Math.max(1, d1);
|
||
double fixedGrowth = (double) f2 / Math.max(1, f1);
|
||
|
||
System.out.printf(
|
||
"test2: defect_growth=%.2fx (n 2x) fixed_growth=%.2fx%n",
|
||
defectGrowth, fixedGrowth);
|
||
|
||
// Defect is O(N² log N): doubling N → ~4× growth (the log N factor is minor)
|
||
assert defectGrowth > 3.0
|
||
: "defect should grow at least 3x with 2x N (O(N² log N)), got " + defectGrowth;
|
||
// Fixed is O(N log N): doubling N → ~2× growth
|
||
assert fixedGrowth <= 3.0
|
||
: "fixed should grow at most 3x with 2x N (O(N log N)), got " + fixedGrowth;
|
||
assert defectGrowth > fixedGrowth
|
||
: "defect growth must exceed fixed growth";
|
||
}
|
||
|
||
// =========================================================================
|
||
// Test 3 — maven-0004: sorted result is correct for both implementations
|
||
// =========================================================================
|
||
|
||
static void test3_sortCorrectnessCheck() {
|
||
int n = 50;
|
||
// Build sortedProjects (reference order 0..n-1)
|
||
List<Integer> sortedProjects = new ArrayList<>(n);
|
||
for (int i = 0; i < n; i++) sortedProjects.add(i);
|
||
|
||
// result list in arbitrary order (reverse)
|
||
List<Integer> defResult = new ArrayList<>(n);
|
||
List<Integer> fixResult = new ArrayList<>(n);
|
||
for (int i = n - 1; i >= 0; i--) {
|
||
defResult.add(i);
|
||
fixResult.add(i);
|
||
}
|
||
|
||
// Defective sort
|
||
defResult.sort((a, b) -> Integer.compare(sortedProjects.indexOf(a), sortedProjects.indexOf(b)));
|
||
|
||
// Fixed sort
|
||
Map<Integer, Integer> orderMap = new HashMap<>(n * 2);
|
||
for (int i = 0; i < n; i++) orderMap.put(sortedProjects.get(i), i);
|
||
fixResult.sort((a, b) -> Integer.compare(orderMap.get(a), orderMap.get(b)));
|
||
|
||
System.out.printf("test3: n=%d correctness check defResult[0]=%d fixResult[0]=%d last=%d%n",
|
||
n, defResult.get(0), fixResult.get(0), defResult.get(n - 1));
|
||
|
||
// Both should produce the same ordering
|
||
assert defResult.equals(fixResult)
|
||
: "defective and fixed sorts produced different orderings";
|
||
// First element should be 0 (lowest index in sortedProjects)
|
||
assert defResult.get(0).equals(0)
|
||
: "first sorted element should be 0, got " + defResult.get(0);
|
||
assert defResult.get(n - 1).equals(n - 1)
|
||
: "last sorted element should be " + (n-1) + ", got " + defResult.get(n-1);
|
||
}
|
||
|
||
// =========================================================================
|
||
// Test 4 — maven-0005: BuildPlanLogger defect vs fixed operation count
|
||
// =========================================================================
|
||
|
||
static void test4_buildPlanLoggerDefectVsFixed() {
|
||
int nodeCount = 200;
|
||
int stepCount = 100;
|
||
|
||
long defectOps = defectiveBuildPlanLoggerCost(nodeCount, stepCount);
|
||
long fixedOps = fixedBuildPlanLoggerCost(nodeCount, stepCount);
|
||
|
||
System.out.printf(
|
||
"test4: nodeCount=%d stepCount=%d defect_comparisons=%d fixed_lookups=%d%n",
|
||
nodeCount, stepCount, defectOps, fixedOps);
|
||
|
||
// Defect: O(M log M) comparator calls × O(N) indexOf each
|
||
// Fixed: O(M log M) × O(1) map lookup
|
||
assert defectOps > fixedOps
|
||
: "defect must do more work than fix";
|
||
// Lower bound: at least M comparator invocations × 2 indexOf scans of avg length N/2
|
||
long lowerBound = (long) stepCount * nodeCount / 2;
|
||
assert defectOps >= lowerBound
|
||
: "defect=" + defectOps + " expected >= " + lowerBound;
|
||
}
|
||
|
||
// =========================================================================
|
||
// Test 5 — maven-0005: scaling node count grows defect super-linearly,
|
||
// fixed grows at most linearly
|
||
// =========================================================================
|
||
|
||
static void test5_buildPlanLoggerScaling() {
|
||
int stepCount = 80;
|
||
int nodes1 = 100;
|
||
int nodes2 = 200; // 2x nodes
|
||
|
||
long d1 = defectiveBuildPlanLoggerCost(nodes1, stepCount);
|
||
long d2 = defectiveBuildPlanLoggerCost(nodes2, stepCount);
|
||
long f1 = fixedBuildPlanLoggerCost(nodes1, stepCount);
|
||
long f2 = fixedBuildPlanLoggerCost(nodes2, stepCount);
|
||
|
||
double defectGrowth = (double) d2 / Math.max(1, d1);
|
||
double fixedGrowth = (double) f2 / Math.max(1, f1);
|
||
|
||
System.out.printf(
|
||
"test5: stepCount=%d defect_growth=%.2fx (nodes 2x) fixed_growth=%.2fx%n",
|
||
stepCount, defectGrowth, fixedGrowth);
|
||
|
||
// Defect: O(N) per indexOf → doubling N doubles cost per comparator call → 2x overall
|
||
assert defectGrowth > 1.5
|
||
: "defect should grow with node count (O(N) indexOf), got " + defectGrowth;
|
||
// Fixed: O(1) per lookup → doubling N has no effect on sort cost (only on build-map cost)
|
||
// With N doubling, build-map is O(N) but sort is O(M log M) × O(1) — fixed lookups unchanged
|
||
assert fixedGrowth <= 2.0
|
||
: "fixed lookups should not grow with node count, got " + fixedGrowth;
|
||
assert defectGrowth > fixedGrowth
|
||
: "defect growth must exceed fixed growth when N doubles";
|
||
}
|
||
|
||
// =========================================================================
|
||
// Main
|
||
// =========================================================================
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("=== MavenGraphBuilderTest ===");
|
||
System.out.println("Modelling CWE-407 defects:");
|
||
System.out.println(" maven-0004: DefaultGraphBuilder sortedProjects.indexOf() → O(N² log N)");
|
||
System.out.println(" maven-0005: BuildPlanLogger sortedNodes().indexOf() → O(N²) per project");
|
||
System.out.println();
|
||
|
||
test1_sortIndexOfVsIndexMap();
|
||
System.out.println(" PASS test1_sortIndexOfVsIndexMap");
|
||
|
||
test2_sortScalingDefectVsFixed();
|
||
System.out.println(" PASS test2_sortScalingDefectVsFixed");
|
||
|
||
test3_sortCorrectnessCheck();
|
||
System.out.println(" PASS test3_sortCorrectnessCheck");
|
||
|
||
test4_buildPlanLoggerDefectVsFixed();
|
||
System.out.println(" PASS test4_buildPlanLoggerDefectVsFixed");
|
||
|
||
test5_buildPlanLoggerScaling();
|
||
System.out.println(" PASS test5_buildPlanLoggerScaling");
|
||
|
||
System.out.println();
|
||
System.out.println("All 5 tests PASSED.");
|
||
}
|
||
}
|