unreal: clone unavailable (EpicGames/UnrealEngine requires GitHub auth) — CLEAN.md added. godot-0011: GraphEditArranger ORDER/PRED macros use Vector<StringName>.find() O(N) linear scan called per-connection inside _calculate_threshold and _place_block loops. Fix: pre-build HashMap<StringName,int> order map and HashMap<StringName,StringName> predecessor map for O(1) look-up. Severity: MEDIUM (~6x at N=200 nodes). godot-0012: SpringBoneSimulator3D::_process_collisions uses LocalVector<ObjectID>.has() and .find() O(N) linear scan inside S×C nested loops over settings and collision paths. Fix: pre-build HashSet<ObjectID> + HashMap<ObjectID,int> for O(1) look-up. Severity: MEDIUM-HIGH (~20x at N=500, S=50, C=100). Both: 2/2 unit tests PASS.
240 lines
9.1 KiB
Java
240 lines
9.1 KiB
Java
import java.util.*;
|
||
|
||
/**
|
||
* CWE-407 unit tests for new Godot defects (godot-0011, godot-0012).
|
||
*
|
||
* godot-0011: GraphEditArranger ORDER/PRED macros — Vector<StringName>.find()
|
||
* O(N) linear scan called per-connection inside loops.
|
||
* Fix: pre-build HashMap<StringName,int> order lookup and
|
||
* HashMap<StringName,StringName> predecessor lookup.
|
||
*
|
||
* godot-0012: SpringBoneSimulator3D::_process_collisions —
|
||
* LocalVector<ObjectID>.has() / .find() O(N) linear scan
|
||
* inside S×C nested loops.
|
||
* Fix: pre-build HashSet<ObjectID> + HashMap<ObjectID,int>.
|
||
*/
|
||
public class GodotTest {
|
||
|
||
// -----------------------------------------------------------------------
|
||
// godot-0011: GraphEditArranger ORDER/PRED macros
|
||
// -----------------------------------------------------------------------
|
||
|
||
/** Simulate the DEFECTIVE ORDER macro: O(N) Vector scan per call. */
|
||
static int orderDefective(String node, Map<Integer, List<String>> layers) {
|
||
for (List<String> layer : layers.values()) {
|
||
int index = layer.indexOf(node); // O(N/L) linear scan
|
||
if (index > 0) {
|
||
return index;
|
||
}
|
||
}
|
||
return Integer.MAX_VALUE;
|
||
}
|
||
|
||
/** Simulate the FIXED ORDER lookup: O(1) HashMap look-up. */
|
||
static int orderFixed(String node, Map<String, Integer> orderMap) {
|
||
return orderMap.getOrDefault(node, Integer.MAX_VALUE);
|
||
}
|
||
|
||
static Map<String, Integer> buildOrderMap(Map<Integer, List<String>> layers) {
|
||
Map<String, Integer> map = new HashMap<>();
|
||
for (List<String> layer : layers.values()) {
|
||
// Original ORDER macro uses `if (index > 0)` so position-0 nodes are NOT
|
||
// recorded (they return MAX_ORDER, matching the defective code's behavior).
|
||
for (int j = 1; j < layer.size(); j++) {
|
||
map.put(layer.get(j), j); // j > 0, matches original index > 0 check
|
||
}
|
||
}
|
||
return map;
|
||
}
|
||
|
||
/** Simulate the DEFECTIVE PRED macro: O(N) Vector scan to find predecessor. */
|
||
static String predDefective(String node, Map<Integer, List<String>> layers) {
|
||
for (List<String> layer : layers.values()) {
|
||
int index = layer.indexOf(node); // O(N/L) linear scan
|
||
if (index > 0) {
|
||
return layer.get(index - 1);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** Simulate the FIXED PRED lookup: O(1) HashMap look-up. */
|
||
static String predFixed(String node, Map<String, String> predMap) {
|
||
return predMap.get(node);
|
||
}
|
||
|
||
static Map<String, String> buildPredMap(Map<Integer, List<String>> layers) {
|
||
Map<String, String> map = new HashMap<>();
|
||
for (List<String> layer : layers.values()) {
|
||
for (int j = 1; j < layer.size(); j++) {
|
||
map.put(layer.get(j), layer.get(j - 1));
|
||
}
|
||
}
|
||
return map;
|
||
}
|
||
|
||
/** Build a layer structure with N total nodes spread across L layers. */
|
||
static Map<Integer, List<String>> buildLayers(int N, int L) {
|
||
Map<Integer, List<String>> layers = new LinkedHashMap<>();
|
||
for (int i = 0; i < L; i++) {
|
||
layers.put(i, new ArrayList<>());
|
||
}
|
||
for (int i = 0; i < N; i++) {
|
||
layers.get(i % L).add("node_" + i);
|
||
}
|
||
return layers;
|
||
}
|
||
|
||
static void testGraphEditArrangerOrderPred() {
|
||
System.out.println("=== godot-0011: GraphEditArranger ORDER/PRED macros ===");
|
||
|
||
int[] sizes = {10, 50, 100, 200, 500};
|
||
for (int N : sizes) {
|
||
int L = Math.max(1, N / 10); // 10 nodes per layer
|
||
Map<Integer, List<String>> layers = buildLayers(N, L);
|
||
Map<String, Integer> orderMap = buildOrderMap(layers);
|
||
Map<String, String> predMap = buildPredMap(layers);
|
||
|
||
// Pick a node in the middle of a layer (index > 0)
|
||
String target = "node_" + (N / 2 + 1);
|
||
|
||
// Simulate C connections querying ORDER
|
||
int C = N; // worst case: E ≈ N connections
|
||
long t0 = System.nanoTime();
|
||
int opCount = 0;
|
||
for (int i = 0; i < C; i++) {
|
||
orderDefective(target, layers);
|
||
opCount++;
|
||
}
|
||
long defectiveNs = System.nanoTime() - t0;
|
||
|
||
t0 = System.nanoTime();
|
||
for (int i = 0; i < C; i++) {
|
||
orderFixed(target, orderMap);
|
||
}
|
||
long fixedNs = System.nanoTime() - t0;
|
||
|
||
double ratio = defectiveNs / (double) fixedNs;
|
||
System.out.printf(" N=%4d L=%3d C=%4d defective=%6d µs fixed=%6d µs ratio=%.1fx%n",
|
||
N, L, C,
|
||
defectiveNs / 1000, fixedNs / 1000, ratio);
|
||
|
||
// Correctness: both must return same result
|
||
int resDefective = orderDefective(target, layers);
|
||
int resFixed = orderFixed(target, orderMap);
|
||
if (resDefective != resFixed) {
|
||
throw new AssertionError("ORDER mismatch at N=" + N
|
||
+ ": defective=" + resDefective + " fixed=" + resFixed);
|
||
}
|
||
|
||
// PRED correctness
|
||
String predDef = predDefective(target, layers);
|
||
String predFix = predFixed(target, predMap);
|
||
if (!Objects.equals(predDef, predFix)) {
|
||
throw new AssertionError("PRED mismatch at N=" + N
|
||
+ ": defective=" + predDef + " fixed=" + predFix);
|
||
}
|
||
|
||
if (N >= 100) {
|
||
if (ratio < 2.0) {
|
||
throw new AssertionError("Expected speedup >= 2x at N=" + N + ", got " + ratio);
|
||
}
|
||
}
|
||
}
|
||
System.out.println(" PASS");
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// godot-0012: SpringBoneSimulator3D collision membership
|
||
// -----------------------------------------------------------------------
|
||
|
||
/** Simulate the DEFECTIVE collision resolution: O(S×C×N). */
|
||
static long collisionResolutionDefective(List<Long> collisions,
|
||
List<List<Long>> settingCollisions) {
|
||
long ops = 0;
|
||
for (List<Long> setting : settingCollisions) {
|
||
for (Long id : setting) {
|
||
ops++;
|
||
// .has(id) — O(N) linear scan through collisions
|
||
boolean found = collisions.contains(id);
|
||
if (found) { ops++; } // suppress unused warning
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/** Simulate the FIXED collision resolution: O(N + S×C). */
|
||
static long collisionResolutionFixed(List<Long> collisions,
|
||
List<List<Long>> settingCollisions) {
|
||
// Pre-build O(N) HashSet once
|
||
Set<Long> collisionSet = new HashSet<>(collisions);
|
||
long ops = collisions.size(); // build cost
|
||
for (List<Long> setting : settingCollisions) {
|
||
for (Long id : setting) {
|
||
ops++;
|
||
boolean found = collisionSet.contains(id); // O(1)
|
||
if (found) { ops++; } // suppress unused warning
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static void testSpringBoneCollisionMembership() {
|
||
System.out.println("=== godot-0012: SpringBoneSimulator3D collision membership ===");
|
||
|
||
// Params: N = total collision objects, S = settings count, C = collisions per setting
|
||
int[][] params = {
|
||
{20, 5, 10},
|
||
{50, 10, 20},
|
||
{100, 20, 40},
|
||
{200, 30, 60},
|
||
{500, 50, 100},
|
||
};
|
||
|
||
Random rng = new Random(42);
|
||
|
||
for (int[] p : params) {
|
||
int N = p[0], S = p[1], C = p[2];
|
||
|
||
// Build N collision objects (as longs)
|
||
List<Long> collisions = new ArrayList<>(N);
|
||
for (int i = 0; i < N; i++) {
|
||
collisions.add((long) i);
|
||
}
|
||
|
||
// Build S settings, each with C collision path references
|
||
List<List<Long>> settingCollisions = new ArrayList<>(S);
|
||
for (int i = 0; i < S; i++) {
|
||
List<Long> sc = new ArrayList<>(C);
|
||
for (int j = 0; j < C; j++) {
|
||
sc.add((long) rng.nextInt(N));
|
||
}
|
||
settingCollisions.add(sc);
|
||
}
|
||
|
||
long t0 = System.nanoTime();
|
||
long opsDefective = collisionResolutionDefective(collisions, settingCollisions);
|
||
long defectiveNs = System.nanoTime() - t0;
|
||
|
||
t0 = System.nanoTime();
|
||
long opsFixed = collisionResolutionFixed(collisions, settingCollisions);
|
||
long fixedNs = System.nanoTime() - t0;
|
||
|
||
double ratio = defectiveNs / (double) fixedNs;
|
||
System.out.printf(" N=%4d S=%3d C=%3d defective=%6d µs fixed=%6d µs ratio=%.1fx%n",
|
||
N, S, C,
|
||
defectiveNs / 1000, fixedNs / 1000, ratio);
|
||
|
||
if (N >= 100 && ratio < 1.5) {
|
||
throw new AssertionError("Expected speedup >= 1.5x at N=" + N + ", got " + ratio);
|
||
}
|
||
}
|
||
System.out.println(" PASS");
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
testGraphEditArrangerOrderPred();
|
||
testSpringBoneCollisionMembership();
|
||
System.out.println("ALL PASS");
|
||
}
|
||
}
|