fix(generators): spell TCP_BASE's keys the way TCP.make declares them (#602) - #609
Conversation
…#602) `examples/generators/options.py`'s `TCP_BASE` named three header fields `TCP.make` does not declare. `make` ends in `**kwargs` and reads nothing out of it, so each was accepted and discarded with no warning and no `TypeError`, and every frame of `examples/captures/options-tcp.pcap` was built from values the generator had not asked for. * `'seq': 1` was `seq_no`, so all 25 captured frames carried sequence number 0. Measured: `TCP(**TCP_BASE).info.seq` was 0, and is now 1. * `'urgent_pointer'` was `urgent`, whose requested value and unused default both happened to be 0. * `'ack_flag': False` was `ack`, while `'ack': 0` bound to `ack` itself -- the acknowledgement flag rather than the number, which is `ack_no` -- so the pair was the wrong way round in both directions. Renamed to the parameters `make` declares, keeping the values the mapping always read as. `options-tcp.pcap` changes in exactly 25 bytes, the low octet of each frame's sequence number; no case status and no `EXPECTED_FAILURES` entry moves. Adds `tests/protocols/test_option_generator_tcp_base_unit.py`: the keys against `inspect.signature`, and the built segment's header against both the data model and the packed octets. Build and test: the new module is 9 passed / 30 subtests; the option round-trip, option-coverage, generated-pcap, transport, changelog, tier-guard and docstring-contract suites are green (`coverage run -m pytest`, scoped).
|
❌ NEEDS CHANGES — head |
Cross-review appendix — PR #609Reviewer: Sonnet; PR authored on Opus 5. Reviewed at head
|
|
Resolving the cross-review's ❌ NEEDS CHANGES, which arrived at 02:28:48Z — two minutes after this PR merged at 02:26:16Z as No follow-up code change is needed. The review's own first line says the fix itself — renaming
Both of those disagreements had the same cause, and neither party's numbers were right: the captures under Established just now on So there is no defect in the option-coverage generator, contrary to what was reported as an incidental finding elsewhere. The assertion message says exactly what to do — "regenerate it with examples/generators/make_samples.py" — and following it resolves the failure completely. Three notes for the record, since this is the interesting part rather than the PR:
Nothing outstanding on this PR. #602 is closed by the merge. |
Fixes #602
What was wrong
examples/generators/options.py'sTCP_BASEnamed three header fields thatTCP.makedoes not declare.makeends its signature with**kwargs: 'Any'and never reads anything out of it, so each keyword was accepted, discarded, and
the parameter it was meant to set kept its default — no warning, no
TypeError, nothing. Every frame ofexamples/captures/options-tcp.pcapwastherefore built from values the generator had not asked for.
TCP_BASEmakedeclares'seq': 1seq_no0'urgent_pointer': 0urgent0'ack_flag': Falseack'ack': 0ackis the ACK flagack_no)So the
ackpair was the wrong way round in both directions at once.What changed
Each key is renamed to the parameter
makedeclares, keeping the values themapping always read as — a SYN-only segment, sequence number 1, everything else
zero. The
#:comment above it now records why the spelling is load-bearing.Measured
On CPython 3.14.7, with the worktree's own tree proven on
sys.path(
PYTHONSAFEPATH=1,assert pcapkit.__file__.startswith(<worktree>)):options-tcp.pcapchanges in exactly 25 bytes and keeps its size (2006):Byte 82 (1-based) is the low octet of the first frame's sequence number — pcap
global header 24, packet header 16, Ethernet 14, IPv4 20, then TCP
seqatoffset 4. One byte per frame, 25 frames,
0x00→0x01. No other capturechanged; the other four
options-*.pcapandtcp.pcapare byte-identical.Nothing moved that a test pins. All 323 generator cases were dumped
label → status → detailbefore and after: the two files are identical, so nocase changed status and no
EXPECTED_FAILURESentry flipped (tcp-option/Quick_Start_Responsestays
PARSE,tcp-option/User_Timeout_OptionstaysCONSTRUCT, the eighttcp-mptcp/*stayOK).Tests
New:
tests/protocols/test_option_generator_tcp_base_unit.py(unit tier — itconstructs its own octets and reads no capture).
test_every_key_is_a_parameter_make_declares— the keys againstinspect.signature(TCP.make). This is the one that generalises: it catches afourth misspelling nobody has made yet.
test_nothing_downstream_of_make_could_have_read_the_leftovers—TCP.readtakes
lengthand nothing else, so forTCPa keywordmakedoes notdeclare is one nothing consumes. Without this the rule above would be too
strict for a protocol that legitimately forwards a read-path keyword through
**kwargs, which this library does and documents — the same generator handsextension=TruetoHIP, whereHIP.readdeclares it andHIP.makedoes not.test_tcp_base_is_the_expected_header— the mapping against anEXPECTED_HEADERwritten out independently. Reading the expectations out ofthe mapping under test would fail with
KeyError, not with the valuemismatch that is the defect.
test_the_header_fields_are_the_ones_expected/..._octets_...— the datamodel and the wire separately, since the data model is what
makepacked and__post_init__parsed straight back.test_the_flag_bits_are_the_ones_expected— all nine flags off the octets,because
nsis absent from the data model (Flags.nsis commented out) andthe schema keeps its bit in
offset.test_the_acknowledgement_flag_and_number_are_distinct—ack=True,ack_no=0xDEADBEEF. Both are falsy inTCP_BASE, so nothing else wouldnotice them swapped back.
Failing before, passing after
Against the unfixed
TCP_BASE, same tree, same interpreter:With the fix:
Worth noting which failures those are: only the two signature/mapping checks
catch the
urgent_pointerandack_flagrenames. Both were invisible to anyassertion about a value, because the value asked for equalled the default that
was used instead. That asymmetry is the argument for deriving the check from
inspect.signaturerather than listing fields.Other suites, all green, scoped (never over the whole tree)
python util/changelog_md.py --checkexits 0.Audited, and clean
The module's six other base mappings were checked the same way — keys against
the signature they are passed to.
IPV4_BASE,_hopopt_base()(against bothHOPOPT.makeandIPv6_Opts.make),_ipv6_route_base(),SCTP_BASEandMH_BASEpass nothing undeclared._hip_base()plusextension=Truepassesone name
HIP.makedoes not declare, and that one is fine:HIP.readdeclaresextension, which is this library's documented way of forwarding a read-pathkeyword through
**kwargs.Two things deliberately not done here
1. Whether
makeshould warn on an undeclared keyword — not shipped.pcapkit/protocols/schema/schema.py:450warnsUnknownFieldWarningfor anunknown schema field;
makedoes not, and that asymmetry is the whole reasonthis defect was invisible. My view is that it should, and that it belongs in its
own issue rather than in a fixture-generator PR:
make's**kwargsis load-bearingacross the tree (
Protocol.__init__reads_layer/_protocolfrom it,HIP.readreads
extension,Frame.unpackforwards_seek_set), so a warning that cannottell "misspelled" from "consumed downstream" would fire on correct code, and
tests/test_docstring_contract.pydocuments that pattern as intentional. Theblast radius is the library, not this file. Left for a separate issue.
2. Four sibling test modules now describe
TCP_BASEinaccurately. They keeptheir own copies of the old spelling and still pass — nothing in them asserts
the match — but their comments have gone stale, and they are outside this PR's
files:
tests/protocols/transport/test_tcp_mptcp_subtype_unit.py:106— "matchingexamples.generators.options.TCP_BASE", with the old keys.tests/protocols/transport/test_tcp_mptcp_capable_length_unit.py:243— same claim.tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py:136— same claim.tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py:142—its
TCP_HEADERis already spelled correctly, but the comment explaining why("That mapping passes
'seq': 1and'ack_flag': False… Measured:TCP(**TCP_BASE).info.seqis0") now describes a state that no longer exists.Each is a comment fix plus, for the first three, renaming their local copies.