B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
264 lines
11 KiB
Java
264 lines
11 KiB
Java
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<Uint32> with Set<Uint32> (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<String, List<TrackedId>> deviceGroupMapping = new ConcurrentHashMap<>();
|
||
|
||
public boolean isGroupPresent(String nodeId, TrackedId groupId) {
|
||
List<TrackedId> 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<String, Set<TrackedId>> deviceGroupMapping = new ConcurrentHashMap<>();
|
||
|
||
public boolean isGroupPresent(String nodeId, TrackedId groupId) {
|
||
Set<TrackedId> 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");
|
||
}
|
||
}
|