Make the legacy smoke scripts runnable again - #387
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new examples/legacy_smoke/_engine_support.py uses from __future__ import annotations, which breaks imports on Python 3.6 despite the repository declaring >=3.6 support.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Restores the “legacy smoke” demonstration scripts under examples/legacy_smoke/ so they run successfully against the current pcapkit.extract(...) API, including correct handling of reassembly flags and engine availability, and updates the committed output fixtures with a reproducible timezone.
Changes:
- Fix legacy smoke scripts to match current extractor/reassembly behavior and avoid hard failures on missing optional engine dependencies.
- Add shared engine availability / fallback detection helper (
_engine_support.py) and wire it into engine/timing/benchmark demos. - Document how to run demos and regenerate committed fixtures; update committed fixtures accordingly.
File summaries
| File | Description |
|---|---|
| examples/legacy_smoke/_engine_support.py | New shared helper for engine preflight/unavailability and detecting silent fallback. |
| examples/legacy_smoke/test_engine.py | Runs each engine, skips unavailable ones, and reports the actual driver used. |
| examples/legacy_smoke/test_time.py | Adds engine preflight and skips unavailable engines before timing loops. |
| examples/legacy_smoke/test_perf.py | Makes pyperf optional and benchmarks only engines that pass preflight. |
| examples/legacy_smoke/test_profile.py | Ensures the cProfile output directory exists when run standalone. |
| examples/legacy_smoke/test_stream.py | Buffers live capture to a temp file to avoid overwriting pinned fixtures. |
| examples/legacy_smoke/test_stream_askpass.py | Same as test_stream.py, but supports sudo -A askpass flow. |
| examples/legacy_smoke/test_api.py | Aligns reassembly knobs with reassembly=True requirement. |
| examples/legacy_smoke/test_analyse.py | Aligns reassembly knobs with reassembly=True requirement. |
| examples/legacy_smoke/README.md | Expanded documentation for running demos, dependencies, and fixture regeneration. |
| examples/legacy_smoke/Makefile | Adds fixtures target with pinned TZ and configurable interpreter. |
| examples/captures/out.json | Regenerated committed fixture output (formatting + timestamp rendering). |
| examples/captures/out.plist | Regenerated committed fixture output (timestamp rendering). |
| examples/captures/out.txt | Regenerated committed fixture output (timestamp rendering). |
| examples/captures/pcapng.txt | Regenerated committed fixture output (timestamp_epoch + structural output changes). |
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🔵 Needs a closer look
The updated legacy_smoke README currently references a non-existent ../samples/make_samples.py path and should be corrected to the actual generator location before merge.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
examples/legacy_smoke/test_time.py:41
- For timing,
time.time_ns()is wall-clock time and can jump backwards/forwards (e.g., NTP adjustments), which can yield noisy or even negative deltas. Use the monotonictime.perf_counter_ns()for duration measurement instead.
now = time.time_ns()
extraction = pcapkit.extract(fin='../captures/in.pcap', store=False, nofile=True, verbose=False, engine=engine) # type: ignore[arg-type]
delta = time.time_ns() - now
examples/legacy_smoke/README.md:23
- The sample-generation command uses
python ../samples/make_samples.py, but there is noexamples/samples/directory in this repo. The generator script lives underexamples/generators/make_samples.py, so fromexamples/legacy_smoke/the correct relative path is../generators/make_samples.py.
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
Twelve of the seventeen runnable demonstrations worked; the rest had rotted against the current API, and nothing in CI runs them so nothing noticed. test_analyse.py passed tcp=True and reasm_strict=True without reassembly=True, so Extractor.reassembly raised UnsupportedCall after parsing every frame. test_api.py had the same omission latently - its three reassembly knobs did nothing, and it passed only because it never touched .reassembly. test_engine.py and test_time.py died partway through on pyshark, which asks for an implicit asyncio event loop that Python 3.14 no longer provides. A demo that dies partway demonstrates nothing, so engines that work now still run and report while an unavailable one is reported as skipped with the reason. That needed care: Extractor does not raise when an engine's package is missing, it warns and silently falls back to its own parser, so the naive version credited PCAP's work to dpkt. The demos now name the driver that actually ran. Classification lives in the new _engine_support.py and covers exactly three cases - ImportError, a missing tshark, and the event-loop RuntimeError - so a real pcapkit failure is still loud. test_perf.py needs pyperf, which pcapkit does not depend on; it now says what to install and points at test_time.py, which does the same comparison with nothing extra. test_profile.py was also broken and was not on the list: cProfile will not create its output directory, so `python test_profile.py` profiled the whole run and then died at the dump. It makes its own temp/ now. Regenerating the four committed outputs was folklore, so `make fixtures` does it and the README says so. They are pinned to TZ=UTC because pcapkit renders frame timestamps in the host's zone: the committed files disagreed with each other, the PCAP three having been generated at UTC-05:00 and pcapng.txt at UTC+08:00. No single zone reproduces all four, and UTC is the only choice that is reproducible anywhere and, for PCAP-NG, correct. Regenerated here against current main. Also fixed a footgun: test_stream.py and test_stream_askpass.py wrote their buffer to ../captures/stream.pcap, a generated fixture the runtime tests pin, so running either would have broken the suite. They use a temporary directory now, and the macOS-only en0 is a named constant. 17/17 runnable scripts exit 0. Library suite unchanged at 508 passed, 4 skipped, 315 subtests.
test_time.py measured durations with time.time_ns(), which is wall clock: an NTP adjustment mid-run can step it backwards and yield a negative delta. perf_counter_ns is monotonic and is what a duration wants. Pre-existing, but this is a timing demo and the file is already being rewritten here. Addresses a suppressed Copilot comment on #387.
1f2b0e7 to
3a4df14
Compare
…or Python 3.6 compatibility Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>
The sample-generation command read `../samples/make_samples.py`, a path left over from before `examples/samplegen/` was renamed to `examples/generators/`. There is no `examples/samples/` directory, so the one command a fresh clone has to run before the other scripts work was the one command that could not. Verified by running it from `examples/legacy_smoke/`, which is the directory the README tells the reader to be in. Copilot also flagged `time.time_ns()` in test_time.py as a suppressed "previously missed" comment; that one is stale -- the file has used the monotonic `time.perf_counter_ns()` since 00e1039, with a comment saying why.
|
Both suppressed comments from the review body checked. One was real, one was stale. Real — Stale — # NOTE: perf_counter_ns is monotonic; time_ns is wall clock and can step
now = time.perf_counter_ns()
...
delta = time.perf_counter_ns() - nowNo change needed there. |
Twelve of the seventeen runnable demonstrations in
examples/legacy_smoke/worked; the rest had rotted against the current API. Nothing in CI runs them, so nothing noticed —testpaths = ["tests"]means these files are never collected.API drift
test_analyse.pypassedtcp=True, reasm_strict=Truebut notreassembly=True, andExtractor.reassemblyis gated on_flag_r. It parsed all 26 frames of its capture and then died withUnsupportedCall.test_api.pyhad the same omission latently: itsip/tcp/reasm_strictknobs did nothing, and it passed only because it never touched.reassembly.test_engine.pyreferencedengine='pipeline'andengine='server', neither of which exists inExtractor.__engine__any more.test_time.pyhad unused top-levelimport dpkt/import pyshark/import scapy.allthat would kill the script on any host missing one.I audited every
extract()call in the directory againstinspect.signature(pcapkit.extract): no unknown keywords remain, and every reassembly knob is now paired withreassembly=True.Honest degradation, and a trap worth knowing
test_engine.pyandtest_time.pydied partway through on pyshark, which asks for an implicit asyncio event loop that Python 3.14 no longer provides — pyshark's bug, not pcapkit's, andtsharkis absent here too. A demo that dies partway demonstrates nothing, so engines that work still run and report, and an unavailable one is reported as skipped with its reason.That needed more care than it looks.
Extractordoes not raise when an engine's package is missing — it warns and silently falls back to its own parser, so the naive version credited PCAP's work to dpkt. The demos now name the driver that actually ran. Verified by blockingdpktat the import hook:Classification lives in a new
_engine_support.pyand covers exactly three cases —ImportError, a missingtshark, and the event-loopRuntimeError. Anything else re-raises, so a real pcapkit failure is still loud.test_perf.pyneedspyperf, which pcapkit does not depend on; it now prints what to install and points attest_time.py, which does the same engine comparison with nothing extra. I deliberately did not addpyperftoPipfile/pyproject.toml— it is a benchmarking harness for one demo, and it would land in every contributor's install for something CI never runs.test_profile.pywas also broken and was not on the list — found by running it.cProfilewill not create its output directory, and onlymake profiledidmkdir -p temp, sopython test_profile.pyprofiled the whole run and then died at the dump. It makes its owntemp/now.Regenerating the committed fixtures is now one command
make fixturesinexamples/legacy_smoke/, documented in its README:Both inputs are committed, so it works on a fresh clone with no
make samples.Why
TZ=UTC: these fixtures were unreproducible because pcapkit renders frame timestamps in the host's zone — and the four committed files disagreed with each other, the PCAP three having been generated at UTC−05:00 andpcapng.txtat UTC+08:00 (both reproduced exactly under those zones to confirm). No single zone reproduces all four. UTC is the only choice that is reproducible anywhere, self-consistent across the set, and — for PCAP-NG — yields the correct epoch. Regenerated here against currentmain, so they will move again when #365–#368 land.Two corrections to what was originally reported:
out.plistandout.txtwere not stale, andpcapng.txt's drift was not purely cosmetic — itstimestamp_epochvalue changed, which is #361.A footgun fixed
test_stream.pyandtest_stream_askpass.pypassedbuffer_path='../captures/stream.pcap'— a generated fixture the runtime tests pin byte-for-byte. Running either would have silently broken the suite. They write to a freshtempfile.mkdtemp()now, printed at startup. The macOS-onlyen0is a namedINTERFACEconstant with a note (Linux is usuallyeth0/wlan0). Neither script is executable here (they needsudo tcpdump), so both were byte-compiled rather than run.Verification
17/17 runnable scripts exit 0 (was 12/17), executed against current
mainwith the renamed../captures/paths. Library suite unchanged: 508 passed, 4 skipped, 315 subtests.Reported, not fixed
Noisy
[ERROR]-level logs on entirely normal input:MissingKeyError: <ExtensionHeader.IPv6_Frag: 44>×12 during ordinary IPv6 fragment reassembly, andMissingKeyError: <OptionType.if_tsoffset: 14>×4 on a capture that simply lacks an optional option. That is #362. Also[WARNING] not enough values to unpack2–10 times per script fromhttpv1.py:278-280, which also emits thousands ofDeprecationWarnings about positionalmaxsplit.