Skip to content

tests: round-trip 258 option codes from the registries, recording the 86 that cannot close the cycle - #440

Merged
JarryShaw merged 8 commits into
mainfrom
test/option-coverage-samples
Sep 17, 2026
Merged

JarryShaw merged 8 commits into
mainfrom
test/option-coverage-samples

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Closes the Test Cases ask on the Help Wanted page, which says what remains wanted is "coverage rather than infrastructure: the protocols and options the suite does not touch".

This adds that coverage for the option/chunk/parameter/block space, in both directions, and the result is the point of the PR: of 258 registry codes exercised, 172 close a construct → parse → construct cycle and 86 do not. Every one of the 86 is recorded in tests/protocols/test_option_roundtrip_unit.py against the file:line that stops it, and asserted to still fail in the recorded way — so fixing one turns the tier red rather than leaving a stale entry behind.

pcapkit/ is untouched: git diff origin/main -- pcapkit/ is empty. This is tests and generators only.

How the codes are enumerated

examples/generators/options.py reads the registries themselves rather than a hand-written list: TCP options 22, MPTCP subtypes 8, IPv4 options 14, HOPOPT 15, IPv6-Opts 15, IPv6-Route 3, SCTP chunks 13 / parameters 8 / causes 13, MH messages 14 / options 20 / extensions 1, HIP parameters 49, HTTP/2 frames 10, PCAP-NG blocks 11 / options 40 / records 3 / secrets 4.

Registries are only ever iterated, which mutates nothing, and single-code resolution goes through ProtocolBase._lookup_registry; a test asserts registry sizes are unchanged after reading. IPv4 and HIP have no registry yet, so those enumerate enum × handler presence, and prefer a real __option__/__parameter__ if one appears — so #434 landing is a no-op here.

166 cases are written into five options-*.pcap fixtures via make_samples.py, regenerating byte-identically across three passes. Fixtures deliberately carry only cases whose parse terminates — a capture containing the SMF_DPD non-progress frame would wedge every reader of it.

The seven known construction defects: all reached, and two are worse than recorded

Each was expected and is asserted, not fixed:

  1. ipv4.py data= where the field is ts_data — the call opens at :1480 and the bad keyword is at :1488. Schema.__init__ only warns, so the value is dropped and post_process then iterates a ListField.
  2. _make_mptcp_* — all 8 subtypes fail, from three distinct causes: KeyError: 'length' (schema/transport/tcp.py:658/790/807/827), no attribute 'kind' (tcp.py:668, three subtypes), and _make_mptcp_join reading self._flags, which exists only while parsing (tcp.py:2675).
  3. hip.py:3445cipher= is not a field of EncryptedParameter, so the cipher id is silently dropped.
  4. CGAParameter.extensions KeyError: 'length' (schema/internet/mh.py:516) — it needs pkt['__packet__']['length'] on the unpack path. The MPTCPUnknown half of that item is the same KeyError at the four tcp.py lines above.
  5. The Quick-Start SchemaField(length=5) is silent corruption, not a failure. A well-formed 8-octet option 1908002adeadbee0 parses "successfully" with SchemaWarning: packet length < 0: -3, decoding nonce as 55 instead of 933982136, and the three unconsumed octets are then read as a fabricated extra option, making the enclosing packet unparseable. Verified independently: parsing QuickStartRequestOption directly with all 8 octets gives the correct nonce=933982136 with no warning, so the schema is right and the selector's hardcoded length is the defect. Identical at hopopt.py:224 and ipv6_opts.py:224. Caveat: the QS cases never reach it, because they fail earlier at CONSTRUCT on a separate defect — func is set only by post_process — so fixing length=5 alone will not make them pass.
  6. _MPTCP.test's 'length': (1, 8) — window 1e0c01 decodes length=60; with the intended (8, 8) it decodes 12. It feeds SchemaField(length=pkt['test']['length']), so it asks for 60 octets of a 12-octet option, and a genuine MP_CAPABLE option cannot be parsed at all.
  7. Negative _option_padding — mechanism verified (while length > 0, then length -= len(data), then _option_padding = length, reaching struct.calcsize('-1s')error: bad char in struct format) but not reachable from the existing corpus: an instrumented sweep of all 15 captures saw only {0: 2651, 1: 3, 52: 1}. It is latent. This is the same family as A wire-derived length underflows into a struct format string, raising bare struct.error on untrusted input #438. The related non-progress hang is reached — two SMF_DPD cases TIMEOUT, which is why the sweep is deadlined.

