java-topology/defects/godot/unit/GodotTest.java
russell@unturf.com 4d73070504 unreal/godot-deeper: CWE-407 findings
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.
2026-03-30 09:50:15 -04:00

240 lines
9.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
}
}