Repository navigation
test: enumerate the 38 __proto__ dispatch-registry entries - #504
Conversation
- Add examples/generators/dispatch.py, a FAMILIES-style enumerator over the seven __proto__ tables (Link, Internet, TCP, UDP, SCTP, pcap Frame, PCAPNG) mirroring examples/generators/options.py: it walks each registry directly, builds one minimal envelope per registered code, decodes it through pcapkit.extract(), and checks the resulting ProtoChain against a pinned expectation. - The expected target is recorded independently in PINNED_TARGETS rather than read out of the registry under test (target=_resolve(entry)), which was self-referential: a mis-pointed entry -- registry size unchanged, class wrong -- passed silently, since dispatch always "reached" whatever the corrupted table said. PINNED_TARGETS fixes that, and a case with no pinned entry now fails test_every_case_has_a_pinned_target. - The reached check also had to move off ProtoChain's own `in`/`index()`, which matches on alias string as well as class identity: Raw itself constructs with alias= set to the dispatching code's own name (pcapkit/protocols/misc/raw.py:144), so retargeting a registry entry at Raw still rendered under the original code's name and satisfied a string-based check. probe() now checks strict class identity via frame.protochain.protocols instead. - Add tests/protocols/test_dispatch_registry_unit.py: fails when a registry gains a code with no case or no pinned target, fails when the live registry stops matching PINNED_TARGETS (test_registry_currently_matches_pinned_target), and otherwise asserts every case dispatches to its pinned target or degrades exactly as recorded in KNOWN_DEGRADED (IPX construction defect #492, x2; NGAP needing a well-formed PER payload, x2). - Covers all 38 entries, including the ones test_dispatch_bindings_unit.py did not: Internet AH/HIP/Mobility_Header/SCTP, Link RARP, pcap Frame IPV4/IPV6, and PCAPNG's matching IPV4/IPV6 pair (previously unverified). Build/test: tests/protocols/ -- 470 passed, 1341 subtests, 0 failures.
0d27064 to
9ff582d
Compare
Diagnosis of the
|
| condition | before the fix | after |
|---|---|---|
pycrate blocked (CI's condition) |
2 failed, 7 passed, 81 subtests | 7 passed, 83 subtests |
pycrate present (local) |
7 passed, 83 subtests | 7 passed, 83 subtests |
Both SUBFAILED lines in the "before" run name the two SCTP cases, matching CI's failure exactly.
The full unit tier — the same command CI runs, pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py' — passes in this branch's worktree on 3.14: 924 passed, 5 skipped, 1867 subtests, 0 failed.
Note on what this was not
This is the harness having pinned an environment-dependent detail too narrowly. It is not a library defect, and the two cases still degrade exactly as recorded — they simply have two legitimate ways of saying so.
#503 added the missing Socket(0x0000) member, so IPX now constructs and parses. Both entries recorded it as degrading, and this harness asserts a recorded degrade still degrades -- so leaving them in fails, by design: AssertionError: True != False : internet/IPX_in_IP was recorded as degrading (...#492) but now reaches its target ('Ethernet:IPv4:IPX:Unknown'). If the defect is fixed, delete its KNOWN_DEGRADED entry. That is the ratchet working. Removed from both the test copy and the generator copy. Verified the cases genuinely reach IPX rather than merely stopping to fail: link/Novell_Inc_0x8137: reached=True chain='Ethernet:IPX:Unknown' internet/IPX_in_IP: reached=True chain='Ethernet:IPv4:IPX:Unknown' 7 passed, 83 subtests, both with pycrate present and with it blocked (CI's condition, since .[test] excludes [NGAP]). KNOWN_DEGRADED now holds only the two SCTP NGAP cases, which are a placeholder-payload limitation rather than a library defect.
Pushed
|
Closes #496. Test-only:
examples/generators/dispatch.pyandtests/protocols/test_dispatch_registry_unit.py, 1,199 insertions, no library code touched.The gap
pcapkit/foundation/registry/protocols.pydefines the registries that decide whichProtocolclass parses the next layer. 38 entries across seven tables —Link.__proto__(7),Internet.__proto__(16),TCP.__proto__(4),UDP.__proto__(3),SCTP.__proto__(2),Frame.__proto__(3),PCAPNG.__proto__(3) — and none of them had an enumerating harness.Existing coverage checked that an entry resolves to the right class object, which is satisfied by a table whose target could not parse a packet if it tried.
tests/protocols/test_dispatch_bindings_unit.pyexists because this class of bug shipped — its docstring records that OSPF "was reachable from no table at all and could not have parsed a packet if it had been" — but it hand-picks 11 cases rather than enumerating.This is the
__proto__equivalent of whattests/protocols/test_option_roundtrip_unit.pyandexamples/generators/options.pyalready give the option registries, and it follows their structure.Two things found while building it, both worth reading
1. The first draft's expectation was self-referential.
cases()built eachCasewithtarget=_resolve(entry)straight fromregistry.items(), so the expected target was read out of the very table under test. It proved "dispatch honours whatever the table says", not "the table says the right thing". I breakage-tested retargetingAHfromah.AHtoraw.Rawwith the registry size unchanged and got 5 passed, 45 subtests — not caught.Fixed with
PINNED_TARGETS, a hand-writtenlabel -> (module, name)table independent of the registry.Casenow carriestarget(pinned) andregistered(whatever the live table resolves to) as separate fields, and a mismatch between them is itself a failure.2. Even a pinned target was not enough, for a genuinely subtle reason.
case.target in frame.protochainstill missed the retarget, becauseProtoChain.__contains__/index()match on alias strings and.id()names rather than class identity — andRawadopts the dispatching protocol's own name (pcapkit/protocols/misc/raw.py:alias = self._info.protocol.name). So an entry keyed onTransType.AHbut retargeted toRawstill renders as'...:AH'and satisfies a string check, with noAHinstance anywhere in the chain.Fixed by asserting strict class identity against
frame.protochain.protocols— the tuple of actualtype(instance)per layer. Worth knowing beyond this PR: a protochain string containing a protocol's name is not evidence that protocol parsed anything.Both breakage shapes, run by me against the final code
Deletion —
AHline removed fromInternet.__proto__:Retargeting —
AH→Raw, registry size unchanged:Caught by two independent tests, both naming
internet/AH, with a message that distinguishes the two possible causes. Restored after each: 7 passed, 83 subtests passed, clean tree.Coverage
All 38 of 38 entries, including the ones that previously had only an identity check and no parse test: AH (51), HIP (139), Mobility_Header (135), SCTP (132), RARP (0x8035), and the raw-IP
Frame.__proto__pair (228/229). ThePCAPNG.__proto__IPV4/IPV6 pair — which the audit flagged as unverified because hand-rolling a minimal SHB+IDB+EPB stream looked riskier than the check was worth — is now confirmed working, built throughpcapkit's ownPCAPNG/Contextconstruction API rather than by hand.34 of 38 reach their target cleanly. 4 are recorded in
KNOWN_DEGRADEDrather than fixed — this module reports defects, it does not carry workarounds:link/Novell_Inc_0x8137andinternet/IPX_in_IP— both hit0 is not a valid Socket(const enum lookups reject values that appear on the wire: RouterAlert(0) is RFC 2113's only defined value #492, fixed in fix(const): accept RFC 2113's Router Alert 0 and IPX's own socket 0 #503; once that merges these entries should be deleted and the cases should pass).NGAP-PDU, out of scope for a minimal probe. Same placeholder and same documented reason as the existingtest_sctp_unit.py::test_ppid_dispatch_hook.KNOWN_DEGRADEDis a ratchet, not a skip-list: a case in it must degrade in the recorded way, the test tells you to delete the entry once the defect is fixed, a separate test rejects entries naming cases that no longer exist, and another keeps degraded cases a small minority.A trap for whoever extends this
ARP/RARP/InARP/DRARP's reported name comes from the wire
operfield (pcapkit/protocols/link/arp.py:176-190), not from the dispatching ethertype, so an ethertype-0x8035 frame withoper=1legitimately reportsEthernet:ARP. The RARP case usesoperin{3,4}for that reason; anything else exercises the wrong alias while appearing to pass.Verification
test_dispatch_registry_unit.py: 7 tests, 83 subtests, 0 failures. Fulltests/protocols/blast radius: 470 passed, 1341 subtests, 0 failures.Three-dot
origin/main...HEADconfirms only the two files. (The two-dot form additionally showspcapkit/__init__.py— that ismain's own1.5.0b3bump appearing as a reversal, not a change here;git merge-treeagainstmainproduces a tree whose__version__is1.5.0b3.)This module constructs its own octets and reads nothing under
examples/captures/, so it belongs to the unit tier and runs on a fresh checkout with nothing generated.