Skip to content

fix(engines): un-hexlify pypcapfile frames before decoding them - #748

Merged
JarryShaw merged 1 commit into
mainfrom
fix/746-pypcapfile-hexlified-frames
Sep 24, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/746-pypcapfile-hexlified-frames

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What is the purpose of your pull request?

  • fix — corrects a defect

Description

Fixes #746. _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 header with struct.unpack — every
field came out garbage without raising. Fix: binascii.unhexlify(packet.packet) before the decoder
call. Chose this over layers>0 at load time, which would decode eagerly inside the lazy generator
and lose _decode()'s per-frame AttributeWarning fallback. _get_decoder's docstring also said
"left as raw bytes" with no decoder; fixed to "left hexlified". Link-layer dispatch is unaffected —
it resolves from the header's ll_type, never frame bytes.

Merge order — do not land before #747 (review: good-to-go, 67c7592e5). Verified on
examples/captures/test.pcap: plain extraction is fine (34/34 frames) both before and after this
PR, but standalone this PR turns a non-crashing extraction into a crashing one —
tcp=True, reassembly=True now raises AddressValueError, ipv4=True, reassembly=True raises
struct.error; both silently returned None on main today. Extractor.run() catches only
EOFError/StopIteration/KeyboardInterrupt, so both escape to the caller. #747 fixes the
toolkit-side cause; combined, both paths decode correctly.

Parity tests (test_new_engine_parity_runtime.py, off-limits, not edited): with this PR alone
on current main, test_pypcapfile_agrees_with_the_default_engine now passes
(1 passed, 4 subtests passed) — #749 (merged) fixed its unrelated mac() double-encoding bug;
#747 plays no part. The other two gated methods (ipv4_reassembly…, traces_only…) still need
#747 — green only with its toolkit fix overlaid.

CI note: none of this runs in CI today. unit-tests.yml is the only workflow that runs pytest,
and its installs (.[test,DPKT], .[test,Scapy,DPKT]) omit PyPCAPFile; .[all] does include it
but only lint/docs/cron/release jobs use .[all], none of which run the suite.

Tests (Python 3.10.21, pypcapfile 0.12.0, throwaway venv): test_pypcapfile_engine.py 22/22 —
added a decoder-input assertion and an unhexlify-failure fallback test, both confirmed to fail
without this change. Coverage on the engine file: 94% (108 stmts, unchanged; only __init__'s real
imports uncovered).

@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…dresses

- mac() in test_new_engine_parity_runtime.py treated pypcapfile's
  Ethernet.dst/.src as raw 6-byte values needing hex-formatting, but
  pypcapfile already returns them as colon-separated ASCII (built via
  b':'.join(...) in its own Ethernet.__init__). Re-encoding through mac()
  produced double-hex-encoded garbage.
- Fixed only the pypcapfile call site to decode the bytes directly. mac()
  itself, and its other call site against pypcap's genuinely raw 6-byte
  slices, are correct as-is and are left untouched.

Note: test_pypcapfile_agrees_with_the_default_engine still fails on
3.10/3.11 after this change, because of the separate, already-tracked
#746 engine defect (unfixed here, PR #748 open) that feeds hex-ASCII text
into pypcapfile's Ethernet() decoder instead of raw bytes -- confirmed by
simulating #746's fix locally, which turns this test fully green on top
of this change. So this test needs #749 and #746 together, contradicting
the "needs: this issue" classification in #749 itself.

Fixes #749.
JarryShaw added a commit that referenced this pull request 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

Copy link
Copy Markdown
Owner Author

❌ NEEDS CHANGES @ 3c5c77fea

Independent cross-review on opus (author was sonnet). Python 3.10.21, pypcapfile 0.12.0, throwaway venv, tree pinned (pcapkit.__file__ printed each run). The code change is correct and minimal — every ask below is the body, a stale docstring, or a missing test.

# claim verdict evidence I obtained
1 unhexlify safe, per-frame AttributeWarning fallback kept ✅ binascii.Error.__mro__ → ValueError, and the call is inside the existing try. Real _decode + real Ethernet on b'abc', b'zz…', b'', short-hex: 4/4 → AttributeWarning "decoding failed", 0 escaped exceptions
2 packet.packet always hexlified here ✅ _read_a_packet: if layers > 0 … else: hexlify(...); the engine's only load_savefile hardcodes layers=0. LAYERS-1 is passed to _declf, never to the load — no path reaches :375 with raw bytes
3 dispatch from header ll_type, not frame bytes ✅ pypcapfile.py:239 → _get_decoder(sfile.header.ll_type) → clookup()
4 "not breaking: nothing works today" ❌ see below — this is the blocker
5 new test fails without the fix ✅ 2 tests fail, not 1: …unhexlifies_the_savefile_bytes… (Expected: mock(b'payload', layers=1) / Actual: mock(b'7061796c6f6164', layers=1)) and …decodes_to_layers_depth… (b'7061796c6f6164' != b'payload'). With the fix 21 passed; pytest and unittest agree both ways
6 parity test "still fails even combined with #747" ❌ stale On main (with 9813aa377) + #748 alone it now PASSES — pytest PASSED + unittest OK, 4 subtests. No EXPECTED_FAILURES and no expectedFailure in that module, so the pass is genuine. #749 fixed the mac() helper; #747 plays no part

Claim 4 — why this is NEEDS CHANGES. On a real Ethernet/IPv4/TCP capture, #748 without #747:

main today + #748 alone
packet2dict OK, returns a (garbage) dict raises AddressValueError: b'10.1.1.2' (len 8 != 4)
ipv4_reassembly returns None raises struct.error: required argument is not an integer (toolkit/pypcapfile.py:104)
tcp_reassembly returns None raises AddressValueError

Extractor.run() catches only EOFError/StopIteration/KeyboardInterrupt (extraction.py:712,722), so these propagate to the caller. "Cannot regress a working caller" is true only for a working one: this turns a non-crashing extraction into a crashing one. And no CI job installs the PyPCAPFile extra (.[test,DPKT], .[test,Scapy,DPKT]), so nothing would catch it — the 2 sibling parity methods go FAIL → ERROR silently. With #747 also applied I confirmed everything is correct (tcp_reassembly → real HELLO-PAYLOAD, len 13). So: right fix, wrong merge order.

Asks — 1. Record the #747 merge dependency and drop "cannot regress a working caller"; #748 must not land first. 2. Refresh the stale parity paragraph per claim 6. 3. _get_decoder's docstring (:326-327) still says frames are "left as raw bytes" — the exact misstatement this PR exists to correct, 15 lines above the change, and contradicted by your own amended test_read_frame_leaves_the_frame_alone_with_no_decoder (now asserts HEXLIFIED_FRAME). 4. No test makes unhexlify itself raise — the existing fallback test mocks the decoder — so hoisting it out of the try in a later refactor would silently turn the per-frame warning into an uncaught binascii.Error.

Nit, non-blocking: the warning reads "decoding failed" for what is now an un-hexlifying failure. Coverage after: 108 stmts / 94%, only 165-175 uncovered — the new line is covered.

❌ NEEDS CHANGES @ 3c5c77fea

@JarryShaw

Copy link
Copy Markdown
Owner Author

❌ NEEDS CHANGES @ 3c5c77fea — code is correct, but #748 must not merge before #747 (it turns three non-crashing toolkit paths into uncaught AddressValueError/struct.error), and the body is stale on the parity test (it now passes).

@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
@JarryShaw

Copy link
Copy Markdown
Owner Author

Precision correction to my own review above: .[all] does include pypcapfile; python_version < '3.12', so I should not have written "no CI job installs the PyPCAPFile extra". The accurate form — and the point stands unchanged — is that unit-tests.yml is the only workflow that runs pytest (verified: it is the sole workflow matching pytest/make test), and it installs .[test,DPKT] / .[test,Scapy,DPKT]. The .[all] jobs (lint, docs, cron, release) do install pypcapfile but never run the suite. So these tests still execute nowhere in CI, and the FAIL → ERROR transition would go unnoticed.

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 force-pushed the fix/746-pypcapfile-hexlified-frames branch from 3c5c77f to 2af634c Compare September 24, 2026 17:05
@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 four asks at 2af634caf, independently re-verified each claim first (all checked out):

# ask done
1 Drop "cannot regress a working caller"; record #747 merge-order dependency Reproduced the crash myself on test.pcap: tcp=True,reassembly=True → AddressValueError, ipv4=True,reassembly=True → struct.error, both silent None on main today. Body now says: do not land before #747.
2 Refresh the stale parity paragraph Confirmed test_pypcapfile_agrees_with_the_default_engine now passes with this PR alone on current main (1 passed, 4 subtests passed) — #749 fixed it, #747 uninvolved. Body rewritten; the other two gated methods still correctly attributed to #747.
3 _get_decoder docstring still said "left as raw bytes" Fixed to "left hexlified".
4 No test makes unhexlify itself raise Added test_read_frame_warns_and_falls_back_when_unhexlifying_fails — non-hex and odd-length payloads, asserts the decoder is never called and the per-frame AttributeWarning fires. Confirmed to fail (decoder was called) against the pre-#746 code.

Nit (non-blocking): left the warning text as "decoding failed" — the {error!r} in the message already discloses whether it was binascii.Error vs a decoder exception, so a wording split didn't seem worth it, but happy to change if you disagree.

Also folded in the CI-blind-spot correction (unit-tests.yml never installs PyPCAPFile, so this doesn't run in CI either way) and the #747 head/label update.

Tests (Python 3.10.21, pypcapfile 0.12.0, throwaway venv, pcapkit.__file__ pinned to this worktree): test_pypcapfile_engine.py 21/21 → 22/22 (net +1 test). Coverage on the engine file: 94% before and after this round (108 stmts, unchanged; only __init__'s real imports uncovered).

Nothing in the review or the coordinator's message turned out to be wrong on my independent check — all six claims plus the self-correction held up.

Flipped review: needs-changes → review: pending (exact-path DELETE, not the bare collection endpoint).

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 2af634caf — supersedes my ❌ NEEDS CHANGES at 3c5c77fea. It must still land after #747 (67c7592e5, review: good-to-go); merging this one first re-introduces the crash in ask 1 below.

Re-review on opus (author sonnet). Python 3.10.21, pypcapfile 0.12.0, throwaway venv, pcapkit.__file__ prefix asserted on every run. The engine diff vs 3c5c77fea is the docstring only, so my ✅ on claims 1/2/3/5 carries over unchanged.

ask verdict evidence I obtained
1 — merge-order constraint ✅ met cannot regress → 0 grep hits; body now carries a bold imperative "do not land before #747", not a note. Reproduced your test.pcap figures exactly — fix on: plain 34/34, tcp+reasm → AddressValueError: b'10.20.30.131' (len 12 != 4), ipv4+reasm → struct.error: required argument is not an integer; fix off: all three OK, 34/34
2 — parity paragraph ✅ met, not overstated States the method passes standalone and that ipv4_reassembly… / traces_only… still need #747. Matches my own 4-tree differential run
3 — _get_decoder docstring ✅ met "left hexlified -- see :meth:run" is accurate: with _declf is None, _decode returns packet whose .packet is hexlify(frame). One inaccuracy removed, none traded in
4 — unhexlify-failure test ✅ met decisively I mutation-tested the actual property: hoisting unhexlify out of the try makes it fail — binascii.Error: Odd-length string at pypcapfile.py:369, pytest SUBFAILED(bad=b'not-hex-at-all') + SUBFAILED(bad=b'abc'), unittest FAILED (errors=2). So it is not merely a decoder-mock assertion; it genuinely drives binascii.Error out of unhexlify and pins the try boundary
nit declined ✅ reasoning holds {error!r} renders Error('Odd-length string') vs struct's error('unpack requires…') — distinguishable. Caveat: it is the bare Error(...), not binascii.Error(...), so the disambiguation rests on the message rather than the class name

Counts. 22 methods derived from source (19 + 3) = measured. Pristine: 22 passed, 14 subtests passed (pytest) / Ran 22 tests … OK (unittest). Against the pre-#746 engine, 3 methods fail, not the "both" the body claims — pytest 4 failed / unittest failures=4 (2 methods + 2 subtests): …unhexlifies_the_savefile_bytes…, …decodes_to_layers_depth…, …when_unhexlifying_fails. Understated, not overstated, so not blocking. Coverage after: 108 stmts / 9 miss / 94%, only 165-175 (__init__'s real imports) uncovered — matches your claim.

Still unverified, as last time: the "before" coverage figure. coverage report renders statement counts against the on-disk file, so a control run taken after restoring the tree is meaningless. Unverified rather than disputed.

CI at time of writing: 21 SUCCESS / 3 SKIPPED / 0 failures, nothing pending. Worth restating that none of it exercises this fix — no pytest job installs PyPCAPFile, so the local runs above remain the only evidence.

✅ GOOD TO MERGE @ 2af634caf — after #747.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 2af634caf (supersedes my ❌ at 3c5c77fea) — all 4 asks met, ask 4 confirmed by mutation test; must still land after #747.

@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 added a commit that referenced this pull request Sep 24, 2026
… bytes (#747)

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 merged commit 074c53e into main Sep 24, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the fix/746-pypcapfile-hexlified-frames branch September 24, 2026 17:46
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
…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.

engines(pypcapfile): _decode() feeds hexlified frame bytes straight to pypcapfile's decoders

1 participant