java-topology/defects/spring/unit/SpringTest.java

203 lines
8.2 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.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Unit test for Spring CWE-407 defects:
* spring-0001: AnnotationTypeMapping.processAliases — aliases.contains (ArrayList) in nested loops
* spring-0002: AbstractApplicationEventMulticaster — allListeners.contains (ArrayList) in loop
*
* Op counting model: each contains() call costs as many ops as there are elements scanned.
* - ArrayList.contains(x): scans up to N elements → N ops
* - HashSet.contains(x): O(1) → 1 op
*
* No JUnit. No external deps. Compile and run:
* javac -d . *.java && java -ea unit.SpringTest
*/
public class SpringTest {
// ---- spring-0001 simulation ----
// Simulates processAliases: for each attribute (A), for each mapping depth (M),
// for each attribute, call aliases.contains → O(A² × M) total inner scan ops
static long slowAliasProcessing(int attributeCount, int mappingDepth) {
long innerScanOps = 0;
List<Integer> aliases = new ArrayList<>();
for (int i = 0; i < attributeCount; i++) {
aliases.clear();
aliases.add(i);
// collectAliases: for each k, scan the ArrayList
for (int k = 0; k < attributeCount; k++) {
// ArrayList.contains scans all current elements
innerScanOps += aliases.size();
if (!aliases.contains(k)) {
aliases.add(k);
}
}
// processAliases(i, aliases): while(mapping depth) + for(each attribute)
for (int depth = 0; depth < mappingDepth; depth++) {
for (int j = 0; j < attributeCount; j++) {
// ArrayList.contains scans all aliases
innerScanOps += aliases.size();
// (don't call aliases.contains again — cost already counted above)
}
}
}
return innerScanOps;
}
static long fastAliasProcessing(int attributeCount, int mappingDepth) {
long innerScanOps = 0;
Set<Integer> aliases = new LinkedHashSet<>();
for (int i = 0; i < attributeCount; i++) {
aliases.clear();
aliases.add(i);
// collectAliases: HashSet.contains is O(1) = 1 op per call
for (int k = 0; k < attributeCount; k++) {
innerScanOps += 1; // O(1) lookup
aliases.add(k); // Set.add handles dedup
}
// processAliases: HashSet.contains O(1) per call
for (int depth = 0; depth < mappingDepth; depth++) {
for (int j = 0; j < attributeCount; j++) {
innerScanOps += 1; // O(1)
}
}
}
return innerScanOps;
}
// ---- spring-0002 simulation ----
// Simulates retrieveApplicationListeners: for each listenerBean, call allListeners.contains.
// allListeners is ArrayList → O(L) scan per call; three calls per bean.
static long slowListenerDedup(int listenerCount) {
long innerScanOps = 0;
List<Integer> allListeners = new ArrayList<>();
// Pre-populate with half the listeners (programmatic registration)
for (int i = 0; i < listenerCount / 2; i++) {
allListeners.add(i);
}
// Iterate bean-name listeners (the other half)
for (int i = listenerCount / 2; i < listenerCount; i++) {
// filteredListeners.contains(unwrappedListener) — scan cost
innerScanOps += allListeners.size();
// allListeners.contains(unwrappedListener) — scan cost
innerScanOps += allListeners.size();
// allListeners.contains(listener) — scan cost
innerScanOps += allListeners.size();
if (!allListeners.contains(i)) {
allListeners.add(i);
}
}
return innerScanOps;
}
static long fastListenerDedup(int listenerCount) {
long innerScanOps = 0;
Set<Integer> allListeners = new LinkedHashSet<>();
for (int i = 0; i < listenerCount / 2; i++) {
allListeners.add(i);
}
for (int i = listenerCount / 2; i < listenerCount; i++) {
// Each contains on LinkedHashSet: O(1) = 1 op
innerScanOps += 1;
innerScanOps += 1;
innerScanOps += 1;
allListeners.add(i);
}
return innerScanOps;
}
public static void main(String[] args) {
int pass = 0;
int total = 0;
// --- spring-0001 tests ---
{
total++;
long slow = slowAliasProcessing(20, 5);
long fast = fastAliasProcessing(20, 5);
// With A=20, M=5: slow has O(A^2 * M) scan ops = ~2000+, fast has O(A * (A + A*M)) = ~2400
// The key difference is in collectAliases: slow scans growing list (avg A/2), fast is 1
boolean ok = slow > fast;
System.out.println("[spring-0001] A=20 M=5: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + String.format("%.1f", (double)slow/Math.max(fast,1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
long slow = slowAliasProcessing(50, 10);
long fast = fastAliasProcessing(50, 10);
boolean ok = slow > fast * 3;
System.out.println("[spring-0001] A=50 M=10: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + String.format("%.1f", (double)slow/Math.max(fast,1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: both paths discover same unique attributes
List<Integer> slowResult = new ArrayList<>();
Set<Integer> fastResult = new LinkedHashSet<>();
int A = 15;
for (int i = 0; i < A; i++) {
if (!slowResult.contains(i)) slowResult.add(i);
fastResult.add(i);
}
// Also add some duplicates
for (int i = 0; i < A / 2; i++) {
if (!slowResult.contains(i)) slowResult.add(i);
fastResult.add(i);
}
boolean ok = slowResult.size() == fastResult.size() &&
new ArrayList<>(fastResult).equals(slowResult);
System.out.println("[spring-0001] correctness A=15: slow_size=" + slowResult.size() +
" fast_size=" + fastResult.size() + " " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
// --- spring-0002 tests ---
{
total++;
long slow = slowListenerDedup(200);
long fast = fastListenerDedup(200);
boolean ok = slow > fast * 10;
System.out.println("[spring-0002] L=200: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
long slow = slowListenerDedup(500);
long fast = fastListenerDedup(500);
boolean ok = slow > fast * 50;
System.out.println("[spring-0002] L=500: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: dedup result must be same size
List<Integer> slowList = new ArrayList<>();
Set<Integer> fastSet = new LinkedHashSet<>();
// Add 100 items including duplicates
for (int i = 0; i < 100; i++) {
int val = i % 80;
if (!slowList.contains(val)) slowList.add(val);
fastSet.add(val);
}
boolean ok = slowList.size() == fastSet.size();
System.out.println("[spring-0002] dedup correctness: slow=" + slowList.size() +
" fast=" + fastSet.size() + " " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
System.out.println("\n" + pass + "/" + total + " PASS");
if (pass != total) {
System.exit(1);
}
}
}