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.
123 lines
3.6 KiB
Python
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)
|