ci: add a per-engine test job for Scapy, PyShark, PyPCAPFile and pcap-ct - #755
Conversation
ed241cf to
a4081b8
Compare
|
❌ NEEDS CHANGES @ All three defects I raised are fixed and verified, and the The guard finding is confirmed, and it is two-way.
Liveness passes in both: Prose is not sufficient, and a tripwire is expressible today — contra "it would need a real per-distribution model". Assert that every (job, gate) pair the guard deems satisfied is satisfied through an import name with exactly one providing extra, else require an allowlist entry. Same shape as the existing machinery, no per-distribution model needed, and it fails loudly on both rows above. Three to fix:
Noted, not blocking: with Labels ❌ NEEDS CHANGES @ |
|
Measurement note from the The skip delta reproduces exactly on a throwaway 3.10 venv (never the repo venv): the precise 27-node subset goes Minor nit, not blocking: the absolute Current verdict is in the review above, re-run against |
a4081b8 to
c2bbf68
Compare
|
Confirmed the guard blind spot myself, and filed it as #762 — but it is being fixed here rather than deferred, since this PR adds the two jobs ( One precision point for the mechanism sentence, which is currently wrong. The two flags do not "both read as needing The conflation is one level down, in A docstring is not enough here, contra "would need a real per-distribution model": asserting that every (job, gate) pair deemed satisfied resolves through an import name with exactly one providing extra — else an explicit allowlist entry — needs no new model and fails loudly on both doctored directions. Also outstanding: |
f172874 to
e902ede
Compare
|
Cross-review verdict at Blocker — all five An intent/assertion mismatch, and the surrounding comment states the correct intent — " Four stale-prose sites, the same pattern as the three fixed last round: What is confirmed good. The #762 fix landed well and the allowlist is not an escape hatch — rewriting an entry to name the wrong distribution fails, adding a vestigial one fails, and deleting one surfaces the real finding, so hiding the defect requires making the guard red. Both doctored install-swaps now yield the right Worth recording: the reviewer withdrew its own proposed design in favour of the author's. Keying the ambiguity check on |
e902ede to
4aa44ae
Compare
|
Second cross-review at The allowlist can re-hide #762 with every check green. I had reported the opposite from the first review, and that was wrong. {('pypcap-parity', 'HAS_PYPCAP'): 'pypcap', ('engine-tests', 'HAS_PCAP_CT'): 'pcap-ct'}They are correct today, but the anti-rot test asserts only The allowlist is also unnecessary, because the discriminator already exists in the source: Only
A new assertion is provably vacuous. Deleting the And the "exactly 4 failures / 4 tests" claim is wrong: stubbing to Both reviews independently found the CI blocker at |
4aa44ae to
d242c86
Compare
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.
d242c86 to
01f4566
Compare
|
Final cross-review at The tripwire is genuinely derived, proven the hard way. That is the exact inverse of the allowlist's failure mode: its anti-rot test read from the thing it was checking, so agreement was tautological. This one cannot. The ordering bug is fixed and verified against the reviewer's own cases, not the author's: Coverage widened rather than merely held. Deleting Counts: 105 passed / 519 subtests under pytest and One non-blocking prose correction, for the next touch of the file rather than another round. Item 5's new docstring names four tests as covering the dotted-key deletion; two of them do not — Unpublished and awaiting the owner. |
Please follow the guide below
make pylint,make mypy,make isort) -- ranisort,mypyandpylinton the three touched Python files; clean except three pre-existing mypy findings and six pre-existing pylint line-length findings, none on a line this PR touchesmake testpasses, and a test case covers the change -- the full suite is forbidden to run here; see the tests run below insteaddocs/source/changelog/and regeneratedCHANGELOG.md, if the change is user-visible -- N/A, changelog centralised in docs(changelog): shared 1.5.0 changelog — long-lived, merges last (#610, #616, #617, #618, #620) #657What is the purpose of your pull request?
ci— workflows or build toolingDescription of your pull request and other information
Fixes #751.
Fixes #762.
Per the maintainer's ruling on #751, third-party engine coverage (Scapy, PyShark,
PyPCAPFile, pcap-ct, PyPCAP) gets its own job(s) in
unit-tests.ymlrather than onemore install line on
test,integrationorgate— "so they dont intertwinewith the other major tests".
engine-tests(3.10-3.14): installs Scapy, PyShark, PyPCAPFile and PCAP_CTtogether, plus a system
libpcapviaapt-getfor PCAP_CT's ctypes loader. ClosesHAS_SCAPY(14 methods),HAS_PYSHARK(3 of 4),HAS_PYPCAPFILE(9 of 15) andHAS_PCAP_CT(1), and closesHAS_RUNTIME(5 methods intest_runtime_engines.py, which reuses that flag name for the four coredependencies plus
dpkt/scapy/pyshark) as a side effect of installing allthree together.
pypcap-parity(3.10-3.11 only, both extras' own version ceiling): a separatejob and venv with a C toolchain and libpcap headers, mirroring
integration'sfixture-tier selection to reach
test_new_engine_parity_runtime.py. This is the"try to build [PyPCAP] and if the CI is not a good suit, then we ripe it"
instruction acted on directly — the job attempts the real
pip install pypcapbuild in CI rather than declining it up front. It also closes the other 6
HAS_PYPCAPFILEmethods that live in the same module. Kept apart fromengine-testsbecause pypcap and pcap-ct both ship a top-levelpcapmodule andcannot share a venv — measured in
pcapkit/foundation/engines/_pcap_backend.py's own module docstring: with bothimportable, pcap-ct's package always shadows upstream's extension.
tsharkis now installed too — update, after review. #751 has a latercomment (19:16:10Z) extending the "try to build, rip it if CI is not a good
fit" instruction to
tsharkas well asPyPCAP; an earlier reading of thisthread had missed it.
test_the_reason_tracks_the_running_interpreterused tohard-assert tshark's absence, which would have flipped the assertion from a
pass to a failure the moment tshark was installed — that part of the original
reasoning was correct, but the fix was to remove the inconsistency, not to
decline the ruling. Its sibling
(
test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous) alreadyself-guards with
shutil.which('tshark'); giving the first test the sameguard (one line, verified both branches locally with a fake
tsharkon$PATH) makes tshark's presence a non-issue, soengine-testsnow installsit via
apt-get(debconf pre-seeded to skip the postinst prompt). Confirmedon real CI: all 5
engine-testslegs still pass with tshark installed.HAS_PYPCAPFILE's marker stays unmodelled, by design.PyPCAPFilecarriespython_version < '3.12', so it installs on 3.10/3.11 and skips cleanly on3.12-3.14 even once taken.
tests/_dependency_gates.py's guard cannot see that —it does not evaluate markers, by its own module docstring — so this is recorded
in
DEPENDENCY_GATE_EXCLUSIONS' prose rather than modelled in the guard itself;teaching the guard markers is out of scope for a workflow-only change.
Cost, chosen rather than discovered: per-job venvs mean per-job installs, and
pypcap-parityadditionally re-runs the whole fixture-tier suite (sampleregeneration plus
integration's selection) a second time on two of the fivelegs, since none of
tests/_dependency_gates.py's three recognised selectionshapes offers a cheaper way to reach one fixture-dependent module. Restricting
that job's matrix to 3.10/3.11 — where its extras' own markers actually resolve —
is what keeps the duplication bounded to two legs instead of five.
What I measured
figures exactly at PR creation):
HAS_SCAPY14,HAS_PYSHARK3,HAS_PYPCAPFILE15(9 unit + 6 parity),
HAS_PYPCAP4,HAS_PCAP_CT1,HAS_RUNTIME5 (scoped totest_runtime_engines.py).HAS_PYSHARKis now 4, not 3 -- this PR's ownlater regression test (
test_a_config_ini_naming_an_off_path_tshark_is_not_read_as_missing)added a third in-file gated method; the fourth is
tests/integration/test_engine_runtime.py:69, covered byintegration/gate,not
test/engine-tests.one), comparing a baseline matching
test's current install line(
.[test,DPKT,crypto,NGAP]) againstengine-tests' full install line: 107passed / 27 skipped -> 134 passed / 0 skipped, 0 failed across every unit-tier
file the new job's gates live in.
pypcap-parityhalf, same method: withPyPCAPFileinstalled andPyPCAPabsent,
test_new_engine_parity_runtime.pygoes from 0 passed / 10 skippedto 6 passed / 4 skipped locally (a local attempt to also build
PyPCAPonthis non-Ubuntu dev box failed only on a nonstandard header search path, not on
the compile itself). Confirmed on this PR's own CI run: both
PyPCAP/PyPCAPFile parity Python 3.10/3.11legs installedpypcap-1.3.0forreal (
Successfully installed … pypcap-1.3.0 pypcapfile-0.12.0 …) and all 4HAS_PYPCAPmethods executed rather than skipped — one of them logging a realAttributeWarningfrom the actual pypcap engine, not a mock. 172 passed / 2skipped / 0 failed on the 3.10 leg. So the "try to build, rip it if CI is not a
good fit" instruction resolved in PyPCAP's favour: it builds cleanly on
ubuntu-latest, nothing to rip.tests/test_tier_guard.py: 96 passed, 511 subtests (was 84 passed / 485subtests on
main; the additional passes and subtests are from latercross-review rounds, not from this bullet's own skip-delta measurement).
tests/foundation/engines/test_pyshark_engine.py, the third touched Pythonfile: 9 passed, 8 subtests. Combined: 105 passed, 519 subtests.
tests/_dependency_gates.DEPENDENCY_GATE_EXCLUSIONS'liveness check (
test_each_exclusion_still_describes_a_gap_that_is_really_there)is what verified every dark job/package pair above; it was the primary check
for this whole change.
isort --check-only,mypyandpylintagainst the three touched Pythonfiles:
isortclean;mypy's three findings andpylint's line-lengthfindings are all outside the lines this PR touches (verified against
origin/main's diff hunks).adding coverage, so the "raise coverage" rule is satisfied by the skip delta
above rather than by new test cases.
Also fixed, after two cross-review rounds (both on a different model, Opus):
three comments (
test,integration,gate) still describing the pypcapfilebug #747/#748 already fixed as the reason
PyPCAPFileisn't installed there;a false "reaches all 14
HAS_SCAPYmethods" claim intests/_dependency_gates.py(engine-testsreaches 10 of 14 — the other 4are in
tests/integration/or match the*_runtime.pyignore-glob, alreadycovered elsewhere); and a stale module-docstring claim that neither
PyPCAPnor
PCAP_CTwas on any job's install line, both now are. That last onesurfaced a real, if currently harmless, gap in the dependency guard: it
cannot distinguish an extra satisfying
HAS_PYPCAPfrom one satisfyingHAS_PCAP_CT(both resolve to needing top-levelpcap), so a job thatinstalled one while reaching the other flag's gate would get a false
non-gap. Verified with
job_reaches()directly that neither new job doesthat today — confirmed, not assumed — and recorded the caveat in the
docstring rather than papering over it.
Not this PR
HAS_VENDOR_DEPS(html5lib) is#738's own remaining scope;engine-testsis newly dark on it too (same reason
testalready is — its ignore-shapeselection reaches the same gate), now recorded in the exclusion.
HAS_RUNTIMEintest_runtime_engines.pyits own flag name (so theskip reason names which dependency was missing) is left as a follow-up; this
guard does not care what a flag is named, only whether the job that reaches it
installs what it asks for, so it was not required to close this issue.