java-topology/defects/xtuple-0001/test/test_xtuple_0001.py
russell@unturf.com 6f11fb7598 xtuple: 5-MOAD scan; xtuple-0001 CWE-407 orutils missingParamList QStringList::contains O(P*M) 52x at P=1000
Scanned xTuple OpenRPT (public C++/Qt report engine used by xTuple ERP).
One MOAD-0001 defect found in orutils.cpp SQL parameter parsing loop.
MOADs 0002/0003/0004/0005 CLEAN.
2026-04-03 14:01:48 -04:00

123 lines
3.6 KiB
Python

"""
test_xtuple_0001.py
Simulates the xtuple-0001 defect:
orQuery::orQuery() uses QStringList::contains() (O(M) linear scan) inside a
while loop that iterates P times over SQL parameter placeholders.
Total complexity: O(P * M) before fix, O(P) amortized after.
Defect: OpenRPT/renderer/orutils.cpp missingParamList.contains() in param loop
Fix: QSet<QString> shadow set for O(1) membership, keep QStringList for output
"""
import time
import sys
export_PYTHONUNBUFFERED = True # noqa: always unbuffered
def parse_params_defective(param_names):
"""
Simulate the defective pattern:
for each param occurrence, check if already in a list (O(M) scan).
param_names: list of param name strings (with repeats, simulating P occurrences).
Returns the deduplicated missing_param_list.
"""
missing_param_list = []
for n in param_names:
# QStringList::contains is O(M) linear scan
if n not in missing_param_list:
missing_param_list.append(n)
return missing_param_list
def parse_params_fixed(param_names):
"""
Simulate the fixed pattern:
use a set for O(1) membership, append to list for ordered output.
"""
missing_param_set = set()
missing_param_list = []
for n in param_names:
if n not in missing_param_set:
missing_param_set.add(n)
missing_param_list.append(n)
return missing_param_list
def build_workload(P, M):
"""
Build a workload of P parameter placeholder occurrences,
drawn from M distinct missing param names (all missing = worst case).
"""
import random
random.seed(42)
names = [f"param_{i}" for i in range(M)]
# repeat names across P occurrences
return [names[i % M] for i in range(P)]
def benchmark(fn, param_names, label, reps=5):
# warm up
fn(param_names)
best = float("inf")
for _ in range(reps):
t0 = time.perf_counter()
result = fn(param_names)
t1 = time.perf_counter()
best = min(best, t1 - t0)
return best, result
def run_test(P, M, min_speedup=3.0):
print(f"\n--- P={P} param occurrences, M={M} distinct missing params ---")
params = build_workload(P, M)
t_defective, r_defective = benchmark(parse_params_defective, params, "defective")
t_fixed, r_fixed = benchmark(parse_params_fixed, params, "fixed")
# Results must be identical (same dedup, same order)
assert r_defective == r_fixed, (
f"FAIL: result mismatch\n defective={r_defective[:5]}...\n fixed={r_fixed[:5]}..."
)
speedup = t_defective / t_fixed if t_fixed > 0 else float("inf")
print(f" defective: {t_defective*1000:.3f} ms")
print(f" fixed: {t_fixed*1000:.3f} ms")
print(f" speedup: {speedup:.1f}x")
if speedup >= min_speedup:
print(f" PASS (speedup {speedup:.1f}x >= {min_speedup}x)")
else:
print(f" FAIL (speedup {speedup:.1f}x < {min_speedup}x)")
return False
return True
if __name__ == "__main__":
all_pass = True
# Small case - may not show speedup (overhead dominates)
# Just verify correctness
params_small = build_workload(100, 50)
r_d = parse_params_defective(params_small)
r_f = parse_params_fixed(params_small)
assert r_d == r_f, "FAIL: small case result mismatch"
print("N=100,M=50: correctness OK")
# Medium case
ok = run_test(P=1000, M=500, min_speedup=3.0)
all_pass = all_pass and ok
# Large case
ok = run_test(P=5000, M=2000, min_speedup=5.0)
all_pass = all_pass and ok
print()
if all_pass:
print("ALL TESTS PASSED")
sys.exit(0)
else:
print("SOME TESTS FAILED")
sys.exit(1)