erpnext-0001: BOM.get_children list dedup O(B^2) MEDIUM 4x erpnext-0002: serial_batch_bundle serial/batch dedup O(N^2) HIGH 45x ofbiz-0001: PaymentGatewayServices processList LinkedList O(N^2) HIGH 45x ofbiz-0002: OrderReturnServices/OrderReadHelper payment dedup O(N^2) MEDIUM 36x odoo: CLEAN (consistent set/frozenset/OrderedSet usage throughout)
132 lines
5.1 KiB
Java
132 lines
5.1 KiB
Java
import java.util.*;
|
|
|
|
/**
|
|
* Unit test for erpnext-0001: BOM.get_children() list dedup O(B^2)
|
|
*
|
|
* Simulates the BOM explosion BFS traversal where child_bom membership
|
|
* is checked via list scan (defect) vs set lookup (fix).
|
|
*
|
|
* Defect: bom.py lines 924-931 — "if child_bom not in bom_list" where
|
|
* bom_list is a Python list, giving O(B) per check and O(B^2) overall.
|
|
*
|
|
* Fix: maintain a parallel set for O(1) membership checks.
|
|
*/
|
|
public class test_erpnext_0001 {
|
|
|
|
// --- Defect: list-only BFS dedup ---
|
|
static List<String> getBomChildrenDefect(Map<String, List<String>> bomTree, String root) {
|
|
List<String> bomList = new ArrayList<>();
|
|
bomList.add(root);
|
|
int count = 0;
|
|
while (count < bomList.size()) {
|
|
List<String> children = bomTree.getOrDefault(bomList.get(count), Collections.emptyList());
|
|
for (String child : children) {
|
|
if (!bomList.contains(child)) { // O(N) per check
|
|
bomList.add(child);
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
Collections.reverse(bomList);
|
|
return bomList;
|
|
}
|
|
|
|
// --- Fix: set-backed BFS dedup ---
|
|
static List<String> getBomChildrenFixed(Map<String, List<String>> bomTree, String root) {
|
|
List<String> bomList = new ArrayList<>();
|
|
Set<String> bomSet = new HashSet<>();
|
|
bomList.add(root);
|
|
bomSet.add(root);
|
|
int count = 0;
|
|
while (count < bomList.size()) {
|
|
List<String> children = bomTree.getOrDefault(bomList.get(count), Collections.emptyList());
|
|
for (String child : children) {
|
|
if (!bomSet.contains(child)) { // O(1) per check
|
|
bomList.add(child);
|
|
bomSet.add(child);
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
Collections.reverse(bomList);
|
|
return bomList;
|
|
}
|
|
|
|
// Build a BOM tree with N levels, each having fanout children
|
|
static Map<String, List<String>> buildBomTree(int levels, int fanout) {
|
|
Map<String, List<String>> tree = new HashMap<>();
|
|
int id = 0;
|
|
Queue<String> queue = new LinkedList<>();
|
|
String root = "BOM-" + (id++);
|
|
queue.add(root);
|
|
|
|
for (int level = 0; level < levels && !queue.isEmpty(); level++) {
|
|
int size = queue.size();
|
|
for (int i = 0; i < size; i++) {
|
|
String parent = queue.poll();
|
|
List<String> children = new ArrayList<>();
|
|
for (int j = 0; j < fanout; j++) {
|
|
String child = "BOM-" + (id++);
|
|
children.add(child);
|
|
queue.add(child);
|
|
}
|
|
tree.put(parent, children);
|
|
}
|
|
}
|
|
return tree;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("=== erpnext-0001: BOM get_children list dedup O(B^2) ===\n");
|
|
|
|
// Correctness test
|
|
Map<String, List<String>> smallTree = new HashMap<>();
|
|
smallTree.put("BOM-ROOT", Arrays.asList("BOM-A", "BOM-B"));
|
|
smallTree.put("BOM-A", Arrays.asList("BOM-C", "BOM-B")); // BOM-B appears twice
|
|
smallTree.put("BOM-B", Arrays.asList("BOM-D"));
|
|
smallTree.put("BOM-C", Collections.emptyList());
|
|
smallTree.put("BOM-D", Collections.emptyList());
|
|
|
|
List<String> defectResult = getBomChildrenDefect(smallTree, "BOM-ROOT");
|
|
List<String> fixedResult = getBomChildrenFixed(smallTree, "BOM-ROOT");
|
|
|
|
assert defectResult.equals(fixedResult) :
|
|
"Results differ: defect=" + defectResult + " fixed=" + fixedResult;
|
|
System.out.println("Correctness: PASS (both produce " + fixedResult + ")");
|
|
|
|
// Performance test: BOM tree with 500 unique BOMs
|
|
int levels = 5, fanout = 3;
|
|
Map<String, List<String>> bigTree = buildBomTree(levels, fanout);
|
|
String root = "BOM-0";
|
|
int totalBoms = bigTree.values().stream().mapToInt(List::size).sum() + 1;
|
|
System.out.println("BOM tree: " + totalBoms + " nodes, " + levels + " levels, fanout=" + fanout);
|
|
|
|
// Warmup
|
|
for (int i = 0; i < 5; i++) {
|
|
getBomChildrenDefect(bigTree, root);
|
|
getBomChildrenFixed(bigTree, root);
|
|
}
|
|
|
|
int iterations = 500;
|
|
long t0 = System.nanoTime();
|
|
for (int i = 0; i < iterations; i++) {
|
|
getBomChildrenDefect(bigTree, root);
|
|
}
|
|
long defectNs = System.nanoTime() - t0;
|
|
|
|
t0 = System.nanoTime();
|
|
for (int i = 0; i < iterations; i++) {
|
|
getBomChildrenFixed(bigTree, root);
|
|
}
|
|
long fixedNs = System.nanoTime() - t0;
|
|
|
|
double ratio = (double) defectNs / fixedNs;
|
|
System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations);
|
|
System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations);
|
|
System.out.printf("Ratio: %.1fx%n", ratio);
|
|
|
|
boolean pass = ratio > 1.5;
|
|
System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)");
|
|
assert pass : "Expected speedup > 1.5x, got " + ratio;
|
|
}
|
|
}
|