java-topology/defects/blender/unit/BlenderTest.java
russell@unturf.com 7d135aa6a1 blender+darktable: 3 new defects, MOADs 0002-0005 scan complete
blender-0004: MOAD-0001 CWE-407 anim_channels_edit.cc
  rearrange_animchannel_islands() calls BLI_findptr(anim_data_visible, channel, ...)
  inside the channel-grouping loop — O(C*V) where C=channels, V=visible channels.
  In a complex rig: C=V=1000, 1,000,000 pointer comparisons per reorder.
  Fix: build blender::Set<void*> from anim_data_visible before loop, O(1) lookup.
  Measured: 250x op-count ratio at C=V=1000.

darktable-0004: MOAD-0004 CWE-312 pwstorage backends
  backend_kwallet.c and backend_apple_keychain.c log credential key/value pairs
  verbatim via dt_print(DT_DEBUG_PWSTORAGE, "storing (%s, %s)", key, value).
  `value` for Piwigo export is JSON including plaintext password.
  Triggered by `darktable -d pwstorage` or `-d all` (common debugging mode).
  Fix: replace value argument with "[REDACTED]" in all four dt_print calls.

darktable-0005: MOAD-0001 CWE-407 modulegroups test_visible O(M*G*P)
  _lib_modulegroups_update_iop_visibility() iterates M=80 IOP modules, calling
  _lib_modulegroups_test_visible() which iterates G=8 groups doing g_list_find_custom
  (linear scan of P=10 module names per group) — O(M*G*P) per UI refresh.
  Called on every search keystroke, module toggle, and group switch.
  Fix: precompute GHashTable of all visible module names; test_visible = O(1).
  Measured: 37.8x op-count ratio at M=80, G=8, P=10.

MOADs 0002/0003/0005 CLEAN for blender; MOADs 0002/0003/0005 CLEAN for darktable.
All 4 blender + 5 darktable unit tests PASS.
2026-03-31 20:49:15 -04:00

268 lines
9.9 KiB
Java

