package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; /** * Unit test for ODL-001: DevicesGroupRegistry CWE-407 defect. * * Models both defective (ArrayList/O(N)) and fixed (HashSet/O(1)) * implementations of DevicesGroupRegistry and measures comparison counts * using an instrumented counter — no wall-clock timing needed. * * Defect: isGroupPresent() calls ArrayList.contains() which does a linear scan. * Called from the outer reconciliation loop over G groups with N already-tracked * groups per node, producing O(G×N) total comparisons per switch reconnect. * * Fix: Replace List with Set (HashSet). contains() becomes O(1). */ public class OdlGroupRegistryTest { // ----------------------------------------------------------------------- // Instrumented counter — incremented by every element comparison // ----------------------------------------------------------------------- static final AtomicLong comparisonCounter = new AtomicLong(0); // ----------------------------------------------------------------------- // Instrumented value type (stand-in for Uint32) // ----------------------------------------------------------------------- static class TrackedId { final long value; TrackedId(long value) { this.value = value; } @Override public boolean equals(Object o) { comparisonCounter.incrementAndGet(); if (this == o) return true; if (!(o instanceof TrackedId)) return false; return value == ((TrackedId) o).value; } @Override public int hashCode() { // Standard hash — NOT instrumented; only equals() counts comparisons. return Long.hashCode(value); } } // ----------------------------------------------------------------------- // Defective registry — ArrayList per node (O(N) contains) // ----------------------------------------------------------------------- static class DefectiveRegistry { private final Map> deviceGroupMapping = new ConcurrentHashMap<>(); public boolean isGroupPresent(String nodeId, TrackedId groupId) { List groups = deviceGroupMapping.get(nodeId); return groups != null && groups.contains(groupId); // O(N) linear scan } public void storeGroup(String nodeId, TrackedId groupId) { deviceGroupMapping.computeIfAbsent(nodeId, k -> new ArrayList<>()).add(groupId); } } // ----------------------------------------------------------------------- // Fixed registry — HashSet per node (O(1) contains) // CWE-407 fix // ----------------------------------------------------------------------- static class FixedRegistry { private final Map> deviceGroupMapping = new ConcurrentHashMap<>(); public boolean isGroupPresent(String nodeId, TrackedId groupId) { Set groups = deviceGroupMapping.get(nodeId); return groups != null && groups.contains(groupId); // O(1) hash lookup } public void storeGroup(String nodeId, TrackedId groupId) { deviceGroupMapping.computeIfAbsent(nodeId, k -> new HashSet<>()).add(groupId); // CWE-407 fix } } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- /** Pre-populate a DefectiveRegistry with N already-tracked groups for nodeId. */ static DefectiveRegistry buildDefective(String nodeId, int n) { DefectiveRegistry reg = new DefectiveRegistry(); for (int i = 0; i < n; i++) { reg.storeGroup(nodeId, new TrackedId(i)); } return reg; } /** Pre-populate a FixedRegistry with N already-tracked groups for nodeId. */ static FixedRegistry buildFixed(String nodeId, int n) { FixedRegistry reg = new FixedRegistry(); for (int i = 0; i < n; i++) { reg.storeGroup(nodeId, new TrackedId(i)); } return reg; } /** * Simulate the reconciliation loop: for each of G groups-to-install, * call isGroupPresent() using a TrackedId that is NOT present (worst-case * for ArrayList — must scan full list before returning false). * * Returns the number of equals() comparisons recorded. */ static long runDefectiveReconciliation(DefectiveRegistry reg, String nodeId, int g) { comparisonCounter.set(0); long absent = 1_000_000L; // ids that do not exist in the registry for (int i = 0; i < g; i++) { reg.isGroupPresent(nodeId, new TrackedId(absent + i)); } return comparisonCounter.get(); } static long runFixedReconciliation(FixedRegistry reg, String nodeId, int g) { comparisonCounter.set(0); long absent = 1_000_000L; for (int i = 0; i < g; i++) { reg.isGroupPresent(nodeId, new TrackedId(absent + i)); } return comparisonCounter.get(); } // ----------------------------------------------------------------------- // Test methods // ----------------------------------------------------------------------- /** * testDefectiveIsQuadratic: * With G=200 installs and N=200 tracked, the defective ArrayList path * must produce at least G*N/2 comparisons (i.e. at least N comparisons * per miss for half the cases on average — in practice all N because * the id is absent). */ static void testDefectiveIsQuadratic() { int G = 200, N = 200; String node = "openflow:1"; DefectiveRegistry reg = buildDefective(node, N); long comparisons = runDefectiveReconciliation(reg, node, G); long expected = (long) G * N; // each miss scans all N entries assert comparisons >= expected : "testDefectiveIsQuadratic FAIL: expected >= " + expected + " comparisons, got " + comparisons; System.out.printf(" testDefectiveIsQuadratic PASS G=%d N=%d comparisons=%d (expected>=%d)%n", G, N, comparisons, expected); } /** * testFixedIsLinear: * With G=200 installs and N=200 tracked, the fixed HashSet path must * produce at most G*2 comparisons (each hash-bucket lookup may hit at * most a handful of equals() calls in a well-distributed set; in practice * usually 0 or 1 for absent keys with no hash collisions). * * Upper bound: G * 4 comparisons — very generous for a 200-entry HashSet. */ static void testFixedIsLinear() { int G = 200, N = 200; String node = "openflow:1"; FixedRegistry reg = buildFixed(node, N); long comparisons = runFixedReconciliation(reg, node, G); long upperBound = (long) G * 4; assert comparisons <= upperBound : "testFixedIsLinear FAIL: expected <= " + upperBound + " comparisons, got " + comparisons; System.out.printf(" testFixedIsLinear PASS G=%d N=%d comparisons=%d (expected<=%d)%n", G, N, comparisons, upperBound); } /** * testRatioAtScale: * Measures defective vs fixed comparison counts at G=200, N=200 and * asserts the ratio is at least 20x, confirming the algorithmic * complexity improvement. */ static void testRatioAtScale() { int G = 200, N = 200; String node = "openflow:1"; DefectiveRegistry defReg = buildDefective(node, N); long defectiveCount = runDefectiveReconciliation(defReg, node, G); FixedRegistry fixReg = buildFixed(node, N); long fixedCount = runFixedReconciliation(fixReg, node, G); // Avoid division by zero: fixed may be 0 comparisons (all hash misses with no collisions) double ratio = fixedCount > 0 ? (double) defectiveCount / fixedCount : defectiveCount; assert ratio >= 20.0 : "testRatioAtScale FAIL: ratio=" + ratio + " (defective=" + defectiveCount + " fixed=" + fixedCount + "), expected >= 20x"; System.out.printf(" testRatioAtScale PASS defective=%d fixed=%d ratio=%.1fx%n", defectiveCount, fixedCount, ratio); } /** * testCorrectnessDefective: * Verifies that the defective implementation still produces correct * boolean results — the defect is performance only, not correctness. */ static void testCorrectnessDefective() { String node = "openflow:1"; DefectiveRegistry reg = new DefectiveRegistry(); TrackedId g1 = new TrackedId(10); TrackedId g2 = new TrackedId(20); TrackedId g3 = new TrackedId(30); reg.storeGroup(node, g1); reg.storeGroup(node, g2); comparisonCounter.set(0); assert reg.isGroupPresent(node, new TrackedId(10)) : "testCorrectnessDefective FAIL: g1 should be present"; assert reg.isGroupPresent(node, new TrackedId(20)) : "testCorrectnessDefective FAIL: g2 should be present"; assert !reg.isGroupPresent(node, new TrackedId(30)) : "testCorrectnessDefective FAIL: g3 should be absent"; assert !reg.isGroupPresent("openflow:2", new TrackedId(10)) : "testCorrectnessDefective FAIL: wrong node"; System.out.println(" testCorrectnessDefective PASS present/absent/wrong-node all correct"); } /** * testCorrectnessFixed: * Same correctness assertions for the fixed HashSet implementation. */ static void testCorrectnessFixed() { String node = "openflow:1"; FixedRegistry reg = new FixedRegistry(); reg.storeGroup(node, new TrackedId(10)); reg.storeGroup(node, new TrackedId(20)); comparisonCounter.set(0); assert reg.isGroupPresent(node, new TrackedId(10)) : "testCorrectnessFixed FAIL: g1 should be present"; assert reg.isGroupPresent(node, new TrackedId(20)) : "testCorrectnessFixed FAIL: g2 should be present"; assert !reg.isGroupPresent(node, new TrackedId(30)) : "testCorrectnessFixed FAIL: g3 should be absent"; assert !reg.isGroupPresent("openflow:2", new TrackedId(10)) : "testCorrectnessFixed FAIL: wrong node"; System.out.println(" testCorrectnessFixed PASS present/absent/wrong-node all correct"); } // ----------------------------------------------------------------------- // Main // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("ODL-001 DevicesGroupRegistry CWE-407 unit tests"); System.out.println("================================================"); testCorrectnessDefective(); testCorrectnessFixed(); testDefectiveIsQuadratic(); testFixedIsLinear(); testRatioAtScale(); System.out.println("================================================"); System.out.println("ALL TESTS PASSED"); } }