package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * CWE-407 unit test: solr-003 * SplitShardCmd.cleanupAfterFailedSplit — subSlices.contains(s.getName()) (List) * inside a loop over all collection slices — O(S × n). * * Slow path: List.contains() — O(n) per slice. * Fast path: HashSet.contains() — O(1) per slice. * * Compile: javac -d . SplitShardSubSlicesContains.java * Run: java -ea unit.SplitShardSubSlicesContains */ public class SplitShardSubSlicesContains { static final class Slice { final String name; final String state; Slice(String name, String state) { this.name = name; this.state = state; } String getName() { return name; } } // ---- Slow: List.contains O(n) per slice ---- static Map slowCleanupStateUpdate(List allSlices, List subSlices) { Map propMap = new HashMap<>(); for (Slice s : allSlices) { if (!subSlices.contains(s.getName())) { // O(n) per iteration continue; } propMap.put(s.getName(), "CONSTRUCTION"); } return propMap; } static long countSlowOps(List allSlices, List subSlices) { long ops = 0; for (Slice s : allSlices) { // each contains() scan is O(n) — count all comparisons for (String sub : subSlices) { ops++; if (sub.equals(s.getName())) break; } } return ops; } // ---- Fast: HashSet.contains O(1) per slice ---- static Map fastCleanupStateUpdate(List allSlices, List subSlices) { Set subSliceSet = new HashSet<>(subSlices); Map propMap = new HashMap<>(); for (Slice s : allSlices) { if (!subSliceSet.contains(s.getName())) { // O(1) continue; } propMap.put(s.getName(), "CONSTRUCTION"); } return propMap; } static long countFastOps(List allSlices, List subSlices) { long ops = subSlices.size(); // build set: O(n) ops += allSlices.size(); // O(1) per lookup return ops; } public static void main(String[] args) { int passed = 0; // ---- Test 1: correctness at small scale ---- { List allSlices = new ArrayList<>(); List subSlices = new ArrayList<>(); // 20 total slices, 3 sub-shards from split for (int i = 0; i < 20; i++) allSlices.add(new Slice("shard" + i, "ACTIVE")); subSlices.add("shard5_0"); subSlices.add("shard5_1"); subSlices.add("shard5_2"); // Add the sub-slices to allSlices too for (String s : subSlices) allSlices.add(new Slice(s, "INACTIVE")); Map slow = slowCleanupStateUpdate(allSlices, subSlices); Map fast = fastCleanupStateUpdate(allSlices, subSlices); assert slow.equals(fast) : "FAIL: slow=" + slow + " fast=" + fast; assert slow.size() == 3 : "FAIL: expected 3 in propMap, got " + slow.size(); System.out.println("PASS test1: correctness — propMap size=" + fast.size()); passed++; } // ---- Test 2: no subslices found ---- { List allSlices = new ArrayList<>(); List subSlices = new ArrayList<>(); for (int i = 0; i < 50; i++) allSlices.add(new Slice("shard" + i, "ACTIVE")); subSlices.add("nonexistent_0"); subSlices.add("nonexistent_1"); Map slow = slowCleanupStateUpdate(allSlices, subSlices); Map fast = fastCleanupStateUpdate(allSlices, subSlices); assert slow.isEmpty() && fast.isEmpty() : "FAIL: expected empty maps"; System.out.println("PASS test2: no subslices found"); passed++; } // ---- Test 3: operation count ratio >= 5x (theoretical max ~n for n subslices) ---- { int S = 5000; // large collection with many shards int n = 8; // MAX_NUM_SUB_SHARDS List allSlices = new ArrayList<>(); List subSlices = new ArrayList<>(); for (int i = 0; i < S; i++) allSlices.add(new Slice("shard" + i, "ACTIVE")); for (int i = 0; i < n; i++) subSlices.add("newshard" + i); // none overlap — worst case for slow: each contains() exhausts all n items long slowOps = countSlowOps(allSlices, subSlices); long fastOps = countFastOps(allSlices, subSlices); double ratio = (double) slowOps / fastOps; System.out.printf("PASS test3: S=%d n=%d slow=%d fast=%d ratio=%.1fx%n", S, n, slowOps, fastOps, ratio); assert ratio >= 5.0 : "FAIL: ratio " + ratio + " < 5x"; passed++; } // ---- Test 4: timing benchmark (larger subSlices list to amplify O(n) factor) ---- { // Use a larger synthetic n (100 sub-shards) to show timing ratio clearly. // The real cap is MAX_NUM_SUB_SHARDS=8 but the algorithmic structure is identical. int S = 5000; int n = 100; List allSlices = new ArrayList<>(); List subSlices = new ArrayList<>(); for (int i = 0; i < S; i++) allSlices.add(new Slice("shard" + i, "ACTIVE")); for (int i = 0; i < n; i++) subSlices.add("newshard" + i); int reps = 500; long t0 = System.nanoTime(); for (int r = 0; r < reps; r++) slowCleanupStateUpdate(allSlices, subSlices); long slowNs = System.nanoTime() - t0; long t1 = System.nanoTime(); for (int r = 0; r < reps; r++) fastCleanupStateUpdate(allSlices, subSlices); long fastNs = System.nanoTime() - t1; double ratio = (double) slowNs / fastNs; System.out.printf("PASS test4: timing S=%d n=%d slow=%.2fms fast=%.2fms ratio=%.1fx%n", S, n, slowNs / 1e6 / reps, fastNs / 1e6 / reps, ratio); assert ratio >= 10.0 : "FAIL: timing ratio " + ratio + " < 10x"; passed++; } System.out.println(passed + "/" + passed + " PASS"); } }