Repository navigation
1566 lines (1450 loc) · 78.1 KB
/
Copy pathunit-tests.yml
File metadata and controls
1566 lines (1450 loc) · 78.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Unit Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_call:
inputs:
gate-only:
description: >-
Run only the single-version gate job instead of the full matrix.
Callers that merely need a pass/fail verdict before shipping set
this to true: the full matrix already runs once per commit from
this workflow's own push and pull_request triggers, so running it
again per caller tests the same commit several times over.
type: boolean
required: false
default: false
permissions:
contents: read
# Only pull-request runs may be superseded. GitHub cancels a *pending* run
# whenever a newer one joins its group -- `cancel-in-progress: false` protects
# only a started one -- so a ref-keyed group on main left a commit with no unit
# tests at all (run 34970682137). Other events key on the run id instead.
#
# Inside a called workflow `github.workflow` and `github.run_id` are the
# caller's, so the literal `unit-tests-` prefix keeps a called run out of its
# caller's group, where it would wait behind the caller and deadlock.
concurrency:
group: unit-tests-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# #1660: the Python versions, from .github/python-versions.toml, for the
# matrix jobs' `fromJSON` and `gate`'s name. util/python_versions.py needs
# only the standard library, so the image's `python3` runs it (its tomllib
# needs 3.11). Runs on every path, `gate-only` included, with no write
# permission, since callers of this reusable workflow grant `contents: read`.
#
# The other single-interpreter jobs (`unittest-ordering`, `changelog`,
# `project-tests`) run the script in a step of their own: setup-python takes
# an empty `python-version` as "the Python on PATH", with only a warning, so
# `unittest-ordering`'s `!cancelled()` would otherwise need a check that this
# job succeeded.
python-versions:
name: Read the Python versions
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
latest: ${{ steps.read.outputs.latest }}
test: ${{ steps.read.outputs.test }}
test-latest: ${{ steps.read.outputs.test-latest }}
test-ktlo: ${{ steps.read.outputs.test-ktlo }}
integration: ${{ steps.read.outputs.integration }}
integration-latest: ${{ steps.read.outputs.integration-latest }}
integration-ktlo: ${{ steps.read.outputs.integration-ktlo }}
pypcap-parity: ${{ steps.read.outputs.pypcap-parity }}
pypcap-parity-ktlo: ${{ steps.read.outputs.pypcap-parity-ktlo }}
engine-supported: ${{ steps.read.outputs.engine-supported }}
engine-ktlo: ${{ steps.read.outputs.engine-ktlo }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: |
.github/python-versions.toml
util/_actions.py
util/python_versions.py
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Read .github/python-versions.toml
id: read
run: python3 util/python_versions.py --github-output
test:
# `github.event_name` is never 'workflow_call' inside a called workflow,
# whose github context is the caller's, so an input is the only reliable
# way for a reusable workflow to know how it was invoked.
name: Python ${{ matrix.python-version }}
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' }}
needs: [changes, python-versions]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
timeout-minutes: 45
# #1672: the level of every annotation a failing step writes -- a warning on
# a KTLO leg, which must not show red, an error on a supported one. The
# steps are shared with `test-ktlo`, whose legs carry `ktlo: true`.
env:
ANNOTATION_LEVEL: ${{ matrix.ktlo && 'warning' || 'error' }}
strategy:
fail-fast: false
# #1660: .github/python-versions.toml's supported tier, CPython's then
# PyPy's; it blocks through `required-checks` (#1633). The KTLO legs are
# `test-ktlo`'s (#1738), and the experimental tier is experimental.yml's,
# so nothing here waits on it (#1732). #1958, the owner's ruling E: PyPy's
# legs run the same `ALL` tier as every CPython leg but `latest`.
#
# `include` marks one leg `coverage: true`, `latest` (#1661), so coverage
# adds no leg to the queue. Uncovered, 3.15's tests step took a median
# 8.1 minutes and coverage cost about 1.8x, which fits the job's 45.
#
# #2243, the owner's option B: a test-only pull request runs the
# `latest` leg alone, the coverage leg, which runs the whole suite with
# `integration`'s `latest` leg and `project-tests`. See `required-checks`.
matrix: ${{ fromJSON(needs.changes.outputs.test-only == 'true' && needs.python-versions.outputs.test-latest || needs.python-versions.outputs.test) }}
# `&test-steps`: `test-ktlo` runs these same steps, by YAML alias.
steps: &test-steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
id: checkout
continue-on-error: ${{ matrix.ktlo == true }}
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
id: python
continue-on-error: ${{ matrix.ktlo == true }}
with:
python-version: ${{ matrix.python-version }}
cache: pip
# DPKT (#729), crypto and NGAP (#738) are here so the unit-tier tests
# gated on them run instead of skipping; each installs cleanly. On
# PyPy 8.0 PyPI has no Linux wheel of cryptography, so pip builds it with
# the image's Rust and libssl-dev (#1958). `cli` (emoji) is not here: its
# only gated tests are in tests/integration, which this job ignores.
# PyPCAPFile is not here either: #751's `engine-tests` and
# `pypcap-parity` jobs cover its gated methods.
#
# #1633: a KTLO leg that cannot install fails fast and once:
# util/pip_install.py writes one annotation naming pip's last ERROR
# line, and every later step needs `steps.install.outcome == 'success'`,
# so it is skipped rather than failing against a missing package. The
# install needs its own interpreter: if setup-python failed, the image's
# `python` would be tested under this leg's name. On a supported leg
# either fails the job; on a KTLO leg the annotation is a warning (#1672).
- name: Install package and test dependencies
id: install
if: ${{ steps.python.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
shell: bash
env:
LEG: Python ${{ matrix.python-version }}${{ matrix.ktlo && ' (KTLO, non-blocking)' || '' }}
run: python util/pip_install.py -e '.[test,DPKT,crypto,NGAP]'
# #1958, the owner's ruling E: latest CPython runs the whole suite, so its
# leg installs everything the suite gates on that pip can install there.
# tests/_dependency_gates.py reads this line as the `latest` leg's
# extras; tests/project/test_whole_suite_on_latest_1958.py pins it to the
# same set as `integration`, `project-tests` and `gate`. Not here: PCAP_CT,
# which needs a system libpcap that `engine-tests` installs for its own
# cell, and PyPCAP and PyPCAPFile, whose markers resolve to nothing on
# `latest`. Never a KTLO leg, so `test-ktlo` always skips it.
- name: Install the whole suite's dependencies on latest
id: install-latest
if: ${{ steps.install.outcome == 'success' && matrix.python-version == needs.python-versions.outputs.latest }}
continue-on-error: ${{ matrix.ktlo == true }}
run: python -m pip install -e '.[test,Scapy,DPKT,crypto,cli,NGAP,vendor,docs,PyShark]'
# 7.10 is the first release with `[run] patch = ["subprocess"]`, which
# .github/coverage.toml relies on to measure the xdist workers.
- name: Install coverage
if: ${{ matrix.coverage }}
run: python -m pip install 'coverage>=7.10'
# Nothing else in the log says what `-n auto` resolved to on this runner.
- name: Report available parallelism
id: parallelism
if: ${{ steps.install.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
run: |
nproc
python -c "import os; print('cpu_count', os.cpu_count())"
- name: Run unit tests
id: tests
if: ${{ steps.install.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
# `--dist load` is pinned explicitly, so a change to xdist's default
# cannot silently change how this suite is distributed. The durations
# table and JUnit file are #1538's per-test timings.
#
# #1958, the test tiers: tests/_tiers.py's module_tier() puts every test
# module in `ALL`, a curated list each stating `__tier__ = 'ALL'` (#1963),
# or else in `LATEST`, and `LATEST` runs on `latest`
# alone, which is also the coverage leg. Every other leg -- `test-ktlo`'s
# too -- passes each module latest_tier_paths() prints as an `--ignore`,
# which takes in the round-trip sweeps (#1732).
#
# #1954, the owner's option 3: the project pins, tests/_tiers.py's
# project_pin_paths(), run once per push in `project-tests`, so `latest`
# passes each as an `--ignore` too. They import no pcapkit, so coverage
# loses nothing.
#
# #2005: on PyPy an xdist worker's RSS grows with every test it runs, so
# the `ALL` tier took the workers past 20 GB. A smaller `-n` only hands
# each worker more tests. So on a PyPy leg util/pytest_shards.py runs
# pytest once per directory `SHARDS` names, then once over the rest,
# and every run starts its workers afresh. Each run but the last writes
# a JUnit file of its own, beside junit.xml; the step fails if any did.
env:
COVERAGE: ${{ matrix.coverage }}
LATEST: ${{ matrix.python-version == needs.python-versions.outputs.latest }}
SHARDS: ${{ startsWith(matrix.python-version, 'pypy') && 'tests/protocols tests/corekit' || '' }}
run: |
if [ "$LATEST" = "true" ]; then
if ! skipped=$(python -c "from tests._tiers import project_pin_paths; print(' '.join(project_pin_paths()))"); then
echo "::${ANNOTATION_LEVEL} title=Could not compute the project pins::tests._tiers.project_pin_paths() failed -- see the traceback above."
exit 1
fi
elif ! skipped=$(python -c "from tests._tiers import latest_tier_paths; print(' '.join(latest_tier_paths()))"); then
echo "::${ANNOTATION_LEVEL} title=Could not compute the LATEST tier::tests._tiers.latest_tier_paths() failed -- see the traceback above."
exit 1
fi
set -- -q -n auto --dist load \
--durations=30 --durations-min=1.0 --junitxml="$RUNNER_TEMP/junit.xml" \
--ignore=tests/integration \
--ignore-glob='*_runtime.py' \
--ignore-glob='*_regression.py'
for module in $skipped; do
set -- "$@" --ignore="$module"
done
if [ "$COVERAGE" = "true" ]; then
# Logs every measured process and its parent, for the check below.
COVERAGE_DEBUG=pid,process COVERAGE_DEBUG_FILE="$PWD/coverage-processes.log" \
python util/cap_file_size.py python -m coverage run --rcfile=.github/coverage.toml -m pytest "$@"
elif [ -n "$SHARDS" ]; then
# #2005: #1963's script, on the leg's interpreter, under the one cap.
python util/cap_file_size.py python util/pytest_shards.py "$SHARDS" -- python -m pytest "$@"
else
python util/cap_file_size.py python -m pytest "$@"
fi
# util/coverage_report.py (#1967) fails, posting no number, unless every
# measured process saved its data (#1387). coverage-comment.yml posts the
# PR comment from the artifact, since callers of this reusable workflow
# grant only `contents: read`. `!cancelled()` so a failing test still
# reports what it measured. #2030: its summary goes to the job's copy too.
- name: Report coverage
if: ${{ !cancelled() && matrix.coverage }}
shell: bash
env:
SHA: ${{ github.sha }}
PYTHON: ${{ matrix.python-version }}
TESTS_OUTCOME: ${{ steps.tests.outcome }}
JOB_SUMMARY_COPY: ${{ runner.temp }}/job-summary.md
run: python util/coverage_report.py
# #1423: this log is the only record of a measured process that saved no
# data. Kept whenever the leg ran, not only on failure: a run that passed
# is the baseline the failing one has to be read against.
- name: Upload coverage process log
if: ${{ !cancelled() && matrix.coverage }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-processes-log
path: coverage-processes.log
retention-days: 30
if-no-files-found: warn
- name: Upload coverage HTML report
if: ${{ !cancelled() && matrix.coverage }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-html
path: coverage-html/
retention-days: 30
- name: Upload coverage summary
if: ${{ !cancelled() && matrix.coverage }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-summary
path: coverage-comment/coverage-summary.json
retention-days: 7
# #1538: ci-timing.yml reads these from the newest push runs. Diagnostic
# only, so an artifact service outage must not fail a gating leg. The glob
# takes a PyPy leg's JUnit file per run too (#2005).
- name: Upload test timings
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-unit-${{ matrix.python-version }}
path: ${{ runner.temp }}/junit*.xml
retention-days: 14
# #1672: the last step of every KTLO matrix job before the job-summary
# steps, util/leg_summary.py (#1967). Each step above that can fail on a
# KTLO leg has an `id` and `continue-on-error: ${{ matrix.ktlo == true }}`,
# so its failure leaves the job at success; this step makes it visible
# again, one warning and one job-summary line per failed step, from each
# `outcome` in `toJSON(steps)`. It runs on the image's /usr/bin/python3, as the leg's
# may have failed to set up. The runner's own `Process completed with
# exit code N.` error for a failed `run:` step cannot be removed, but
# does not change the job's conclusion.
- name: KTLO summary
if: ${{ !cancelled() && matrix.ktlo == true }}
continue-on-error: ${{ matrix.ktlo == true }}
shell: bash
env:
LEG: Python ${{ matrix.python-version }}${{ matrix.ktlo && ' (KTLO, non-blocking)' || '' }}
STEPS: ${{ toJSON(steps) }}
JOB_SUMMARY_COPY: ${{ runner.temp }}/job-summary.md
run: /usr/bin/python3 util/leg_summary.py ktlo
# #2030: the job's summary, for the push report util/check_suite_report.py
# writes, as the API serves none. Each step above that writes the summary
# copies it to job-summary.md, through util/_actions.py's
# JOB_SUMMARY_COPY or a `tee`; this names the job beside it, and the
# upload takes both. On `always()`, so a failed or timed-out leg's
# summary is kept too, and masked, as a diagnostic upload is.
- name: Name the job summary
if: ${{ always() }}
continue-on-error: true
shell: bash
env:
JOB_NAME: Python ${{ matrix.python-version }}${{ matrix.ktlo && ' (KTLO)' || '' }}
run: if [ -s "$RUNNER_TEMP/job-summary.md" ]; then printf '%s\n' "$JOB_NAME" > "$RUNNER_TEMP/job-summary.name"; fi
- name: Upload the job summary
if: ${{ always() }}
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: job-summary-${{ github.job }}-${{ strategy.job-index }}
path: |
${{ runner.temp }}/job-summary.md
${{ runner.temp }}/job-summary.name
if-no-files-found: ignore
retention-days: 7
# The KTLO tier (#1633): end-of-life versions that still build and test but
# never block. #1738, the owner's option A: their legs are this job's, not
# `test`'s, and neither `required-checks` nor `unittest-ordering` `needs:`
# it, so a failure no step can contain -- a lost runner, the job cap --
# fails nothing that gates. The steps are `test`'s, by YAML alias, so the
# leg concludes success and `KTLO summary` reports each failed step as a
# warning (#1672). The job-level `continue-on-error` is the backstop for the
# rest, so the Unit Tests run its `workflow_run` consumers read stays green.
#
# A version `ubuntu-latest` cannot provide carries its `runner` from the
# file as `os`: 3.8 and 3.9 need ubuntu-24.04, the newest image
# setup-python builds them for. A brownout of a deprecated image fails the
# leg before any step runs, which only the job-level guard contains.
test-ktlo:
name: Python ${{ matrix.python-version }} (KTLO)
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' && needs.changes.outputs.test-only != 'true' }}
needs: [changes, python-versions]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
timeout-minutes: 45
continue-on-error: true
env:
ANNOTATION_LEVEL: ${{ matrix.ktlo && 'warning' || 'error' }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.python-versions.outputs.test-ktlo) }}
steps: *test-steps
integration:
name: Integration Python ${{ matrix.python-version }}
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' }}
needs: [changes, python-versions]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
timeout-minutes: 45
# See the `test` job above.
env:
ANNOTATION_LEVEL: ${{ matrix.ktlo && 'warning' || 'error' }}
strategy:
fail-fast: false
# The supported tier of the `test` job above, without its coverage leg;
# the KTLO legs are `integration-ktlo`'s (#1738). On a test-only pull
# request, the `latest` leg alone, as in `test` (#2243).
matrix: ${{ fromJSON(needs.changes.outputs.test-only == 'true' && needs.python-versions.outputs.integration-latest || needs.python-versions.outputs.integration) }}
# `&integration-steps`: `integration-ktlo` runs these same steps.
steps: &integration-steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
id: checkout
continue-on-error: ${{ matrix.ktlo == true }}
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
id: python
continue-on-error: ${{ matrix.ktlo == true }}
with:
python-version: ${{ matrix.python-version }}
cache: pip
# Scapy builds examples/generators/pcap.py's and legacy.py's captures;
# pcapng.py needs requests (#1894), from the test extra. DPKT (#729) and
# `cli` (#738) unlock the gated methods under tests/integration, which
# this job's selection always includes. `crypto` and `NGAP` gate only
# unit-tier modules, which `test` runs. PyPCAPFile's methods here are
# #751's `pypcap-parity` job's, in a venv of its own.
#
# A KTLO leg that cannot install stops here, as in the `test` job above.
- name: Install package, test and generator dependencies
id: install
if: ${{ steps.python.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
shell: bash
env:
LEG: Python ${{ matrix.python-version }}${{ matrix.ktlo && ' (KTLO, non-blocking)' || '' }}
run: python util/pip_install.py -e '.[test,Scapy,DPKT,cli]'
# #1958: the `test` job's step of the same name. On `latest` this job runs
# the whole fixture tier, so it installs the whole suite's set too.
- name: Install the whole suite's dependencies on latest
id: install-latest
if: ${{ steps.install.outcome == 'success' && matrix.python-version == needs.python-versions.outputs.latest }}
continue-on-error: ${{ matrix.ktlo == true }}
run: python -m pip install -e '.[test,Scapy,DPKT,crypto,cli,NGAP,vendor,docs,PyShark]'
# util/regenerate_samples.py annotates a failure as a fixture failure, not
# a test one, and warns when the PCAP-NG captures are stand-ins.
- name: Regenerate sample captures
id: samples
if: ${{ steps.install.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
shell: bash
run: python util/regenerate_samples.py
# See the `test` job above for why this step exists.
- name: Report available parallelism
id: parallelism
if: ${{ steps.install.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
run: |
nproc
python -c "import os; print('cpu_count', os.cpu_count())"
# `test` already ran every unit-tier test on this commit, so this selects
# only the fixture-dependent tier plus the unit-tier modules whose capture
# read `test` skips (#715; see tests/_tiers.py's fixture_tier_paths()).
# The selection is asked of tests/_tiers.py (FIXTURE_TIER_SELECTOR) so the
# two cannot drift, and a failure to compute it never falls back to a
# bare `pytest -q`, which would silently re-run the whole suite.
#
# #1958, the test tiers: the fixture tier's `LATEST` modules run on
# `latest` alone. Every other leg asks for the `ALL` tier's part,
# FIXTURE_ALL_TIER_SELECTOR, which replaces a directory holding a `LATEST`
# module by its `ALL` ones, since pytest applies no ignore to a path it
# was handed.
#
# `read -ra` splits the selection on whitespace, as an unquoted
# expansion would, but with no pathname expansion of a node ID.
- name: Run full test suite
id: suite
if: ${{ steps.install.outcome == 'success' }}
continue-on-error: ${{ matrix.ktlo == true }}
shell: bash
env:
LATEST: ${{ matrix.python-version == needs.python-versions.outputs.latest }}
run: |
cannot_compute() {
echo "::${ANNOTATION_LEVEL} title=Could not compute the fixture-tier selection::tests._tiers.fixture_tier_paths() failed -- see the traceback above. Refusing to fall back to a bare 'pytest -q', which would silently re-run the entire suite instead of failing loudly."
exit 1
}
if [ "$LATEST" = "true" ]; then
selection=$(python -c "from tests._tiers import fixture_tier_paths; print(' '.join(fixture_tier_paths()))") || cannot_compute
else
selection=$(python -c "from tests._tiers import fixture_tier_paths; print(' '.join(fixture_tier_paths('ALL')))") || cannot_compute
echo "Not latest, so the ALL tier alone (#1958)."
fi
if [ -z "$selection" ]; then
echo "::${ANNOTATION_LEVEL} title=Fixture-tier selection is empty::tests._tiers.fixture_tier_paths() returned nothing, which cannot be right -- tests/integration alone should always be part of it. Refusing to run pytest with no arguments."
exit 1
fi
echo "Fixture-dependent selection: $selection"
read -ra selection <<< "$selection"
python util/cap_file_size.py python -m pytest -q -n auto --dist load \
--durations=30 --durations-min=1.0 --junitxml="$RUNNER_TEMP/junit.xml" "${selection[@]}"
# See the `test` job's step of the same name.
- name: Upload test timings
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-integration-${{ matrix.python-version }}
path: ${{ runner.temp }}/junit.xml
retention-days: 14
# #1672: see the `test` job's step of the same name.
- name: KTLO summary
if: ${{ !cancelled() && matrix.ktlo == true }}
continue-on-error: ${{ matrix.ktlo == true }}
shell: bash
env:
LEG: Python ${{ matrix.python-version }}${{ matrix.ktlo && ' (KTLO, non-blocking)' || '' }}
STEPS: ${{ toJSON(steps) }}
JOB_SUMMARY_COPY: ${{ runner.temp }}/job-summary.md
run: /usr/bin/python3 util/leg_summary.py ktlo
# #2030: see the `test` job's steps of the same names.
- name: Name the job summary
if: ${{ always() }}
continue-on-error: true
shell: bash
env:
JOB_NAME: Integration Python ${{ matrix.python-version }}${{ matrix.ktlo && ' (KTLO)' || '' }}
run: if [ -s "$RUNNER_TEMP/job-summary.md" ]; then printf '%s\n' "$JOB_NAME" > "$RUNNER_TEMP/job-summary.name"; fi
- name: Upload the job summary
if: ${{ always() }}
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: job-summary-${{ github.job }}-${{ strategy.job-index }}
path: |
${{ runner.temp }}/job-summary.md
${{ runner.temp }}/job-summary.name
if-no-files-found: ignore
retention-days: 7
# `integration`'s KTLO legs, apart for the reasons on `test-ktlo` (#1738).
integration-ktlo:
name: Integration Python ${{ matrix.python-version }} (KTLO)
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' && needs.changes.outputs.test-only != 'true' }}
needs: [changes, python-versions]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
timeout-minutes: 45
continue-on-error: true
env:
ANNOTATION_LEVEL: ${{ matrix.ktlo && 'warning' || 'error' }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.python-versions.outputs.integration-ktlo) }}
steps: *integration-steps
# Per-engine coverage for the third-party capture engines (#845): every
# (Python version, engine) cell resolves to one outcome, decided by the cell
# script's `expect` case below (anything not listed there is `supported`):
#
# * `supported` -- installs through its pyproject.toml extra, and
# its dedicated unit-tier tests must pass.
# * `unsupported` -- installable, but its PYTHON_CEILING declines it at
# run time (PyShark on 3.14+, PyPCAPFile on 3.12+).
# The raw distribution is installed past the extra's
# marker, and the same tests' version-aware
# assertions prove the decline fired.
# * `not-installable` -- PyPCAP on 3.12+: pcap.c does not compile against
# the 3.12+ C API. The raw distribution is installed
# past the marker to prove the failure is real, and
# that failure is the expected outcome, logged as a
# `::notice`; if it ever builds, the cell fails.
#
# One job per engine loops over its interpreters, each in a fresh venv: the
# queue is the bottleneck, so a job per cell would pay checkout, setup and
# apt for a minute of work, and the engine is what varies the environment
# (its apt packages, and pcap-ct and pypcap both installing a top-level
# `pcap`). tests/_dependency_gates.py's _engine_matrix_variants() reads
# `matrix.engine` as the one axis.
#
# #1633: the supported cells share one step, and any one failing fails the
# job. The KTLO cells are a step of their own with step-level
# `continue-on-error: true`, so a KTLO failure is warnings and a job-summary
# row (#1672) while the job, and `required-checks`, stay green. A version
# with a `runner` in .github/python-versions.toml is no cell here, since this
# job installs every interpreter on `ubuntu-latest`.
#
# Scoped out (#845): per-engine integration coverage, which needs regenerated
# captures and stays in aggregate in `integration` and `pypcap-parity`, and
# the multi-engine HAS_RUNTIME gate. There is no DPKT cell (#1538): it would
# run only what `test` runs.
engine-tests:
name: Engines (${{ matrix.engine }})
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' && needs.changes.outputs.test-only != 'true' }}
needs: [changes, python-versions]
runs-on: ubuntu-latest
timeout-minutes: 30
env:
ENGINE: ${{ matrix.engine }}
# Read by util/apt_packages.py's plan and install; see the note above
# the plan step.
APT_PACKAGES: ${{ matrix.engine == 'PyShark' && 'tshark' || matrix.engine == 'PCAP_CT' && 'libpcap0.8' || matrix.engine == 'PyPCAP' && 'build-essential libpcap-dev' || '' }}
APT_SET: ${{ matrix.engine }}
APT_CACHE_SAVE: 'true'
strategy:
fail-fast: false
matrix:
engine:
- Scapy
- PyShark
- PyPCAPFile
- PyPCAP
- PCAP_CT
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Each interpreter is then reachable as `python3.X`. The KTLO ones get a
# step of their own so that losing one cannot fail the job, first because
# the last version installed is the default `python`.
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
if: ${{ needs.python-versions.outputs.engine-ktlo != '' }}
continue-on-error: true
with:
python-version: ${{ needs.python-versions.outputs.engine-ktlo }}
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ needs.python-versions.outputs.engine-supported }}
# Only the engines that need a system package get one: tshark, which
# PyShark wraps; libpcap0.8, which PCAP_CT's ctypes loader finds at run
# time; and PyPCAP's compiler and headers, on every cell -- the
# `not-installable` ones too, so their failure is a genuine compile
# failure rather than no compiler.
#
# #974: an unbounded apt stall eats the whole job, whose cap reports
# `cancelled` and so fails `Required checks passed`. Not `-o
# Acquire::Retries=3 -o Acquire::http::Timeout=30`, which #974 proposed:
# both are noble's defaults already, and that per-`select()` timeout never
# fires on a mirror that trickles. Hence `timeout(1)` around each apt
# command: 60s update, 180s install (#1306), two attempts, inside the
# 10-minute step cap, which stays the hang detector.
#
# #1432: a slow mirror, not a dead one, can kill both attempts. So:
#
# * **The .deb files are cached**, so a warm run downloads nothing. The
# key is a digest of the `Inst` lines of `apt-get install -s` -- every
# package and exact version this image still lacks -- plus ImageOS,
# arch and APT_SET, so a stale set is never an exact hit. ImageVersion
# is left out, or the weekly image rollout would force a cold miss.
# apt uses a cached file only when its size and hash match the signed
# index, so a real install failure still fails on a hit.
# * **On a miss, attempt 2 goes to another mirror.** apt fails over
# within /etc/apt/apt-mirrors.txt only on an error, never on slowness,
# so azure is dropped from it before the retry.
#
# TestAptRetry in tests/project/test_unit_tests_queue.py pins the 180s
# bound. APT_PACKAGES, the job's env, is what both the plan and the
# install read, so the key always describes the set installed.
#
# The plan, install and collect steps run util/apt_packages.py (#1967);
# its docstring says what each subcommand does. The seed step, one copy
# and a count, stays inline. `pypcap-parity` runs the same steps; every
# body but the install's is identical to these.
- name: Plan this engine's system packages
id: apt-plan
if: env.APT_PACKAGES != ''
timeout-minutes: 4
run: /usr/bin/python3 util/apt_packages.py plan
- name: Restore this engine's cached .deb files
id: apt-cache
if: steps.apt-plan.outputs.key != ''
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/apt-debs/*.deb
key: ${{ steps.apt-plan.outputs.key }}
restore-keys: ${{ steps.apt-plan.outputs.restore-prefix }}
- name: Seed apt's archive with this engine's cached .deb files
if: steps.apt-cache.outputs.cache-matched-key != ''
run: |
set -eu
sudo cp "$RUNNER_TEMP"/apt-debs/*.deb /var/cache/apt/archives/
echo "Seeded $(find "$RUNNER_TEMP/apt-debs" -name '*.deb' | wc -l) .deb files"
- name: Install this engine's system packages
timeout-minutes: 10
run: /usr/bin/python3 util/apt_packages.py install --name "$ENGINE"
# #2019: from Ubuntu 25.04, apparmor 4.1 ships an enforcing profile for
# /usr/bin/tshark (/etc/apparmor.d/tshark) whose own file rules read only
# /tmp and /usr/share/wireshark, so on an ubuntu-26.04 image `tshark -r`
# is denied every capture under $GITHUB_WORKSPACE -- "You don't have
# permission to read the file", a TSharkCrashException with retcode 3.
# The profile's `include if exists <local/tshark>` is its site hook, so
# one read rule for the checkout goes there, in the profile's own
# `file r <glob>,` form, and `apparmor_parser -r` recompiles the profile
# with that include and replaces the loaded one. 24.04 has no such
# profile, so there this is a no-op. `pypcap-parity` runs the same body.
- name: Let tshark read the checkout past its AppArmor profile
if: env.ENGINE == 'PyShark'
run: |
set -eu
if [ -f /etc/apparmor.d/tshark ]; then
sudo mkdir -p /etc/apparmor.d/local
echo "file r ${GITHUB_WORKSPACE}/**," | sudo tee -a /etc/apparmor.d/local/tshark >/dev/null
sudo apparmor_parser -r /etc/apparmor.d/tshark
fi
- name: Collect this engine's installed .deb files for the cache
if: >-
env.APT_CACHE_SAVE == 'true' && steps.apt-plan.outputs.key != ''
&& steps.apt-cache.outputs.cache-hit != 'true'
run: /usr/bin/python3 util/apt_packages.py collect
- name: Save this engine's .deb files to the cache
if: >-
env.APT_CACHE_SAVE == 'true' && steps.apt-plan.outputs.key != ''
&& steps.apt-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/apt-debs/*.deb
key: ${{ steps.apt-plan.outputs.key }}
# The per-cell script, one cell in a venv of its own. Written to a file so
# each cell runs as a separate `bash` process: `set -e` is suspended
# inside a function called from an `if`, so an inline function would let
# a failed install carry on silently.
#
# `expect` also decides how the engine is installed: `supported` through
# its extra, the others as the raw distribution, bypassing the marker
# that would otherwise skip it silently (#845 finding 2). `set +e` covers
# only that install; the pip upgrade and the `test` extra run under
# `set -eu`, as a broken base install would otherwise read as "expected" (#845).
- name: Write the per-cell script
run: |
cat > "$RUNNER_TEMP/engine-cell.sh" <<'CELL'
set -eu
PY="$1"
CELL_NAME="$ENGINE on Python $PY"
case "$ENGINE:$PY" in
PyShark:3.14|PyShark:3.15) EXPECT=unsupported ;;
PyPCAPFile:3.12|PyPCAPFile:3.13|PyPCAPFile:3.14|PyPCAPFile:3.15) EXPECT=unsupported ;;
PyPCAP:3.12|PyPCAP:3.13|PyPCAP:3.14|PyPCAP:3.15) EXPECT=not-installable ;;
*) EXPECT=supported ;;
esac
echo "Cell: $CELL_NAME, expecting '$EXPECT'"
"python$PY" -m venv "$RUNNER_TEMP/venv-$PY"
# shellcheck disable=SC1090
. "$RUNNER_TEMP/venv-$PY/bin/activate"
python --version
python -m pip install -U pip setuptools wheel
python -m pip install -e '.[test]'
# RAW_PKGS is an array, not a string, so PCAP_CT's two-package case
# ("pcap-ct libpcap") reaches pip as two argv entries.
case "$ENGINE" in
Scapy) EXTRA=Scapy; RAW_PKGS=(scapy); IMPORT_NAME=scapy ;;
PyShark) EXTRA=PyShark; RAW_PKGS=(pyshark); IMPORT_NAME=pyshark ;;
PyPCAPFile) EXTRA=PyPCAPFile; RAW_PKGS=(pypcapfile); IMPORT_NAME=pcapfile ;;
PyPCAP) EXTRA=PyPCAP; RAW_PKGS=(pypcap); IMPORT_NAME=pcap ;;
PCAP_CT) EXTRA=PCAP_CT; RAW_PKGS=(pcap-ct libpcap); IMPORT_NAME=pcap ;;
*) echo "::${ANNOTATION_LEVEL}::unknown engine $ENGINE"; exit 1 ;;
esac
set +e
if [ "$EXPECT" = "supported" ]; then
output=$(python -m pip install -e ".[$EXTRA]" 2>&1)
else
output=$(python -m pip install "${RAW_PKGS[@]}" 2>&1)
fi
status=$?
set -e
echo "$output"
if [ "$status" -ne 0 ] && [ "$EXPECT" != "not-installable" ]; then
echo "::${ANNOTATION_LEVEL} title=$ENGINE failed to install on Python $PY::This cell expects '$EXPECT', which requires a successful install. pip exited $status."
exit 1
fi
if [ "$status" -eq 0 ] && [ "$EXPECT" = "not-installable" ]; then
echo "::${ANNOTATION_LEVEL} title=$ENGINE now installs on Python $PY::This cell is recorded as not-installable in this job's expect case (and in pyproject.toml's marker for $ENGINE). It installed cleanly -- update both rather than leaving this green for the wrong reason."
exit 1
fi
if [ "$status" -ne 0 ]; then
# A not-installable cell's point is a genuine C-API compile
# failure, not a flaky index: match pip's own network vocabulary
# and require the absence of a compile marker. Not a bare "5xx or
# 429" scan, which pip's "(NNN kB)" sizes and compiler line numbers
# would turn permanently red (#845).
if echo "$output" | grep -qiE \
'read timed out|connection (reset|refused|aborted)|temporary failure in name resolution|max retries exceeded|could not fetch url|newconnectionerror|readtimeouterror|httpsconnectionpool|retrying \(retry\(|http error (5[0-9][0-9]|429)|status code 429|too many 429 error responses|no matching distribution found' \
&& ! echo "$output" | grep -qiE \
'failed building wheel|error: command .* failed with exit code|subprocess-exited-with-error'; then
echo "::${ANNOTATION_LEVEL} title=$ENGINE's install failure on Python $PY looks transient%2C not a compile failure::pip exited $status, but the output matches a network/index failure pattern rather than the C-API compile failure this cell records as expected. Re-run the job rather than trusting this as a genuine 'not-installable' result."
exit 1
fi
echo "::notice title=$ENGINE is not installable on Python $PY (expected)::pip exited $status, matching this cell's recorded expectation ('not-installable'), and the output does not match a transient-failure pattern. No tests run for this cell -- this is a deliberate skip, not a pass."
echo "| $PY | $ENGINE | $EXPECT | not installable (expected) |" | tee -a "$RUNNER_TEMP/job-summary.md" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# #845 finding 2: a `supported` cell whose extra resolves to nothing
# still exits 0 and every test would then skip, so require the
# module this cell exists to exercise.
if ! python -c "import $IMPORT_NAME" 2>/dev/null; then
echo "::${ANNOTATION_LEVEL} title=$ENGINE installed but $IMPORT_NAME does not import on Python $PY::pip exited 0 but the module this cell exists to exercise is not importable -- a green install that unlocked nothing."
exit 1
fi
case "$ENGINE" in
Scapy) TEST_PATHS="tests/toolkit/test_scapy_unit.py tests/foundation/engines/test_scapy_engine.py tests/protocols/transport/test_sctp_unit.py tests/foundation/engines/test_engine_verbose_callback_1874_unit.py" ;;
PyShark) TEST_PATHS="tests/toolkit/test_pyshark_unit.py tests/foundation/engines/test_pyshark_engine.py tests/foundation/engines/test_engine_verbose_callback_1874_unit.py" ;;
PyPCAPFile) TEST_PATHS="tests/toolkit/test_pypcapfile_unit.py tests/foundation/engines/test_pypcapfile_engine.py tests/foundation/engines/test_engine_verbose_callback_1874_unit.py" ;;
PyPCAP) TEST_PATHS="tests/toolkit/test_pypcap_unit.py tests/foundation/engines/test_pypcap_engine.py tests/foundation/engines/test_pcap_backend.py tests/foundation/engines/test_engine_verbose_callback_1874_unit.py" ;;
PCAP_CT) TEST_PATHS="tests/toolkit/test_pcap_ct_unit.py tests/foundation/engines/test_pcap_ct_engine.py tests/foundation/engines/test_pcap_backend.py tests/foundation/engines/test_engine_verbose_callback_1874_unit.py" ;;
esac
echo "Running: $TEST_PATHS"
# shellcheck disable=SC2086
if ! python util/cap_file_size.py python -m pytest -q -n auto --dist load $TEST_PATHS; then
echo "::${ANNOTATION_LEVEL} title=$ENGINE tests failed on Python $PY::See the 'Python $PY ($ENGINE)' group in this step's log."
exit 1
fi
echo "| $PY | $ENGINE | $EXPECT | passed |" | tee -a "$RUNNER_TEMP/job-summary.md" >> "$GITHUB_STEP_SUMMARY"
CELL
{
echo "### Engine cells: $ENGINE"
echo
echo "| Python | Engine | Expect | Result |"
echo "| --- | --- | --- | --- |"
} | tee -a "$RUNNER_TEMP/job-summary.md" >> "$GITHUB_STEP_SUMMARY"
# See the `test` job above for why this step exists.
- name: Report available parallelism
run: |
nproc
python -c "import os; print('cpu_count', os.cpu_count())"
# Every cell runs even after one fails, so one red run reports all of
# them; the step fails at the end if any did. ANNOTATION_LEVEL is the
# level of the cell script's annotations (#1672).
- name: Run the supported cells
shell: bash
env:
PYTHONS: ${{ needs.python-versions.outputs.engine-supported }}
ANNOTATION_LEVEL: error
run: |
failed=()
for py in $PYTHONS; do
echo "::group::Python $py ($ENGINE)"
if ! bash "$RUNNER_TEMP/engine-cell.sh" "$py"; then
failed+=("$py")
echo "| $py | $ENGINE | | FAILED |" | tee -a "$RUNNER_TEMP/job-summary.md" >> "$GITHUB_STEP_SUMMARY"
fi
echo "::endgroup::"
done
if [ "${#failed[@]}" -ne 0 ]; then
echo "::error title=$ENGINE failed on Python ${failed[*]}::See each cell's group in this step's log and the job summary."
exit 1
fi
# KTLO, non-blocking (#1633); with no KTLO cell it loops over nothing.
# #1672: this step is the KTLO cells' report, as `KTLO summary` is a KTLO
# leg's in the matrix jobs, and an interpreter the setup step above could
# not install fails here.
- name: Run the KTLO cells (non-blocking)
shell: bash
continue-on-error: true
env:
PYTHONS: ${{ needs.python-versions.outputs.engine-ktlo }}
ANNOTATION_LEVEL: warning
run: |
failed=()
for py in $PYTHONS; do
echo "::group::Python $py ($ENGINE)"
if ! bash "$RUNNER_TEMP/engine-cell.sh" "$py"; then
failed+=("$py")
echo "| $py | $ENGINE | | FAILED (KTLO, non-blocking) |" | tee -a "$RUNNER_TEMP/job-summary.md" >> "$GITHUB_STEP_SUMMARY"
fi
echo "::endgroup::"
done
if [ "${#failed[@]}" -ne 0 ]; then
echo "::warning title=$ENGINE failed on Python ${failed[*]} (KTLO%2C non-blocking)::${failed[*]} is end-of-life and KTLO per #1633; this does not fail the job."
exit 1
fi
# #2030: see the `test` job's steps of the same names; the steps above
# copy the cells' table themselves, with `tee`.
- name: Name the job summary
if: ${{ always() }}
continue-on-error: true
shell: bash
env:
JOB_NAME: Engines (${{ matrix.engine }})
run: if [ -s "$RUNNER_TEMP/job-summary.md" ]; then printf '%s\n' "$JOB_NAME" > "$RUNNER_TEMP/job-summary.name"; fi
- name: Upload the job summary
if: ${{ always() }}
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: job-summary-${{ github.job }}-${{ strategy.job-index }}
path: |
${{ runner.temp }}/job-summary.md
${{ runner.temp }}/job-summary.name
if-no-files-found: ignore
retention-days: 7
# #751's ruling: try building upstream PyPCAP in CI, and drop it if CI is
# not a good fit. If a clean run of this job's install step goes red -- not
# a flake -- the fix is deleting this job and returning HAS_PYPCAP to
# tests/_dependency_gates.DEPENDENCY_GATE_EXCLUSIONS with that run linked.
#
# Apart from `engine-tests`: it needs a compiler that job has no other use
# for, and its `pcap` module collides with pcap-ct's. The HAS_PYPCAP-gated
# module, tests/foundation/engines/test_new_engine_parity_runtime.py, reads
# generated captures, so this mirrors `integration`'s fixture-tier selection
# and pays its run cost a second time; tests/_dependency_gates.py's guard
# knows no cheaper selection that reaches it. The same module carries some of
# HAS_PYPCAPFILE's gated methods, so PyPCAPFile is installed too.
#
# Both extras' markers are "python_version < '3.12'", so
# util/python_versions.py takes only the versions below 3.12 that run on
# `ubuntu-latest`, from both tiers (#1660); the KTLO ones are
# `pypcap-parity-ktlo`'s (#1738).
pypcap-parity:
name: PyPCAP/PyPCAPFile parity Python ${{ matrix.python-version }}
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' && needs.changes.outputs.test-only != 'true' }}
needs: [changes, python-versions]
runs-on: ubuntu-latest
timeout-minutes: 45
env:
# Read by util/apt_packages.py's plan and install; see `engine-tests`'s
# note on it.
APT_PACKAGES: build-essential libpcap-dev tshark
APT_SET: pypcap-parity
# Every cell installs the same set under the same key; only one saves it,
# or another's save fails with `Unable to reserve cache`. The supported
# leg, so the cache does not depend on a leg that may not get that far.
APT_CACHE_SAVE: ${{ matrix.python-version == '3.11' }}
# See the `test` job above.
ANNOTATION_LEVEL: ${{ matrix.ktlo && 'warning' || 'error' }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.python-versions.outputs.pypcap-parity) }}
# `&pypcap-parity-steps`: `pypcap-parity-ktlo` runs these same steps.
steps: &pypcap-parity-steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
id: checkout
continue-on-error: ${{ matrix.ktlo == true }}
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
id: python
continue-on-error: ${{ matrix.ktlo == true }}
with:
python-version: ${{ matrix.python-version }}
cache: pip
# pypcap ships no wheel: it compiles pcap.c against libpcap, so both the
# headers and the shared library have to be present before pip builds it.
#
# tshark joins them per #845's finding 1: this job's selection is the
# only place tests/integration/test_engine_runtime.py's real
# ``engine='pyshark'`` extraction is reachable. Without tshark the engine
# declines, the extractor silently falls back to the default parser, and
# the test passes on that fallback's output.
#
# The apt steps are `engine-tests`'s, for the reasons in the note above
# its plan step, with APT_SET keeping this job's cache entries apart; this
# job has failed the same ways (#1307, #1432).
- name: Plan the system packages
id: apt-plan
if: env.APT_PACKAGES != ''
continue-on-error: ${{ matrix.ktlo == true }}
timeout-minutes: 4
run: /usr/bin/python3 util/apt_packages.py plan
- name: Restore the cached .deb files
id: apt-cache
if: steps.apt-plan.outputs.key != ''
continue-on-error: ${{ matrix.ktlo == true }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/apt-debs/*.deb
key: ${{ steps.apt-plan.outputs.key }}
restore-keys: ${{ steps.apt-plan.outputs.restore-prefix }}
- name: Seed apt's archive with the cached .deb files
id: apt-seed
if: steps.apt-cache.outputs.cache-matched-key != ''
continue-on-error: ${{ matrix.ktlo == true }}
run: |
set -eu
sudo cp "$RUNNER_TEMP"/apt-debs/*.deb /var/cache/apt/archives/
echo "Seeded $(find "$RUNNER_TEMP/apt-debs" -name '*.deb' | wc -l) .deb files"
- name: Install libpcap headers, a C toolchain, and tshark
id: apt-install
continue-on-error: ${{ matrix.ktlo == true }}
timeout-minutes: 10
run: /usr/bin/python3 util/apt_packages.py install
# #2019: tshark's AppArmor profile on Ubuntu 25.04 and later; see
# `engine-tests`'s step of the same name, whose body this is.
- name: Let tshark read the checkout past its AppArmor profile
id: apparmor
continue-on-error: ${{ matrix.ktlo == true }}
run: |
set -eu
if [ -f /etc/apparmor.d/tshark ]; then
sudo mkdir -p /etc/apparmor.d/local
echo "file r ${GITHUB_WORKSPACE}/**," | sudo tee -a /etc/apparmor.d/local/tshark >/dev/null
sudo apparmor_parser -r /etc/apparmor.d/tshark
fi
- name: Collect the installed .deb files for the cache
if: >-