Larger finds beyond that list

  • PCAP-NG __post_init__ packs and re-parses on one instance sharing self._opt, making 27 of 40 block options unconstructible.
  • HIP cannot be parsed without extension=Truealias reads _info before it is assigned — and its len = total // 8 + 4 cannot represent a single parameter, so the harness uses two copies and pins the single-parameter defect in its own test.
  • 16 HIP parameters return tuples where _make_* needs lists — reconstruct-only failures, exactly the class the third leg of the cycle exists to catch.
  • 6 of 10 HTTP/2 frames raise KeyError: 'flags', and PRIORITY checks length != 9 for a frame make always builds as 14.

Scope deliberately narrowed

PCAP-NG writes no capture. All 58 codes are exercised and recorded, but the construction API cannot produce a reproducible file: _make_block_shb takes byte order from sys.byteorder, and the TLS/WireGuard key-log writers stamp datetime.now() with no override. examples/generators/pcapng.py already covers that space by hand-rolling octets, which is presumably why.

Verification

Suite at the current head 0e6a9e15c: 926 passed / 17 skipped / 1268 subtests (567 s) on 3.14.7, zero failures. That is 943 collected, confirmed by counting collection directly.

This figure was 886 / 17 when the PR was opened and is corrected here rather than silently: 886 + 17 = 903 collected, which matches no sha now on the branch, because two pass-through merges of main have landed since. Collection is 926 at both d7240dc0c and 4535f72f2 and 943 at 651665214 onwards, the +17 being #436's tests arriving — tests/protocols/test_dispatch_bindings_unit.py 0 → 11 methods and tests/protocols/link/test_link_unit.py 19 → 25. None of it is this PR's work.

Two things about baselines worth keeping, since both have caught agents on this repo. First, git archive origin/main measures 859 passed / 35 skipped, but 18 of those skips are an artifact of the archive not being a git tree — all tests/test_tier_guard.py, "git could not be run" — and they pass in a real checkout, so use git clone. Second, CI's own numbers are a different scope again and are not comparable to a local run: Unit Tests runs pytest -q --ignore=tests/integration and never regenerates the sample captures, while Integration runs the full suite after a deliberately partial regeneration on runners lacking the optional engines (pcap, pcapfile).

All 19 pre-existing capture files byte-identical to an untouched origin/main tree, and all 30 tree/json dumps under ip=True, tcp=True, reassembly=True byte-identical. mypy 9 errors, all pre-existing classes — 5 in _support.py identical to main, 4 scapy.all stub errors of the class pcap.py already has 15 of. pylint 10.00/10. Tree under test proven on every run via pcapkit.__file__ under PYTHONSAFEPATH=1.

One coupling to know about before merging

tests/_support.py on main has no time_limit; it lives on the unmerged #432. It is vendored here byte-identically to that branch, so whichever lands first, the second merges cleanly. If #432's version changes before merge, this copy must be re-synced.

…d option code

The sample corpus covered the parse path for common protocols and almost none of
the option, chunk, parameter and block space -- and none of the *construction*
side of it, which is why defects there keep being found by hand.

- Add `examples/generators/options.py`. It enumerates 258 codes from the dispatch
  registries themselves (TCP, MPTCP, IPv4, HOPOPT, IPv6-Opts, IPv6-Route, SCTP
  chunks/parameters/causes, MH messages/options/extensions, HIP parameters,
  HTTP/2 frames, PCAP-NG blocks/options/records/secrets), then for each one
  constructs it, parses it back, constructs it again from what was parsed, and
  compares the octets. Registries are iterated, never subscripted, so no lookup
  grows a shared `defaultdict`; `handler()` goes through `_lookup_registry`.
- Write the 166 cases whose parse terminates into five `options-*.pcap` fixtures,
  wired into `make_samples.py`. They regenerate byte-identically.
- Add `tests/protocols/test_option_roundtrip_unit.py`: 172 of 258 cases close the
  cycle; the other 86 are recorded case by case against the `file:line` that
  stops each, and are asserted to still fail in the recorded way so a fix turns
  the tier red rather than leaving a stale entry.
- Add `tests/protocols/test_option_coverage_runtime.py`, reading the fixtures
  through the extraction interface and both dumpers.
- Vendor `time_limit` into `tests/_support.py`, byte-identical to #431's copy;
  two HOPOPT/IPv6-Opts cases hang rather than fail, so the sweep is deadlined.

No library file is touched: the 86 failures are reported, not fixed.

886 passed, 17 skipped (877 before, adjusting for 18 tier-guard tests that only
skip in a non-git tree). Every pre-existing capture, and its `tree` and `json`
dump under `ip=True, tcp=True, reassembly=True`, is byte-identical to origin/main.
…erpreter, not the code

Three things, all consequences of #432, #434 and #439 landing under the branch.

