Skip to content

fix(toolkit): parse PyPCAPFile's dotted-decimal IPv4 text, not packed bytes - #747

Merged
JarryShaw merged 1 commit into
mainfrom
fix/743-pypcapfile-address-parsing
Sep 24, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/743-pypcapfile-address-parsing

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

What is the purpose of your pull request?

  • fix — corrects a defect

Description

Fixes #743. pypcapfile's IP.src/.dst are dotted-decimal ASCII text in a ctypes.c_char_p
(e.g. b'10.1.1.2'), confirmed against pypcapfile 0.12.0's own ip.py — not packed 4-byte
values, so ipaddress.IPv4Address(ipv4.src) raised AddressValueError and ipv4_header()'s
struct.pack('...II', ipv4.src, ...) raised struct.error.

  • One chokepoint, not four: _parse_ipv4_address() parses once; call sites cast with
    int(...) where struct.pack needs packed form, str(...) elsewhere. It also accepts a
    packed 4-byte bytes or an int directly — defensive for a different pypcapfile
    release/fork, and needed anyway since existing stand-ins model the field as int.
  • Found the same defect on IP.opt/.payload (hex-ASCII, not raw, once decoding stops short
    of the transport layer — exactly how the engine calls it): added _maybe_unhex(), used at
    three more sites. Both helpers wrap/fall back rather than assume one wire form.
  • IPv6: pypcapfile has no IPv6 decoder at all; ipv6_reassembly() already raises
    UnsupportedCall unconditionally, so no defect there.
  • Not breaking: nothing currently works through this path on 3.10/3.11 (AddressValueError/
    struct.error on every call), so making it work correctly cannot regress a working caller.

