java-topology/defects/maven/unit/MavenReactorManagerTest.java

259 lines
10 KiB
Java

package unit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* MavenReactorManagerTest
*
* Models two CWE-407 defects in Apache Maven:
*
* maven-0006: ReactorManager.blackList
* File: impl/maven-core/src/main/java/org/apache/maven/execution/ReactorManager.java
* Defective: private List<String> blackList = new ArrayList<>()
* blackList(String id) calls blackList.contains(id) — O(N) linear scan.
* Recursive cascade over N projects: contains() at step k costs k comparisons
* → total O(N²) comparisons.
* Fixed: private Set<String> blackList = new HashSet<>()
* blackList.add(id) is O(1) per call; total O(N).
*
* maven-0007: DefaultMavenExecutionRequest.addPluginGroup / addPluginGroups
* File: impl/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java
* Defective: private List<String> pluginGroups = new ArrayList<>()
* addPluginGroup() calls ArrayList.contains() — O(G) per call.
* addPluginGroups() loops over G groups → O(G²) total comparisons.
* Fixed: private Set<String> pluginGroups = new LinkedHashSet<>()
* Each add() is O(1); total O(G).
*
* Comparison counts (not wall-clock) are measured by simulating the ArrayList
* linear scan cost explicitly: contains() on a list of size k costs k comparisons.
*/
public class MavenReactorManagerTest {
// =========================================================================
// Models for maven-0006: ReactorManager ArrayList blackList
// =========================================================================
/**
* Defective: ArrayList.contains() requires scanning the list linearly.
* We count comparisons explicitly: checking a list of size k costs k ops.
*/
static class DefectiveReactorManager {
final List<String> blackList = new ArrayList<>();
final Map<String, List<String>> dependents;
long comparisons = 0;
DefectiveReactorManager(Map<String, List<String>> dependents) {
this.dependents = dependents;
}
boolean listContains(String id) {
// Simulate ArrayList.contains() scan cost: O(size) comparisons
comparisons += blackList.size();
return blackList.contains(id);
}
void blackList(String id) {
if (!listContains(id)) {
blackList.add(id);
List<String> deps = dependents.getOrDefault(id, List.of());
for (String dep : deps) {
blackList(dep);
}
}
}
}
/**
* Fixed: HashSet.contains() / add() is O(1) — 1 comparison per call.
*/
static class FixedReactorManager {
final Set<String> blackList = new HashSet<>();
final Map<String, List<String>> dependents;
long comparisons = 0;
FixedReactorManager(Map<String, List<String>> dependents) {
this.dependents = dependents;
}
void blackList(String id) {
comparisons++; // HashSet.add() is O(1)
if (blackList.add(id)) {
List<String> deps = dependents.getOrDefault(id, List.of());
for (String dep : deps) {
blackList(dep);
}
}
}
}
/**
* Build a linear dependency chain: p0 -> p1 -> p2 -> ... -> p(N-1).
* Blacklisting p0 triggers a recursive cascade over all N projects.
*/
static Map<String, List<String>> buildLinearChain(int n) {
Map<String, List<String>> deps = new HashMap<>();
for (int i = 0; i < n - 1; i++) {
deps.put("p" + i, List.of("p" + (i + 1)));
}
return deps;
}
static long[] runBlacklistComparisons(int n) {
Map<String, List<String>> deps = buildLinearChain(n);
DefectiveReactorManager defective = new DefectiveReactorManager(deps);
defective.blackList("p0");
long defectiveCmp = defective.comparisons;
FixedReactorManager fixed = new FixedReactorManager(deps);
fixed.blackList("p0");
long fixedCmp = fixed.comparisons;
// Defective: at step k the blackList has k elements, so contains() costs k.
// For N projects: sum(0, 1, ..., N-1) = N*(N-1)/2 comparisons → O(N²).
// Fixed: N calls each costing 1 → O(N).
return new long[]{ defectiveCmp, fixedCmp };
}
// =========================================================================
// Models for maven-0007: DefaultMavenExecutionRequest ArrayList pluginGroups
// =========================================================================
static class DefectivePluginGroupRequest {
List<String> pluginGroups = new ArrayList<>();
long comparisons = 0;
void addPluginGroup(String group) {
// Simulate ArrayList.contains() cost = list size
comparisons += pluginGroups.size();
if (!pluginGroups.contains(group)) {
pluginGroups.add(group);
}
}
void addPluginGroups(List<String> groups) {
for (String g : groups) addPluginGroup(g);
}
}
static class FixedPluginGroupRequest {
LinkedHashSet<String> pluginGroups = new LinkedHashSet<>();
long comparisons = 0;
void addPluginGroup(String group) {
comparisons++; // HashSet.add() is O(1)
pluginGroups.add(group);
}
void addPluginGroups(List<String> groups) {
for (String g : groups) addPluginGroup(g);
}
}
static long[] runPluginGroupComparisons(int g) {
List<String> groups = new ArrayList<>();
for (int i = 0; i < g; i++) groups.add("org.apache.plugin" + i);
DefectivePluginGroupRequest defective = new DefectivePluginGroupRequest();
defective.addPluginGroups(groups);
long defCmp = defective.comparisons;
FixedPluginGroupRequest fixed = new FixedPluginGroupRequest();
fixed.addPluginGroups(groups);
long fixCmp = fixed.comparisons;
// Defective: at step k list has k elements, contains() costs k.
// sum(0..G-1) = G*(G-1)/2 → O(G²).
// Fixed: G calls each O(1) → O(G).
return new long[]{ defCmp, fixCmp };
}
// =========================================================================
// Test runner
// =========================================================================
public static void main(String[] args) {
int pass = 0;
int fail = 0;
System.out.println("=== maven-0006: ReactorManager ArrayList blackList ===");
for (int n : new int[]{ 10, 50, 100, 200 }) {
long[] r = runBlacklistComparisons(n);
long slow = r[0], fast = r[1];
long expectedSlow = (long) n * (n - 1) / 2; // N*(N-1)/2
boolean slowMatchesQuadratic = slow == expectedSlow;
boolean fastMatchesLinear = fast == n;
boolean slowWorse = slow > fast;
boolean ok = slowMatchesQuadratic && fastMatchesLinear && slowWorse;
if (ok) pass++; else fail++;
String s = ok ? "PASS" : "FAIL";
System.out.printf(
" N=%-3d | slow=%6d cmp (expect %6d=N²/2) | fast=%3d cmp (expect %3d=N) | %s%n",
n, slow, expectedSlow, fast, n, s);
}
// Verify ratio grows quadratically
long[] r10 = runBlacklistComparisons(10);
long[] r100 = runBlacklistComparisons(100);
// At N=10: slow/fast = 45/10 = 4.5; at N=100: 4950/100 = 49.5 → ratio*10 at 100
boolean ratioGrows = (r100[0] * r10[1]) > (r10[0] * r100[1]);
if (ratioGrows) pass++; else fail++;
System.out.printf(
" ratio@N=10=%.1f ratio@N=100=%.1f — grows with N: %s%n",
(double) r10[0] / r10[1], (double) r100[0] / r100[1],
ratioGrows ? "PASS" : "FAIL");
System.out.println("\n=== maven-0007: DefaultMavenExecutionRequest ArrayList pluginGroups ===");
for (int g : new int[]{ 10, 50, 100, 200 }) {
long[] r = runPluginGroupComparisons(g);
long slow = r[0], fast = r[1];
long expectedSlow = (long) g * (g - 1) / 2;
boolean slowMatchesQuadratic = slow == expectedSlow;
boolean fastMatchesLinear = fast == g;
boolean slowWorse = slow > fast;
boolean ok = slowMatchesQuadratic && fastMatchesLinear && slowWorse;
if (ok) pass++; else fail++;
String s = ok ? "PASS" : "FAIL";
System.out.printf(
" G=%-3d | slow=%6d cmp (expect %6d=G²/2) | fast=%3d (expect %3d=G) | %s%n",
g, slow, expectedSlow, fast, g, s);
}
// Verify deduplication still works in fixed version
FixedPluginGroupRequest dedup = new FixedPluginGroupRequest();
List<String> dupes = new ArrayList<>();
for (int i = 0; i < 50; i++) dupes.add("org.apache.plugin" + (i % 10));
dedup.addPluginGroups(dupes);
boolean dedupCorrect = dedup.pluginGroups.size() == 10;
if (dedupCorrect) pass++; else fail++;
System.out.printf(" Dedup: 50 adds (10 unique) → size=%d == 10: %s%n",
dedup.pluginGroups.size(), dedupCorrect ? "PASS" : "FAIL");
// Verify insertion order preserved in fixed version
FixedPluginGroupRequest ordered = new FixedPluginGroupRequest();
ordered.addPluginGroup("alpha");
ordered.addPluginGroup("beta");
ordered.addPluginGroup("gamma");
ordered.addPluginGroup("alpha"); // duplicate — must be ignored
List<String> orderedList = new ArrayList<>(ordered.pluginGroups);
boolean orderCorrect = orderedList.equals(List.of("alpha", "beta", "gamma"));
if (orderCorrect) pass++; else fail++;
System.out.printf(" Order [alpha, beta, gamma]: %s%n", orderCorrect ? "PASS" : "FAIL");
System.out.println("\n=== Summary ===");
System.out.printf(" %d/%d PASS%n", pass, pass + fail);
if (fail > 0) {
throw new AssertionError(fail + " test(s) FAILED");
}
}
}