- `INTERPRETER_GAPS`: seven PCAP-NG name-resolution cases round-trip from Python
  3.11 on and fail to construct on 3.10, so one recorded outcome per case no
  longer suffices. The root cause is #439 in the *schema* hierarchy:
  `NameResolutionBlock.post_process` asks
  `isinstance(record, (IPv4Record, IPv6Record))` at
  `pcapkit/protocols/schema/misc/pcapng.py:1248`, every `Schema` subclass shares
  one `_abc_impl` on <= 3.10 (measured: `EndRecord._abc_impl is
  IPv4Record._abc_impl` is True on 3.10.20, False on 3.14.7), and the block's
  terminating `EndRecord` therefore tests True and has `.names` read off it.
  Measured in the real path, not inferred. The `Info` data models are unaffected
  on both interpreters, and that boundary is asserted too.
  The table overrides rather than sits beside `EXPECTED_FAILURES`, because the
  three `ns_dns*` options fail on every interpreter but for different reasons.
- `test_schema_isinstance_is_interpreter_dependent` pins that mechanism, so the
  seven are excused by evidence about a named library bug rather than by a
  version comparison. On >= 3.11 they are still held to `OK`; fixing #439 turns
  3.10 red.
- The two SMF_DPD entries: `hopopt-option/SMF_DPD` now round-trips and its entry
  is deleted, since #429's over-read fix landed with #432's progress guard.
  `ipv6-opts-option/SMF_DPD` raises instead of hanging and is re-recorded as
  `PARSE`. The two schema modules are line-for-line duplicates, so that is one
  fix applied once where it was needed twice. The sweep keeps its deadline: it
  guards the next non-progress defect, not this one.

Also: #434 gave IPv4 and HIP real registries, so the generator now reads
`HIP.__parameter__` instead of falling back to enum-crossed-with-handler.

No library file is touched.

3.14.7: 258 cases, 173 round-trip. 3.10.20: 258 cases, 169 round-trip -- the
difference is exactly the four cases in `INTERPRETER_GAPS` that pass on 3.14.
The entry claimed #432's fix "was applied once where it was needed twice". That
was wrong, and it was an inference from the behaviour rather than something
measured: #432 touched both schema modules symmetrically, 26 lines each, changing
`'len': (1, 8)` to `(8, 8)` and adding the `+ 2` to the selector's `SchemaField`
in each. Neither file still carries `(1, 8)`.

The real cause is one line, older than #432. Normalising the two modules for
their protocol names leaves exactly one structural difference:
`ipv6_opts.SMFIdentificationBasedDPDOption` declares a second, redundant `test`
`ForwardMatchField` at :434 that `hopopt`'s equivalent does not. The enclosing
`_SMFDPDOption` already has one in both modules, and it is that outer field the
selector reads -- nothing reads the nested copy, and it is the only field in the
class with no `#:` comment. A `ForwardMatchField` consumes nothing but still
occupies a slot in `__buffer__`, so the nested schema over-reports its size by an
octet and `OptionField` mis-counts the option area.

Measured on the same octets, `1100080100010100`, identically on 3.10.20 and
3.14.7 -- so this one is not interpreter-dependent:

    hopopt    __fields__ = [type, len, info, tid, id]        len(schema) = 3
    ipv6_opts __fields__ = [type, len, test, info, tid, id]  len(schema) = 4

    HOPOPT(...)    -> options=[SMF_DPD, PadN]
    IPv6_Opts(...) -> ProtocolError: IPv6-Opts: invalid format
                      at pcapkit/protocols/internet/ipv6_opts.py:497

Comment and `Gap.defect` text only; the recorded status and fragment are
unchanged, and no library file is touched.
Comment thread tests/protocols/test_option_roundtrip_unit.py Outdated
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review of PR #440 at head d7240dc0c21a1c618a6c88741ba00325e12fa5c9

Copilot is out of tokens, so I reviewed this by hand as an adversarial pass over the recorded expectations, since the PR itself changes no library code (git diff origin/main pr-440 -- pcapkit/ is empty, confirmed) and its entire value is in whether tests/protocols/test_option_roundtrip_unit.py and examples/generators/options.py say true things.

CI

gh pr view 440 --json statusCheckRollup at the time of this review: every check green, including Python 3.10 and Integration Python 3.10 (the two that were red for hours per the task brief). Gate (full suite, Python 3.14) and Docs test gate show SKIPPED, which is a gating condition, not a failure. git rev-list --count HEAD..origin/main is 0, so this repo checkout is at main tip -- no stale-checkout risk.

What I ran, and against what

