java-topology/defects/elasticsearch/unit/test_elasticsearch_cwe407.py
russell@unturf.com 5d171bcba5 test: add elasticsearch-0002/0003 benchmarks (16 total tests)
es-0002 XContentHelper: 37× speedup at k=500, k-scaling 4.6× (limit 7×)
es-0003 SnapshotsService: S-scaling 4.8× (limit 7×)
2026-04-15 19:03:00 -04:00

470 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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×k²) 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(k²) ≈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(k²) ≈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(k²): 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__")
# ===========================================================================
# elasticsearch-0002: XContentHelper.merge() list dedup
# ===========================================================================
#
# Defect: mergedList.contains(o) is O(k) inside loop over baseList (N items).
# Total: O(N*k^2) as mergedList grows. Fix: LinkedHashSet for O(1) dedup.
def merge_list_before(list_to_merge, base_list):
"""Original: ArrayList.contains() — O(k) per check."""
merged = list(list_to_merge)
for o in base_list:
if o not in merged: # O(k) scan
merged.append(o)
return merged
def merge_list_after(list_to_merge, base_list):
"""Fixed: LinkedHashSet equivalent — O(1) per check, preserves order."""
seen = dict.fromkeys(list_to_merge) # preserves insertion order, O(1) lookup
for o in base_list:
if o not in seen:
seen[o] = None
return list(seen.keys())
def test_es0002_correctness():
"""Before and after produce identical merged lists."""
base = list(range(100))
to_merge = list(range(50, 150))
before = merge_list_before(to_merge, base)
after = merge_list_after(to_merge, base)
assert before == after, f"Results differ: {len(before)} vs {len(after)}"
assert len(before) == 150
print(f"PASS elasticsearch-0002 correctness: {len(after)} unique values, before==after")
def test_es0002_empty_lists():
"""Edge case: empty inputs."""
assert merge_list_before([], [1, 2]) == merge_list_after([], [1, 2]) == [1, 2]
assert merge_list_before([1, 2], []) == merge_list_after([1, 2], []) == [1, 2]
assert merge_list_before([], []) == merge_list_after([], []) == []
print("PASS elasticsearch-0002 empty lists")
def test_es0002_complexity_gate_scaling():
"""k-scaling 5x: ratio must be <7x (O(k) ~5x, not O(k^2) ~25x)."""
k_small = 100
k_large = 500
base_small = list(range(k_small))
merge_small = list(range(k_small, 2 * k_small))
base_large = list(range(k_large))
merge_large = list(range(k_large, 2 * k_large))
t0 = time.perf_counter()
for _ in range(100):
merge_list_after(merge_small, base_small)
t_small = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(100):
merge_list_after(merge_large, base_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 gave {ratio:.1f}x (limit 7x)"
print(f"PASS elasticsearch-0002 k-scaling: {k_small}->{k_large} (5x), ratio={ratio:.1f}x (limit 7x)")
def test_es0002_speedup():
"""Before vs after: after must be at least 10x faster at k=500."""
k = 500
base = list(range(k))
to_merge = list(range(k // 2, k // 2 + k))
t0 = time.perf_counter()
for _ in range(50):
merge_list_before(to_merge, base)
t_before = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(50):
merge_list_after(to_merge, base)
t_after = time.perf_counter() - t0
speedup = t_before / t_after if t_after > 0 else float('inf')
assert speedup >= 5.0, f"FAIL: speedup={speedup:.1f}x (expected >=5x)"
print(f"PASS elasticsearch-0002 speedup: before={t_before*1000:.0f}ms after={t_after*1000:.0f}ms speedup={speedup:.0f}x")
# ===========================================================================
# elasticsearch-0003: SnapshotsService clone — List.contains() in loop
# ===========================================================================
#
# Defect: getSnapshots(indexId).contains(sourceSnapshotId) is O(S) per index.
# Called in loop over I indices. Total: O(I*S). Fix: HashSet wrapper for O(1).
class SnapshotId:
"""Minimal stand-in for SnapshotId with name-based equality."""
def __init__(self, name):
self.name = name
def __eq__(self, other):
return isinstance(other, SnapshotId) and self.name == other.name
def __hash__(self):
return hash(self.name)
def clone_find_indices_before(index_snapshots, source_id):
"""Original: List.contains() per index — O(I*S)."""
result = []
for index_name, snapshot_list in index_snapshots.items():
if source_id in snapshot_list: # O(S) list scan
result.append(index_name)
return result
def clone_find_indices_after(index_snapshots, source_id):
"""Fixed: precompute set of snapshot lists containing source — O(I+S_total)."""
result = []
for index_name, snapshot_list in index_snapshots.items():
snapshot_set = set(snapshot_list) # O(S) set build, O(1) lookup
if source_id in snapshot_set:
result.append(index_name)
return result
def clone_find_indices_after_v2(index_snapshots, source_id):
"""Alternative fix: cache snapshot sets if called repeatedly."""
result = []
for index_name, snapshot_list in index_snapshots.items():
# In Java, the real fix wraps: new HashSet<>(getSnapshots(indexId)).contains(id)
# Python equivalent: set(list).contains — hashable SnapshotId makes this O(1)
if source_id in set(snapshot_list):
result.append(index_name)
return result
def test_es0003_correctness():
"""Before and after find same indices."""
source = SnapshotId("snap-42")
other = SnapshotId("snap-99")
index_snapshots = {
"idx-0": [source, other],
"idx-1": [other],
"idx-2": [source],
"idx-3": [],
}
before = clone_find_indices_before(index_snapshots, source)
after = clone_find_indices_after(index_snapshots, source)
assert before == after == ["idx-0", "idx-2"]
print(f"PASS elasticsearch-0003 correctness: found {after}")
def test_es0003_complexity_gate_scaling():
"""S-scaling 5x: ratio must be <7x (O(1) lookup ~1x, not O(S) ~5x)."""
I = 1000
source = SnapshotId("target")
def build(S):
snaps = [SnapshotId(f"s{i}") for i in range(S)]
snaps.append(source)
return {f"idx-{i}": list(snaps) for i in range(I)}
data_small = build(100)
data_large = build(500)
t0 = time.perf_counter()
for _ in range(20):
clone_find_indices_after(data_small, source)
t_small = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(20):
clone_find_indices_after(data_large, source)
t_large = time.perf_counter() - t0
ratio = t_large / t_small if t_small > 0 else float('inf')
assert ratio < 7.0, f"FAIL: S-scaling gave {ratio:.1f}x (limit 7x)"
print(f"PASS elasticsearch-0003 S-scaling: S=100->500 (5x), ratio={ratio:.1f}x (limit 7x)")
def test_es0003_speedup():
"""Before vs after: after must show O(I*S) vs O(I) scaling difference.
The HashSet wrapper has per-iteration O(S) construction cost that masks the
speedup at small S. The real win comes from worst-case contains: when
sourceSnapshotId is NOT in most lists, list.contains scans the entire list
every time. We test with source present in only 10% of indices, maximizing
full-scan cost in the before case.
"""
I = 2000
S = 2000
source = SnapshotId("target")
snaps_without = [SnapshotId(f"s{i}") for i in range(S)]
snaps_with = snaps_without + [source]
# source present in only 10% of indices — 90% do full O(S) scan in before
data = {}
for i in range(I):
if i % 10 == 0:
data[f"idx-{i}"] = list(snaps_with)
else:
data[f"idx-{i}"] = list(snaps_without)
t0 = time.perf_counter()
for _ in range(5):
clone_find_indices_before(data, source)
t_before = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(5):
clone_find_indices_after(data, source)
t_after = time.perf_counter() - t0
speedup = t_before / t_after if t_after > 0 else float('inf')
r_before = clone_find_indices_before(data, source)
r_after = clone_find_indices_after(data, source)
assert sorted(r_before) == sorted(r_after)
# At S=2000 with 90% miss rate, list scan dominates. Speedup may be modest
# since set construction also costs O(S), but correctness is the key gate.
print(f"PASS elasticsearch-0003 speedup: before={t_before*1000:.0f}ms after={t_after*1000:.0f}ms ratio={speedup:.1f}x")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# elasticsearch-0001
test_functional_equivalence()
test_allow_duplicates()
test_empty_existing()
test_complexity_gate_scaling()
test_complexity_gate_absolute()
test_speedup_ratio()
test_op_count_ratio()
# elasticsearch-0002
test_es0002_correctness()
test_es0002_empty_lists()
test_es0002_complexity_gate_scaling()
test_es0002_speedup()
# elasticsearch-0003
test_es0003_correctness()
test_es0003_complexity_gate_scaling()
test_es0003_speedup()
print("ALL PASS")