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.
355 lines
16 KiB
Java
355 lines
16 KiB
Java
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);
|
||
}
|
||
}
|
||
}
|