Skip to content

Commit e5b509a

Browse files
committed
test: DNSNsec at-cap eviction + unbounded benchmark pool
**Cross-type NSec spot-test (Kōan).** Add ``test_cache_dnsnsec_at_cap_evicts_prior_record``: fill the cache to ``_MAX_CACHE_RECORDS`` with ``DNSAddress`` records, add a single ``DNSNsec``, assert the counter stays at cap (NSEC routed through eviction), the NSEC is reachable via ``cache.cache[nsec.key]`` (orphan fix covers NSEC's key-collision path), and the earliest fill record was the eviction victim. Complements the existing flood test — together they pin NSEC against both ``new``-flag drift and cross-type ``_async_add`` regressions. Uses direct cache lookup instead of ``async_get_unique`` because the latter's type stub excludes ``DNSNsec``. **Unbounded benchmark generator (Copilot inline at test_cache_bound.py:53).** The previous bench used ``iter(_make_records(100_000, ...))`` which raises ``StopIteration`` once exhausted — fast operations under pytest-codspeed can exceed that count. Replace with ``_unbounded_records`` (``itertools.count`` + lazy ``DNSAddress`` construction) so the iterator survives any iteration count CodSpeed chooses. The construction work moves into the timed body but is a constant per-iteration overhead and doesn't distort relative measurements across runs.
1 parent 38b1517 commit e5b509a

2 files changed

Lines changed: 47 additions & 7 deletions

File tree

tests/benchmarks/test_cache_bound.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Iterator
6+
from itertools import count
7+
58
from pytest_codspeed import BenchmarkFixture
69

710
from zeroconf import DNSAddress, DNSCache, current_time_millis
811
from zeroconf.const import _CLASS_IN, _MAX_CACHE_RECORDS, _TYPE_A
912

1013

11-
def _make_records(count: int, now: float, prefix: str = "bench") -> list[DNSAddress]:
14+
def _make_records(count_: int, now: float, prefix: str = "bench") -> list[DNSAddress]:
1215
return [
1316
DNSAddress(
1417
f"{prefix}-{i}.local.",
@@ -18,10 +21,23 @@ def _make_records(count: int, now: float, prefix: str = "bench") -> list[DNSAddr
1821
bytes(((i >> 24) & 0xFF, (i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF)),
1922
created=now + i,
2023
)
21-
for i in range(count)
24+
for i in range(count_)
2225
]
2326

2427

28+
def _unbounded_records(now: float, prefix: str = "evict") -> Iterator[DNSAddress]:
29+
"""Unbounded generator of unique-name DNSAddress records."""
30+
for i in count():
31+
yield DNSAddress(
32+
f"{prefix}-{i}.local.",
33+
_TYPE_A,
34+
_CLASS_IN,
35+
120,
36+
bytes(((i >> 24) & 0xFF, (i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF)),
37+
created=now + i,
38+
)
39+
40+
2541
def test_cache_add_below_cap(benchmark: BenchmarkFixture) -> None:
2642
"""Adding records while the cache is well below the cap (no eviction)."""
2743
now = current_time_millis()
@@ -38,15 +54,14 @@ def test_cache_add_at_cap_evicts(benchmark: BenchmarkFixture) -> None:
3854
3955
Pre-fills the cache to ``_MAX_CACHE_RECORDS`` outside the timed body so
4056
only the eviction-path adds are measured. Each benchmark iteration
41-
consumes one fresh unique record from a pre-built pool, keeping the
42-
cache permanently at the cap and the work per iteration to a single
43-
``_async_add`` + ``_async_evict_oldest`` cycle.
57+
pulls one fresh unique record from an unbounded generator, keeping the
58+
cache permanently at the cap. The generator avoids the iteration-count
59+
cap that a pre-built pool would impose for very fast operations.
4460
"""
4561
now = current_time_millis()
4662
cache = DNSCache()
4763
cache.async_add_records(_make_records(_MAX_CACHE_RECORDS, now, prefix="fill"))
48-
# Large pool so the iterator outlives any reasonable codspeed run count.
49-
pool = iter(_make_records(100_000, now + _MAX_CACHE_RECORDS, prefix="evict"))
64+
pool = _unbounded_records(now + _MAX_CACHE_RECORDS)
5065

5166
@benchmark
5267
def _evict_one() -> None:

tests/test_cache.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,31 @@ def test_cache_eviction_victim_shares_key_with_new_record() -> None:
583583
assert total == cache._total_records
584584

585585

586+
def test_cache_dnsnsec_at_cap_evicts_prior_record() -> None:
587+
"""A single DNSNsec arriving at the cap evicts one prior record and stays reachable."""
588+
cache = r.DNSCache()
589+
now = r.current_time_millis()
590+
cache.async_add_records(
591+
_addr(f"fill-{i}.local.", i, created=now + i) for i in range(const._MAX_CACHE_RECORDS)
592+
)
593+
assert cache._total_records == const._MAX_CACHE_RECORDS
594+
595+
nsec = r.DNSNsec(
596+
"nsec-arrival.local.",
597+
const._TYPE_NSEC,
598+
const._CLASS_IN,
599+
120,
600+
"nsec-arrival.local.",
601+
[const._TYPE_A],
602+
)
603+
cache.async_add_records([nsec])
604+
605+
assert cache._total_records == const._MAX_CACHE_RECORDS
606+
assert nsec in cache.cache[nsec.key]
607+
# The earliest-created fill record is gone (FIFO-ish eviction).
608+
assert "fill-0.local." not in cache.cache
609+
610+
586611
def test_cache_dnsnsec_flood_is_bounded() -> None:
587612
"""DNSNsec records honour ``_MAX_CACHE_RECORDS`` (no bypass via the ``new`` flag)."""
588613
cache = r.DNSCache()

0 commit comments

Comments
 (0)