Tests (Python 3.10.21, pypcapfile 0.12.0, throwaway venv — the extra is python_version < '3.12'-gated so the repo's 3.14 venv never runs these): of the 10 HAS_PYPCAPFILE-gated
methods, 7 failed before this change (4 in tests/toolkit/test_pypcapfile_unit.py, 3 in
tests/foundation/engines/test_new_engine_parity_runtime.py) — confirms #743's count. This PR
turns the 4 in test_pypcapfile_unit.py green (19/19 in that file). The 3 in
test_new_engine_parity_runtime.py stay red — root-caused while verifying this fix to a
separate, out-of-scope defect: the engine (pcapkit/foundation/engines/pypcapfile.py, not
this module) feeds hex-encoded frame bytes straight into pypcapfile's decoders without
un-hexlifying first, so every field is garbage before reaching this toolkit. Filed as #746.

Added 5 new unit tests for the two helpers, each confirmed to fail (ImportError) without this
change. coverage run on pcapkit/toolkit/pypcapfile.py: 100% (113 stmts/36 branches, up from
100/34 pre-change).

@JarryShaw

Copy link
Copy Markdown
Owner Author

❌ NEEDS CHANGES @ f7c9bdb03 — same ASCII-text defect class unfixed at pypcapfile.py:262-263, packet2dict made self-inconsistent at :299-301, and 2 of 4 new _maybe_unhex sites unasserted (mutation-probed).

@JarryShaw

Copy link
Copy Markdown
Owner Author

❌ NEEDS CHANGES @ f7c9bdb03 — the same ASCII-text defect class is left unfixed at pcapkit/toolkit/pypcapfile.py:262-263, packet2dict's output is made self-inconsistent at :299-301, and 2 of the 4 new _maybe_unhex call sites are unasserted.

Measured in a throwaway venv: Python 3.10.21, pypcapfile 0.12.0, PYTHONSAFEPATH=1 + PYTHONPATH at the tree, pcapkit.__file__ asserted. No pytest-cov.

Claim Verdict Evidence I obtained
IP.src/.dst are dotted-decimal ASCII in a c_char_p ✅ ip.py:46-47; reads back as bytes b'10.1.1.2'
7/10 gated fail pre-fix, 4 toolkit + 3 parity ✅ exact main: 4 methods error (2× struct.error: required argument is not an integer, 2× AddressValueError: b'10.1.1.2' (len 8 != 4)); parity 3 fail. unittest prints errors=5 — one method has 2 subTests, method count is 4
the 3 parity failures are unaffected ✅ merged into 9b2d927c2: same 3 methods, byte-identical assertion strings, 4/4 toolkit now ok
ipv4_header byte-exact ✅ hl=5 → 20 B, hl=6 → 24 B, both == the true header; opt=b'\x00' reads back b'' (ctypes NUL-truncation), so no stray byte
IPv6: no decoder, refuses unconditionally ✅ pcapfile/protocols/network/ = ['__init__.py', 'ip.py']; UnsupportedCall on every call
100%, 113 stmts / 36 branches ✅ coverage run -m pytest tests/toolkit/ → 113 0 36 0 100%; tests/toolkit/ 33 passed / 25 skipped / 20 subtests, exit 0. Your 38/20 is the same 58 collected — 5 more run with scapy/dpkt present. Baseline 100/34 not re-derived

Must change

Judgement calls, not blockers

  • _maybe_unhex earns its place — without it ipv4_header returns 28 B for an hl=6 packet — but "keeps this safe for already-raw input" is false. b'001122223333aA4455660000' is a valid 24-byte TCP header (data offset 6, NS+ACK+FIN) of entirely hex-ASCII bytes; it halves to 12 B and _transport then returns None, dropping the segment silently. Only reachable on the raw-bytes fork path the note claims to protect, so: reword rather than remove.
  • _parse_ipv4_address's int branch exists only because FakeIP.src is an int — production widened to preserve the fixture that hid toolkit: pypcapfile adapter reads dotted-decimal addresses as packed bytes, so 7 of 10 gated tests fail when enabled #743. Drop it and make the stand-ins b'10.1.1.2'; the packed-4-byte branch is sound (no dotted quad is 4 bytes) and worth keeping.
  • Body: Fixes #743., one type ticked, fix only, changelog N/A — all fine. make test/make pylint boxes are unticked with no note where ci(unit-tests): install DPKT so the dpkt-gated tests actually run #737 annotates each; isort clean, and ad-hoc mypy flags nothing new (:538 unused-ignore is pre-existing, :446 on main).

❌ NEEDS CHANGES @ f7c9bdb03 — fix/file :262-263, unhex at :299-301, and add tests that fail when _maybe_unhex is removed at :288 and :415.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
… bytes

PyPCAPFile's `IP.src`/`.dst` are dotted-decimal ASCII text in a `ctypes.c_char_p`
(e.g. `b'10.1.1.2'`), not packed 4-byte values -- so every
`ipaddress.IPv4Address(ipv4.src)` call raised `AddressValueError`, and
`ipv4_header()`'s `struct.pack('...II', ipv4.src, ...)` raised `struct.error`.

- `_parse_ipv4_address()` is the single chokepoint, used at every affected site.
  It also accepts a packed 4-byte `bytes` value for a different PyPCAPFile
  release/fork; an `int` branch was dropped after review -- it existed only to
  keep this module's own int-typed test stand-ins passing, which is exactly the
  fixture that hid this defect. The stand-ins now use dotted-decimal `bytes`.
- `IP.opt`/`.payload` have the same defect, one class over: hex-ASCII, not raw,
  once decoding stops short of the transport layer. `_maybe_unhex()` fixes this
  at four sites, including one review caught that a first pass missed
  (`ipv4_reassembly`'s fragment payload). Its docstring now states the residual
  false-positive risk plainly instead of overclaiming safety.
- Same defect again on `Ethernet.src`/`.dst`: PyPCAPFile pre-formats them as
  colon-ASCII text, which `_ethernet2dict` was passing through unexamined
  against a raw-bytes stand-in that no longer matched reality.
  `_parse_mac_address()` normalises both forms to the lowercase colon-hex
  string the default engine's own `_read_mac_addr` uses.
- `_layer2dict` un-hexes an Ethernet/IP layer's raw nested payload before
  recursing, fixing an internal inconsistency the opt/payload fix on its own
  left behind: `opt` correctly decoded while the nested `'Raw'` payload stayed
  hex-encoded (and twice its true length) in the same dict.
- IPv6: confirmed PyPCAPFile has no IPv6 decoder at all (no defect there;
  `ipv6_reassembly` already raises `UnsupportedCall` unconditionally).

Verified on Python 3.10.21 with `pypcapfile` 0.12.0 in a throwaway venv: of
the 10 gated methods, 7 failed before this change (4 in
tests/toolkit/test_pypcapfile_unit.py, 3 in
tests/foundation/engines/test_new_engine_parity_runtime.py), matching #743.
This fix turns the 4 in test_pypcapfile_unit.py green. The 3 in
test_new_engine_parity_runtime.py stay red, byte-identical before and after --
root-caused while verifying this fix to a separate, out-of-scope defect in the
PyPCAPFile *engine* (not this toolkit module): it feeds hex-encoded frame bytes
straight into PyPCAPFile's own decoders without un-hexlifying first. Filed as
#746, fix open as #748.

28 unit tests (up from 14), each new one confirmed to fail without its fix; a
mutation pass confirms every `_maybe_unhex`/`_parse_*` call site is asserted.
`coverage run` on pcapkit/toolkit/pypcapfile.py: 100% (130 stmts/44 branches,
up from 100/34 pre-change).

Fixes #743.
@JarryShaw
JarryShaw force-pushed the fix/743-pypcapfile-address-parsing branch from f7c9bdb to 67c7592 Compare September 24, 2026 16:37
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 24, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Addressed all six findings at 67c7592e5 (force-pushed, --force-with-lease):

  1. _ethernet2dict — added _parse_mac_address(), normalises PyPCAPFile's colon-ASCII
    Ethernet.src/.dst (and a raw 6-byte form) to the lowercase colon-hex str the default
    engine's _read_mac_addr uses. Confirmed with a real-decoder test; FakeEthernet's raw
    6-byte stand-in now round-trips through .hex(':') instead of passing through unexamined.
  2. _layer2dict inconsistency — un-hexes an Ethernet/IP layer's raw nested payload before
    recursing, so opt and the nested 'Raw'/raw_len agree. Two new real-decoder tests (IP and
    Ethernet cases).
  3. _maybe_unhex safety claim — reworded; kept the helper. Added a test with your exact
    24-byte example documenting the residual false positive rather than hiding it.
  4. Coverage gaps at :288/:415 (now shifted) — added direct real-decoder tests for both;
    re-ran the mutation pass on all five _maybe_unhex sites, each now breaks a test when reverted.
  5. Narrowed _parse_ipv4_address — dropped the int branch, rejects non-bytes outright.
    FakeIP.src/.dst now use dotted-decimal bytes instead of int.
  6. Test count 14→28, all confirmed to fail without their fix. Coverage 100% (130 stmts/44
    branches, up from 100/34). The 3 test_new_engine_parity_runtime.py failures stay
    byte-identical before/after — still engines(pypcapfile): _decode() feeds hexlified frame bytes straight to pypcapfile's decoders #746/fix(engines): un-hexlify pypcapfile frames before decoding them #748, untouched (off-limits).

Labels: review: pending (was review: needs-changes), confirmed via gh api repos/JarryShaw/PyPCAPKit/issues/747/labels.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 67c7592e5 — all six findings from my f7c9bdb03 review are addressed; that verdict is superseded. Three caveats below, none a regression.

Python 3.10.21, pypcapfile 0.12.0, /tmp venv, PYTHONSAFEPATH=1, pcapkit.__file__ asserted under /tmp/pr747m. No CI install line carries the PyPCAPFile extra (.[test,DPKT], .[test,Scapy,DPKT]), so all 10 HAS_PYPCAPFILE-gated methods run nowhere in CI — 23/23 green is silent on this fix, and the run below is the only evidence that exists.

# Finding Status Evidence I obtained
1 _ethernet2dict ASCII MAC ✅ correct, not merely present 1136 frames over 5 real captures (in/arp/tcp/ipv4/http.pcap), pypcapfile's real decoder fed raw bytes, 0 mismatches against the default engine. ethernet.py:218 cited exactly and mirrored verbatim; the py38 guard is the house pattern (ethernet.py:53, arp.py:72, pcapng.py:224)
2 _layer2dict inconsistency ✅ on the live path, ⚠️ whitelist engine path: opt=b'\x94\x04\x00\x00' beside nested raw_len=32 == the true 32. But tcp.py:56-57 and udp.py:28 hexlify too — 4 hexlifying classes, 2 whitelisted
3 2 unasserted _maybe_unhex sites ✅ re-derived independently pristine source from git show, each mutation read back off disk before running: all 6 sites CAUGHT; _ipv4_2dict → 2 failed, ipv4_reassembly → 1 failed, both exit 0 at f7c9bdb03; unmutated baseline and post-restore runs both exit 0
4 _maybe_unhex over-claim ✅ note now admits the silent halving; my 24-byte all-hex-digit example is a test
5 int branch dropped ✅ no regression all 12 in-library call sites pass ipv4.src/.dst, always bytes; helper is private and not in __all__
6 Counts ✅ 28 methods derived from source (19 + 9, was 14); python -m unittest → Ran 28 … OK; tests/toolkit/ 47 passed / 20 skipped / 24 subtests / 0 failed; coverage 130 0 44 0 100%

Parity re-derived against current origin/main 9813aa377, since #749 changed these messages after my last review: same 3 methods, byte-identical before/after — '34:30:33:33:31:61' != '40:33:1a:d1:85:1c', [] != [...], set() is not true. Merge gate: --no-ff into 9813aa377 clean, tests/toolkit/ 47/20/0, real-decoder class 9/9 OK.

Caveats — follow-ups, not blockers

  • _layer2dict's whitelist is 2 of 4. With a TCP layer in the tree: raw=b'68656c6c6f20776f726c6421', raw_len=24 for a 12-byte payload. Latent, since LAYERS = 2 never decodes transport — but the note's stated reason ("nothing here establishes that it is PyPCAPFile-hexlified rather than genuinely raw") is false for TCP/UDP, where pypcapfile's own source establishes it. Correct the reason, or add the two names.
  • The top-level undecoded frame is untouched and is reachable (_decode returns the packet whole when _declf is None): raw_len=140 for a 70-byte frame, raw=b'40331ad1851ca45e…'. Pre-existing and not an internal inconsistency, so fairly out of scope here — but it is the same class.
  • _parse_mac_address validates nothing on its ASCII path: b'hello world, not a mac' comes back verbatim and uppercase stays uppercase, against a Returns: promising lowercase colon-hex. Lenient is the right call — validating would turn a engines(pypcapfile): _decode() feeds hexlified frame bytes straight to pypcapfile's decoders #746-corrupted frame into a hard raise — so soften the docstring rather than the code.

✅ GOOD TO MERGE @ 67c7592e5 — supersedes my ❌ NEEDS CHANGES at f7c9bdb03.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Correcting my own claim in the verdict above, and the same wording I used on #745 and #748.

I wrote "no CI install line carries the PyPCAPFile extra". That is wrong as phrased: .[all] does
include it
— pyproject.toml:225, "pypcapfile; python_version < '3.12'" — and six workflow lines
install .[all] (cron-conda.yml:111,247,254, deploy-pages.yml:111, cron-vendor.yml:114,
create-release.yml:413,420, lint.yml:101).

The conclusion is unchanged and now exactly grounded: unit-tests.yml is the only workflow that
runs pytest
, and its three install lines are .[test,DPKT] (:84) and .[test,Scapy,DPKT]
(:139, :268). None carries PyPCAPFile or all. So all 10 HAS_PYPCAPFILE-gated methods still
execute nowhere in CI and this PR's 23/23 green is still silent on its own fix — but the reason is
that the .[all] jobs never run the suite, not that nothing installs the extra.

Credit where due: #748's reviewer caught this and self-corrected there first.

JarryShaw added a commit that referenced this pull request Sep 24, 2026
Fixes #746. `pcapfile.savefile._read_a_packet(layers=0)` hexlifies the whole
frame into ASCII text and stops there (confirmed in `pcapfile/savefile.py`
0.12.0); `_decode()` fed that straight into `Ethernet(packet.packet,
layers=1)`, whose `__init__` unpacks its 14-byte header with `struct.unpack`
-- every field decoded came out garbage, without raising.

- One-line fix: `binascii.unhexlify(packet.packet)` before the decoder call.
  Chose this over loading with `layers>0` instead: that would decode eagerly
  inside the lazy packet generator, losing `_decode()`'s per-frame
  `AttributeWarning` fallback for a malformed frame.
- Link-layer dispatch (`_get_decoder`) is unaffected: it resolves the decoder
  class from the savefile header's `ll_type`, never from frame bytes. Fixed
  its docstring too -- it still said frames are left "as raw bytes" with no
  decoder; they are left hexlified.
- Tests: fixed stand-in packets in `test_pypcapfile_engine.py` that carried
  raw ASCII instead of the hexlified bytes a real `layers=0` load returns;
  added a test asserting the decoder receives un-hexlified bytes, and one
  asserting a not-valid-hex packet falls back with a warning without ever
  reaching the decoder. Both confirmed to fail without this change.

Merge order: do not land before #747 -- standalone, this turns a
non-crashing extraction into a crashing one for `tcp=True`/`ipv4=True` with
`reassembly=True`; see PR description for the reproduction.

Tests (Python 3.10.21, `pypcapfile` 0.12.0, throwaway venv):
`test_pypcapfile_engine.py` 22/22. Coverage on the engine file: 94% (108
stmts, unchanged; only `__init__`'s real imports uncovered).
@JarryShaw
JarryShaw merged commit 98905fa into main Sep 24, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the fix/743-pypcapfile-address-parsing branch September 24, 2026 17:41
JarryShaw added a commit that referenced this pull request Sep 24, 2026
Fixes #746. `pcapfile.savefile._read_a_packet(layers=0)` hexlifies the whole
frame into ASCII text and stops there (confirmed in `pcapfile/savefile.py`
0.12.0); `_decode()` fed that straight into `Ethernet(packet.packet,
layers=1)`, whose `__init__` unpacks its 14-byte header with `struct.unpack`
-- every field decoded came out garbage, without raising.

- One-line fix: `binascii.unhexlify(packet.packet)` before the decoder call.
  Chose this over loading with `layers>0` instead: that would decode eagerly
  inside the lazy packet generator, losing `_decode()`'s per-frame
  `AttributeWarning` fallback for a malformed frame.
- Link-layer dispatch (`_get_decoder`) is unaffected: it resolves the decoder
  class from the savefile header's `ll_type`, never from frame bytes. Fixed
  its docstring too -- it still said frames are left "as raw bytes" with no
  decoder; they are left hexlified.
- Tests: fixed stand-in packets in `test_pypcapfile_engine.py` that carried
  raw ASCII instead of the hexlified bytes a real `layers=0` load returns;
  added a test asserting the decoder receives un-hexlified bytes, and one
  asserting a not-valid-hex packet falls back with a warning without ever
  reaching the decoder. Both confirmed to fail without this change.

Merge order: do not land before #747 -- standalone, this turns a
non-crashing extraction into a crashing one for `tcp=True`/`ipv4=True` with
`reassembly=True`; see PR description for the reproduction.

Tests (Python 3.10.21, `pypcapfile` 0.12.0, throwaway venv):
`test_pypcapfile_engine.py` 22/22. Coverage on the engine file: 94% (108
stmts, unchanged; only `__init__`'s real imports uncovered).
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…arning count

Round 8: ran the merge-base/changed-files/cited-path intersection to
completion (0c7f2b7..origin/main: 43 commits/124 files; 54 cited paths,
34 of them touched by that range) instead of trusting a tense grep.

- :1540/:1542 -- "93 of the 95 sites"/"48 of the 49 record lengths" ->
  94 of 95 / 49 of 49; zero old-expression sites remain on main.
- :1544-1546, :1566 -- "LOCATOR_SET keeps the old expression ... is
  currently right" / "is unchanged in both respects" -> past tense;
  #679 fixed both LOCATOR_SET sites.
- :1566 -- "HIP_COPIES stays at two" -> past tense; #679 fixed
  LOCATOR_SET's Length unit, #689 then dropped HIP_COPIES to one.
- :1976-1977 -- "pcapng.txt ... wants a separate refresh" -> past
  tense; #685 removed it from the index instead of regenerating it.
- :2327-2328 -- "55 warnings on main before this change, 56 after"
  (-b html) -> 53/54; the raw `grep -c WARNING:` double-counts two
  Scapy import lines as Sphinx warnings, confirmed live on this head
  with both -b dummy and -b html (real 36/raw 38 with const/reg.rst
  excluded, same +2 gap either way).
- :834 -- stale ``protocol.py:1016`` -> ``:1413`` (the actual
  ``self._file.read()`` call inside ``_read_fileng``).
- :1969 -- the #646 entry's coverage renumbering was wrong twice over
  (first ``1153 to 1265``, then ``1153 to 1443``, the latter being
  main's ``def`` line, which always executes and can never be the
  single miss). Corrected to ``1248 to 1360``, the ``warn(...)``
  statement's line before/after #646's own diff.

Regenerated CHANGELOG.md from the edited entries.

changelog_md.py --check: exit 0. pytest tests/project/test_changelog_md.py -q:
47 passed, 37 subtests.

Follow-up: origin/main advanced through #726/#740/#741/#742 (to 0a3abff)
and then #747/#748 (to 074c53e) while this sat at good-to-go; #726 moved
three more claims anchored on files it touched.

- :834 -- ``protocol.py:1413`` -> ``:1411``; #726 shifted the
  ``self._file.read()`` call in ``_read_fileng`` by -2 lines.
- :2382-83 -- traceflow.py "Line 406" -> "Line 424"; #742 inserted 18
  lines above the ``#: Type[Dumper]: Dumper class.`` comment.
- :2099-2104, :2187-89 -- the "seven code-keyed parser registrars" and
  ``Option.register`` are no longer presence-only. #726, fixing #718,
  gave all seven -- and ``Option.register`` itself -- the same identity
  guard ``register_protocol`` already had; reworded both passages to
  say so, confirmed against the guards' own current docstrings.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…aches it

#729 and #738 were one defect twice: a HAS_*-gated suite whose dependency no CI
job installs, skipping silently because `pytest -q` prints no skip reasons. #737
and #740 fixed the install lines; nothing held them there.

- tests/_dependency_gates.py derives per flag: its gates (AST over class- AND
  method-level skipUnless), the pytest-running jobs reaching them (_tiers'
  is_unit_tier / fixture_tier_paths, at node-ID granularity), and whether that
  job's install line carries an extra providing it. Extras come from
  pyproject.toml; only import-name -> distribution is hand-written.
- Seven known gaps carry a reason each in DEPENDENCY_GATE_EXCLUSIONS; an entry
  that outlives its gap fails, since declared must equal derived, both ways.
- New: HAS_RUNTIME in test_runtime_engines.py also wants dpkt/scapy/pyshark, so
  5 methods skip on `test` and `gate`. Absent from #738. Tracked in #751.
- Corrects #745 and #738: HAS_CRAWLER_DEPS is not dark (`test` has carried
  requests and bs4 since #507); HAS_VENDOR_DEPS lacks only html5lib;
  HAS_PYPCAPFILE gates 15 methods, not 10 (#747 grew the class 4 -> 9).

37 -> 84 tests in tests/test_tier_guard.py under plain unittest; 98% branch
coverage of the new module. Deleting `crypto` from the `test` job's install line
in a scratch copy makes the guard fail, naming all 14 ESP gates.

Fixes #745.
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…arning count

Round 8: ran the merge-base/changed-files/cited-path intersection to
completion (0c7f2b7..origin/main: 43 commits/124 files; 54 cited paths,
34 of them touched by that range) instead of trusting a tense grep.

- :1540/:1542 -- "93 of the 95 sites"/"48 of the 49 record lengths" ->
  94 of 95 / 49 of 49; zero old-expression sites remain on main.
- :1544-1546, :1566 -- "LOCATOR_SET keeps the old expression ... is
  currently right" / "is unchanged in both respects" -> past tense;
  #679 fixed both LOCATOR_SET sites.
- :1566 -- "HIP_COPIES stays at two" -> past tense; #679 fixed
  LOCATOR_SET's Length unit, #689 then dropped HIP_COPIES to one.
- :1976-1977 -- "pcapng.txt ... wants a separate refresh" -> past
  tense; #685 removed it from the index instead of regenerating it.
- :2327-2328 -- "55 warnings on main before this change, 56 after"
  (-b html) -> 53/54; the raw `grep -c WARNING:` double-counts two
  Scapy import lines as Sphinx warnings, confirmed live on this head
  with both -b dummy and -b html (real 36/raw 38 with const/reg.rst
  excluded, same +2 gap either way).
- :834 -- stale ``protocol.py:1016`` -> ``:1413`` (the actual
  ``self._file.read()`` call inside ``_read_fileng``).
- :1969 -- the #646 entry's coverage renumbering was wrong twice over
  (first ``1153 to 1265``, then ``1153 to 1443``, the latter being
  main's ``def`` line, which always executes and can never be the
  single miss). Corrected to ``1248 to 1360``, the ``warn(...)``
  statement's line before/after #646's own diff.

Regenerated CHANGELOG.md from the edited entries.

changelog_md.py --check: exit 0. pytest tests/project/test_changelog_md.py -q:
47 passed, 37 subtests.

Follow-up: origin/main advanced through #726/#740/#741/#742 (to 0a3abff)
and then #747/#748 (to 074c53e) while this sat at good-to-go; #726 moved
three more claims anchored on files it touched.

- :834 -- ``protocol.py:1413`` -> ``:1411``; #726 shifted the
  ``self._file.read()`` call in ``_read_fileng`` by -2 lines.
- :2382-83 -- traceflow.py "Line 406" -> "Line 424"; #742 inserted 18
  lines above the ``#: Type[Dumper]: Dumper class.`` comment.
- :2099-2104, :2187-89 -- the "seven code-keyed parser registrars" and
  ``Option.register`` are no longer presence-only. #726, fixing #718,
  gave all seven -- and ``Option.register`` itself -- the same identity
  guard ``register_protocol`` already had; reworded both passages to
  say so, confirmed against the guards' own current docstrings.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 948ac49 came back NEEDS CHANGES: the round-12 edit fixed two
sites of the harmonisation claim and left its twin, plus its own reasoning,
asserting the opposite; and four numbers anchored on files the merges touched
had drifted independently of #726.

- :1877-1878, :1883-1884 (#675) -- "carries the guarded ``if code in
  cls.__xxx__: warn(...)``" / "every sibling warns on mere presence" ->
  past tense, noting #726 later gave all seven the identity guard this
  entry's own comparison assumes they lack.
- :2100-2109 -- dropped the retained "yields two keys and never reaches
  one key twice" (false: ``Internet.register(TransType.TCP, TCP)`` warns
  once, incumbent.klass is TCP) and "leaves a different-class test
  undecidable" (contradicted by :2404-2406's own ``incumbent is not
  protocol`` definition); replaced with the actual false positive the
  guard has -- pre-seeded ``ModuleDescriptor`` incumbents never compare
  equal to the resolved class.
- :2192 -- reflowed the ``Option.register`` paragraph (orphan lines
  fixed alongside).
- :2387-2388 -- the ``Type[Dumper]`` quote now matches what is actually
  at line 424 (post-#709-fix), rather than the pre-fix bare form.
- :945 -- ``README.md`` (103) -> (102).
- :1136 -- "75 of the 117 modules" -> "77 ... after #647 below adds
  the same ending to three more" (drifted via #647, independent of the
  four merges).
- :2119 -- dropped the irreproducible pylint "364 messages" figure;
  kept mypy's 112, which does reproduce.
- :2119 -- "326 registry writes" -> 327 (``R1CounterParameter``'s
  second code, from #690).

Also fixed six false claims in the PR body (separate from the .rst):
hunk/line counts, six-commits -> 46, the 5-row table's implied total,
"not trimmed", main's red/green state, and the now-unreachable
cherry-pick target.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…arning count

Round 8: ran the merge-base/changed-files/cited-path intersection to
completion (0c7f2b7..origin/main: 43 commits/124 files; 54 cited paths,
34 of them touched by that range) instead of trusting a tense grep.

- :1540/:1542 -- "93 of the 95 sites"/"48 of the 49 record lengths" ->
  94 of 95 / 49 of 49; zero old-expression sites remain on main.
- :1544-1546, :1566 -- "LOCATOR_SET keeps the old expression ... is
  currently right" / "is unchanged in both respects" -> past tense;
  #679 fixed both LOCATOR_SET sites.
- :1566 -- "HIP_COPIES stays at two" -> past tense; #679 fixed
  LOCATOR_SET's Length unit, #689 then dropped HIP_COPIES to one.
- :1976-1977 -- "pcapng.txt ... wants a separate refresh" -> past
  tense; #685 removed it from the index instead of regenerating it.
- :2327-2328 -- "55 warnings on main before this change, 56 after"
  (-b html) -> 53/54; the raw `grep -c WARNING:` double-counts two
  Scapy import lines as Sphinx warnings, confirmed live on this head
  with both -b dummy and -b html (real 36/raw 38 with const/reg.rst
  excluded, same +2 gap either way).
- :834 -- stale ``protocol.py:1016`` -> ``:1413`` (the actual
  ``self._file.read()`` call inside ``_read_fileng``).
- :1969 -- the #646 entry's coverage renumbering was wrong twice over
  (first ``1153 to 1265``, then ``1153 to 1443``, the latter being
  main's ``def`` line, which always executes and can never be the
  single miss). Corrected to ``1248 to 1360``, the ``warn(...)``
  statement's line before/after #646's own diff.

Regenerated CHANGELOG.md from the edited entries.

changelog_md.py --check: exit 0. pytest tests/project/test_changelog_md.py -q:
47 passed, 37 subtests.

Follow-up: origin/main advanced through #726/#740/#741/#742 (to 0a3abff)
and then #747/#748 (to 074c53e) while this sat at good-to-go; #726 moved
three more claims anchored on files it touched.

- :834 -- ``protocol.py:1413`` -> ``:1411``; #726 shifted the
  ``self._file.read()`` call in ``_read_fileng`` by -2 lines.
- :2382-83 -- traceflow.py "Line 406" -> "Line 424"; #742 inserted 18
  lines above the ``#: Type[Dumper]: Dumper class.`` comment.
- :2099-2104, :2187-89 -- the "seven code-keyed parser registrars" and
  ``Option.register`` are no longer presence-only. #726, fixing #718,
  gave all seven -- and ``Option.register`` itself -- the same identity
  guard ``register_protocol`` already had; reworded both passages to
  say so, confirmed against the guards' own current docstrings.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 948ac49 came back NEEDS CHANGES: the round-12 edit fixed two
sites of the harmonisation claim and left its twin, plus its own reasoning,
asserting the opposite; and four numbers anchored on files the merges touched
had drifted independently of #726.

- :1877-1878, :1883-1884 (#675) -- "carries the guarded ``if code in
  cls.__xxx__: warn(...)``" / "every sibling warns on mere presence" ->
  past tense, noting #726 later gave all seven the identity guard this
  entry's own comparison assumes they lack.
- :2100-2109 -- dropped the retained "yields two keys and never reaches
  one key twice" (false: ``Internet.register(TransType.TCP, TCP)`` warns
  once, incumbent.klass is TCP) and "leaves a different-class test
  undecidable" (contradicted by :2404-2406's own ``incumbent is not
  protocol`` definition); replaced with the actual false positive the
  guard has -- pre-seeded ``ModuleDescriptor`` incumbents never compare
  equal to the resolved class.
- :2192 -- reflowed the ``Option.register`` paragraph (orphan lines
  fixed alongside).
- :2387-2388 -- the ``Type[Dumper]`` quote now matches what is actually
  at line 424 (post-#709-fix), rather than the pre-fix bare form.
- :945 -- ``README.md`` (103) -> (102).
- :1136 -- "75 of the 117 modules" -> "77 ... after #647 below adds
  the same ending to three more" (drifted via #647, independent of the
  four merges).
- :2119 -- dropped the irreproducible pylint "364 messages" figure;
  kept mypy's 112, which does reproduce.
- :2119 -- "326 registry writes" -> 327 (``R1CounterParameter``'s
  second code, from #690).

Also fixed six false claims in the PR body (separate from the .rst):
hunk/line counts, six-commits -> 46, the 5-row table's implied total,
"not trimmed", main's red/green state, and the now-unreachable
cherry-pick target.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 1749cc0 came back NEEDS CHANGES: round 13 fixed five of
the nine sites and introduced four new false claims doing it, including
two inside the flagship rewrite -- swapping one inaccuracy for another
is this document's recurring failure mode.

- :1136-37 -- "75 ... 77 now, after #647 ... adds ... three more" was
  internally inconsistent (75+3=78, not 77). Traced #647's own diff
  (fc32d1b): it adds ``_missing_`` to three IntFlag classes across
  only two *new* files -- ``tcp/flags.py`` and ``ftp/command.py`` --
  since the third, ``TransportProtocol``, shares ``reg/apptype.py``
  with the already-counted ``AppType``. Module delta is +2, matching
  75+2=77; reworded to say so.
- :1879-88 -- dropped "the comparison below assumes they still lack"
  it, which was false about text 8 lines below in the same diff
  (already past-tensed). Also reflowed three orphan lines this
  introduced (`passes, whereas`, `it twice with nothing`, `none of
  the`).
- :2107-19 -- "These tables also ship pre-seeded" over-generalised:
  verified live (``ProtocolBase.__proto__`` is 0 entries,
  ``Transport.__proto__ is ProtocolBase.__proto__`` -- True) that 2 of
  7 have nothing pre-seeded. Scoped to the five that do (Link 7,
  Internet 16, Frame 3, PCAPNG 3, SCTP 2). Also fixed "the guard
  resolves only the incoming class", which contradicts the guard's own
  docstring ("the comparison itself resolves nothing") -- resolution is
  the earlier ``isinstance(protocol, ModuleDescriptor)`` step, three
  lines above the guard, not something the guard does.
- :2129-30 -- dropped the invented "327th" ordinal (327 total stays;
  traced-write instrumentation via ``sys`` hooks found the seeding is
  literal dict construction, not ``.register()`` calls, so I could not
  reproduce an ordinal with confidence -- said "one of them" instead
  of guessing).
- :7-8 -- "between #326 and #509" now says the programme continued
  past it (verified: 193 distinct #nnn refs, max #726, 103 above 509).
- PR body -- "7 hunks, 1168+/11-" was the previous head's figure, not
  this one's; replaced with the actual command
  (``git diff --shortstat da697fa -- docs/source/changelog/1.5.0.rst``)
  and today's figure (9 hunks, 1152+/16-), since a hardcoded count here
  has now gone stale twice.

Left alone per this round's scope: :1969/:1974 (before/after claim,
not falsified by #726's later +1), mypy "112" (correct, re-ran with
the project's own flags), ":2122" 13-to-14 (correct at its delta
scope), and the other 121 cited paths (unaffected by main's one new
commit, #745, confirmed test-only).

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…arning count

Round 8: ran the merge-base/changed-files/cited-path intersection to
completion (0c7f2b7..origin/main: 43 commits/124 files; 54 cited paths,
34 of them touched by that range) instead of trusting a tense grep.

- :1540/:1542 -- "93 of the 95 sites"/"48 of the 49 record lengths" ->
  94 of 95 / 49 of 49; zero old-expression sites remain on main.
- :1544-1546, :1566 -- "LOCATOR_SET keeps the old expression ... is
  currently right" / "is unchanged in both respects" -> past tense;
  #679 fixed both LOCATOR_SET sites.
- :1566 -- "HIP_COPIES stays at two" -> past tense; #679 fixed
  LOCATOR_SET's Length unit, #689 then dropped HIP_COPIES to one.
- :1976-1977 -- "pcapng.txt ... wants a separate refresh" -> past
  tense; #685 removed it from the index instead of regenerating it.
- :2327-2328 -- "55 warnings on main before this change, 56 after"
  (-b html) -> 53/54; the raw `grep -c WARNING:` double-counts two
  Scapy import lines as Sphinx warnings, confirmed live on this head
  with both -b dummy and -b html (real 36/raw 38 with const/reg.rst
  excluded, same +2 gap either way).
- :834 -- stale ``protocol.py:1016`` -> ``:1413`` (the actual
  ``self._file.read()`` call inside ``_read_fileng``).
- :1969 -- the #646 entry's coverage renumbering was wrong twice over
  (first ``1153 to 1265``, then ``1153 to 1443``, the latter being
  main's ``def`` line, which always executes and can never be the
  single miss). Corrected to ``1248 to 1360``, the ``warn(...)``
  statement's line before/after #646's own diff.

Regenerated CHANGELOG.md from the edited entries.

changelog_md.py --check: exit 0. pytest tests/project/test_changelog_md.py -q:
47 passed, 37 subtests.

Follow-up: origin/main advanced through #726/#740/#741/#742 (to 0a3abff)
and then #747/#748 (to 074c53e) while this sat at good-to-go; #726 moved
three more claims anchored on files it touched.

- :834 -- ``protocol.py:1413`` -> ``:1411``; #726 shifted the
  ``self._file.read()`` call in ``_read_fileng`` by -2 lines.
- :2382-83 -- traceflow.py "Line 406" -> "Line 424"; #742 inserted 18
  lines above the ``#: Type[Dumper]: Dumper class.`` comment.
- :2099-2104, :2187-89 -- the "seven code-keyed parser registrars" and
  ``Option.register`` are no longer presence-only. #726, fixing #718,
  gave all seven -- and ``Option.register`` itself -- the same identity
  guard ``register_protocol`` already had; reworded both passages to
  say so, confirmed against the guards' own current docstrings.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 948ac49 came back NEEDS CHANGES: the round-12 edit fixed two
sites of the harmonisation claim and left its twin, plus its own reasoning,
asserting the opposite; and four numbers anchored on files the merges touched
had drifted independently of #726.

- :1877-1878, :1883-1884 (#675) -- "carries the guarded ``if code in
  cls.__xxx__: warn(...)``" / "every sibling warns on mere presence" ->
  past tense, noting #726 later gave all seven the identity guard this
  entry's own comparison assumes they lack.
- :2100-2109 -- dropped the retained "yields two keys and never reaches
  one key twice" (false: ``Internet.register(TransType.TCP, TCP)`` warns
  once, incumbent.klass is TCP) and "leaves a different-class test
  undecidable" (contradicted by :2404-2406's own ``incumbent is not
  protocol`` definition); replaced with the actual false positive the
  guard has -- pre-seeded ``ModuleDescriptor`` incumbents never compare
  equal to the resolved class.
- :2192 -- reflowed the ``Option.register`` paragraph (orphan lines
  fixed alongside).
- :2387-2388 -- the ``Type[Dumper]`` quote now matches what is actually
  at line 424 (post-#709-fix), rather than the pre-fix bare form.
- :945 -- ``README.md`` (103) -> (102).
- :1136 -- "75 of the 117 modules" -> "77 ... after #647 below adds
  the same ending to three more" (drifted via #647, independent of the
  four merges).
- :2119 -- dropped the irreproducible pylint "364 messages" figure;
  kept mypy's 112, which does reproduce.
- :2119 -- "326 registry writes" -> 327 (``R1CounterParameter``'s
  second code, from #690).

Also fixed six false claims in the PR body (separate from the .rst):
hunk/line counts, six-commits -> 46, the 5-row table's implied total,
"not trimmed", main's red/green state, and the now-unreachable
cherry-pick target.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 1749cc0 came back NEEDS CHANGES: round 13 fixed five of
the nine sites and introduced four new false claims doing it, including
two inside the flagship rewrite -- swapping one inaccuracy for another
is this document's recurring failure mode.

- :1136-37 -- "75 ... 77 now, after #647 ... adds ... three more" was
  internally inconsistent (75+3=78, not 77). Traced #647's own diff
  (fc32d1b): it adds ``_missing_`` to three IntFlag classes across
  only two *new* files -- ``tcp/flags.py`` and ``ftp/command.py`` --
  since the third, ``TransportProtocol``, shares ``reg/apptype.py``
  with the already-counted ``AppType``. Module delta is +2, matching
  75+2=77; reworded to say so.
- :1879-88 -- dropped "the comparison below assumes they still lack"
  it, which was false about text 8 lines below in the same diff
  (already past-tensed). Also reflowed three orphan lines this
  introduced (`passes, whereas`, `it twice with nothing`, `none of
  the`).
- :2107-19 -- "These tables also ship pre-seeded" over-generalised:
  verified live (``ProtocolBase.__proto__`` is 0 entries,
  ``Transport.__proto__ is ProtocolBase.__proto__`` -- True) that 2 of
  7 have nothing pre-seeded. Scoped to the five that do (Link 7,
  Internet 16, Frame 3, PCAPNG 3, SCTP 2). Also fixed "the guard
  resolves only the incoming class", which contradicts the guard's own
  docstring ("the comparison itself resolves nothing") -- resolution is
  the earlier ``isinstance(protocol, ModuleDescriptor)`` step, three
  lines above the guard, not something the guard does.
- :2129-30 -- dropped the invented "327th" ordinal (327 total stays;
  traced-write instrumentation via ``sys`` hooks found the seeding is
  literal dict construction, not ``.register()`` calls, so I could not
  reproduce an ordinal with confidence -- said "one of them" instead
  of guessing).
- :7-8 -- "between #326 and #509" now says the programme continued
  past it (verified: 193 distinct #nnn refs, max #726, 103 above 509).
- PR body -- "7 hunks, 1168+/11-" was the previous head's figure, not
  this one's; replaced with the actual command
  (``git diff --shortstat da697fa -- docs/source/changelog/1.5.0.rst``)
  and today's figure (9 hunks, 1152+/16-), since a hardcoded count here
  has now gone stale twice.

Left alone per this round's scope: :1969/:1974 (before/after claim,
not falsified by #726's later +1), mypy "112" (correct, re-ran with
the project's own flags), ":2122" 13-to-14 (correct at its delta
scope), and the other 121 cited paths (unaffected by main's one new
commit, #745, confirmed test-only).

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at b2ac58b came back NEEDS CHANGES: round 15 fixed four
sites clean but swapped in two new inaccuracies, and left one
round-fourteen defect (body :26) unfixed.

- :7 -- "past #726" was wrong direction: #726 is the max ref in the
  document (193 distinct, min #251, max #726), not one exceeded ->
  "reaching #726".
- :2110-19 -- "ProtocolBase and Transport share one dict, starting and
  staying empty until a subclass registers" was false three ways,
  verified live against origin/main (pcapkit.__file__ asserted):
  Transport.register() itself raises UnsupportedCall (abstract); TCP
  and UDP keep their own separate __proto__ (4 and 3 entries), not the
  shared one, so registering on them leaves the shared dict at 0; only
  a direct ProtocolBase.register() call fills it. Narrowing to "five
  of these seven" also hid that TCP/UDP are pre-seeded too, which is
  exactly where the false positive bites in the transport family --
  restored that.
- body :26 -- "26 entry commits" -> 27 (commits whose subject starts
  "docs(changelog): the 1.5.0 entry/entries for", verified by grep),
  28 bullets added and 0 removed (verified via the .rst diff against
  da697fa; one commit, 6a956c4, adds two bullets for #648/#649).
- body :41 -- dropped the hardcoded "9 hunks, 1152+/16-" figure
  entirely (it had already drifted to 1154+ by the time of this
  commit) and named the second command needed for the hunk count,
  since --shortstat cannot print one.

On the ordinal question raised last round: dropping it was still right
(the asserted "327th" was wrong), but "no ordinal is derivable" does
not hold either -- the writes are at pcapkit/protocols/schema/schema.py,
not the 8 dict-literal registrar sites my instrumentation covered, and
they are traceable. Left the text as "one of them being
R1CounterParameter's second code" (no ordinal asserted, no false
derivability claim either) rather than reopen a site outside this
round's scope.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.
JarryShaw added a commit that referenced this pull request Sep 24, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  a system libpcap via apt-get) across 3.10-3.14. Closes HAS_SCAPY,
  HAS_PYSHARK (2 of 3 gates), HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Attempts HAS_PYPCAP (4 gates) per
  "try to build and if CI is not a good fit, then rip it" -- confirmed
  building cleanly on real CI, so it stays rather than getting ripped --
  and closes the remaining 6 HAS_PYPCAPFILE gates in the same module.
  Kept apart from `engine-tests`: pypcap and pcap-ct both ship a
  top-level `pcap` module and cannot share a venv.
- No tshark binary is installed for PyShark: reading its two
  HAS_PYSHARK-gated engine tests shows they assert the "tshark not
  found" reason on a host that has none, so installing one would flip
  a pass into a failure on four of five legs rather than exercise
  anything.
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, and add `engine-tests` to HAS_VENDOR_DEPS'
  dark jobs (its ignore-shape selection reaches the same html5lib gate
  `test` does).
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix two comments on the `test` and `integration` jobs left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed there, and one said "or on either
  job below", no longer true now that `engine-tests` installs it.

Adds no tests; makes 27 previously-skipped unit-tier methods run for
real (measured before/after on a throwaway venv, matching `test`'s own
install line as the baseline). Confirmed on the real PR CI run: all 5
`engine-tests` legs and both `pypcap-parity` legs pass, with PyPCAP
actually building and its 4 gated methods executing on 3.10/3.11 (not
skipping) and PyPCAPFile's other 6 methods passing alongside it.
tests/test_tier_guard.py: 84 passed, 508 subtests (was 485 on main).
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 24, 2026
JarryShaw added a commit that referenced this pull request Sep 24, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Adds no tests; makes 27 previously-skipped unit-tier methods run for
real (measured before/after on a throwaway venv, matching `test`'s own
install line as the baseline). Confirmed on real PR CI: all 5
`engine-tests` legs and both `pypcap-parity` legs pass, with PyPCAP
actually building and its 4 gated methods executing rather than
skipping. tests/test_tier_guard.py: 84 passed, 508 subtests (was 485).
JarryShaw added a commit that referenced this pull request Sep 25, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Cross-review round found four more defects, all fixed here:

- tests/_dependency_gates.py: the "ambiguity stays live but happens not
  to bite" note above was wrong to call safe -- a job installing the
  *wrong* half of the pypcap/pcap-ct ambiguity read as satisfied too,
  since extras_providing() only checked whether *some* extra in common
  shipped the module. Doctoring pypcap-parity to install PCAP_CT (still
  reaches 4 HAS_PYPCAP gates) and engine-tests to install PyPCAP (still
  reaches 1 HAS_PCAP_CT gate) both passed the old guard with zero
  unexplained gaps. Added ambiguous_satisfactions() and
  AMBIGUOUS_PROVIDER_ALLOWLIST: a satisfied gate resolved through a
  module more than one *distribution* ships (MODULE_PROVIDERS names more
  than one for it) now has to resolve to exactly the one distribution the
  allowlist names for that (job, flag) pair, not merely share some
  distribution with the job's install line. Both doctored scenarios now
  report a finding; the real, undoctored workflow reports none. Also
  corrected the module docstring's own claim that the two flags "both
  read as needing pcap alone" -- flag_requirements() resolves them apart
  correctly ({'pcap'} vs {'pcap._pcap'}); the conflation is one level
  down, in extras_providing()'s top-level-package truncation. Fixes
  #762.
- tests/foundation/engines/test_pyshark_engine.py: both tshark-presence
  checks keyed on shutil.which('tshark'), which disagrees with
  PyShark.unsupported_reason()'s own oracle (pyshark's
  get_process_path(), which reads a ./config.ini before PATH) --
  pre-existing in test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous
  and copied into test_the_reason_tracks_the_running_interpreter by this
  PR. A ./config.ini naming an off-PATH tshark stand-in makes `which`
  say absent while the real oracle finds it, failing both. Added
  _tshark_missing(), probing the same way production does, and a
  regression test constructing exactly that config.ini case.
- .github/workflows/unit-tests.yml:456: "unlike the three jobs above"
  was stale -- this PR's own `engine-tests` and `pypcap-parity` bring
  the true count to four; corrected and named all four explicitly so a
  fifth insertion has to confront the list rather than the count alone.
- tests/_dependency_gates.py: gated_scopes()'s docstring said HAS_DPKT
  reaches its 20 methods through 6 class decorators and none through a
  method decorator; exact-match AST counting (not the substring search
  that folded HAS_PYPCAPFILE into HAS_PYPCAP in an earlier round) gives
  8, not zero. Corrected.

Adds no tests for the per-engine job coverage itself; makes 27
previously-skipped unit-tier methods run for real (measured
before/after on a throwaway venv, matching `test`'s own install line as
the baseline). Confirmed on real PR CI: all 5 `engine-tests` legs and
both `pypcap-parity` legs pass, with PyPCAP actually building and its 4
gated methods executing rather than skipping. This round adds 7 tests
for the four fixes above (2 falsifiability tests reproducing the
doctored ambiguity scenarios, an anti-rot liveness check on the new
allowlist, a message-format check, a manual-construction test for the
unlisted-pair branch, and a config.ini regression test), each shown to
fail against the pre-fix code. tests/test_tier_guard.py +
test_pyshark_engine.py: 98 passed, 517 subtests (was 84 passed, 508
subtests).
JarryShaw added a commit that referenced this pull request Sep 25, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Cross-review round found four more defects, all fixed here:

- tests/_dependency_gates.py: the "ambiguity stays live but happens not
  to bite" note above was wrong to call safe -- a job installing the
  *wrong* half of the pypcap/pcap-ct ambiguity read as satisfied too,
  since extras_providing() only checked whether *some* extra in common
  shipped the module. Doctoring pypcap-parity to install PCAP_CT (still
  reaches 4 HAS_PYPCAP gates) and engine-tests to install PyPCAP (still
  reaches 1 HAS_PCAP_CT gate) both passed the old guard with zero
  unexplained gaps. Added ambiguous_satisfactions() and
  AMBIGUOUS_PROVIDER_ALLOWLIST: a satisfied gate resolved through a
  module more than one *distribution* ships (MODULE_PROVIDERS names more
  than one for it) now has to resolve to exactly the one distribution the
  allowlist names for that (job, flag) pair, not merely share some
  distribution with the job's install line. Both doctored scenarios now
  report a finding; the real, undoctored workflow reports none. Also
  corrected the module docstring's own claim that the two flags "both
  read as needing pcap alone" -- flag_requirements() resolves them apart
  correctly ({'pcap'} vs {'pcap._pcap'}); the conflation is one level
  down, in extras_providing()'s top-level-package truncation.
- tests/foundation/engines/test_pyshark_engine.py: both tshark-presence
  checks keyed on shutil.which('tshark'), which disagrees with
  PyShark.unsupported_reason()'s own oracle (pyshark's
  get_process_path(), which reads a ./config.ini before PATH) --
  pre-existing in test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous
  and copied into test_the_reason_tracks_the_running_interpreter by this
  PR. A ./config.ini naming an off-PATH tshark stand-in makes `which`
  say absent while the real oracle finds it, failing both. Added
  _tshark_missing(), probing the same way production does, and a
  regression test constructing exactly that config.ini case.
- .github/workflows/unit-tests.yml:456: "unlike the three jobs above"
  was stale -- this PR's own `engine-tests` and `pypcap-parity` bring
  the true count to four; corrected and named all four explicitly so a
  fifth insertion has to confront the list rather than the count alone.
- tests/_dependency_gates.py: gated_scopes()'s docstring said HAS_DPKT
  reaches its 20 methods through 6 class decorators and none through a
  method decorator; exact-match AST counting (not the substring search
  that folded HAS_PYPCAPFILE into HAS_PYPCAP in an earlier round) gives
  8, not zero. Corrected.

Adds no tests for the per-engine job coverage itself; makes 27
previously-skipped unit-tier methods run for real (measured
before/after on a throwaway venv, matching `test`'s own install line as
the baseline). Confirmed on real PR CI: all 5 `engine-tests` legs and
both `pypcap-parity` legs pass, with PyPCAP actually building and its 4
gated methods executing rather than skipping. This round adds 7 tests
for the four fixes above (2 falsifiability tests reproducing the
doctored ambiguity scenarios, an anti-rot liveness check on the new
allowlist, a message-format check, a manual-construction test for the
unlisted-pair branch, and a config.ini regression test), each shown to
fail against the pre-fix code. tests/test_tier_guard.py +
test_pyshark_engine.py: 98 passed, 517 subtests (was 84 passed, 508
subtests).

Fixes #762.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Cross-review round found four more defects, all fixed here:

- tests/_dependency_gates.py: the "ambiguity stays live but happens not
  to bite" note above was wrong to call safe -- a job installing the
  *wrong* half of the pypcap/pcap-ct ambiguity read as satisfied too,
  since extras_providing() only checked whether *some* extra in common
  shipped the module. Doctoring pypcap-parity to install PCAP_CT (still
  reaches 4 HAS_PYPCAP gates) and engine-tests to install PyPCAP (still
  reaches 1 HAS_PCAP_CT gate) both passed the old guard with zero
  unexplained gaps. Added ambiguous_satisfactions() and
  AMBIGUOUS_PROVIDER_ALLOWLIST: a satisfied gate resolved through a
  module more than one *distribution* ships (MODULE_PROVIDERS names more
  than one for it) now has to resolve to exactly the one distribution the
  allowlist names for that (job, flag) pair, not merely share some
  distribution with the job's install line. Both doctored scenarios now
  report a finding; the real, undoctored workflow reports none. Also
  corrected the module docstring's own claim that the two flags "both
  read as needing pcap alone" -- flag_requirements() resolves them apart
  correctly ({'pcap'} vs {'pcap._pcap'}); the conflation is one level
  down, in extras_providing()'s top-level-package truncation.
- tests/foundation/engines/test_pyshark_engine.py: both tshark-presence
  checks keyed on shutil.which('tshark'), which disagrees with
  PyShark.unsupported_reason()'s own oracle (pyshark's
  get_process_path(), which reads a ./config.ini before PATH) --
  pre-existing in test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous
  and copied into test_the_reason_tracks_the_running_interpreter by this
  PR. A ./config.ini naming an off-PATH tshark stand-in makes `which`
  say absent while the real oracle finds it, failing both. Added
  _tshark_missing(), probing the same way production does, and a
  regression test constructing exactly that config.ini case.
- .github/workflows/unit-tests.yml:456: "unlike the three jobs above"
  was stale -- this PR's own `engine-tests` and `pypcap-parity` bring
  the true count to four; corrected and named all four explicitly so a
  fifth insertion has to confront the list rather than the count alone.
- tests/_dependency_gates.py: gated_scopes()'s docstring said HAS_DPKT
  reaches its 20 methods through 6 class decorators and none through a
  method decorator; exact-match AST counting (not the substring search
  that folded HAS_PYPCAPFILE into HAS_PYPCAP in an earlier round) gives
  8, not zero. Corrected.

Adds no tests for the per-engine job coverage itself; makes 27
previously-skipped unit-tier methods run for real (measured
before/after on a throwaway venv, matching `test`'s own install line as
the baseline). Confirmed on real PR CI: all 5 `engine-tests` legs and
both `pypcap-parity` legs pass, with PyPCAP actually building and its 4
gated methods executing rather than skipping. This round adds 7 tests
for the four fixes above (2 falsifiability tests reproducing the
doctored ambiguity scenarios, an anti-rot liveness check on the new
allowlist, a message-format check, a manual-construction test for the
unlisted-pair branch, and a config.ini regression test), each shown to
fail against the pre-fix code.

CI then went red on real `engine-tests` legs, on the regression test
the round above added:

- tests/foundation/engines/test_pyshark_engine.py: that test asserted
  `shutil.which('tshark') is None` as its premise -- true on the machine
  it was written on, false on every `engine-tests` leg, which apt-get
  installs a real `/usr/bin/tshark` a few steps earlier in the same job.
  The claim the test actually needs is narrower: that `which` cannot
  resolve *the stand-in* it just built, not that the host has no tshark
  anywhere on PATH. Rewritten to construct both worlds explicitly --
  PATH pointing at an empty directory, and PATH pointing at a directory
  holding an unrelated, real, executable `tshark` standing in for the
  one `engine-tests` installs -- and to assert against the stand-in's
  own path rather than against the host's state, so it cannot depend on
  which world it happens to run in again.
- Swept every comment touched by the last two rounds for the same
  failure mode -- prose asserting what the code used to do rather than
  what it does now -- and found four more:
  - unit-tests.yml:316-317 and _dependency_gates.py's own HAS_PYSHARK
    exclusion both still said the fix was "checks shutil.which('tshark')
    first" / "self-guard (shutil.which('tshark'))"; both now describe the
    real oracle (`_tshark_missing()`, itself probing pyshark's
    get_process_path()).
  - _dependency_gates.py's HAS_PYSHARK exclusion also said "3 gated
    methods run nowhere" / "two of them assert what
    PyShark.unsupported_reason says" -- the regression test above is
    itself HAS_PYSHARK-gated, so the true counts are 4 and three.
  - test_pyshark_engine.py's own module docstring claimed tshark "is
    not installed here" and that "installing Wireshark was not an
    option" -- both false once `engine-tests` collects this same module
    with a real tshark on PATH. Rewritten to describe both states
    without assuming either.
  - test_pyshark_engine.py's `_tshark_missing()` docstring said "both
    call sites" and "the two tests below" -- there are three call sites
    now; reworded to not name a count that the next test added here
    would have to remember to bump.

tests/test_tier_guard.py + test_pyshark_engine.py: 98 passed, 519
subtests (was 84 passed, 508 subtests before this PR's cross-review
rounds).

Fixes #762.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Cross-review round found four more defects, all fixed here:

- tests/_dependency_gates.py: the "ambiguity stays live but happens not
  to bite" note above was wrong to call safe -- a job installing the
  *wrong* half of the pypcap/pcap-ct ambiguity read as satisfied too,
  since extras_providing() only checked whether *some* extra in common
  shipped the module. Doctoring pypcap-parity to install PCAP_CT (still
  reaches 4 HAS_PYPCAP gates) and engine-tests to install PyPCAP (still
  reaches 1 HAS_PCAP_CT gate) both passed the old guard with zero
  unexplained gaps. Added ambiguous_satisfactions() and
  AMBIGUOUS_PROVIDER_ALLOWLIST: a satisfied gate resolved through a
  module more than one *distribution* ships (MODULE_PROVIDERS names more
  than one for it) now has to resolve to exactly the one distribution the
  allowlist names for that (job, flag) pair, not merely share some
  distribution with the job's install line. Both doctored scenarios now
  report a finding; the real, undoctored workflow reports none. Also
  corrected the module docstring's own claim that the two flags "both
  read as needing pcap alone" -- flag_requirements() resolves them apart
  correctly ({'pcap'} vs {'pcap._pcap'}); the conflation is one level
  down, in extras_providing()'s top-level-package truncation.
- tests/foundation/engines/test_pyshark_engine.py: both tshark-presence
  checks keyed on shutil.which('tshark'), which disagrees with
  PyShark.unsupported_reason()'s own oracle (pyshark's
  get_process_path(), which reads a ./config.ini before PATH) --
  pre-existing in test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous
  and copied into test_the_reason_tracks_the_running_interpreter by this
  PR. A ./config.ini naming an off-PATH tshark stand-in makes `which`
  say absent while the real oracle finds it, failing both. Added
  _tshark_missing(), probing the same way production does, and a
  regression test constructing exactly that config.ini case.
- .github/workflows/unit-tests.yml:456: "unlike the three jobs above"
  was stale -- this PR's own `engine-tests` and `pypcap-parity` bring
  the true count to four; corrected and named all four explicitly so a
  fifth insertion has to confront the list rather than the count alone.
- tests/_dependency_gates.py: gated_scopes()'s docstring said HAS_DPKT
  reaches its 20 methods through 6 class decorators and none through a
  method decorator; exact-match AST counting (not the substring search
  that folded HAS_PYPCAPFILE into HAS_PYPCAP in an earlier round) gives
  8, not zero. Corrected.

Adds no tests for the per-engine job coverage itself; makes 27
previously-skipped unit-tier methods run for real (measured
before/after on a throwaway venv, matching `test`'s own install line as
the baseline). Confirmed on real PR CI: all 5 `engine-tests` legs and
both `pypcap-parity` legs pass, with PyPCAP actually building and its 4
gated methods executing rather than skipping. This round adds 7 tests
for the four fixes above (2 falsifiability tests reproducing the
doctored ambiguity scenarios, an anti-rot liveness check on the new
allowlist, a message-format check, a manual-construction test for the
unlisted-pair branch, and a config.ini regression test), each shown to
fail against the pre-fix code.

CI then went red on real `engine-tests` legs, on the regression test
the round above added:

- tests/foundation/engines/test_pyshark_engine.py: that test asserted
  `shutil.which('tshark') is None` as its premise -- true on the machine
  it was written on, false on every `engine-tests` leg, which apt-get
  installs a real `/usr/bin/tshark` a few steps earlier in the same job.
  The claim the test actually needs is narrower: that `which` cannot
  resolve *the stand-in* it just built, not that the host has no tshark
  anywhere on PATH. Rewritten to construct both worlds explicitly --
  PATH pointing at an empty directory, and PATH pointing at a directory
  holding an unrelated, real, executable `tshark` standing in for the
  one `engine-tests` installs -- and to assert against the stand-in's
  own path rather than against the host's state, so it cannot depend on
  which world it happens to run in again.
- Swept every comment touched by the last two rounds for the same
  failure mode -- prose asserting what the code used to do rather than
  what it does now -- and found four more:
  - unit-tests.yml:316-317 and _dependency_gates.py's own HAS_PYSHARK
    exclusion both still said the fix was "checks shutil.which('tshark')
    first" / "self-guard (shutil.which('tshark'))"; both now describe the
    real oracle (`_tshark_missing()`, itself probing pyshark's
    get_process_path()).
  - _dependency_gates.py's HAS_PYSHARK exclusion also said "3 gated
    methods run nowhere" / "two of them assert what
    PyShark.unsupported_reason says" -- the regression test above is
    itself HAS_PYSHARK-gated, so the true counts are 4 and three.
  - test_pyshark_engine.py's own module docstring claimed tshark "is
    not installed here" and that "installing Wireshark was not an
    option" -- both false once `engine-tests` collects this same module
    with a real tshark on PATH. Rewritten to describe both states
    without assuming either.
  - test_pyshark_engine.py's `_tshark_missing()` docstring said "both
    call sites" and "the two tests below" -- there are three call sites
    now; reworded to not name a count that the next test added here
    would have to remember to bump.

tests/test_tier_guard.py + test_pyshark_engine.py: 98 passed, 519
subtests (was 84 passed, 508 subtests before this PR's cross-review
rounds).

A second, independent cross-review found the #762 guard above could
still be defeated -- flipping a job's install line and its allowlist
entry together stayed silently green, since nothing tied
AMBIGUOUS_PROVIDER_ALLOWLIST's values to the distribution that is
actually *correct* for a flag, only to whatever the job installs.
Removed the allowlist rather than hardening its test:

- tests/_dependency_gates.py: added module_flag_exclusions()/
  flag_exclusions(), the mirror of module_flag_requirements()/
  flag_requirements() that recovers the *negative* half of a flag's
  probe those two correctly drop (HAS_PYPCAP's own `not
  importable('pcap._pcap')`). Added module_providers(), which -- unlike
  extras_providing(), deliberately untouched -- resolves a dotted import
  name exactly when MODULE_PROVIDERS has a dedicated entry for it, so
  `pcap._pcap` now maps to `('pcap-ct',)` alone instead of falling back
  to plain `pcap`'s two-wide entry. Subtracting the exclusion's exact
  providers from the requirement's leaves exactly one legitimate
  distribution per flag, derived rather than hand-written.
  ambiguous_satisfactions() now flags a gate whenever what
  dependency_gate_gaps() would call "satisfied" disagrees with that
  derived set -- resolving to the disqualified distribution (the #762
  shape) or to more than one legitimate one at once. Both doctored rows
  are still caught, now with no allowlist to keep in sync; verified by
  independently stubbing out each half (flag_exclusions returning
  nothing; MODULE_PROVIDERS's pcap._pcap widened back to both) and
  confirming each reopens exactly the row it protects.
- The same review found the guard's own scope too wide: gating
  ambiguity on `len(MODULE_PROVIDERS[name]) > 1` false-positives on
  `html5lib`, which has two entries for the *same* one distribution
  (`beautifulsoup4[html5lib]`, never bare `html5lib` -- pyproject.toml
  never declares it) rather than two competing ones. Added
  MUTUALLY_EXCLUSIVE_IMPORTS, declaring only `pcap` as genuinely
  contested, and scoped ambiguous_satisfactions() to it. Verified by
  doctoring `test` to gain the `vendor` extra (closing #738's
  HAS_VENDOR_DEPS gap): zero findings, where the old length-based scope
  produced one with no wrong half to report.
- A third finding: the NON_DISTRIBUTION_FLAGS-skip assertion added for
  ambiguous_satisfactions() used HAS_PYSHARK, which never reaches the
  ambiguity branch at all (`pyshark` has one provider) -- doubly
  vacuous, since deleting the skip line left it passing too. Dropped
  that assertion and added a real one on the doctored pypcap-parity
  workflow, where HAS_PYPCAP does reach the branch and there is a real
  finding to suppress.
- Pre-existing, fixed while in the file: `tests/_dependency_gates.py`'s
  own module docstring said six of the other seven workflows install
  `.[all]`; codeql-analysis.yml installs nothing explicitly and
  python-compatibility.yml installs a bare `.`, so it is five.

tests/test_tier_guard.py + test_pyshark_engine.py: 100 passed, 519
subtests.

Fixes #762.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Cross-review round found four more defects, all fixed here:

- tests/_dependency_gates.py: the "ambiguity stays live but happens not
  to bite" note above was wrong to call safe -- a job installing the
  *wrong* half of the pypcap/pcap-ct ambiguity read as satisfied too,
  since extras_providing() only checked whether *some* extra in common
  shipped the module. Doctoring pypcap-parity to install PCAP_CT (still
  reaches 4 HAS_PYPCAP gates) and engine-tests to install PyPCAP (still
  reaches 1 HAS_PCAP_CT gate) both passed the old guard with zero
  unexplained gaps. Added ambiguous_satisfactions() and
  AMBIGUOUS_PROVIDER_ALLOWLIST: a satisfied gate resolved through a
  module more than one *distribution* ships (MODULE_PROVIDERS names more
  than one for it) now has to resolve to exactly the one distribution the
  allowlist names for that (job, flag) pair, not merely share some
  distribution with the job's install line. Both doctored scenarios now
  report a finding; the real, undoctored workflow reports none. Also
  corrected the module docstring's own claim that the two flags "both
  read as needing pcap alone" -- flag_requirements() resolves them apart
  correctly ({'pcap'} vs {'pcap._pcap'}); the conflation is one level
  down, in extras_providing()'s top-level-package truncation.
- tests/foundation/engines/test_pyshark_engine.py: both tshark-presence
  checks keyed on shutil.which('tshark'), which disagrees with
  PyShark.unsupported_reason()'s own oracle (pyshark's
  get_process_path(), which reads a ./config.ini before PATH) --
  pre-existing in test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous
  and copied into test_the_reason_tracks_the_running_interpreter by this
  PR. A ./config.ini naming an off-PATH tshark stand-in makes `which`
  say absent while the real oracle finds it, failing both. Added
  _tshark_missing(), probing the same way production does, and a
  regression test constructing exactly that config.ini case.
- .github/workflows/unit-tests.yml:456: "unlike the three jobs above"
  was stale -- this PR's own `engine-tests` and `pypcap-parity` bring
  the true count to four; corrected and named all four explicitly so a
  fifth insertion has to confront the list rather than the count alone.
- tests/_dependency_gates.py: gated_scopes()'s docstring said HAS_DPKT
  reaches its 20 methods through 6 class decorators and none through a
  method decorator; exact-match AST counting (not the substring search
  that folded HAS_PYPCAPFILE into HAS_PYPCAP in an earlier round) gives
  8, not zero. Corrected.

Adds no tests for the per-engine job coverage itself; makes 27
previously-skipped unit-tier methods run for real (measured
before/after on a throwaway venv, matching `test`'s own install line as
the baseline). Confirmed on real PR CI: all 5 `engine-tests` legs and
both `pypcap-parity` legs pass, with PyPCAP actually building and its 4
gated methods executing rather than skipping. This round adds 7 tests
for the four fixes above (2 falsifiability tests reproducing the
doctored ambiguity scenarios, an anti-rot liveness check on the new
allowlist, a message-format check, a manual-construction test for the
unlisted-pair branch, and a config.ini regression test), each shown to
fail against the pre-fix code.

CI then went red on real `engine-tests` legs, on the regression test
the round above added:

- tests/foundation/engines/test_pyshark_engine.py: that test asserted
  `shutil.which('tshark') is None` as its premise -- true on the machine
  it was written on, false on every `engine-tests` leg, which apt-get
  installs a real `/usr/bin/tshark` a few steps earlier in the same job.
  The claim the test actually needs is narrower: that `which` cannot
  resolve *the stand-in* it just built, not that the host has no tshark
  anywhere on PATH. Rewritten to construct both worlds explicitly --
  PATH pointing at an empty directory, and PATH pointing at a directory
  holding an unrelated, real, executable `tshark` standing in for the
  one `engine-tests` installs -- and to assert against the stand-in's
  own path rather than against the host's state, so it cannot depend on
  which world it happens to run in again.
- Swept every comment touched by the last two rounds for the same
  failure mode -- prose asserting what the code used to do rather than
  what it does now -- and found four more:
  - unit-tests.yml:316-317 and _dependency_gates.py's own HAS_PYSHARK
    exclusion both still said the fix was "checks shutil.which('tshark')
    first" / "self-guard (shutil.which('tshark'))"; both now describe the
    real oracle (`_tshark_missing()`, itself probing pyshark's
    get_process_path()).
  - _dependency_gates.py's HAS_PYSHARK exclusion also said "3 gated
    methods run nowhere" / "two of them assert what
    PyShark.unsupported_reason says" -- the regression test above is
    itself HAS_PYSHARK-gated, so the true counts are 4 and three.
  - test_pyshark_engine.py's own module docstring claimed tshark "is
    not installed here" and that "installing Wireshark was not an
    option" -- both false once `engine-tests` collects this same module
    with a real tshark on PATH. Rewritten to describe both states
    without assuming either.
  - test_pyshark_engine.py's `_tshark_missing()` docstring said "both
    call sites" and "the two tests below" -- there are three call sites
    now; reworded to not name a count that the next test added here
    would have to remember to bump.

tests/test_tier_guard.py + test_pyshark_engine.py: 98 passed, 519
subtests (was 84 passed, 508 subtests before this PR's cross-review
rounds).

A second, independent cross-review found the #762 guard above could
still be defeated -- flipping a job's install line and its allowlist
entry together stayed silently green, since nothing tied
AMBIGUOUS_PROVIDER_ALLOWLIST's values to the distribution that is
actually *correct* for a flag, only to whatever the job installs.
Removed the allowlist rather than hardening its test:

- tests/_dependency_gates.py: added module_flag_exclusions()/
  flag_exclusions(), the mirror of module_flag_requirements()/
  flag_requirements() that recovers the *negative* half of a flag's
  probe those two correctly drop (HAS_PYPCAP's own `not
  importable('pcap._pcap')`). Added module_providers(), which -- unlike
  extras_providing(), deliberately untouched -- resolves a dotted import
  name exactly when MODULE_PROVIDERS has a dedicated entry for it, so
  `pcap._pcap` now maps to `('pcap-ct',)` alone instead of falling back
  to plain `pcap`'s two-wide entry. Subtracting the exclusion's exact
  providers from the requirement's leaves exactly one legitimate
  distribution per flag, derived rather than hand-written.
  ambiguous_satisfactions() now flags a gate whenever what
  dependency_gate_gaps() would call "satisfied" disagrees with that
  derived set -- resolving to the disqualified distribution (the #762
  shape) or to more than one legitimate one at once. Both doctored rows
  are still caught, now with no allowlist to keep in sync; verified by
  independently stubbing out each half (flag_exclusions returning
  nothing; MODULE_PROVIDERS's pcap._pcap widened back to both) and
  confirming each reopens exactly the row it protects.
- The same review found the guard's own scope too wide: gating
  ambiguity on `len(MODULE_PROVIDERS[name]) > 1` false-positives on
  `html5lib`, which has two entries for the *same* one distribution
  (`beautifulsoup4[html5lib]`, never bare `html5lib` -- pyproject.toml
  never declares it) rather than two competing ones. Added
  MUTUALLY_EXCLUSIVE_IMPORTS, declaring only `pcap` as genuinely
  contested, and scoped ambiguous_satisfactions() to it. Verified by
  doctoring `test` to gain the `vendor` extra (closing #738's
  HAS_VENDOR_DEPS gap): zero findings, where the old length-based scope
  produced one with no wrong half to report.
- A third finding: the NON_DISTRIBUTION_FLAGS-skip assertion added for
  ambiguous_satisfactions() used HAS_PYSHARK, which never reaches the
  ambiguity branch at all (`pyshark` has one provider) -- doubly
  vacuous, since deleting the skip line left it passing too. Dropped
  that assertion and added a real one on the doctored pypcap-parity
  workflow, where HAS_PYPCAP does reach the branch and there is a real
  finding to suppress.
- Pre-existing, fixed while in the file: `tests/_dependency_gates.py`'s
  own module docstring said six of the other seven workflows install
  `.[all]`; codeql-analysis.yml installs nothing explicitly and
  python-compatibility.yml installs a bare `.`, so it is five.

tests/test_tier_guard.py + test_pyshark_engine.py: 100 passed, 519
subtests.

A third, independent cross-review confirmed the mechanism survives
(six mutations all turn it red, including doctoring the real workflow
and emptying MUTUALLY_EXCLUSIVE_IMPORTS together, which the swap
tests' own literal install-line strings still catch) and found four
smaller things:

- tests/test_tier_guard.py:1615: a deleted blank line before
  `class DependencyGateFalsifiabilityTests` (PEP 8 E302; `tests/` is
  not linted by `make pylint`, so nothing else would have caught it).
  Restored.
- Four stale figures in the PR description, all from revision 1
  (`HAS_PYSHARK`'s count moved from 3 to 4 once the tshark regression
  test above became its own third in-file gated method; "both/two
  touched Python files" is now three; the `tests/test_tier_guard.py`
  test count needed to be split from `test_pyshark_engine.py`'s
  rather than presented as one number covering both). Corrected in
  the description directly.
- MUTUALLY_EXCLUSIVE_IMPORTS was the one new hand-written table with
  no liveness check -- omitting a future contested name would not be
  caught, only a wrong entry among existing ones. Added
  contested_imports(): counts, per MODULE_PROVIDERS entry, how many
  alternatives some declared extra actually resolves (1 for
  html5lib -- bare html5lib is declared by zero extras -- 2 for pcap),
  and a name qualifies at 2+. A new test asserts it equals
  MUTUALLY_EXCLUSIVE_IMPORTS; a falsifiability test doctors in a
  module with two live alternatives and confirms the comparison
  disagrees when it is not added.
- Two latent gaps in ambiguous_satisfactions(): an excluded module
  with no MODULE_PROVIDERS entry raised a bare, untested KeyError
  (test_every_gated_flag_is_classified now loops over excluded
  modules too, giving them the same deliberate contract required ones
  already have); and disqualified was computed before the
  MUTUALLY_EXCLUSIVE_IMPORTS scope check rather than after, so an
  unrelated flag's negated probe on an unmapped module could crash
  the whole function instead of being scoped out of it -- moved the
  computation inside the scope-checked branch. Extracted
  _disqualified_providers() to also close the related, lower-priority
  gap: falling back to a contested top-level's full entry for an
  excluded dotted path with no exact entry of its own would
  over-disqualify and misdiagnose a real satisfaction as ambiguous;
  it now fails loudly instead, naming the missing entry.
- test_no_provider_mapping_or_exclusion_is_vestigial's docstring
  claimed the two tables are "exactly as wide as the suite needs them
  to be" without qualification; its own `needed` computation is
  self-referential for dotted keys, so deleting `pcap._pcap` moves
  both sides of the comparison together and goes uncaught by this
  test specifically (five others still pin it). Narrowed the
  docstring to say so.

tests/test_tier_guard.py + test_pyshark_engine.py: 105 passed, 519
subtests.

Fixes #762.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…-ct (#755)

Per the ruling on #751: third-party engine coverage gets its own job(s)
in unit-tests.yml, installing the engine extras across the full
3.10-3.14 matrix, rather than one more install line on `test`,
`integration` or `gate`.

- `engine-tests`: installs Scapy, PyShark, PyPCAPFile and PCAP_CT (plus
  system libpcap and tshark via apt-get) across 3.10-3.14. Closes
  HAS_SCAPY, HAS_PYSHARK, HAS_PYPCAPFILE (9 of 15), HAS_PCAP_CT and
  HAS_RUNTIME (test_runtime_engines.py's reused flag, closed as a side
  effect of installing dpkt+scapy+pyshark together).
- `pypcap-parity`: a separate job/venv on 3.10-3.11 only (both extras'
  own marker ceiling), with a C toolchain and libpcap headers, mirroring
  `integration`'s fixture-tier selection to reach
  test_new_engine_parity_runtime.py. Closes HAS_PYPCAP (4 gates,
  confirmed building on real CI) and the remaining 6 HAS_PYPCAPFILE
  gates in the same module. Kept apart from `engine-tests`: pypcap and
  pcap-ct both ship a top-level `pcap` module and cannot share a venv.
- Installing `tshark` needed one upstream fix first:
  test_pyshark_engine.py's test_the_reason_tracks_the_running_interpreter
  hard-asserted tshark's absence, unlike its sibling
  test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous, which
  already self-guards with shutil.which('tshark'). Gave it the same
  guard so the file no longer disagrees with itself about whether
  tshark may be present, then installed it (debconf pre-seeded to avoid
  the postinst prompt).
- tests/_dependency_gates.py: update DEPENDENCY_GATE_EXCLUSIONS' reasons
  for the six gates above, add `engine-tests` to HAS_VENDOR_DEPS' dark
  jobs, and correct the module docstring's pypcap/pcap-ct ambiguity note
  now that both extras are on a (different) job's install line -- the
  ambiguity stays live but happens not to bite here, since neither job's
  selection reaches the other flag's gate; confirmed with job_reaches()
  directly rather than assumed.
- tests/test_tier_guard.py: extend the three job-selection/removal
  assertions that enumerated the workflow's jobs by name to include the
  two new ones.
- Fix three comments (on `test`, `integration` and `gate`) left stale by
  #747/#748: they still described a since-fixed pypcapfile bug as the
  reason PyPCAPFile is not installed, and one said "or on either job
  below", no longer true now that `engine-tests` installs it.

Cross-review round found four more defects, all fixed here:

- tests/_dependency_gates.py: the "ambiguity stays live but happens not
  to bite" note above was wrong to call safe -- a job installing the
  *wrong* half of the pypcap/pcap-ct ambiguity read as satisfied too,
  since extras_providing() only checked whether *some* extra in common
  shipped the module. Doctoring pypcap-parity to install PCAP_CT (still
  reaches 4 HAS_PYPCAP gates) and engine-tests to install PyPCAP (still
  reaches 1 HAS_PCAP_CT gate) both passed the old guard with zero
  unexplained gaps. Added ambiguous_satisfactions() and
  AMBIGUOUS_PROVIDER_ALLOWLIST: a satisfied gate resolved through a
  module more than one *distribution* ships (MODULE_PROVIDERS names more
  than one for it) now has to resolve to exactly the one distribution the
  allowlist names for that (job, flag) pair, not merely share some
  distribution with the job's install line. Both doctored scenarios now
  report a finding; the real, undoctored workflow reports none. Also
  corrected the module docstring's own claim that the two flags "both
  read as needing pcap alone" -- flag_requirements() resolves them apart
  correctly ({'pcap'} vs {'pcap._pcap'}); the conflation is one level
  down, in extras_providing()'s top-level-package truncation.
- tests/foundation/engines/test_pyshark_engine.py: both tshark-presence
  checks keyed on shutil.which('tshark'), which disagrees with
  PyShark.unsupported_reason()'s own oracle (pyshark's
  get_process_path(), which reads a ./config.ini before PATH) --
  pre-existing in test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous
  and copied into test_the_reason_tracks_the_running_interpreter by this
  PR. A ./config.ini naming an off-PATH tshark stand-in makes `which`
  say absent while the real oracle finds it, failing both. Added
  _tshark_missing(), probing the same way production does, and a
  regression test constructing exactly that config.ini case.
- .github/workflows/unit-tests.yml:456: "unlike the three jobs above"
  was stale -- this PR's own `engine-tests` and `pypcap-parity` bring
  the true count to four; corrected and named all four explicitly so a
  fifth insertion has to confront the list rather than the count alone.
- tests/_dependency_gates.py: gated_scopes()'s docstring said HAS_DPKT
  reaches its 20 methods through 6 class decorators and none through a
  method decorator; exact-match AST counting (not the substring search
  that folded HAS_PYPCAPFILE into HAS_PYPCAP in an earlier round) gives
  8, not zero. Corrected.

Adds no tests for the per-engine job coverage itself; makes 27
previously-skipped unit-tier methods run for real (measured
before/after on a throwaway venv, matching `test`'s own install line as
the baseline). Confirmed on real PR CI: all 5 `engine-tests` legs and
both `pypcap-parity` legs pass, with PyPCAP actually building and its 4
gated methods executing rather than skipping. This round adds 7 tests
for the four fixes above (2 falsifiability tests reproducing the
doctored ambiguity scenarios, an anti-rot liveness check on the new
allowlist, a message-format check, a manual-construction test for the
unlisted-pair branch, and a config.ini regression test), each shown to
fail against the pre-fix code.

CI then went red on real `engine-tests` legs, on the regression test
the round above added:

- tests/foundation/engines/test_pyshark_engine.py: that test asserted
  `shutil.which('tshark') is None` as its premise -- true on the machine
  it was written on, false on every `engine-tests` leg, which apt-get
  installs a real `/usr/bin/tshark` a few steps earlier in the same job.
  The claim the test actually needs is narrower: that `which` cannot
  resolve *the stand-in* it just built, not that the host has no tshark
  anywhere on PATH. Rewritten to construct both worlds explicitly --
  PATH pointing at an empty directory, and PATH pointing at a directory
  holding an unrelated, real, executable `tshark` standing in for the
  one `engine-tests` installs -- and to assert against the stand-in's
  own path rather than against the host's state, so it cannot depend on
  which world it happens to run in again.
- Swept every comment touched by the last two rounds for the same
  failure mode -- prose asserting what the code used to do rather than
  what it does now -- and found four more:
  - unit-tests.yml:316-317 and _dependency_gates.py's own HAS_PYSHARK
    exclusion both still said the fix was "checks shutil.which('tshark')
    first" / "self-guard (shutil.which('tshark'))"; both now describe the
    real oracle (`_tshark_missing()`, itself probing pyshark's
    get_process_path()).
  - _dependency_gates.py's HAS_PYSHARK exclusion also said "3 gated
    methods run nowhere" / "two of them assert what
    PyShark.unsupported_reason says" -- the regression test above is
    itself HAS_PYSHARK-gated, so the true counts are 4 and three.
  - test_pyshark_engine.py's own module docstring claimed tshark "is
    not installed here" and that "installing Wireshark was not an
    option" -- both false once `engine-tests` collects this same module
    with a real tshark on PATH. Rewritten to describe both states
    without assuming either.
  - test_pyshark_engine.py's `_tshark_missing()` docstring said "both
    call sites" and "the two tests below" -- there are three call sites
    now; reworded to not name a count that the next test added here
    would have to remember to bump.

tests/test_tier_guard.py + test_pyshark_engine.py: 98 passed, 519
subtests (was 84 passed, 508 subtests before this PR's cross-review
rounds).

A second, independent cross-review found the #762 guard above could
still be defeated -- flipping a job's install line and its allowlist
entry together stayed silently green, since nothing tied
AMBIGUOUS_PROVIDER_ALLOWLIST's values to the distribution that is
actually *correct* for a flag, only to whatever the job installs.
Removed the allowlist rather than hardening its test:

- tests/_dependency_gates.py: added module_flag_exclusions()/
  flag_exclusions(), the mirror of module_flag_requirements()/
  flag_requirements() that recovers the *negative* half of a flag's
  probe those two correctly drop (HAS_PYPCAP's own `not
  importable('pcap._pcap')`). Added module_providers(), which -- unlike
  extras_providing(), deliberately untouched -- resolves a dotted import
  name exactly when MODULE_PROVIDERS has a dedicated entry for it, so
  `pcap._pcap` now maps to `('pcap-ct',)` alone instead of falling back
  to plain `pcap`'s two-wide entry. Subtracting the exclusion's exact
  providers from the requirement's leaves exactly one legitimate
  distribution per flag, derived rather than hand-written.
  ambiguous_satisfactions() now flags a gate whenever what
  dependency_gate_gaps() would call "satisfied" disagrees with that
  derived set -- resolving to the disqualified distribution (the #762
  shape) or to more than one legitimate one at once. Both doctored rows
  are still caught, now with no allowlist to keep in sync; verified by
  independently stubbing out each half (flag_exclusions returning
  nothing; MODULE_PROVIDERS's pcap._pcap widened back to both) and
  confirming each reopens exactly the row it protects.
- The same review found the guard's own scope too wide: gating
  ambiguity on `len(MODULE_PROVIDERS[name]) > 1` false-positives on
  `html5lib`, which has two entries for the *same* one distribution
  (`beautifulsoup4[html5lib]`, never bare `html5lib` -- pyproject.toml
  never declares it) rather than two competing ones. Added
  MUTUALLY_EXCLUSIVE_IMPORTS, declaring only `pcap` as genuinely
  contested, and scoped ambiguous_satisfactions() to it. Verified by
  doctoring `test` to gain the `vendor` extra (closing #738's
  HAS_VENDOR_DEPS gap): zero findings, where the old length-based scope
  produced one with no wrong half to report.
- A third finding: the NON_DISTRIBUTION_FLAGS-skip assertion added for
  ambiguous_satisfactions() used HAS_PYSHARK, which never reaches the
  ambiguity branch at all (`pyshark` has one provider) -- doubly
  vacuous, since deleting the skip line left it passing too. Dropped
  that assertion and added a real one on the doctored pypcap-parity
  workflow, where HAS_PYPCAP does reach the branch and there is a real
  finding to suppress.
- Pre-existing, fixed while in the file: `tests/_dependency_gates.py`'s
  own module docstring said six of the other seven workflows install
  `.[all]`; codeql-analysis.yml installs nothing explicitly and
  python-compatibility.yml installs a bare `.`, so it is five.

tests/test_tier_guard.py + test_pyshark_engine.py: 100 passed, 519
subtests.

A third, independent cross-review confirmed the mechanism survives
(six mutations all turn it red, including doctoring the real workflow
and emptying MUTUALLY_EXCLUSIVE_IMPORTS together, which the swap
tests' own literal install-line strings still catch) and found four
smaller things:

- tests/test_tier_guard.py:1615: a deleted blank line before
  `class DependencyGateFalsifiabilityTests` (PEP 8 E302; `tests/` is
  not linted by `make pylint`, so nothing else would have caught it).
  Restored.
- Four stale figures in the PR description, all from revision 1
  (`HAS_PYSHARK`'s count moved from 3 to 4 once the tshark regression
  test above became its own third in-file gated method; "both/two
  touched Python files" is now three; the `tests/test_tier_guard.py`
  test count needed to be split from `test_pyshark_engine.py`'s
  rather than presented as one number covering both). Corrected in
  the description directly.
- MUTUALLY_EXCLUSIVE_IMPORTS was the one new hand-written table with
  no liveness check -- omitting a future contested name would not be
  caught, only a wrong entry among existing ones. Added
  contested_imports(): counts, per MODULE_PROVIDERS entry, how many
  alternatives some declared extra actually resolves (1 for
  html5lib -- bare html5lib is declared by zero extras -- 2 for pcap),
  and a name qualifies at 2+. A new test asserts it equals
  MUTUALLY_EXCLUSIVE_IMPORTS; a falsifiability test doctors in a
  module with two live alternatives and confirms the comparison
  disagrees when it is not added.
- Two latent gaps in ambiguous_satisfactions(): an excluded module
  with no MODULE_PROVIDERS entry raised a bare, untested KeyError
  (test_every_gated_flag_is_classified now loops over excluded
  modules too, giving them the same deliberate contract required ones
  already have); and disqualified was computed before the
  MUTUALLY_EXCLUSIVE_IMPORTS scope check rather than after, so an
  unrelated flag's negated probe on an unmapped module could crash
  the whole function instead of being scoped out of it -- moved the
  computation inside the scope-checked branch. Extracted
  _disqualified_providers() to also close the related, lower-priority
  gap: falling back to a contested top-level's full entry for an
  excluded dotted path with no exact entry of its own would
  over-disqualify and misdiagnose a real satisfaction as ambiguous;
  it now fails loudly instead, naming the missing entry.
- test_no_provider_mapping_or_exclusion_is_vestigial's docstring
  claimed the two tables are "exactly as wide as the suite needs them
  to be" without qualification; its own `needed` computation is
  self-referential for dotted keys, so deleting `pcap._pcap` moves
  both sides of the comparison together and goes uncaught by this
  test specifically (five others still pin it). Narrowed the
  docstring to say so.

tests/test_tier_guard.py + test_pyshark_engine.py: 105 passed, 519
subtests.

Fixes #762.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

toolkit: pypcapfile adapter reads dotted-decimal addresses as packed bytes, so 7 of 10 gated tests fail when enabled

1 participant