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 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 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 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 sortedProjects = new ArrayList<>(n); for (int i = 0; i < n; i++) sortedProjects.add(i); // CWE-407 fix: build orderMap in O(N) Map 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 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. * 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 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 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 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 indexMap = new IdentityHashMap<>(nodeCount * 2); for (int i = 0; i < nodeCount; i++) { indexMap.put(sortedNodes.get(i), i); } // steps (same as defective version) List 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 sortedProjects = new ArrayList<>(n); for (int i = 0; i < n; i++) sortedProjects.add(i); // result list in arbitrary order (reverse) List defResult = new ArrayList<>(n); List 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 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."); } }