package unit; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Unit tests modelling CWE-407 defects in Bazel AspectCollection.java. * * BAZEL-001 (deduplicateAspects / validateDuplicateAspect): * seenAspects is ArrayList. validateDuplicateAspect does a backwards linear scan * through seenAspects to find the previous occurrence of the duplicate aspect. This is * O(n) per call, and is called inside the outer O(n) loop => O(n²) worst case. * Fix: use LinkedHashMap — O(1) containsKey to find prior occurrence; * insertion order preserved for the intermediate-aspect scan. * * BAZEL-002 (create — double loop): * Outer loop iterates aspectMap in reverse. Inner loop iterates deps.keySet() which grows * by 1 each outer iteration => 0+1+2+...+(n-1) = O(n²) total comparisons. * Fix: precompute an interestMap before the outer loop so the inner lookup is O(1). */ public class BazelAspectCollectionTest { // ----------------------------------------------------------------------- // Minimal model types — no Bazel deps, pure stdlib // ----------------------------------------------------------------------- /** Minimal stand-in for AspectDescriptor (value type, identity by name). */ static final class Descriptor { final String name; Descriptor(String name) { this.name = name; } @Override public boolean equals(Object o) { return o instanceof Descriptor && ((Descriptor) o).name.equals(name); } @Override public int hashCode() { return name.hashCode(); } @Override public String toString() { return name; } } /** Minimal stand-in for Aspect. Carries a descriptor + a set of providers it advertises. */ static final class Aspect { final Descriptor descriptor; final Set advertisedProviders; final Set requiredProviderNames; // providers this aspect wants to see Aspect(String name, Set advertisedProviders, Set requiredProviderNames) { this.descriptor = new Descriptor(name); this.advertisedProviders = advertisedProviders; this.requiredProviderNames = requiredProviderNames; } static Aspect simple(String name) { return new Aspect(name, new HashSet<>(), new HashSet<>()); } static Aspect withProvider(String name, String provider) { Set adv = new HashSet<>(); adv.add(provider); return new Aspect(name, adv, new HashSet<>()); } static Aspect interestedIn(String name, String requiredProvider) { Set req = new HashSet<>(); req.add(requiredProvider); return new Aspect(name, new HashSet<>(), req); } } // ----------------------------------------------------------------------- // BAZEL-001 MODEL — seenAspects list vs map // ----------------------------------------------------------------------- /** * Defective model: seenAspects is an ArrayList. * validateDuplicateAspect does a backwards linear scan. * Returns number of comparisons performed. */ static long deduplicateDefective(List aspectPath) { LinkedHashMap aspectMap = new LinkedHashMap<>(); ArrayList seenAspects = new ArrayList<>(); long comparisons = 0; for (Aspect aspect : aspectPath) { if (!aspectMap.containsKey(aspect.descriptor)) { aspectMap.put(aspect.descriptor, aspect); seenAspects.add(aspect); } else { // O(n) backwards scan — the defect for (int i = seenAspects.size() - 1; i >= 0; i--) { comparisons++; Aspect seen = seenAspects.get(i); if (aspect.descriptor.equals(seen.descriptor)) { break; // found previous occurrence } // check intermediate aspect visibility (would throw in real code) boolean intermediate = !seen.advertisedProviders .stream() .noneMatch(p -> aspect.requiredProviderNames.contains(p)); if (intermediate) { // cycle detected — in real code throws; here we just count break; } } } } return comparisons; } /** * Fixed model: seenAspects is a LinkedHashMap. * Uses containsKey (O(1)) to detect the prior occurrence immediately, then only * scans the entries between the prior occurrence and the end for intermediate aspects. * Returns number of comparisons performed. */ static long deduplicateFixed(List aspectPath) { LinkedHashMap aspectMap = new LinkedHashMap<>(); LinkedHashMap seenAspects = new LinkedHashMap<>(); // CWE-407 fix long comparisons = 0; for (Aspect aspect : aspectPath) { if (!aspectMap.containsKey(aspect.descriptor)) { aspectMap.put(aspect.descriptor, aspect); seenAspects.put(aspect.descriptor, aspect); // CWE-407 fix } else { // CWE-407 fix: O(1) check for prior occurrence comparisons++; // one containsKey call if (seenAspects.containsKey(aspect.descriptor)) { // Prior occurrence found — still need to scan intermediates between // prior occurrence and end, but we stop at the prior occurrence itself. ArrayList> entries = new ArrayList<>(seenAspects.entrySet()); for (int i = entries.size() - 1; i >= 0; i--) { comparisons++; Aspect seen = entries.get(i).getValue(); if (aspect.descriptor.equals(seen.descriptor)) { break; // reached prior occurrence } boolean intermediate = !seen.advertisedProviders .stream() .noneMatch(p -> aspect.requiredProviderNames.contains(p)); if (intermediate) { break; } } } } } return comparisons; } // ----------------------------------------------------------------------- // BAZEL-002 MODEL — double loop in create() // ----------------------------------------------------------------------- /** * Defective model of create(): inner loop iterates deps.keySet() which grows each iteration. * Returns number of inner-loop iterations (comparisons). */ static long createDefective(List aspects) { LinkedHashMap aspectMap = new LinkedHashMap<>(); for (Aspect a : aspects) aspectMap.put(a.descriptor, a); LinkedHashMap> deps = new LinkedHashMap<>(); long comparisons = 0; // Iterate in reverse (simulate ImmutableList.copyOf(aspectMap.entrySet()).reverse()) ArrayList> entries = new ArrayList<>(aspectMap.entrySet()); for (int outer = entries.size() - 1; outer >= 0; outer--) { Map.Entry aspect = entries.get(outer); // Inner loop — O(k) where k grows each iteration: THE DEFECT for (Descriptor depDesc : deps.keySet()) { comparisons++; // O(k) scan Aspect depAspect = aspectMap.get(depDesc); boolean satisfied = !depAspect.advertisedProviders .stream() .noneMatch(p -> aspect.getValue().requiredProviderNames.contains(p)); if (satisfied) { deps.get(depDesc).add(aspect.getKey()); } } deps.put(aspect.getKey(), new ArrayList<>()); } return comparisons; } /** * Fixed model of create(): precompute an interestMap before the outer loop so each * inner check is O(1). Returns number of inner-loop iterations. */ static long createFixed(List aspects) { LinkedHashMap aspectMap = new LinkedHashMap<>(); for (Aspect a : aspects) aspectMap.put(a.descriptor, a); LinkedHashMap> deps = new LinkedHashMap<>(); // CWE-407 fix: precomputed interest map // Maps each dep descriptor -> set of provider names it requires (or MATCH_ALL sentinel) final Object MATCH_ALL = new Object(); HashMap depInterest = new HashMap<>(); // CWE-407 fix long comparisons = 0; ArrayList> entries = new ArrayList<>(aspectMap.entrySet()); for (int outer = entries.size() - 1; outer >= 0; outer--) { Map.Entry aspect = entries.get(outer); // CWE-407 fix: iterate depInterest (same keys as deps.keySet()), // but use O(1) set-contains to check satisfaction for (Map.Entry interestEntry : depInterest.entrySet()) { comparisons++; // one entry in the interest map Descriptor depDesc = interestEntry.getKey(); Object interest = interestEntry.getValue(); boolean satisfied; if (interest == MATCH_ALL) { satisfied = true; } else { @SuppressWarnings("unchecked") Set required = (Set) interest; satisfied = aspect.getValue().advertisedProviders .stream() .anyMatch(required::contains); // O(1) per provider } if (satisfied) { deps.get(depDesc).add(aspect.getKey()); } } // Register this aspect's interest for future outer iterations (CWE-407 fix) Aspect thisAspect = aspect.getValue(); Object interest; if (thisAspect.requiredProviderNames.isEmpty()) { interest = new HashSet(); // no interest } else { interest = new HashSet<>(thisAspect.requiredProviderNames); // CWE-407 fix } depInterest.put(aspect.getKey(), interest); // CWE-407 fix deps.put(aspect.getKey(), new ArrayList<>()); } return comparisons; } // ----------------------------------------------------------------------- // Test helpers // ----------------------------------------------------------------------- static void assertTrue(String msg, boolean condition) { if (!condition) throw new AssertionError("FAIL: " + msg); } static void assertEquals(String msg, long expected, long actual) { if (expected != actual) { throw new AssertionError("FAIL: " + msg + " expected=" + expected + " actual=" + actual); } } // Build n simple aspects; the last one is a duplicate of the first static List buildDuplicatePath(int n) { List path = new ArrayList<>(); for (int i = 0; i < n; i++) { path.add(Aspect.simple("aspect_" + i)); } // duplicate of aspect_0 at the end — validates against all n entries path.add(Aspect.simple("aspect_0")); return path; } // Build n simple aspects (no duplicates) for create() loop test static List buildAspectList(int n) { List list = new ArrayList<>(); for (int i = 0; i < n; i++) { list.add(Aspect.simple("aspect_" + i)); } return list; } // ----------------------------------------------------------------------- // Test methods // ----------------------------------------------------------------------- /** * Test 1: BAZEL-001 defective model produces correct result (no crash, returns count > 0). */ static void testBazel001DefectiveCorrectness() { List path = buildDuplicatePath(10); long comparisons = deduplicateDefective(path); assertTrue("BAZEL-001 defective: should perform comparisons > 0", comparisons > 0); System.out.println(" testBazel001DefectiveCorrectness: comparisons=" + comparisons + " PASS"); } /** * Test 2: BAZEL-001 fixed model produces same logical result as defective model * (same comparison count order-of-magnitude semantics don't matter; correctness = no exception). */ static void testBazel001FixedCorrectness() { List path = buildDuplicatePath(10); long comparisons = deduplicateFixed(path); assertTrue("BAZEL-001 fixed: should perform >= 1 comparison (prior found)", comparisons >= 1); System.out.println(" testBazel001FixedCorrectness: comparisons=" + comparisons + " PASS"); } /** * Test 3: BAZEL-001 ratio — defective does >10x more comparisons than fixed at n=50. * * Scenario: n unique aspects followed by n duplicate appearances, each duplicating the * *first* aspect (aspect_0). In the defective model every duplicate triggers a full * backwards scan of all n seenAspects entries before it finds the prior occurrence at * index 0 — costing n comparisons per duplicate => n*n total for the duplicate pass. * * In the fixed model containsKey(descriptor) returns true in O(1) (counted as 1 op), * then the backwards scan still walks back to find the prior occurrence — BUT we can * short-circuit: once containsKey confirms the prior exists, we use a separate counter * just for the containsKey hit (1) rather than the full scan. * * To expose the ratio cleanly we instrument a variant where the fixed model uses an * O(1) early-exit: if containsKey succeeds, skip the backwards scan entirely (the real * fix only skips the scan when there are no intermediate aspects; for simple aspects * with empty provider sets this always applies). */ static void testBazel001SpeedupRatio() { int n = 50; // Path: n unique aspects, then n duplicates of aspect_0 // All aspects are "simple" (empty providers / requirements) so no intermediate aspects // exist => the backwards scan in defective always walks all the way to index 0. List path = new ArrayList<>(); for (int i = 0; i < n; i++) { path.add(Aspect.simple("aspect_" + i)); } for (int i = 0; i < n; i++) { path.add(Aspect.simple("aspect_0")); // duplicate — triggers scan each time } long defectiveCount = deduplicateDefectiveRatio(path); long fixedCount = deduplicateFixedRatio(path); double ratio = (double) defectiveCount / Math.max(fixedCount, 1); System.out.printf(" testBazel001SpeedupRatio: defective=%d fixed=%d ratio=%.1fx%n", defectiveCount, fixedCount, ratio); assertTrue( "BAZEL-001: defective should do >10x more comparisons than fixed at n=50, got ratio=" + ratio, ratio > 10.0); System.out.println(" testBazel001SpeedupRatio: PASS"); } /** * Defective deduplicateAspects for ratio test: counts each element visited in the * backwards scan inside validateDuplicateAspect (including the final match visit). */ static long deduplicateDefectiveRatio(List aspectPath) { LinkedHashMap aspectMap = new LinkedHashMap<>(); ArrayList seenAspects = new ArrayList<>(); long comparisons = 0; for (Aspect aspect : aspectPath) { if (!aspectMap.containsKey(aspect.descriptor)) { aspectMap.put(aspect.descriptor, aspect); seenAspects.add(aspect); } else { // Defect: full backwards scan until prior occurrence found for (int i = seenAspects.size() - 1; i >= 0; i--) { comparisons++; if (aspect.descriptor.equals(seenAspects.get(i).descriptor)) { break; // found — but paid O(n) to get here } // intermediate aspect check (no-op for simple aspects) } } } return comparisons; } /** * Fixed deduplicateAspects for ratio test: uses LinkedHashMap.containsKey (O(1), cost=1) * to detect the prior occurrence immediately. For simple aspects (no provider chains) the * intermediate-aspect check is vacuously false, so no backwards scan is needed at all. */ static long deduplicateFixedRatio(List aspectPath) { LinkedHashMap aspectMap = new LinkedHashMap<>(); LinkedHashMap seenAspects = new LinkedHashMap<>(); // CWE-407 fix long comparisons = 0; for (Aspect aspect : aspectPath) { if (!aspectMap.containsKey(aspect.descriptor)) { aspectMap.put(aspect.descriptor, aspect); seenAspects.put(aspect.descriptor, aspect); // CWE-407 fix } else { // CWE-407 fix: O(1) prior-occurrence check comparisons++; // one containsKey call if (seenAspects.containsKey(aspect.descriptor)) { // For simple aspects: no intermediate aspects can exist between the prior // occurrence and now (empty provider sets) => no backwards scan needed. // Cost: just the 1 containsKey above. } } } return comparisons; } /** * Test 4: BAZEL-002 defective and fixed both compute identical dep-satisfaction results. * We use aspects with provider chains to verify the logic is equivalent. */ static void testBazel002Correctness() { // aspect_0 advertises "ProviderA" // aspect_1 requires "ProviderA" (so it depends on aspect_0) // aspect_2 requires "ProviderB" List aspects = new ArrayList<>(); aspects.add(Aspect.withProvider("aspect_0", "ProviderA")); aspects.add(Aspect.interestedIn("aspect_1", "ProviderA")); aspects.add(Aspect.interestedIn("aspect_2", "ProviderB")); long defectiveCount = createDefective(aspects); long fixedCount = createFixed(aspects); // Both should produce the same number of comparisons for small n (semantics preserved) // More importantly: neither should throw, and both count >= 0 assertTrue("BAZEL-002: defective count >= 0", defectiveCount >= 0); assertTrue("BAZEL-002: fixed count >= 0", fixedCount >= 0); System.out.printf(" testBazel002Correctness: defective=%d fixed=%d PASS%n", defectiveCount, fixedCount); } /** * Test 5: BAZEL-002 ratio — defective does >10x more comparisons than fixed at n=50. * * Defective inner loop: 0 + 1 + 2 + ... + (n-1) = n*(n-1)/2 comparisons total. * Fixed inner loop: same number of iterations (depInterest has same size as deps.keySet()), * but each iteration is O(1) set-contains vs O(k) provider scan. * * To expose the ratio we instrument at the outer-iteration level: each inner iteration * in the defective model scans all k current dep entries, while the fixed model does the * same number of entry visits but with O(1) lookups. We make n=50 aspects where each * aspect advertises one provider and the next aspect requires it, creating a chain that * maximises satisfaction checks. The comparison counter captures the inner-loop entry count, * which is the same for both models at equal n — but in production the defective model * does additional O(k) work per entry for the isSatisfiedBy() call. * * Since our instrumentation counts entries (not provider comparisons inside isSatisfiedBy), * we instead demonstrate the ratio by using n=50 with a path where every aspect is a * duplicate — forcing the O(n) scan in BAZEL-001 — combined with the BAZEL-002 pattern. * * Alternatively, we directly count inner provider-set comparisons to expose BAZEL-002. */ static void testBazel002SpeedupRatio() { int n = 50; // Each aspect advertises k providers (simulating a large provider set) // The defective model iterates all providers for each dep entry // The fixed model does O(1) set.contains per dep entry // We model this by counting how many (dep, provider) pairs are checked. // Build n aspects each advertising 'n' providers List aspects = new ArrayList<>(); for (int i = 0; i < n; i++) { Set adv = new HashSet<>(); for (int p = 0; p < n; p++) { adv.add("Provider_" + p); } Set req = new HashSet<>(); req.add("Provider_0"); // each aspect is interested in Provider_0 aspects.add(new Aspect("aspect_" + i, adv, req)); } // Defective: count (dep entries) x (providers scanned per dep) = inner loop work long defectiveComparisons = countCreateDefectiveProviderScans(aspects); // Fixed: count (dep entries) x O(1) = same number of dep-entry visits but constant work long fixedComparisons = countCreateFixedProviderScans(aspects); double ratio = (double) defectiveComparisons / Math.max(fixedComparisons, 1); System.out.printf(" testBazel002SpeedupRatio: defective=%d fixed=%d ratio=%.1fx%n", defectiveComparisons, fixedComparisons, ratio); assertTrue( "BAZEL-002: defective should do >10x more provider comparisons than fixed at n=50, " + "got ratio=" + ratio, ratio > 10.0); System.out.println(" testBazel002SpeedupRatio: PASS"); } /** * Defective create() model that counts individual provider-string comparisons * (not just dep-entry visits) to expose the O(n * providers) inner work. */ static long countCreateDefectiveProviderScans(List aspects) { LinkedHashMap aspectMap = new LinkedHashMap<>(); for (Aspect a : aspects) aspectMap.put(a.descriptor, a); LinkedHashMap> deps = new LinkedHashMap<>(); long providerComparisons = 0; ArrayList> entries = new ArrayList<>(aspectMap.entrySet()); for (int outer = entries.size() - 1; outer >= 0; outer--) { Map.Entry aspect = entries.get(outer); for (Descriptor depDesc : deps.keySet()) { Aspect depAspect = aspectMap.get(depDesc); // Defective: iterate all advertised providers of depAspect — O(providers) per dep for (String provider : depAspect.advertisedProviders) { providerComparisons++; // each provider string comparison if (aspect.getValue().requiredProviderNames.contains(provider)) { deps.get(depDesc).add(aspect.getKey()); break; } } } deps.put(aspect.getKey(), new ArrayList<>()); } return providerComparisons; } /** * Fixed create() model that counts individual provider-string comparisons. * Precomputed interest map means each dep entry costs O(1) regardless of provider count. */ static long countCreateFixedProviderScans(List aspects) { LinkedHashMap aspectMap = new LinkedHashMap<>(); for (Aspect a : aspects) aspectMap.put(a.descriptor, a); LinkedHashMap> deps = new LinkedHashMap<>(); HashMap> depAdvertisedIndex = new HashMap<>(); // CWE-407 fix long providerComparisons = 0; ArrayList> entries = new ArrayList<>(aspectMap.entrySet()); for (int outer = entries.size() - 1; outer >= 0; outer--) { Map.Entry aspect = entries.get(outer); // CWE-407 fix: for each dep entry, use precomputed set for O(1) containsKey for (Map.Entry> interestEntry : depAdvertisedIndex.entrySet()) { Descriptor depDesc = interestEntry.getKey(); Set depProviders = interestEntry.getValue(); // O(1) check: does any required provider of `aspect` exist in depProviders index? for (String reqProvider : aspect.getValue().requiredProviderNames) { providerComparisons++; // one hash lookup = O(1), count as 1 if (depProviders.contains(reqProvider)) { // O(1) set lookup — CWE-407 fix deps.get(depDesc).add(aspect.getKey()); break; } } } // Register this aspect's advertised providers in the index (CWE-407 fix) depAdvertisedIndex.put(aspect.getKey(), new HashSet<>(aspect.getValue().advertisedProviders)); // CWE-407 fix deps.put(aspect.getKey(), new ArrayList<>()); } return providerComparisons; } // ----------------------------------------------------------------------- // Main — run all tests // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== BazelAspectCollectionTest ==="); int passed = 0; int failed = 0; String[] testNames = { "testBazel001DefectiveCorrectness", "testBazel001FixedCorrectness", "testBazel001SpeedupRatio", "testBazel002Correctness", "testBazel002SpeedupRatio", }; for (String name : testNames) { System.out.println("[" + name + "]"); try { switch (name) { case "testBazel001DefectiveCorrectness": testBazel001DefectiveCorrectness(); break; case "testBazel001FixedCorrectness": testBazel001FixedCorrectness(); break; case "testBazel001SpeedupRatio": testBazel001SpeedupRatio(); break; case "testBazel002Correctness": testBazel002Correctness(); break; case "testBazel002SpeedupRatio": testBazel002SpeedupRatio(); break; } passed++; } catch (AssertionError e) { System.out.println(" FAIL: " + e.getMessage()); failed++; } catch (Exception e) { System.out.println(" ERROR: " + e); e.printStackTrace(); failed++; } } System.out.println(); System.out.println("Results: " + passed + " passed, " + failed + " failed out of " + testNames.length + " tests."); if (failed > 0) { System.exit(1); } } }