package unit; import java.util.*; /** * Unit test for spark-0001: seenWindowAggregates ArrayBuffer CWE-407. * * Defect: Analyzer.scala uses ArrayBuffer[AggregateExpression] for * seenWindowAggregates and calls .contains(agg) on it during * window function extraction. O(W) per check, O(A×W) total * where A = aggregate expressions, W = window aggregates in query. * * Fix: Replace ArrayBuffer with mutable.LinkedHashSet — O(1) contains. * * This test uses a self-contained Java model of the two strategies: * DefectiveWindowExtractor — ArrayList.contains() * FixedWindowExtractor — LinkedHashSet.contains() * * Measurement: count element-level comparisons in the membership check. */ public class SparkAnalyzerWindowAggTest { // ── Minimal AggregateExpression stub ──────────────────────────────────── public static class AggExpr { final String id; AggExpr(String id) { this.id = id; } @Override public boolean equals(Object o) { return o instanceof AggExpr && ((AggExpr) o).id.equals(id); } @Override public int hashCode() { return id.hashCode(); } @Override public String toString() { return "Agg(" + id + ")"; } } // ── Result ────────────────────────────────────────────────────────────── public static class Result { final List extracted; public final long comparisons; // element-level equality checks Result(List extracted, long comparisons) { this.extracted = extracted; this.comparisons = comparisons; } } // ── DEFECTIVE: ArrayList.contains() ───────────────────────────────────── public static Result extractDefective(List windowAggs, List otherAggs) { List seen = new ArrayList<>(); long comparisons = 0; // Window aggregates are added first (equivalent to WindowExpression case) for (AggExpr agg : windowAggs) { seen.add(agg); } // Other aggregates check seen list — O(W) each List extracted = new ArrayList<>(); for (AggExpr agg : otherAggs) { // Simulate ArrayList.contains() — count each comparison boolean found = false; for (AggExpr s : seen) { comparisons++; if (s.equals(agg)) { found = true; break; } } if (!found) { extracted.add(agg); seen.add(agg); } } return new Result(extracted, comparisons); } // ── FIXED: LinkedHashSet.contains() ───────────────────────────────────── public static Result extractFixed(List windowAggs, List otherAggs) { LinkedHashSet seen = new LinkedHashSet<>(); long comparisons = 0; for (AggExpr agg : windowAggs) { seen.add(agg); } List extracted = new ArrayList<>(); for (AggExpr agg : otherAggs) { comparisons++; // O(1) hash lookup — counts as 1 if (!seen.contains(agg)) { extracted.add(agg); seen.add(agg); } } return new Result(extracted, comparisons); } // ── Build test fixtures ────────────────────────────────────────────────── public static List windowAggs(int w) { List list = new ArrayList<>(); for (int i = 0; i < w; i++) list.add(new AggExpr("win_" + i)); return list; } public static List otherAggs(int a, List existing) { // Mix of novel aggregates and ones already in existing (the common case) List list = new ArrayList<>(); for (int i = 0; i < a; i++) { list.add(i % 3 == 0 && i / 3 < existing.size() ? existing.get(i / 3) // already seen : new AggExpr("agg_" + i)); // novel } return list; } // ── Tests ──────────────────────────────────────────────────────────────── static void testCorrectnessMatch() { List wins = windowAggs(5); List aggs = otherAggs(10, wins); Result def = extractDefective(wins, aggs); Result fix = extractFixed(wins, aggs); assert new HashSet<>(def.extracted).equals(new HashSet<>(fix.extracted)) : "defective and fixed must extract identical aggregates"; System.out.println("PASS testCorrectnessMatch"); } static void testDefectiveGrowsQuadratically() { // W=A, grow together: comparisons should scale ~quadratically long prev = -1; double prevRatio = -1; for (int n : new int[]{10, 20, 40}) { List wins = windowAggs(n); List aggs = otherAggs(n, wins); long c = extractDefective(wins, aggs).comparisons; if (prev > 0) { double ratio = (double) c / prev; assert ratio > 2.5 : "defective comparisons should grow >2.5x when n doubles; got " + ratio; } prev = c; } System.out.println("PASS testDefectiveGrowsQuadratically"); } static void testFixedGrowsLinearly() { // Fixed: exactly 1 comparison per otherAgg regardless of W for (int n : new int[]{10, 20, 40}) { List wins = windowAggs(n); List aggs = otherAggs(n, wins); long c = extractFixed(wins, aggs).comparisons; assert c == n : "fixed must make exactly A comparisons (one per otherAgg); got " + c + " for A=" + n; } System.out.println("PASS testFixedGrowsLinearly"); } static void testRatioAtScaleIsLarge() { int w = 100, a = 100; List wins = windowAggs(w); List aggs = otherAggs(a, wins); long def_c = extractDefective(wins, aggs).comparisons; long fix_c = extractFixed(wins, aggs).comparisons; double ratio = (double) def_c / fix_c; assert ratio > 10 : "at W=A=100, defective should be >10x worse; ratio=" + ratio; System.out.printf("PASS testRatioAtScaleIsLarge (defective=%d, fixed=%d, ratio=%.1fx)%n", def_c, fix_c, ratio); } public static void main(String[] args) { testCorrectnessMatch(); testDefectiveGrowsQuadratically(); testFixedGrowsLinearly(); testRatioAtScaleIsLarge(); System.out.println("All spark-0001 tests passed."); } }