Cloned fresh (git clone + git fetch origin pr-440:pr-440 + git checkout pr-440) into /tmp/pr440review/pr440-tree, HEAD confirmed d7240dc0c21a1c618a6c88741ba00325e12fa5c9. A second clean clone of main (c28ffc287...) went to /tmp/pr440review/main-tree as a baseline. Two interpreters, both with PYTHONSAFEPATH=1, PYTHONPATH pointed at the tree under test, and pcapkit.__file__ printed and confirmed resolving inside that tree before trusting any result:

  • /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python -- 3.14.7, has scapy 2.7.0.
  • /tmp/mh310-a3a0ca27/venv/bin/python -- 3.10.20, no scapy (re-pointed at the PR tree via PYTHONPATH; verified it wasn't silently resolving pcapkit from the venv's own main-checkout install).

Ran:

  • python -m unittest tests.protocols.test_option_roundtrip_unit -v on both interpreters -- 7/7 pass on 3.14.7, 7/7 pass on 3.10.20.
  • Loaded examples/generators/options.py directly and cross-checked every one of the 258 cases' roundtrip() outcome against _gap_for() on both interpreters: 258 total, 173 OK / 85 recorded-gap / 0 mismatches on 3.14.7; 169 OK / 89 recorded-gap / 0 mismatches on 3.10.20 (SCHEMA_ABC_IS_PER_CLASS=False there, as expected). Matches the PR's claimed 173/169 exactly.
  • examples/generators/make_samples.py (3.14.7, which has scapy) to actually generate the five options-*.pcap fixtures, then python -m unittest tests.protocols.test_option_coverage_runtime -v on both interpreters against the generated captures -- 3/3 pass on each.
  • Determinism: regenerated the five captures twice more (a fresh process, and again under PYTHONHASHSEED=12345) -- byte-identical to the first run in both cases (cmp clean on all five).
  • Cross-contamination check on the "PCAP-NG writes no fixture" reasoning: generated captures from the main baseline (generators pcap, pcapng, legacy only) and from the PR tree (which adds options as a fourth generator, run last), and compared the 13 pre-existing generated files -- all 13 byte-identical between main and the PR. So adding the registry-driven sweep after the others has no observable side effect on their output, which is also indirect evidence the registries aren't being mutated by enumeration.
  • Counted pcapng-block (11) + pcapng-option (40) + pcapng-record (3) + pcapng-secrets (4) cases = 58, matching the "58 codes exercised without a fixture" claim exactly; and the 27-entry pcapng-option dict-comprehension in EXPECTED_FAILURES (14 if_* + 4 epb_* + 3 ns_* + 5 isb_* + 1 pack_*) matches "27 of 40 block options unconstructible" from the shared-self._opt cause.
  • Spot-verified ~15 file:line citations in EXPECTED_FAILURES/INTERPRETER_GAPS directly against git show origin/main:<path>, including the two the task flagged as previously mis-attributed: hopopt.py and ipv6_opts.py both show 'len': (8, 8) (not (1, 8)) at their _SMFDPDOption definitions, confirming corekit: stop the option and list loops spinning forever on a truncated area (#431) #432 landed symmetrically; and ipv6_opts.py:434 does carry the extra test: 'SMFDPDTestFlag' = ForwardMatchField(...) field that hopopt.py's equivalent class lacks, confirming the ipv6_opts' SMFIdentificationBasedDPDOption has a stray test field HOPOPT lacks, so identical octets fail there #441 re-attribution in this branch is the correct one (not the "missing corekit: stop the option and list loops spinning forever on a truncated area (#431) #432 fix" explanation the task says was wrong). Also confirmed IPv4.__option__ (ipv4.py:196) and HIP.__parameter__ (hip.py:377, not __param__) are real registries post-protocols: migrate IPv4 option and HIP parameter dispatch to the registry pattern (#429) #434, pcapng.py:1248's isinstance(record, (IPv4Record, IPv6Record)) matches the Schema subclasses share one ABCMeta cache on Python <=3.10, so isinstance/issubclass return whichever answer was asked first #439 attribution verbatim, and ProtocolBase._lookup_registry (protocol.py:1242) reads via if code in registry before ever subscripting, so enumeration genuinely cannot grow a registry.

Finding

One inline comment posted (below), on tests/protocols/test_option_roundtrip_unit.py line 276:

fragment='invalid format' is too generic for at least 6 of the ~85 EXPECTED_FAILURES entries (tcp-option/User_Timeout_Option, ipv6-route-type/Source_Route, ipv6-route-type/Type_2_Routing_Header, hip-parameter/HOST_ID, httpv2-frame/PRIORITY, ipv6-opts-option/SMF_DPD). Each of these sits in a source file that raises dozens of textually-different-but-substring-identical "...invalid format" ProtocolErrors from other, unrelated checks (measured: 26 sites in tcp.py, 31 in hip.py, 3 in ipv6_route.py, 12 in httpv2.py, 5 in ipv6_opts.py). assertIn(fragment, outcome.detail) can't distinguish the recorded defect from a different one at a nearby line in the same file, which is exactly the failure mode test_round_trip_is_identity_or_a_recorded_gap's own docstring says the two-directional check exists to prevent. Demonstrated with actual captured outcome.detail strings for all six cases in the comment. Not a functional defect in the library, and not something I'd block merge on -- it's a specificity gap in the test's own guard-rail, worth a tightening follow-up.

What I did not cover

  • Did not exhaustively re-derive all ~85 EXPECTED_FAILURES/INTERPRETER_GAPS entries' root causes from scratch -- spot-checked roughly 15-20 of the more load-bearing ones (the two re-attributed SMF_DPD entries, the Schema subclasses share one ABCMeta cache on Python <=3.10, so isinstance/issubclass return whichever answer was asked first #439 isinstance mechanism, the protocols: migrate IPv4 option and HIP parameter dispatch to the registry pattern (#429) #434 registries, the pcapng packet_data / self._opt / _isb_interface_id claims, the IPv6 Quick-Start SchemaField(length=5) claim) plus the six flagged above for fragment looseness; did not check all 27 pcapng-option/self._opt entries' individual line numbers or the 16 HIP tuple-vs-list entries' individual attributions line-by-line.
  • Did not independently reproduce the full CI matrix (909 passed/17 skipped on 3.14, 838 passed/88 skipped on 3.10) or the "30 tree/json dumps byte-identical" claim -- ran the two new test modules specifically (10/10 passing on both interpreters) rather than the whole suite, and trusted the now-green CI rollup for the rest.
  • Did not audit pylint output or anything style-level, per the review brief.

Verdict

Good to merge. The enumeration is genuinely registry-driven (confirmed non-mutating, confirmed IPv4/HIP now use real registries post-#434), the determinism story for the five new fixtures holds under repeated regeneration and a different hash seed, the numeric claims (258/173/169/85/58/27) all check out exactly against a live run on both interpreters, the two SMF_DPD re-attributions are correct, and CI is green including the previously-red 3.10 jobs. The one finding above is a minor test-quality gap in a handful of expected-failure fragments, not a defect in the library or in the round-trip mechanism itself.

The recorded fragment is what makes an EXPECTED_FAILURES entry
self-maintaining: it has to fail again *in the recorded way*, or the entry
gets revisited. Seven entries recorded only `invalid format` or `invalid
parameter`, and those are substrings of 205 and 3 messages respectively
across 13 modules -- 31 in internet/hip.py, 26 in transport/tcp.py -- so a
regression at any of them satisfied the check.

Each now carries the alias and whatever bracketed code the message prints,
measured from the real detail rather than guessed:

- tcp-option/User_Timeout_Option   TCP: [OptNo 28] invalid format
- ipv6-opts-option/SMF_DPD         IPv6-Opts: invalid format
- hip-parameter/HIP_TRANSFORM      HIPv2: [ParamNo 577] invalid parameter
- hip-parameter/HOST_ID            HIPv2: invalid format
- httpv2-frame/PRIORITY            HTTP/2: [Type 2] invalid format

Five of those now match exactly one raise site: the bracketed code picks out
the one handler that can print it, and the two bare-alias forms are the only
sites in their module that omit the bracket at all.

`fragment` also accepts a tuple, all of whose members must appear. That is
for the two ipv6-route entries, whose message interpolates a bare `type` in a
method that has no such parameter and so renders `[TypeNo <class 'type'>]`
(#442). Matching the literal rendering would bake that bug into the table and
turn it red when #442 is fixed, which says nothing about the round trip; the
stable parts either side still narrow it to the three `[TypeNo` lines of
ipv6_route.py. The assertion failure now also prints the detail it got.

Verified on 3.14.7 and 3.10.20: 7 passed, 299 subtests each. Negative
control -- deliberately wrong fragments in both the string and the tuple form
-- fails 3 subtests, so the assertion is reached rather than vacuous.
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review of 4535f72f2 at sha 651665214

Standing in for Copilot review. Per the dispatch instructions, the last verdict was given at d7240dc0c (good to merge, with one inline finding about over-broad EXPECTED_FAILURES fragments); the only new reviewable content since then is 4535f72f2 ("tests: pin the round-trip gaps to messages that identify their site"), plus a pass-through merge of main (PR #436, not reviewed here). This comment attacks that commit's claims directly.

Scope confirmed

  • Fetched refs/pull/440/head65166521476efdb5c3a4a5c484c899292bdbb486, matching the requested prefix 651665214.
  • origin/main = f50436a8a1f4cfd9e2f9dc96080fa9f4d8ca08a4, matching the stated base.
  • git rev-list --count pr440-review..origin/main = 0 — the branch is fully caught up with main.
  • 4535f72f2 --stat touches exactly one file: tests/protocols/test_option_roundtrip_unit.py (39 insertions, 14 deletions), as claimed.

CI status (checked, and waited for completion)

At dispatch time several checks were IN_PROGRESS (Python 3.10/3.13/3.15 unit tests, several Integration jobs). Re-polled gh pr view 440 --json statusCheckRollup until settled. Final state: every check is SUCCESS except two legitimately-conditional SKIPPED gates (Docs test gate, Gate (full suite, Python 3.14)) and deploy-pages/CodeQL which are also green. No red checks anywhere.

Claim 1 — the fragment counts (verified exact)

Ran git grep -c "invalid format" origin/main -- pcapkit/ and git grep -c "invalid parameter" origin/main -- pcapkit/:

httpv1.py 1, httpv2.py 12, hip.py 31, hopopt.py 19, ipv4.py 22, ipv6_opts.py 19,
ipv6_route.py 3, mh.py 26, pcapng.py 2, schema/hip.py 2, schema/ipv4.py 4,
sctp.py 38, tcp.py 26   → sum = 205, across 13 modules
"invalid parameter": hip.py 3, nowhere else

Both numbers (205 across 13 modules, 3 all in hip.py) match the commit message exactly, as do the per-module call-outs (hip.py:31, tcp.py:26).

Claim 2 — each fragment against the real outcome.detail (verified exact)

Checked out 651665214 into an isolated worktree and ran the harness directly (not through the test's assertions) to print the raw detail for all seven cases. Interpreter: /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python 3.14.7, PYTHONSAFEPATH=1, PYTHONPATH pointed at the checked-out tree, pcapkit.__file__ printed and asserted to be under that tree before trusting anything:

pcapkit.__file__ = .../agent-a4d4d937fcc41ff27/pcapkit/__init__.py
tcp-option/User_Timeout_Option        -> 'ProtocolError: TCP: [OptNo 28] invalid format'
ipv6-opts-option/SMF_DPD              -> 'ProtocolError: IPv6-Opts: invalid format'
hip-parameter/HIP_TRANSFORM           -> 'ProtocolError: HIPv2: [ParamNo 577] invalid parameter'
hip-parameter/HOST_ID                 -> 'ProtocolError: HIPv2: invalid format'
httpv2-frame/PRIORITY                 -> 'ProtocolError: HTTP/2: [Type 2] invalid format'
ipv6-route-type/Source_Route          -> "ProtocolError: IPv6-Route [TypeNo <class 'type'>]: invalid format"
ipv6-route-type/Type_2_Routing_Header -> "ProtocolError: IPv6-Route: [TypeNo <class 'type'>] invalid format"

Every recorded fragment/tuple is a genuine substring/subset of the real detail. No broken entry.

Spot-checked the "narrows to one site" sub-claims:

  • Enum_Option.User_Timeout_Option = 28 (pcapkit/const/tcp/option.py:106) against the 25 other [OptNo {schema.kind}] invalid format raises in tcp.py — the bracketed value is what narrows it, since the raise template itself is shared code across ~25 methods.
  • Enum_Parameter.HIP_TRANSFORM = 577 (pcapkit/const/hip/parameter.py:55) — matches.
  • Frame.PRIORITY = 0x02 (pcapkit/const/http/frame.py:28) against the site at httpv2.py:572, which matches the defect's cited line exactly.
  • git grep -n "invalid format" pcapkit/protocols/internet/ipv6_opts.py on main: line 497 (raise ProtocolError('IPv6-Opts: invalid format')) is the only one of 19 hits with no [OptNo ...] bracket — matches the claim.
  • Same for hip.py: line 761 (f'HIPv{version}: invalid format') is the only bracket-less raise that can render "HIPv2: invalid format" (the other bracket-less pair at 539/541 renders "HIP: invalid format", a different string that wouldn't satisfy this fragment) — matches.

Claim 3 — the assertion loop, and the empty-string form (verified)

fragments = ((gap.fragment,) if isinstance(gap.fragment, str) else gap.fragment)
for fragment in fragments:
    if not fragment:
        continue
    self.assertIn(fragment, outcome.detail, ...)
  • String form → wrapped in a 1-tuple, asserted once. Correct.
  • Tuple form → iterated as-is, every non-empty member asserted. Correct.
  • Empty string '' → wrapped as ('',), if not fragment: continue skips it, so only status is checked. Six existing entries (hip-parameter/ENCRYPTED, three pcapng-block/*, two pcapng-secrets/*) rely on exactly this path and all pass cleanly in the full run below — the empty case is not regressed.
  • test_expected_failures_name_real_cases compares (status, fragment) tuples between EXPECTED_FAILURES and INTERPRETER_GAPS for overlapping labels. INTERPRETER_GAPS only holds pcapng-* labels, which never intersect the seven touched here, so this is a non-issue in practice, but the comparison is plain tuple equality and works fine whether fragment is a str or a tuple[str, ...] regardless.

Claim 4 — the tuple form's reason to exist (verified against the actual issue)

Read GitHub issue #442 (open): it names exactly the three sites ipv6_route.py:462,506,546, confirms the bare {type} resolves to the builtin because none of the three enclosing methods bind a type parameter, and proposes the fix header.type (plus a colon-placement cleanup at :462 to match :506/:546's convention). Neither part of that proposed fix touches the substrings 'IPv6-Route', '[TypeNo', or 'invalid format' — only the interpolated value between [TypeNo and ], and the colon's position relative to the bracket. So the tuple fragment is provably stable across the fix #442 proposes, which is exactly the claim. Matching the literal rendering (<class 'type'>) instead would indeed have gone red the moment #442 lands, for a reason unrelated to the round-trip defect these entries actually pin (ipv6_route.py:276, unit-conversion bug).

Claim 5 — negative control (reproduced)

Copied the test file, changed the TCP entry's fragment to 'TCP: [OptNo 99] invalid format' and both ipv6-route-type tuples' last member to 'nonsense', ran it in place (then deleted the copy — tree confirmed clean after, git status = "nothing to commit"):

FAILED (failures=3)
  case='tcp-option/User_Timeout_Option'        -- 'TCP: [OptNo 99] ...' not found
  case='ipv6-route-type/Source_Route'          -- 'nonsense' not found
  case='ipv6-route-type/Type_2_Routing_Header' -- 'nonsense' not found

Exactly 3 subtest failures, matching the commit's negative-control claim — the assertion is reached, not vacuous.

The 7/299 figure (reproduced exactly, both interpreters)

Using a CountingResult that hooks addSubTest, ran the whole OptionRoundTripTests class:

Interpreter tree confirmed via pcapkit.__file__ tests run subtests failures
3.14.7 (.venv) under the checked-out worktree 7 299 0
3.10.20 (/tmp/mh310-a3a0ca27/venv, tree forced via sys.path[0]) under the checked-out worktree 7 299 0

Matches "7 passed / 299 subtests on both 3.14.7 and 3.10.20" exactly.

The "full suite 909 passed / 17 skipped" figure — could not reproduce; flagging, not blocking

This one doesn't check out. CI's own Unit Tests jobs (the make test selection, i.e. pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py') report 721 passed, 69 skipped, 1040 subtests passed, identically across Python 3.10–3.15. CI's Integration Python * jobs report 855 passed, 88 skipped, 1188 subtests passed, also identically across versions. I reproduced the 3.10 unit-job number exactly locally (721/69/1040, tree confirmed via pcapkit.__file__). My local 3.14.7 run came back as 782 passed/8 skipped/1082 subtests — 61 more passes than CI's own 3.14 job, which is a dev-venv-has-more-optional-deps artifact (RUNTIME_DEPS-gated tests running instead of skipping), not a code issue. None of these four numbers is 909/17, and I did not run make samples && pytest -q (the truly-unfiltered selection, needing generated fixtures) to check whether that configuration hits it — that's the one combination I didn't try. What I can say confidently: every configuration I ran was 100% clean, zero failures, zero errors, matching the green CI. The specific "909/17" figure in the commit message appears to be inaccurate or drawn from an environment/selection I couldn't reproduce; it doesn't affect the correctness of the diff itself.

Verdict

GOOD TO MERGE at sha 651665214. Commit 4535f72f2 does exactly what it claims for the seven EXPECTED_FAILURES entries: every new fragment was measured against the real outcome.detail (not guessed), the counts backing the "too generic" argument check out exactly against origin/main, the tuple form is implemented correctly for the string/tuple/empty-string cases and is genuinely robust to the fix issue #442 proposes, and the negative control is reproducibly non-vacuous. CI is fully green on this sha. The one soft spot is the commit message's "full suite 909 passed / 17 skipped" line, which I could not reproduce under CI's own selection or locally on either interpreter (real numbers: 721/69 unit, 855/88 integration, consistently, with zero failures throughout) — worth a correction in the message, but not a reason to hold the CR.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Thanks — the numbers do disagree, but the stale one is the PR body's, not the 909 / 17 figure. Settled by counting collection at each sha rather than comparing run summaries:

sha collected
d7240dc0c (previous verdict) 926
4535f72f2 (my fragment commit) 926
651665214 (current head, after the main merge) 943

So:

  • 909 passed / 17 skipped = 926, which is exactly the collection at 4535f72f2 — the tree I ran it on. That figure is right for that sha. My commit added no test methods, which is why d7240dc0c and 4535f72f2 collect identically.
  • The body's 886 passed / 17 skipped = 903, which matches no sha currently on this branch. It was measured before two rounds of main merges landed here, so it is simply out of date rather than wrong at the time.
  • 943 − 926 = 17 is precisely what the pass-through merge of main brought in: tests/protocols/test_dispatch_bindings_unit.py 0 → 11 test methods and tests/protocols/link/test_link_unit.py 19 → 25, i.e. +11 and +6. Those are protocols: bind FTP-DATA, HTTP-alt, OSPF, L2TP and the 802.1ad S-Tag, and fix four dispatch defects #436's tests, not this PR's.

On CI's own figures (721 / 69 for Unit Tests, 855 / 88 for Integration) — those are not comparable to a local pytest tests -q, because the two jobs run different scopes:

  • Unit Tests runs python -m pytest -q --ignore=tests/integration and does not regenerate the sample captures, so everything fixture-dependent skips. Hence the far larger skip count.
  • Integration runs python -m pytest -q after a "Regenerate sample captures" step, but that step is partial by design, and the runners lack the optional engine dependencies (pcap, pcapfile, and friends) that are installed here — so tests that pass locally skip there.

That difference is expected rather than a discrepancy: neither job is trying to reproduce a local full-suite number. What matters for the verdict is that every configuration came back with zero failures, which both of us observed.

I am updating the body's suite line to the current post-merge measurement so it stops misleading the next reader. Good catch on the mismatch even though the cause turned out to be elsewhere — the body had been carrying a figure from two merges ago and nobody had noticed.

@JarryShaw
JarryShaw merged commit a4c8d62 into main Sep 17, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the test/option-coverage-samples branch September 17, 2026 23:27
JarryShaw added a commit that referenced this pull request Sep 18, 2026
Picks up #440 (option round-trip harness) so its EXPECTED_FAILURES
table can drop the SMF_DPD entry this PR fixes.
JarryShaw added a commit that referenced this pull request Sep 18, 2026
…entry

#440's round-trip harness recorded this case as a PARSE gap; the fix in this
branch closes the round trip, so the entry is removed rather than left
stale. Kept the surrounding comment as history of both the #432 and #441
fixes, matching how the hopopt half of the same story was already handled.
JarryShaw added a commit that referenced this pull request Sep 18, 2026
…d the CGA extension carrier

PR #440's round-trip harness enumerates the registries, so completing
`MH.__option__` and `MH.__extension__` added 64 cases to it -- and seven of them
were red. Two different reasons, and only one of them is a defect.

Four are the harness constructing an option with no arguments at all, which for
these four is not a well-formed instance of the option:

* Service Selection: `Length` of 0 "is not allowed" and the identifier is 1-255
  octets [RFC 5149 section 3].
* Redirect: "Both the 'K' and 'N' flags cannot be set or unset simultaneously"
  [RFC 6463 section 4.2], so with neither address given the option's own length
  is undetermined.
* Access Network Identifier: "MUST contain at least one ANI sub-option"
  [RFC 6757 section 3]; it is a pure container.
* LMA-Controlled MAG Parameters: likewise at least one sub-option
  [RFC 8127 section 3].

The constructors refuse all four correctly, so the validations stay and
`_mh_option_overrides` gains the arguments that make the codes reachable --
which is what that table is already for, alongside the eleven MH options whose
no-argument default is likewise invalid. This raises coverage rather than
avoiding anything: all four now round-trip carrying real content, e.g. the ANI
option emits `340f 010d 0004 77696669 06 001122334455`.

The other three are `mh-extension/Exp_FFF*`, and they are #445 rather than mine.
Measured: all four extension codes -- including `Multi_Prefix`, which the table
already records -- fail identically with `PARSE / KeyError: 'length'`, raised
from `SchemaField.unpack` at `corekit/fields/misc.py:619` by way of
`CGAParameter.extensions`, before any extension schema is unpacked at all. A CGA
extension has no carrier but the CGA Parameters option, so the whole registry is
unreachable until #445 and #446 land. Three `EXPECTED_FAILURES` entries record
that, grouped with the existing one and naming the same site; that entry's
`file:line` is also refreshed, since this branch moved the lambda from :516 to
:873.

`pep.rst` claimed all four CGA extensions round-trip byte-for-byte. Their
handlers do, when driven directly, but nothing can reach them through the public
API, so the claim is narrowed to what is actually true and points at the
recorded gap instead.

Suite on 3.14: 973 passed, 17 skipped, 1544 subtests. On 3.10: 899 passed, 91
skipped, 1446 subtests. Baseline e2d8ed6 on 3.14: 942 passed, 1256 subtests.
The option harness is 7 passed / 363 subtests on both interpreters, and
`make_samples.py` regenerates all 24 captures byte-identically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant