313 lines
12 KiB
Java
313 lines
12 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* BulletAlgorithm — unit tests for three Bullet Physics CWE-407 defects.
|
||
*
|
||
* bullet-0001: btGhostObject::addOverlappingObjectInternal / removeOverlappingObjectInternal
|
||
* m_overlappingObjects.findLinearSearch(otherObject) called per broadphase pair per step.
|
||
* Slow: ArrayList.contains / indexOf — O(N) per call → O(N²) over N overlapping objects.
|
||
* Fast: HashMap<Object,Integer> index — O(1) per call.
|
||
*
|
||
* bullet-0002: btCollisionObject::checkCollideWithOverride
|
||
* m_objectsWithoutCollisionCheck.findLinearSearch(co) inside processAllOverlappingPairs.
|
||
* Slow: ArrayList.contains — O(E) per pair × M pairs → O(M·E).
|
||
* Fast: HashSet.contains — O(1) per pair.
|
||
*
|
||
* bullet-0003: btSortedOverlappingPairCache::removeOverlappingPair / findPair
|
||
* m_overlappingPairArray.findLinearSearch(pair) per removal.
|
||
* Slow: ArrayList.indexOf — O(P) per removal × P removals → O(P²).
|
||
* Fast: HashMap<key,index> — O(1) per removal.
|
||
*
|
||
* Each test: correctness assertion + performance ratio >= 5x at N=500-1000.
|
||
* Prints: N/N PASS
|
||
*/
|
||
public class BulletAlgorithm {
|
||
|
||
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);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// bullet-0001: btGhostObject overlapping-object add/remove
|
||
// -----------------------------------------------------------------------
|
||
|
||
/** Slow path: mirrors btGhostObject::addOverlappingObjectInternal (btAlignedObjectArray::findLinearSearch). */
|
||
static int[] simulateGhostSlow(int[] bodies) {
|
||
List<Integer> overlapping = new ArrayList<>();
|
||
long ops = 0;
|
||
// add phase
|
||
for (int b : bodies) {
|
||
int idx = overlapping.indexOf(b); // O(N) scan
|
||
ops += overlapping.size();
|
||
if (idx < 0) overlapping.add(b);
|
||
}
|
||
// remove phase
|
||
for (int b : bodies) {
|
||
int idx = overlapping.indexOf(b); // O(N) scan
|
||
ops += overlapping.size();
|
||
if (idx >= 0) {
|
||
overlapping.set(idx, overlapping.get(overlapping.size() - 1));
|
||
overlapping.remove(overlapping.size() - 1);
|
||
}
|
||
}
|
||
return new int[]{overlapping.size(), (int)ops};
|
||
}
|
||
|
||
/** Fast path: HashMap<Object,Integer> index — mirrors btHashMap fix. */
|
||
static int[] simulateGhostFast(int[] bodies) {
|
||
List<Integer> overlapping = new ArrayList<>();
|
||
Map<Integer, Integer> index = new HashMap<>();
|
||
long ops = 0;
|
||
// add phase
|
||
for (int b : bodies) {
|
||
ops++;
|
||
if (!index.containsKey(b)) {
|
||
index.put(b, overlapping.size());
|
||
overlapping.add(b);
|
||
}
|
||
}
|
||
// remove phase
|
||
for (int b : bodies) {
|
||
ops++;
|
||
Integer pos = index.remove(b);
|
||
if (pos != null) {
|
||
int last = overlapping.size() - 1;
|
||
if (pos != last) {
|
||
Integer moved = overlapping.get(last);
|
||
overlapping.set(pos, moved);
|
||
index.put(moved, pos);
|
||
}
|
||
overlapping.remove(last);
|
||
}
|
||
}
|
||
return new int[]{overlapping.size(), (int)ops};
|
||
}
|
||
|
||
static void testGhostOverlapping() {
|
||
// Correctness: both paths produce same final state
|
||
int N = 20;
|
||
int[] bodies = new int[N];
|
||
for (int i = 0; i < N; i++) bodies[i] = i;
|
||
// add duplicates to stress dedup
|
||
int[] withDups = new int[N + 5];
|
||
System.arraycopy(bodies, 0, withDups, 0, N);
|
||
withDups[N] = 3; withDups[N+1] = 7; withDups[N+2] = 11; withDups[N+3] = 1; withDups[N+4] = 0;
|
||
|
||
int[] slowResult = simulateGhostSlow(withDups);
|
||
int[] fastResult = simulateGhostFast(withDups);
|
||
|
||
if (slowResult[0] != fastResult[0]) {
|
||
fail("bullet-0001 correctness N=20+dups",
|
||
"final size mismatch: slow=" + slowResult[0] + " fast=" + fastResult[0]);
|
||
return;
|
||
}
|
||
pass("bullet-0001 correctness N=20+dups");
|
||
|
||
// Performance: measure op count ratio at N=500
|
||
int P = 500;
|
||
int[] bigBodies = new int[P];
|
||
for (int i = 0; i < P; i++) bigBodies[i] = i;
|
||
|
||
long slowOps = 0, fastOps = 0;
|
||
for (int rep = 0; rep < 50; rep++) {
|
||
slowOps += simulateGhostSlow(bigBodies)[1];
|
||
fastOps += simulateGhostFast(bigBodies)[1];
|
||
}
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf(" bullet-0001 P=500 slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
|
||
slowOps, fastOps, ratio);
|
||
if (ratio < 5.0) {
|
||
fail("bullet-0001 performance P=500", "ratio=" + ratio + " < 5x");
|
||
return;
|
||
}
|
||
pass("bullet-0001 performance ratio >= 5x at P=500");
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// bullet-0002: btCollisionObject::checkCollideWithOverride
|
||
// -----------------------------------------------------------------------
|
||
|
||
static long benchCheckCollideSlow(int M, int E) {
|
||
// E exclusions, M pair checks
|
||
List<Integer> exclusions = new ArrayList<>();
|
||
for (int i = 0; i < E; i++) exclusions.add(i);
|
||
long ops = 0;
|
||
for (int p = 0; p < M; p++) {
|
||
int candidate = p % (E + 5); // most misses
|
||
ops += exclusions.size(); // count comparisons
|
||
boolean found = exclusions.contains(candidate);
|
||
if (found) ops -= (exclusions.size() - exclusions.indexOf(candidate) - 1);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static long benchCheckCollideFast(int M, int E) {
|
||
Set<Integer> exclusions = new HashSet<>();
|
||
for (int i = 0; i < E; i++) exclusions.add(i);
|
||
long ops = 0;
|
||
for (int p = 0; p < M; p++) {
|
||
int candidate = p % (E + 5);
|
||
ops++; // O(1) hash lookup
|
||
exclusions.contains(candidate);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static void testCheckCollideWith() {
|
||
// Correctness: same decisions for same inputs
|
||
int M = 30, E = 8;
|
||
List<Integer> exclusionList = new ArrayList<>();
|
||
Set<Integer> exclusionSet = new HashSet<>();
|
||
for (int i = 0; i < E; i++) { exclusionList.add(i); exclusionSet.add(i); }
|
||
|
||
boolean mismatch = false;
|
||
for (int p = 0; p < M; p++) {
|
||
int candidate = p % (E + 5);
|
||
boolean slow = !exclusionList.contains(candidate);
|
||
boolean fast = !exclusionSet.contains(candidate);
|
||
if (slow != fast) { mismatch = true; break; }
|
||
}
|
||
if (mismatch) {
|
||
fail("bullet-0002 correctness M=30 E=8", "decision mismatch");
|
||
return;
|
||
}
|
||
pass("bullet-0002 correctness M=30 E=8");
|
||
|
||
// Performance: op-count ratio at M=1000 E=20
|
||
int bM = 1000, bE = 20;
|
||
long slowOps = benchCheckCollideSlow(bM, bE);
|
||
long fastOps = benchCheckCollideFast(bM, bE);
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf(" bullet-0002 M=%d E=%d slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
|
||
bM, bE, slowOps, fastOps, ratio);
|
||
if (ratio < 5.0) {
|
||
fail("bullet-0002 performance M=1000 E=20", "ratio=" + ratio + " < 5x");
|
||
return;
|
||
}
|
||
pass("bullet-0002 performance ratio >= 5x at M=1000 E=20");
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// bullet-0003: btSortedOverlappingPairCache::removeOverlappingPair
|
||
// -----------------------------------------------------------------------
|
||
|
||
static long benchSortedCacheSlow(int P) {
|
||
// P pairs, remove all — mirrors btSortedOverlappingPairCache::removeOverlappingPair
|
||
List<Integer> pairArray = new ArrayList<>();
|
||
for (int i = 0; i < P; i++) pairArray.add(i);
|
||
long ops = 0;
|
||
for (int i = 0; i < P; i++) {
|
||
int key = i;
|
||
// findLinearSearch: scan all remaining
|
||
for (int j = 0; j < pairArray.size(); j++) {
|
||
ops++;
|
||
if (pairArray.get(j).equals(key)) {
|
||
pairArray.set(j, pairArray.get(pairArray.size() - 1));
|
||
pairArray.remove(pairArray.size() - 1);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static long benchSortedCacheFast(int P) {
|
||
// P pairs, remove all — HashMap<key, index>
|
||
List<Integer> pairArray = new ArrayList<>();
|
||
Map<Integer, Integer> pairIndex = new HashMap<>(P * 2);
|
||
for (int i = 0; i < P; i++) { pairArray.add(i); pairIndex.put(i, i); }
|
||
long ops = 0;
|
||
for (int i = 0; i < P; i++) {
|
||
ops++; // O(1) map lookup
|
||
Integer pos = pairIndex.remove(i);
|
||
if (pos != null) {
|
||
int last = pairArray.size() - 1;
|
||
if (pos != last) {
|
||
Integer moved = pairArray.get(last);
|
||
pairArray.set(pos, moved);
|
||
pairIndex.put(moved, pos);
|
||
}
|
||
pairArray.remove(last);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static void testSortedCacheRemove() {
|
||
// Correctness: both produce empty array after removing all pairs
|
||
int P = 20;
|
||
List<Integer> slowArray = new ArrayList<>();
|
||
for (int i = 0; i < P; i++) slowArray.add(i);
|
||
for (int i = 0; i < P; i++) {
|
||
int idx = slowArray.indexOf(i);
|
||
if (idx >= 0) {
|
||
slowArray.set(idx, slowArray.get(slowArray.size() - 1));
|
||
slowArray.remove(slowArray.size() - 1);
|
||
}
|
||
}
|
||
|
||
List<Integer> fastArray = new ArrayList<>();
|
||
Map<Integer, Integer> fastIndex = new HashMap<>();
|
||
for (int i = 0; i < P; i++) { fastArray.add(i); fastIndex.put(i, i); }
|
||
for (int i = 0; i < P; i++) {
|
||
Integer pos = fastIndex.remove(i);
|
||
if (pos != null) {
|
||
int last = fastArray.size() - 1;
|
||
if (pos != last) {
|
||
Integer moved = fastArray.get(last);
|
||
fastArray.set(pos, moved);
|
||
fastIndex.put(moved, pos);
|
||
}
|
||
fastArray.remove(last);
|
||
}
|
||
}
|
||
|
||
if (slowArray.size() != 0 || fastArray.size() != 0) {
|
||
fail("bullet-0003 correctness P=20", "not empty: slow=" + slowArray.size() + " fast=" + fastArray.size());
|
||
return;
|
||
}
|
||
pass("bullet-0003 correctness P=20 (both empty after all removals)");
|
||
|
||
// Performance: op-count ratio at P=500
|
||
long slowOps = benchSortedCacheSlow(500);
|
||
long fastOps = benchSortedCacheFast(500);
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf(" bullet-0003 P=500 slow_ops=%,d fast_ops=%,d ratio=%.0fx%n",
|
||
slowOps, fastOps, ratio);
|
||
if (ratio < 5.0) {
|
||
fail("bullet-0003 performance P=500", "ratio=" + ratio + " < 5x");
|
||
return;
|
||
}
|
||
pass("bullet-0003 performance ratio >= 5x at P=500");
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("Bullet Physics CWE-407 unit tests");
|
||
System.out.println("=".repeat(60));
|
||
|
||
System.out.println("\n[bullet-0001] btGhostObject::addOverlappingObjectInternal");
|
||
testGhostOverlapping();
|
||
|
||
System.out.println("\n[bullet-0002] btCollisionObject::checkCollideWithOverride");
|
||
testCheckCollideWith();
|
||
|
||
System.out.println("\n[bullet-0003] btSortedOverlappingPairCache::removeOverlappingPair");
|
||
testSortedCacheRemove();
|
||
|
||
System.out.println();
|
||
System.out.printf("%d/%d PASS%n", passed, total);
|
||
if (passed != total) System.exit(1);
|
||
}
|
||
}
|