java-topology/defects/opentoonz-0001/unit/OpentoonzAutocloseTest.java
russell@unturf.com be19bb7757 openshot+opentoonz: 3 CWE-407 defects, all 5 MOADs scanned
openshot-0001: QueryObject.filter(id=x) O(N) scan called in loop over
selected clips/transitions — O(S*C) total. Fix: build id->clip dict once.

libopenshot-0001: std::find(display_classes...) inside per-frame detection
loop in ObjectDetection.cpp — O(D*C) per frame. Fix: unordered_set.

opentoonz-0001: std::find(closingSegments...) inside endpoint loop in
autoclose.cpp spotResearchOnePoint() — O(E*C) per vectorization call, runs
per-frame during painted cell rendering. Fix: set-based dedup.

MOADs 2-5 CLEAN: no god-object coupling defects, no leaked thread context,
no credential logging (SVN password is CLI arg not logged), no thundering
herd in image/frame caches (all properly mutex-guarded).

3/3 unit tests PASS.
2026-03-31 21:21:27 -04:00

203 lines
7.2 KiB
Java

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Test for opentoonz-0001: CWE-407 spotResearchOnePoint() in autoclose.cpp
* calls std::find on a std::vector<Segment> inside the endpoint loop.
*
* Pattern (toonz/sources/toonzlib/autoclose.cpp):
* for (int i = 0; i < (int)endpoints.size(); ++i) { // E iterations
* Segment segment = ...;
* std::find(closingSegments.begin(), closingSegments.end(), segment); // O(C)
* }
* // C grows with each push_back — O(E * C) -> O(E^2) worst case
*
* Fix: use std::set<Segment> for O(log C) or std::unordered_set for O(1).
*
* Compile and run (no build tool required):
* javac defects/opentoonz-0001/unit/OpentoonzAutocloseTest.java \
* -d /tmp/opentoonz-0001
* java -cp /tmp/opentoonz-0001 OpentoonzAutocloseTest
*/
public class OpentoonzAutocloseTest {
private static int passed = 0;
private static int failed = 0;
// --- Minimal Segment model (pair of integer points) ---
static final class Segment {
final int x1, y1, x2, y2;
Segment(int x1, int y1, int x2, int y2) {
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Segment)) return false;
Segment s = (Segment) o;
return x1 == s.x1 && y1 == s.y1 && x2 == s.x2 && y2 == s.y2;
}
@Override
public int hashCode() {
int h = 17;
h = h * 31 + x1;
h = h * 31 + y1;
h = h * 31 + x2;
h = h * 31 + y2;
return h;
}
}
// --- Defective: linear scan dedup ---
static List<Segment> deduplicateDefective(List<Segment> candidates,
List<Segment> existing) {
List<Segment> closing = new ArrayList<>(existing);
for (Segment seg : candidates) { // E iterations
boolean found = false;
for (Segment s : closing) { // O(C) linear scan
if (s.equals(seg)) { found = true; break; }
}
if (!found) {
closing.add(seg); // C grows
}
}
return closing;
}
// --- Fixed: hash set dedup ---
static List<Segment> deduplicateFixed(List<Segment> candidates,
List<Segment> existing) {
Set<Segment> seen = new HashSet<>(existing.size() * 2 + candidates.size() * 2);
List<Segment> closing = new ArrayList<>(existing);
seen.addAll(existing);
for (Segment seg : candidates) { // E iterations
if (seen.add(seg)) { // O(1) hash set
closing.add(seg);
}
}
return closing;
}
// --- Helpers ---
static List<Segment> buildSegments(int count) {
List<Segment> segs = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
segs.add(new Segment(i, i, i + 1, i + 1));
}
return segs;
}
static List<Segment> buildDuplicates(List<Segment> source, int dupeCount) {
List<Segment> dupes = new ArrayList<>(dupeCount);
for (int i = 0; i < dupeCount; i++) {
dupes.add(source.get(i % source.size()));
}
return dupes;
}
// --- Tests ---
static void testNoDuplicatesAdded() {
List<Segment> existing = buildSegments(20);
List<Segment> candidates = buildDuplicates(existing, 10); // all duplicates
List<Segment> defResult = deduplicateDefective(candidates, existing);
List<Segment> fixResult = deduplicateFixed(candidates, existing);
check("defective: no duplicates added (stays at 20)", defResult.size() == 20);
check("fixed: no duplicates added (stays at 20)", fixResult.size() == 20);
}
static void testNewSegmentsAdded() {
List<Segment> existing = buildSegments(10);
List<Segment> newSegs = buildSegments(5);
// shift them so they don't collide with existing
List<Segment> shifted = new ArrayList<>();
for (Segment s : newSegs) shifted.add(new Segment(s.x1 + 1000, s.y1, s.x2, s.y2));
List<Segment> defResult = deduplicateDefective(shifted, existing);
List<Segment> fixResult = deduplicateFixed(shifted, existing);
check("defective: 5 new segments added (total 15)", defResult.size() == 15);
check("fixed: 5 new segments added (total 15)", fixResult.size() == 15);
}
static void testResultsMatchUnderMixedInput() {
List<Segment> existing = buildSegments(30);
List<Segment> duplicates = buildDuplicates(existing, 15);
List<Segment> newSegs = new ArrayList<>();
for (int i = 0; i < 10; i++) newSegs.add(new Segment(i + 5000, i, i + 1, i + 1));
List<Segment> candidates = new ArrayList<>();
candidates.addAll(duplicates);
candidates.addAll(newSegs);
List<Segment> defResult = deduplicateDefective(candidates, existing);
List<Segment> fixResult = deduplicateFixed(candidates, existing);
check("results match between defective and fixed (mixed input, 30+10=40 expected)",
defResult.size() == fixResult.size() && fixResult.size() == 40);
}
static void testPerformanceRatio() {
// E=500 endpoints, C grows to ~500 — simulates a complex inked frame
// The defective impl is O(E*C) ~ O(500*500) = 250,000 comparisons
int E = 500;
List<Segment> existing = new ArrayList<>();
List<Segment> candidates = buildSegments(E); // all unique
int iterations = 200;
// Warm up JIT
for (int i = 0; i < 5; i++) {
deduplicateDefective(candidates, existing);
deduplicateFixed(candidates, existing);
}
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) deduplicateDefective(candidates, existing);
long defectiveNs = System.nanoTime() - t0;
t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) deduplicateFixed(candidates, existing);
long fixedNs = System.nanoTime() - t0;
double ratio = (double) defectiveNs / fixedNs;
System.out.printf(" [perf] defective=%.1fms fixed=%.1fms ratio=%.1fx%n",
defectiveNs / 1e6, fixedNs / 1e6, ratio);
check("fixed path is at least 2.5x faster (E=500, all unique segments)", ratio >= 2.5);
}
// --- Harness ---
static void check(String desc, boolean cond) {
if (cond) {
System.out.println(" PASS: " + desc);
passed++;
} else {
System.out.println(" FAIL: " + desc);
failed++;
}
}
public static void main(String[] args) {
System.out.println("=== OpentoonzAutocloseTest (opentoonz-0001, CWE-407) ===\n");
testNoDuplicatesAdded();
testNewSegmentsAdded();
testResultsMatchUnderMixedInput();
testPerformanceRatio();
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
if (failed > 0) System.exit(1);
}
}