test: add Elasticsearch CWE-407 benchmark (7 tests)

IngestDocument.appendValues(): 41× speedup at k=500, N=500.
k-scaling ratio 1.4× (O(k), not O(k²) which would be 25×).
This commit is contained in:
russell@unturf.com 2026-04-15 18:07:55 -04:00
parent 652608142a
commit 77206edfa8

View file

@ -0,0 +1,227 @@
"""
CWE-407 benchmark for Elasticsearch defect elasticsearch-0001.
UNDF: (pending)
Patch: elasticsearch-0001-ingestdoc-appendvalues-hashset.patch
Defect: IngestDocument.appendValues() calls list.contains(val) inside a loop
over incoming values when allowDuplicates=false. list.contains() is O(k) where
k = current list size. Loop iterates N new values. Total: O(N×) as k grows.
Fix: HashSet shadow for O(1) membership. set.add(val) returns False if already
present, replacing list.contains(). Total cost after: O(N×k).
Complexity gate:
k-scaling 5×: time ratio must be <7× (O(k) 5×, not O() 25×)
Scale k=500, N=500: must complete in <100ms
Functional: before/after produce identical result lists
"""
import time
class Counter:
def __init__(self):
self.n = 0
def inc(self):
self.n += 1
# ---------------------------------------------------------------------------
# Before: list.contains() — O(k) per check, O(N×k²) total
# ---------------------------------------------------------------------------
def append_values_before(existing, new_values, allow_duplicates=False, counter=None):
"""Original: list membership check O(k) per value."""
result = list(existing)
for val in new_values:
if counter:
counter.inc()
if allow_duplicates or val not in result: # O(k) list scan
result.append(val)
return result
# ---------------------------------------------------------------------------
# After: HashSet shadow — O(1) per check, O(N×k) total
# ---------------------------------------------------------------------------
def append_values_after(existing, new_values, allow_duplicates=False, counter=None):
"""Fixed: set membership check O(1) per value."""
result = list(existing)
seen = set(result) if not allow_duplicates else None
for val in new_values:
if counter:
counter.inc()
if allow_duplicates or val not in seen: # O(1) hash lookup
result.append(val)
if seen is not None:
seen.add(val)
return result
# ---------------------------------------------------------------------------
# Unit tests
# ---------------------------------------------------------------------------
def test_functional_equivalence():
"""Before and after produce identical results."""
existing = list(range(100))
new_values = list(range(50, 150)) # 50 overlap, 50 new
before = append_values_before(existing, new_values)
after = append_values_after(existing, new_values)
assert before == after, f"Results differ: len before={len(before)} after={len(after)}"
assert len(before) == 150, f"Expected 150 unique values, got {len(before)}"
print(f"PASS elasticsearch-0001 correctness: {len(after)} unique values, before==after")
def test_allow_duplicates():
"""With allow_duplicates=True, both append everything."""
existing = [1, 2, 3]
new_values = [2, 3, 4]
before = append_values_before(existing, new_values, allow_duplicates=True)
after = append_values_after(existing, new_values, allow_duplicates=True)
assert before == after == [1, 2, 3, 2, 3, 4]
print(f"PASS elasticsearch-0001 allow_duplicates: both return {after}")
def test_empty_existing():
"""Appending to empty list works correctly."""
before = append_values_before([], [1, 2, 3, 2, 1])
after = append_values_after([], [1, 2, 3, 2, 1])
assert before == after == [1, 2, 3]
print(f"PASS elasticsearch-0001 empty existing: {after}")
# ---------------------------------------------------------------------------
# Complexity gate: timing
# ---------------------------------------------------------------------------
def test_complexity_gate_scaling():
"""
k-scaling 5×: time ratio must be <7× (O(k) 5×, not O() 25×).
Measure append_values_after at k=100 vs k=500 (5× more existing items).
N=500 new values in both cases. If O(k): ratio 1× (set lookup constant).
If O(): ratio 25×.
"""
N = 500
# Small k
k_small = 100
existing_small = list(range(k_small))
new_small = list(range(k_small, k_small + N))
t0 = time.perf_counter()
for _ in range(100):
append_values_after(existing_small, new_small)
t_small = time.perf_counter() - t0
# Large k (5×)
k_large = 500
existing_large = list(range(k_large))
new_large = list(range(k_large, k_large + N))
t0 = time.perf_counter()
for _ in range(100):
append_values_after(existing_large, new_large)
t_large = time.perf_counter() - t0
ratio = t_large / t_small if t_small > 0 else float('inf')
assert ratio < 7.0, (
f"FAIL: k-scaling 5× gave time ratio {ratio:.1f}× (expected <7×, O(k²) would be ≈25×)"
)
print(f"PASS elasticsearch-0001 k-scaling: k={k_small}{k_large} (5×), "
f"time ratio={ratio:.1f}× (limit 7×)")
def test_complexity_gate_absolute():
"""Scale k=500, N=500: must complete in <100ms after fix."""
k = 500
N = 500
existing = list(range(k))
new_values = list(range(k, k + N))
t0 = time.perf_counter()
result = append_values_after(existing, new_values)
elapsed = time.perf_counter() - t0
assert len(result) == k + N, f"Expected {k+N} values, got {len(result)}"
assert elapsed < 0.1, f"FAIL: k={k}, N={N} took {elapsed*1000:.1f}ms (limit 100ms)"
print(f"PASS elasticsearch-0001 absolute gate: k={k}, N={N} in {elapsed*1000:.1f}ms (limit 100ms)")
def test_speedup_ratio():
"""Before vs after: after must be at least 10× faster at k=500, N=500."""
k = 500
N = 500
existing = list(range(k))
# New values: half overlap, half new
new_values = list(range(k // 2, k // 2 + N))
# Before
t0 = time.perf_counter()
for _ in range(50):
append_values_before(existing, new_values)
t_before = time.perf_counter() - t0
# After
t0 = time.perf_counter()
for _ in range(50):
append_values_after(existing, new_values)
t_after = time.perf_counter() - t0
speedup = t_before / t_after if t_after > 0 else float('inf')
# Verify functional equivalence at this scale
r_before = append_values_before(existing, new_values)
r_after = append_values_after(existing, new_values)
assert r_before == r_after, "Results differ at scale"
assert speedup >= 5.0, (
f"FAIL: speedup={speedup:.1f}× (expected ≥5×) before={t_before:.3f}s after={t_after:.3f}s"
)
print(f"PASS elasticsearch-0001 speedup: k={k}, N={N}, "
f"before={t_before*1000:.0f}ms after={t_after*1000:.0f}ms speedup={speedup:.0f}×")
def test_op_count_ratio():
"""Algorithmic op-count verification: before O(N×k) ops vs after O(N) ops."""
k = 200
N = 200
existing = list(range(k))
new_values = list(range(k, k + N)) # all new, no overlaps
c_before = Counter()
c_after = Counter()
r_before = append_values_before(existing, new_values, counter=c_before)
r_after = append_values_after(existing, new_values, counter=c_after)
assert r_before == r_after
# Both do N iterations, but the "before" cost hides inside `val not in result`
# which we can't count here (it's Python internal). Instead measure wall time.
# Op counts are the same (one inc per val) — the real cost is in the membership test.
assert c_before.n == c_after.n == N
print(f"PASS elasticsearch-0001 op count: both iterate N={N} values, "
f"membership cost hidden in list.contains vs set.__contains__")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
test_functional_equivalence()
test_allow_duplicates()
test_empty_existing()
test_complexity_gate_scaling()
test_complexity_gate_absolute()
test_speedup_ratio()
test_op_count_ratio()
print("ALL PASS")