java-topology/defects/maven/unit/MavenGraphBuilderTest.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

373 lines
16 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.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.");
}
}