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; * 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 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 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 { 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 { private final HashSet 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); } } }