import java.util.*;
/**
* CWE-407 simulation tests for Blender defects.
*
* blender-0001: node_runtime.cc socket chain cycle detection Vector.contains() O(D^2)
* blender-0002: usd_skel_convert.cc used_indices dedup std::find O(J^2)
* blender-0003: shader_tool.cc visited_files std::find O(D*V)
* blender-0004: anim_channels_edit.cc rearrange_animchannel_islands BLI_findptr O(C*V)
*/
public class BlenderTest {
// ========== blender-0001: socket chain cycle detection ==========
/** Simulates find_logical_origins_for_socket_recursive with Vector.contains cycle check */
static int traceSocketChainDefective(Map<Integer, List<Integer>> links, int start) {
List<Integer> chain = new ArrayList<>();
return traceRecursiveDefective(links, start, chain);
}
static int traceRecursiveDefective(Map<Integer, List<Integer>> links, int socket, List<Integer> chain) {
// Defect: linear scan for cycle detection
if (chain.contains(socket)) {
return 0; // cycle detected
}
chain.add(socket);
int count = 1;
for (int linked : links.getOrDefault(socket, Collections.emptyList())) {
count += traceRecursiveDefective(links, linked, chain);
}
chain.remove(chain.size() - 1);
return count;
}
/** Fixed: HashSet for O(1) cycle detection */
static int traceSocketChainFixed(Map<Integer, List<Integer>> links, int start) {
List<Integer> chain = new ArrayList<>();
Set<Integer> chainSet = new HashSet<>();
return traceRecursiveFixed(links, start, chain, chainSet);
}
static int traceRecursiveFixed(Map<Integer, List<Integer>> links, int socket,
List<Integer> chain, Set<Integer> chainSet) {
if (chainSet.contains(socket)) {
return 0;
}
chain.add(socket);
chainSet.add(socket);
int count = 1;
for (int linked : links.getOrDefault(socket, Collections.emptyList())) {
count += traceRecursiveFixed(links, linked, chain, chainSet);
}
chain.remove(chain.size() - 1);
chainSet.remove(socket);
return count;
}
static boolean testSocketChainCycleDetection() {
// Build a long reroute chain: 0 -> 1 -> 2 -> ... -> D-1
int D = 2000;
Map<Integer, List<Integer>> links = new HashMap<>();
for (int i = 0; i < D - 1; i++) {
links.put(i, List.of(i + 1));
}
links.put(D - 1, Collections.emptyList());
// Warmup
for (int i = 0; i < 3; i++) {
traceSocketChainDefective(links, 0);
traceSocketChainFixed(links, 0);
}
long t0 = System.nanoTime();
for (int i = 0; i < 5; i++) traceSocketChainDefective(links, 0);
long defective = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < 5; i++) traceSocketChainFixed(links, 0);
long fixed = System.nanoTime() - t0;
double ratio = (double) defective / fixed;
System.out.printf(" blender-0001 socket chain cycle: defective=%dms fixed=%dms ratio=%.1fx%n",
defective / 1_000_000, fixed / 1_000_000, ratio);
return ratio > 2.0;
}
// ========== blender-0002: USD skel used_indices dedup ==========
/** Defective: std::find on vector for dedup */
static List<Integer> collectUsedIndicesDefective(int[] jointIndices) {
List<Integer> usedIndices = new ArrayList<>();
for (int index : jointIndices) {
if (!usedIndices.contains(index)) {
usedIndices.add(index);
}
}
return usedIndices;
}
/** Fixed: Set for O(1) dedup */
static List<Integer> collectUsedIndicesFixed(int[] jointIndices) {
Set<Integer> seen = new HashSet<>();
List<Integer> usedIndices = new ArrayList<>();
for (int index : jointIndices) {
if (seen.add(index)) {
usedIndices.add(index);
}
}
return usedIndices;
}
static boolean testUsedIndicesDedup() {
// Simulate a high-poly mesh with many joint weight entries
int J = 10000;
int numJoints = 200;
Random rng = new Random(42);
int[] jointIndices = new int[J];
for (int i = 0; i < J; i++) {
jointIndices[i] = rng.nextInt(numJoints);
}
// Warmup
for (int i = 0; i < 3; i++) {
collectUsedIndicesDefective(jointIndices);
collectUsedIndicesFixed(jointIndices);
}
long t0 = System.nanoTime();
for (int i = 0; i < 100; i++) collectUsedIndicesDefective(jointIndices);
long defective = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < 100; i++) collectUsedIndicesFixed(jointIndices);
long fixed = System.nanoTime() - t0;
double ratio = (double) defective / fixed;
System.out.printf(" blender-0002 USD skel dedup: defective=%dms fixed=%dms ratio=%.1fx%n",
defective / 1_000_000, fixed / 1_000_000, ratio);
return ratio > 2.0;
}
// ========== blender-0003: shader_tool visited_files ==========
/** Defective: std::find on visited vector — isolate visited membership */
static int processShaderDepsDefective(List<String> resolvedFiles) {
List<String> visited = new ArrayList<>();
int processed = 0;
for (String file : resolvedFiles) {
// Defect: linear scan of visited list
if (!visited.contains(file)) {
visited.add(file);
processed++;
}
}
return processed;
}
/** Fixed: HashSet for visited check */
static int processShaderDepsFixed(List<String> resolvedFiles) {
Set<String> visitedSet = new HashSet<>();
int processed = 0;
for (String file : resolvedFiles) {
if (visitedSet.add(file)) {
processed++;
}
}
return processed;
}
static boolean testShaderToolVisited() {
int D = 5000; // dependencies (many unique files to grow visited list)
List<String> resolvedFiles = new ArrayList<>();
Random rng = new Random(42);
// Many unique files so visited list grows large
for (int i = 0; i < D; i++) resolvedFiles.add("shader_" + rng.nextInt(D) + ".glsl");
// Warmup
for (int i = 0; i < 3; i++) {
processShaderDepsDefective(resolvedFiles);
processShaderDepsFixed(resolvedFiles);
}
long t0 = System.nanoTime();
for (int i = 0; i < 20; i++) processShaderDepsDefective(resolvedFiles);
long defective = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < 20; i++) processShaderDepsFixed(resolvedFiles);
long fixed = System.nanoTime() - t0;
double ratio = (double) defective / fixed;
System.out.printf(" blender-0003 shader visited: defective=%dms fixed=%dms ratio=%.1fx%n",
defective / 1_000_000, fixed / 1_000_000, ratio);
return ratio > 2.0;
}
// ========== blender-0004: anim channel rearrange island BLI_findptr ==========
/**
* Simulates rearrange_animchannel_islands: for each channel in list,
* check membership in anim_data_visible.
* Defective: linear scan (BLI_findptr = List.contains).
*/
static int rearrangeIslandsDefective(List<Integer> channelList, List<Integer> visibleChannels) {
int ops = 0;
for (int channel : channelList) {
// BLI_findptr: O(V) linear scan
for (int vis : visibleChannels) {
ops++;
if (vis == channel) break;
}
}
return ops;
}
/** Fixed: Set<> from anim_data_visible for O(1) lookup per channel. */
static int rearrangeIslandsFixed(List<Integer> channelList, List<Integer> visibleChannels) {
int ops = 0;
Set<Integer> visibleSet = new HashSet<>(visibleChannels);
ops += visibleChannels.size(); // one-time build
for (int channel : channelList) {
visibleSet.contains(channel); // O(1)
ops++;
}
return ops;
}
static boolean testAnimChannelRearrangeIslands() {
// 100 objects * 10 NLA tracks = 1000 channels; all visible
int C = 1000;
int V = 1000;
List<Integer> channelList = new ArrayList<>();
List<Integer> visibleChannels = new ArrayList<>();
for (int i = 0; i < C; i++) channelList.add(i);
for (int i = 0; i < V; i++) visibleChannels.add(i); // all visible, worst case
int defectOps = rearrangeIslandsDefective(channelList, visibleChannels);
int fixedOps = rearrangeIslandsFixed(channelList, visibleChannels);
double ratio = (double) defectOps / fixedOps;
System.out.printf(" blender-0004 anim island rearrange: defect=%d fixed=%d ratio=%.1fx%n",
defectOps, fixedOps, ratio);
return ratio > 10.0;
}
// ========== Main ==========
public static void main(String[] args) {
System.out.println("Blender CWE-407 unit tests");
System.out.println("=========================");
boolean p1 = testSocketChainCycleDetection();
boolean p2 = testUsedIndicesDedup();
boolean p3 = testShaderToolVisited();
boolean p4 = testAnimChannelRearrangeIslands();
System.out.println();
System.out.printf("blender-0001 socket chain cycle: %s%n", p1 ? "PASS" : "FAIL");
System.out.printf("blender-0002 USD skel dedup: %s%n", p2 ? "PASS" : "FAIL");
System.out.printf("blender-0003 shader visited: %s%n", p3 ? "PASS" : "FAIL");
System.out.printf("blender-0004 anim island rearrange: %s%n", p4 ? "PASS" : "FAIL");
if (!p1 || !p2 || !p3 || !p4) {
System.exit(1);
}
System.out.println("\nAll 4 tests PASS");
}
}