253 lines
9.1 KiB
Java
253 lines
9.1 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* Box2DAlgorithm — unit test for Box2D CWE-407 defect.
|
||
*
|
||
* box2d-0001: b2BroadPhase b2UnBufferMove() linear scan of moveArray.
|
||
*
|
||
* File: src/broad_phase.c lines 71–88
|
||
* Defect: b2UnBufferMove() first removes proxyKey from moveSet in O(1),
|
||
* then scans moveArray linearly to find the array index for RemoveSwap.
|
||
* With N buffered proxies, destroying all bodies triggers O(N²) comparisons.
|
||
*
|
||
* for ( int i = 0; i < count; ++i )
|
||
* {
|
||
* if ( bp->moveArray.data[i] == proxyKey ) // linear scan
|
||
* {
|
||
* b2IntArray_RemoveSwap( &bp->moveArray, i );
|
||
* break;
|
||
* }
|
||
* }
|
||
*
|
||
* Fix: maintain a parallel HashMap<proxyKey, arrayIndex> (moveIndex).
|
||
* On bufferMove: record index. On unBufferMove: O(1) lookup, then
|
||
* RemoveSwap with displaced-element index fix-up.
|
||
*
|
||
* The dev comment reads: "Purge from move buffer. Linear search."
|
||
* and a TODO: "todo if I can iterate the move set then I don't need the moveArray"
|
||
*
|
||
* Tests: correctness + performance ratio >= 5x at N=500-1000.
|
||
* Prints: N/N PASS
|
||
*/
|
||
public class Box2DAlgorithm {
|
||
|
||
static int passed = 0;
|
||
static int total = 0;
|
||
|
||
static void pass(String name) {
|
||
passed++;
|
||
total++;
|
||
System.out.println(" PASS " + name);
|
||
}
|
||
|
||
static void fail(String name, String reason) {
|
||
total++;
|
||
System.out.println(" FAIL " + name + " — " + reason);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Slow path: mirrors b2UnBufferMove (linear scan of moveArray)
|
||
// -----------------------------------------------------------------------
|
||
static class SlowMoveBuffer {
|
||
Set<Integer> moveSet = new HashSet<>();
|
||
List<Integer> moveArray = new ArrayList<>();
|
||
|
||
void bufferMove(int proxyKey) {
|
||
if (moveSet.add(proxyKey)) {
|
||
moveArray.add(proxyKey);
|
||
}
|
||
}
|
||
|
||
/** Returns number of comparisons made during the linear scan. */
|
||
long unBufferMove(int proxyKey) {
|
||
long ops = 0;
|
||
if (moveSet.remove(proxyKey)) {
|
||
for (int i = 0; i < moveArray.size(); i++) {
|
||
ops++;
|
||
if (moveArray.get(i).equals(proxyKey)) {
|
||
int last = moveArray.size() - 1;
|
||
moveArray.set(i, moveArray.get(last));
|
||
moveArray.remove(last);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
List<Integer> snapshot() { return new ArrayList<>(moveArray); }
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Fast path: O(1) removal via index map (the proposed fix)
|
||
// -----------------------------------------------------------------------
|
||
static class FastMoveBuffer {
|
||
Set<Integer> moveSet = new HashSet<>();
|
||
List<Integer> moveArray = new ArrayList<>();
|
||
Map<Integer, Integer> indexMap = new HashMap<>(); // proxyKey → array index
|
||
|
||
void bufferMove(int proxyKey) {
|
||
if (moveSet.add(proxyKey)) {
|
||
indexMap.put(proxyKey, moveArray.size());
|
||
moveArray.add(proxyKey);
|
||
}
|
||
}
|
||
|
||
/** Returns 1 (one map lookup) per call regardless of array size. */
|
||
long unBufferMove(int proxyKey) {
|
||
if (moveSet.remove(proxyKey)) {
|
||
Integer idx = indexMap.remove(proxyKey);
|
||
if (idx != null) {
|
||
int last = moveArray.size() - 1;
|
||
if (idx != last) {
|
||
Integer displaced = moveArray.get(last);
|
||
moveArray.set(idx, displaced);
|
||
indexMap.put(displaced, idx);
|
||
}
|
||
moveArray.remove(last);
|
||
}
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
List<Integer> snapshot() { return new ArrayList<>(moveArray); }
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Test helpers
|
||
// -----------------------------------------------------------------------
|
||
|
||
static long runSlowDestroyAll(int n) {
|
||
SlowMoveBuffer buf = new SlowMoveBuffer();
|
||
for (int i = 0; i < n; i++) buf.bufferMove(i);
|
||
long ops = 0;
|
||
for (int i = 0; i < n; i++) ops += buf.unBufferMove(i);
|
||
return ops;
|
||
}
|
||
|
||
static long runFastDestroyAll(int n) {
|
||
FastMoveBuffer buf = new FastMoveBuffer();
|
||
for (int i = 0; i < n; i++) buf.bufferMove(i);
|
||
long ops = 0;
|
||
for (int i = 0; i < n; i++) ops += buf.unBufferMove(i);
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Tests
|
||
// -----------------------------------------------------------------------
|
||
|
||
static void testCorrectness() {
|
||
// Buffer N proxies, remove half, check both paths have identical final arrays.
|
||
int N = 50;
|
||
SlowMoveBuffer slow = new SlowMoveBuffer();
|
||
FastMoveBuffer fast = new FastMoveBuffer();
|
||
|
||
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
|
||
|
||
// Remove even keys
|
||
for (int i = 0; i < N; i += 2) { slow.unBufferMove(i); fast.unBufferMove(i); }
|
||
|
||
List<Integer> slowSnap = slow.snapshot();
|
||
List<Integer> fastSnap = fast.snapshot();
|
||
|
||
Collections.sort(slowSnap);
|
||
Collections.sort(fastSnap);
|
||
|
||
if (!slowSnap.equals(fastSnap)) {
|
||
fail("box2d-0001 correctness N=50 half-remove",
|
||
"snapshots differ: slow=" + slowSnap + " fast=" + fastSnap);
|
||
return;
|
||
}
|
||
pass("box2d-0001 correctness N=50 half-remove (snapshots match)");
|
||
}
|
||
|
||
static void testCorrectnessWithDups() {
|
||
// Duplicate bufferMove calls should be idempotent in both paths.
|
||
int N = 20;
|
||
SlowMoveBuffer slow = new SlowMoveBuffer();
|
||
FastMoveBuffer fast = new FastMoveBuffer();
|
||
|
||
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
|
||
// buffer the same keys again — should not double-add
|
||
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
|
||
|
||
if (slow.moveArray.size() != N) {
|
||
fail("box2d-0001 dup-bufferMove slow dedup", "size=" + slow.moveArray.size());
|
||
return;
|
||
}
|
||
if (fast.moveArray.size() != N) {
|
||
fail("box2d-0001 dup-bufferMove fast dedup", "size=" + fast.moveArray.size());
|
||
return;
|
||
}
|
||
pass("box2d-0001 dup-bufferMove N=20 (both deduplicate correctly)");
|
||
}
|
||
|
||
static void testPerformanceDestroyAll() {
|
||
// N=1000: all proxies buffered then all destroyed.
|
||
// Slow: sum of array sizes scanned — O(N²/2).
|
||
// Fast: exactly N ops — O(N).
|
||
int N = 1000;
|
||
long slowOps = runSlowDestroyAll(N);
|
||
long fastOps = runFastDestroyAll(N);
|
||
|
||
// Slow expected: N + (N-1) + ... + 1 = N*(N+1)/2 (worst case ordering)
|
||
// Actual may vary by removal order; just verify ratio.
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf(" box2d-0001 N=%d slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
|
||
N, slowOps, fastOps, ratio);
|
||
|
||
if (fastOps != N) {
|
||
fail("box2d-0001 performance N=1000", "fast_ops=" + fastOps + " expected=" + N);
|
||
return;
|
||
}
|
||
if (ratio < 5.0) {
|
||
fail("box2d-0001 performance N=1000", "ratio=" + ratio + " < 5x");
|
||
return;
|
||
}
|
||
pass("box2d-0001 performance ratio >= 5x at N=1000 (destroy-all)");
|
||
}
|
||
|
||
static void testPerformanceHalfFill() {
|
||
// N=800 buffered, N/2 removed (mixed static/dynamic pattern).
|
||
int N = 800;
|
||
SlowMoveBuffer slow = new SlowMoveBuffer();
|
||
FastMoveBuffer fast = new FastMoveBuffer();
|
||
for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); }
|
||
|
||
long slowOps = 0, fastOps = 0;
|
||
for (int i = 0; i < N / 2; i++) {
|
||
slowOps += slow.unBufferMove(i);
|
||
fastOps += fast.unBufferMove(i);
|
||
}
|
||
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf(" box2d-0001 N=%d half-remove slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
|
||
N, slowOps, fastOps, ratio);
|
||
|
||
if (ratio < 5.0) {
|
||
fail("box2d-0001 performance N=800 half-remove", "ratio=" + ratio + " < 5x");
|
||
return;
|
||
}
|
||
pass("box2d-0001 performance ratio >= 5x at N=800 (half-remove)");
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("Box2D CWE-407 unit tests");
|
||
System.out.println("=".repeat(60));
|
||
System.out.println();
|
||
System.out.println("[box2d-0001] b2BroadPhase::b2UnBufferMove linear scan");
|
||
|
||
testCorrectness();
|
||
testCorrectnessWithDups();
|
||
testPerformanceDestroyAll();
|
||
testPerformanceHalfFill();
|
||
|
||
System.out.println();
|
||
System.out.printf("%d/%d PASS%n", passed, total);
|
||
if (passed != total) System.exit(1);
|
||
}
|
||
}
|