java-topology/defects/spring/unit/SpringBeanFactoryTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

355 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.Arrays;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedHashSet;
/**
* Unit tests for CWE-407 defects in Spring Framework.
*
* spring-0001 (HIGH): BeanFactoryUtils.mergeNamesWithParent() used ArrayList
* for dedup — merged.contains(beanName) is O(|result|) per parentResult
* element, giving O(|result| × |parentResult|) = O(B²) overall.
*
* spring-0002 (LOW-MEDIUM): ImportStack extends ArrayDeque<ConfigurationClass>;
* ArrayDeque.contains() is O(n), called in processMemberClasses() and
* isChainedImportOnStack() once per candidate.
*
* Run: java -ea -cp . unit.SpringBeanFactoryTest
*/
public class SpringBeanFactoryTest {
// -----------------------------------------------------------------------
// spring-0001 models
// -----------------------------------------------------------------------
/** Defective: ArrayList-based merge with O(n) contains per element. */
static MergeResult mergeDefective(String[] result, String[] parentResult) {
long comparisons = 0;
ArrayList<String> merged = new ArrayList<>(result.length + parentResult.length);
merged.addAll(Arrays.asList(result));
for (String beanName : parentResult) {
// Each contains() scan walks the entire merged list — O(|merged|)
comparisons += merged.size();
if (!merged.contains(beanName)) {
merged.add(beanName);
}
}
return new MergeResult(merged.toArray(new String[0]), comparisons);
}
/** Fixed: LinkedHashSet-based merge with O(1) add (set dedup). */
static MergeResult mergeFixed(String[] result, String[] parentResult) {
long comparisons = 0;
LinkedHashSet<String> merged = new LinkedHashSet<>(Arrays.asList(result));
for (String beanName : parentResult) {
comparisons += 1; // O(1) hash lookup per element
merged.add(beanName); // Set.add() is idempotent; no contains() guard
}
return new MergeResult(merged.toArray(new String[0]), comparisons);
}
static class MergeResult {
final String[] names;
final long comparisons;
MergeResult(String[] names, long comparisons) {
this.names = names;
this.comparisons = comparisons;
}
}
// -----------------------------------------------------------------------
// spring-0002 models
// -----------------------------------------------------------------------
/** Defective: plain ArrayDeque — contains() is O(n). */
static class DefectiveImportStack extends ArrayDeque<String> {
long containsCalls = 0;
long totalProbes = 0;
@Override
public boolean contains(Object o) {
containsCalls++;
totalProbes += size(); // ArrayDeque scans all elements
return super.contains(o);
}
}
/** Fixed: ArrayDeque + parallel HashSet — contains() is O(1). */
static class FixedImportStack extends ArrayDeque<String> {
private final HashSet<String> members = new HashSet<>();
long containsCalls = 0;
long totalProbes = 0;
@Override
public void push(String item) {
super.push(item);
members.add(item);
}
@Override
public String pop() {
String item = super.pop();
members.remove(item);
return item;
}
@Override
public void clear() {
super.clear();
members.clear();
}
@Override
public boolean contains(Object o) {
containsCalls++;
totalProbes += 1; // O(1) hash lookup
return members.contains(o);
}
}
// -----------------------------------------------------------------------
// Test 1 — spring-0001: correctness (output must match)
// -----------------------------------------------------------------------
static void test1_spring0001_correctness() {
String[] result = {"beanA", "beanB", "beanC"};
String[] parentResult = {"beanB", "beanD", "beanE", "beanC"};
MergeResult defective = mergeDefective(result, parentResult);
MergeResult fixed = mergeFixed(result, parentResult);
assert Arrays.equals(defective.names, fixed.names)
: "spring-0001 correctness: output mismatch — defective=" +
Arrays.toString(defective.names) + " fixed=" + Arrays.toString(fixed.names);
// Expected: beanA, beanB, beanC, beanD, beanE (result first, then new-only from parent)
String[] expected = {"beanA", "beanB", "beanC", "beanD", "beanE"};
assert Arrays.equals(fixed.names, expected)
: "spring-0001 correctness: wrong output — got " + Arrays.toString(fixed.names);
System.out.println("PASS test1_spring0001_correctness: output=" + Arrays.toString(fixed.names));
}
// -----------------------------------------------------------------------
// Test 2 — spring-0001: O(B²) vs O(B) ratio proves quadratic growth
// -----------------------------------------------------------------------
static void test2_spring0001_complexity_ratio() {
// Small scale: B=50 total beans, all in result, 50 in parentResult (all dupes)
int small = 50;
String[] smallResult = new String[small];
String[] smallParent = new String[small];
for (int i = 0; i < small; i++) {
smallResult[i] = "bean-" + i;
smallParent[i] = "bean-" + i; // all duplicates → worst case for contains()
}
// Large scale: B=500
int large = 500;
String[] largeResult = new String[large];
String[] largeParent = new String[large];
for (int i = 0; i < large; i++) {
largeResult[i] = "bean-" + i;
largeParent[i] = "bean-" + i;
}
MergeResult defSmall = mergeDefective(smallResult, smallParent);
MergeResult defLarge = mergeDefective(largeResult, largeParent);
MergeResult fixSmall = mergeFixed(smallResult, smallParent);
MergeResult fixLarge = mergeFixed(largeResult, largeParent);
// Defective: comparisons should scale ~quadratically (10x input → ~100x comparisons)
double defRatio = (double) defLarge.comparisons / defSmall.comparisons;
// Fixed: comparisons should scale ~linearly (10x input → ~10x comparisons)
double fixRatio = (double) fixLarge.comparisons / fixSmall.comparisons;
System.out.printf(" defective comparisons: small=%d large=%d ratio=%.1fx%n",
defSmall.comparisons, defLarge.comparisons, defRatio);
System.out.printf(" fixed comparisons: small=%d large=%d ratio=%.1fx%n",
fixSmall.comparisons, fixLarge.comparisons, fixRatio);
// Defective ratio should be ~100 (quadratic), fixed ratio should be ~10 (linear)
assert defRatio > 50.0
: "spring-0001 complexity: defective ratio should be >50x at 10x scale, got " + defRatio;
assert fixRatio < 20.0
: "spring-0001 complexity: fixed ratio should be <20x at 10x scale, got " + fixRatio;
assert defRatio > fixRatio * 3
: "spring-0001 complexity: defective should grow much faster than fixed, ratios: def=" +
defRatio + " fix=" + fixRatio;
System.out.printf("PASS test2_spring0001_complexity_ratio: defective=%.0fx fixed=%.0fx%n",
defRatio, fixRatio);
}
// -----------------------------------------------------------------------
// Test 3 — spring-0001: absolute comparison counts prove O(B²)
// -----------------------------------------------------------------------
static void test3_spring0001_absolute_counts() {
// With B beans all in result and all duplicated in parentResult:
// defective does sum_{k=B}^{2B-1} k ≈ (3B²/2) comparisons
// fixed does exactly B comparisons (one hash probe per parentResult element)
int B = 200;
String[] result = new String[B];
String[] parent = new String[B];
for (int i = 0; i < B; i++) {
result[i] = "bean-" + i;
parent[i] = "bean-" + i;
}
MergeResult def = mergeDefective(result, parent);
MergeResult fix = mergeFixed(result, parent);
// Defective: each of B parentResult elements triggers a full scan of merged
// (which grows from B to B + new additions). Worst case (all dupes): B*B scans.
long expectedDefMin = (long) B * B; // lower bound: B elements × B scans each
assert def.comparisons >= expectedDefMin
: "spring-0001 counts: defective should do >=" + expectedDefMin +
" comparisons, got " + def.comparisons;
// Fixed: exactly B hash probes (one per parentResult element)
assert fix.comparisons == B
: "spring-0001 counts: fixed should do exactly " + B + " comparisons, got " + fix.comparisons;
long speedup = def.comparisons / fix.comparisons;
System.out.printf("PASS test3_spring0001_absolute_counts: defective=%d fixed=%d speedup=%dx%n",
def.comparisons, fix.comparisons, speedup);
}
// -----------------------------------------------------------------------
// Test 4 — spring-0002: ImportStack contains() complexity
// -----------------------------------------------------------------------
static void test4_spring0002_importstack_complexity() {
int N = 100; // N config classes pushed onto stack
DefectiveImportStack defStack = new DefectiveImportStack();
FixedImportStack fixStack = new FixedImportStack();
// Push N items, then call contains() N times for items at various positions
for (int i = 0; i < N; i++) {
defStack.push("config-" + i);
fixStack.push("config-" + i);
}
// Query contains for each item — worst case, item is at the tail (oldest push)
for (int i = 0; i < N; i++) {
boolean defFound = defStack.contains("config-" + i);
boolean fixFound = fixStack.contains("config-" + i);
assert defFound == fixFound
: "spring-0002 correctness: mismatch at i=" + i +
" defective=" + defFound + " fixed=" + fixFound;
}
// Query for absent items (false lookups — also O(n) in defective)
for (int i = N; i < 2 * N; i++) {
boolean defFound = defStack.contains("config-" + i);
boolean fixFound = fixStack.contains("config-" + i);
assert !defFound : "spring-0002: defective found absent item " + i;
assert !fixFound : "spring-0002: fixed found absent item " + i;
}
System.out.printf(" defective: calls=%d totalProbes=%d avg=%.1f per call%n",
defStack.containsCalls, defStack.totalProbes,
(double) defStack.totalProbes / defStack.containsCalls);
System.out.printf(" fixed: calls=%d totalProbes=%d avg=%.1f per call%n",
fixStack.containsCalls, fixStack.totalProbes,
(double) fixStack.totalProbes / fixStack.containsCalls);
// Defective average probes per call should be O(N); fixed should be O(1)
double defAvg = (double) defStack.totalProbes / defStack.containsCalls;
double fixAvg = (double) fixStack.totalProbes / fixStack.containsCalls;
assert defAvg > N / 2.0
: "spring-0002: defective avg probes should be >N/2=" + (N/2) + ", got " + defAvg;
assert fixAvg == 1.0
: "spring-0002: fixed avg probes should be exactly 1.0, got " + fixAvg;
assert defStack.totalProbes > fixStack.totalProbes * (N / 4)
: "spring-0002: defective probes should be >> fixed probes";
System.out.printf("PASS test4_spring0002_importstack_complexity: " +
"defective_avg=%.0f fixed_avg=%.0f speedup=%.0fx%n",
defAvg, fixAvg, defAvg / fixAvg);
}
// -----------------------------------------------------------------------
// Test 5 — spring-0001: empty parentResult short-circuit (edge case)
// -----------------------------------------------------------------------
static void test5_spring0001_empty_parent_shortcircuit() {
String[] result = {"beanA", "beanB"};
String[] parentResult = {};
// Both should return result unchanged (zero comparisons)
MergeResult def = mergeDefective(result, parentResult);
MergeResult fix = mergeFixed(result, parentResult);
// When parentResult is empty the loop body never executes
assert def.comparisons == 0 && fix.comparisons == 0
: "spring-0001 edge: empty parent should produce zero comparisons";
assert Arrays.equals(def.names, result) && Arrays.equals(fix.names, result)
: "spring-0001 edge: empty parent should return result unchanged";
System.out.println("PASS test5_spring0001_empty_parent_shortcircuit");
}
// -----------------------------------------------------------------------
// Test 6 — spring-0002: pop() removes from HashSet (stack discipline)
// -----------------------------------------------------------------------
static void test6_spring0002_push_pop_discipline() {
FixedImportStack stack = new FixedImportStack();
stack.push("A");
stack.push("B");
stack.push("C");
assert stack.contains("A") : "spring-0002 discipline: A should be present";
assert stack.contains("B") : "spring-0002 discipline: B should be present";
assert stack.contains("C") : "spring-0002 discipline: C should be present";
String popped = stack.pop();
assert popped.equals("C") : "spring-0002 discipline: LIFO — expected C, got " + popped;
assert !stack.contains("C") : "spring-0002 discipline: C should be absent after pop";
assert stack.contains("A") : "spring-0002 discipline: A still present after C pop";
assert stack.contains("B") : "spring-0002 discipline: B still present after C pop";
stack.clear();
assert !stack.contains("A") : "spring-0002 discipline: A absent after clear";
assert !stack.contains("B") : "spring-0002 discipline: B absent after clear";
assert stack.isEmpty() : "spring-0002 discipline: stack should be empty after clear";
System.out.println("PASS test6_spring0002_push_pop_discipline");
}
// -----------------------------------------------------------------------
// main
// -----------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("=== SpringBeanFactoryTest — CWE-407 spring-0001 / spring-0002 ===");
int passed = 0;
int failed = 0;
Runnable[] tests = {
SpringBeanFactoryTest::test1_spring0001_correctness,
SpringBeanFactoryTest::test2_spring0001_complexity_ratio,
SpringBeanFactoryTest::test3_spring0001_absolute_counts,
SpringBeanFactoryTest::test4_spring0002_importstack_complexity,
SpringBeanFactoryTest::test5_spring0001_empty_parent_shortcircuit,
SpringBeanFactoryTest::test6_spring0002_push_pop_discipline,
};
for (Runnable test : tests) {
try {
test.run();
passed++;
} catch (AssertionError e) {
System.out.println("FAIL: " + e.getMessage());
failed++;
}
}
System.out.println("---");
System.out.println("Results: " + passed + " passed, " + failed + " failed");
if (failed > 0) {
System.exit(1);
}
}
}