Skip to content

Commit 2b797dc

Browse files
authored
ci: queue the ordering cells after the unit legs, run Engines 3.15 on main only, drop Engines (DPKT) (#1538) (#1551)
1 parent 3be3fdd commit 2b797dc

5 files changed

Lines changed: 102 additions & 43 deletions

File tree

‎.github/workflows/unit-tests.yml‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,8 @@ jobs:
443443
# ever starts building, the cell fails loudly.
444444
#
445445
# One job per engine loops over the six interpreters, each in a fresh venv,
446-
# rather than one job per cell (6 Pythons x 6 engines): the queue, not the
447-
# runners, is the bottleneck, and 36 jobs would each pay checkout, setup and
446+
# rather than one job per cell (6 Pythons x 5 engines): the queue, not the
447+
# runners, is the bottleneck, and 30 jobs would each pay checkout, setup and
448448
# apt for a minute of work. One job per *engine* rather than per Python
449449
# because the engine is what varies the environment: the apt packages
450450
# are per engine (installed once per job instead of six times), and pcap-ct
@@ -459,17 +459,17 @@ jobs:
459459
# the job summary.
460460
#
461461
# 3.15 is experimental-prerelease per #845's ruling: present so a regression
462-
# is visible, but never blocking. It runs as its own step with step-level
463-
# `continue-on-error: true`, so a 3.15 failure shows as a failed step and an
464-
# error annotation while the job -- and `required-checks`, which reads this
465-
# job's result -- stays green. The 3.10-3.14 cells share one step, and any
466-
# one of them failing fails the job.
462+
# is visible, but never blocking; since #1538 it runs on main pushes only.
463+
# It is its own step with step-level `continue-on-error: true`, so a 3.15
464+
# failure shows as a failed step and an error annotation while the job --
465+
# and `required-checks`, which reads its result -- stays green. The 3.10-3.14
466+
# cells share one step, and any one of them failing fails the job.
467467
#
468-
# Scoped deliberately out (#845): per-engine integration
469-
# coverage (needs regenerated captures, so it stays in aggregate in
470-
# `integration` and `pypcap-parity`), and the multi-engine HAS_RUNTIME gate
471-
# plus the incidental crypto/NGAP/vendor coverage the pre-#845 shared venv
472-
# carried.
468+
# Scoped deliberately out (#845): per-engine integration coverage (needs
469+
# regenerated captures, so it stays in aggregate in `integration` and
470+
# `pypcap-parity`), the multi-engine HAS_RUNTIME gate and the incidental
471+
# crypto/NGAP/vendor coverage of the pre-#845 shared venv. #1538 dropped the
472+
# DPKT cell: it ran only test_dpkt_unit.py, which `test` runs with DPKT.
473473
engine-tests:
474474
name: Engines (${{ matrix.engine }})
475475
if: ${{ inputs.gate-only != true && needs.changes.outputs.code != 'false' }}
@@ -486,7 +486,6 @@ jobs:
486486
fail-fast: false
487487
matrix:
488488
engine:
489-
- DPKT
490489
- Scapy
491490
- PyShark
492491
- PyPCAPFile
@@ -731,7 +730,6 @@ jobs:
731730
# RAW_PKGS is an array, not a string, so PCAP_CT's two-package case
732731
# ("pcap-ct libpcap") reaches pip as two argv entries.
733732
case "$ENGINE" in
734-
DPKT) EXTRA=DPKT; RAW_PKGS=(dpkt); IMPORT_NAME=dpkt ;;
735733
Scapy) EXTRA=Scapy; RAW_PKGS=(scapy); IMPORT_NAME=scapy ;;
736734
PyShark) EXTRA=PyShark; RAW_PKGS=(pyshark); IMPORT_NAME=pyshark ;;
737735
PyPCAPFile) EXTRA=PyPCAPFile; RAW_PKGS=(pypcapfile); IMPORT_NAME=pcapfile ;;
@@ -787,7 +785,6 @@ jobs:
787785
fi
788786
789787
case "$ENGINE" in
790-
DPKT) TEST_PATHS="tests/toolkit/test_dpkt_unit.py" ;;
791788
Scapy) TEST_PATHS="tests/toolkit/test_scapy_unit.py tests/foundation/engines/test_scapy_engine.py tests/protocols/transport/test_sctp_unit.py" ;;
792789
PyShark) TEST_PATHS="tests/toolkit/test_pyshark_unit.py tests/foundation/engines/test_pyshark_engine.py" ;;
793790
PyPCAPFile) TEST_PATHS="tests/toolkit/test_pypcapfile_unit.py tests/foundation/engines/test_pypcapfile_engine.py" ;;
@@ -834,8 +831,9 @@ jobs:
834831
exit 1
835832
fi
836833
837-
# Non-blocking by #845's ruling -- see this job's comment above.
834+
# Non-blocking by #845, main pushes only by #1538 -- see this job's comment.
838835
- name: Run the 3.15 cell (experimental, non-blocking)
836+
if: ${{ github.event_name != 'pull_request' }}
839837
shell: bash
840838
continue-on-error: true
841839
run: |
@@ -1191,10 +1189,12 @@ jobs:
11911189
# maintainer's call to make separately from landing it.
11921190
unittest-ordering:
11931191
name: Plain unittest ordering (${{ matrix.label || matrix.leg }})
1194-
# Not on pull requests: it is not a required check, and its 11 cells
1195-
# would be a sixth of every PR push's jobs. It still runs on every push
1196-
# to main, which is the run that tests what actually lands.
1197-
if: ${{ inputs.gate-only != true && github.event_name != 'pull_request' }}
1192+
# Every push to main and no PR (#1052 item H): not required, and its 11
1193+
# cells would be a sixth of each PR push's jobs. `needs: [test]` (#1538)
1194+
# queues them behind this push's own legs; `!cancelled()` still runs them
1195+
# if `test` fails, so a red cell still fails the run that gates publishing.
1196+
if: ${{ !cancelled() && inputs.gate-only != true && github.event_name != 'pull_request' }}
1197+
needs: [test]
11981198
runs-on: ubuntu-latest
11991199
# 45, not 20: `timeout-minutes` is job-level, so it has to cover
12001200
# checkout/setup/install (~39s) as well as the leg itself, and this is

‎docs/source/contributing/workflows.rst‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ repository target the same reusable workflow, with the same input:
245245
not one: ``gate`` (the full suite, one Python version) and ``changelog``
246246
(checks ``CHANGELOG.md`` against its source entry). It skips the other
247247
**eight** -- the five matrix jobs, ``test`` (five Python legs),
248-
``integration`` (five), ``engine-tests`` (one job per engine, each looping
249-
over six Python versions), ``pypcap-parity`` (two) and ``unittest-ordering``
248+
``integration`` (five), ``engine-tests`` (one job for each of five engines,
249+
each looping over six Python versions), ``pypcap-parity`` (two) and ``unittest-ordering``
250250
(eleven matrix cells), each of which has already run once for this commit
251251
from Unit Tests' own ``push`` trigger (and, all but ``unittest-ordering``,
252252
which runs on ``main`` pushes only, from its ``pull_request`` trigger), so
@@ -480,6 +480,32 @@ while the pool stays saturated, and the first runs with JUnit will show whether
480480
``W7``'s 10-second test line needs raising. What to do when one fires is on
481481
:doc:`conventions/process`.
482482

483+
Work Kept Off the Pull-Request Queue (`#1538 <https://github.com/JarryShaw/PyPCAPKit/issues/1538>`__)
484+
-----------------------------------------------------------------------------------------------------
485+
486+
Three cuts in :file:`unit-tests.yml` keep work that cannot gate a merge from competing
487+
with pull-request jobs for the 20-runner pool. None touches a required check or adds a
488+
permission to the reusable workflow.
489+
490+
* ``unittest-ordering`` runs on ``main`` pushes only, per `#1052
491+
<https://github.com/JarryShaw/PyPCAPKit/issues/1052>`__ item H, and has
492+
``needs: [test]``, so its eleven cells join the queue once ``test`` finishes rather
493+
than when the push run is created, ahead of that run's own ``Python`` legs and of
494+
pull-request jobs created later in a merge wave. Its ``if:`` opens with
495+
``!cancelled()``, so the cells still run when ``test`` fails, and a red cell still
496+
fails the push run that GitHub Pages, Conda Update and Vendor Update (and so Create
497+
Release) wait on. The cost is wall time: the push run, and those workflows, finish
498+
about one slowest cell later, about 6 min before any queue wait (estimate).
499+
* ``engine-tests``' ``Run the 3.15 cell (experimental, non-blocking)`` step runs on
500+
``main`` pushes only. This narrows `#845 <https://github.com/JarryShaw/PyPCAPKit/issues/845>`__'s
501+
ruling -- present so a regression is visible, never blocking -- from every run to
502+
every ``main`` push: a 3.15 regression shows within one merge. The step keeps
503+
``continue-on-error: true``.
504+
* ``engine-tests`` has no ``DPKT`` cell. Its only path was
505+
:file:`tests/toolkit/test_dpkt_unit.py`, which ``test`` runs with ``DPKT`` installed
506+
on 3.10-3.14. Dropped with it: that file in a venv holding only ``test`` and ``DPKT``,
507+
and on 3.15.
508+
483509
Required Status Checks
484510
----------------------
485511

‎tests/_dependency_gates.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ class Exclusion(NamedTuple):
327327
'engine can start.\n\n'
328328
"#751's dedicated engine-tests job now takes this: it installs PCAP_CT and a "
329329
'system libpcap (apt-get libpcap0.8) on its own PCAP_CT matrix cell, across '
330-
'the same 3.10-3.14 (plus non-blocking 3.15) matrix -- rebuilt per #849 as one '
330+
'the same 3.10-3.14 (plus non-blocking 3.15, main pushes only) matrix -- '
331+
'rebuilt per #849 as one '
331332
'venv per (Python version, engine) pair rather than the one shared venv #751 '
332333
'first landed, so PCAP_CT is kept apart from PyPCAP by construction now, not '
333334
'merely by convention -- since pypcap and pcap-ct both install a top-level '
@@ -390,7 +391,7 @@ class Exclusion(NamedTuple):
390391
"side effect of installing everything together. #849's rebuild removes that "
391392
'shared venv: each matrix.engine cell now installs only "test" plus its own '
392393
'engine extra and runs only that engine\'s own explicit TEST_PATHS -- '
393-
'tests/toolkit/test_dpkt_unit.py and five siblings, never tests/vendor/ -- so '
394+
'tests/toolkit/test_scapy_unit.py and four siblings, never tests/vendor/ -- so '
394395
'no cell reaches these gates at all any more, let alone installs vendor for '
395396
'them. This is not a *new* gap this guard reports, because '
396397
':func:`dependency_gate_gaps` only counts a job that reaches a gate without '
@@ -546,7 +547,7 @@ class Exclusion(NamedTuple):
546547
"one shared venv, closing this gap as a side effect: all 5 methods ran "
547548
"wherever engine-tests did. #849's rebuild removes that shared venv -- each "
548549
'matrix.engine cell now installs exactly one engine\'s own extra, so no cell '
549-
'installs all three together any more, and none of the six explicit '
550+
'installs all three together any more, and none of the five explicit '
550551
'TEST_PATHS lists names test_runtime_engines.py at all, so engine-tests no '
551552
'longer reaches this gate either. Like the HAS_SCAPY and HAS_VENDOR_DEPS '
552553
'entries above, this does not surface as a wider Gap -- a job that stops '
@@ -601,7 +602,7 @@ class Exclusion(NamedTuple):
601602
'tests/vendor/test_vendor_reg_apptype_generator_unit.py -- the ignore-shape '
602603
'leg and the whole-suite one. engine-tests used to be a third, back when its '
603604
"one shared venv mirrored test's ignore-shape exactly; #849's rebuild to one "
604-
"explicit TEST_PATHS list per matrix.engine cell means none of the six now "
605+
"explicit TEST_PATHS list per matrix.engine cell means none of the five now "
605606
'names tests/vendor/ at all, so engine-tests no longer reaches this gate and '
606607
'has dropped out of this entry\'s dark dict accordingly -- not widened or '
607608
'narrowed against a gap that is still there, but genuinely gone, the case '
@@ -1319,7 +1320,7 @@ def _engine_matrix_variants(name: 'str', section: 'str') -> 'Optional[tuple[Job,
13191320
in its pytest step. Modelling that as one :class:`Job` with every engine's
13201321
extra and a ``'whole-suite'`` selection -- the reading a single static
13211322
install-line/selection pair would produce -- is exactly the false
1322-
confidence #745 exists to catch: it would credit the DPKT cell with
1323+
confidence #745 exists to catch: it would credit the Scapy cell with
13231324
PyPCAP's install and credit every cell with reaching every other engine's
13241325
tests. One :class:`Job` per engine, sharing this job's ``name``, is what
13251326
:func:`dependency_gate_gaps`'s ``(flag, job.name)`` aggregation expects --

‎tests/project/test_unit_tests_queue.py‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
* The ``changes`` classifier must call anything outside ``docs/`` and ``*.md``
1111
code, and a Markdown file under a code directory code too.
1212
* ``unittest-ordering`` stays off pull requests and out of the required set,
13-
and ``engine-tests``'s 3.15 cell stays non-blocking through a step-level
14-
``continue-on-error`` rather than a job-level one.
13+
and queues behind ``test`` without needing it to pass (#1538);
14+
``engine-tests``'s 3.15 cell stays non-blocking through a step-level
15+
``continue-on-error`` rather than a job-level one, runs on ``main`` pushes
16+
only, and the job has no ``DPKT`` cell (#1538).
1517
* The classifier, the engine cell loops and the apt retry are executed too: the
1618
classifier against a scratch merge commit, the others with their commands
1719
stubbed on ``PATH``.
@@ -224,6 +226,32 @@ def test_engine_tests_is_one_job_per_engine_with_a_non_blocking_315_step(self) -
224226
self.assertNotIn('continue-on-error', gating)
225227
self.assertIn('for py in 3.10 3.11 3.12 3.13 3.14;', gating)
226228

229+
def test_unittest_ordering_queues_behind_test_and_survives_its_failure(self) -> None:
230+
"""#1538 P4(a): queued after ``test``, and ``!cancelled()`` so a red ``test`` still runs it.
231+
232+
Without the status function the implicit ``success()`` would skip every cell
233+
whenever ``test`` failed, and the push run would stop failing on ordering.
234+
235+
"""
236+
head = header(job('unittest-ordering'))
237+
self.assertRegex(head, r'(?m)^ needs: \[test\]$')
238+
self.assertIn(" if: ${{ !cancelled() && inputs.gate-only != true"
239+
" && github.event_name != 'pull_request' }}\n", head)
240+
241+
def test_the_315_cell_runs_on_main_pushes_only(self) -> None:
242+
"""#1538 P6: a step-level ``if:``, which narrows #845's ruling to ``main`` pushes."""
243+
experimental = step(job('engine-tests'), 'Run the 3.15 cell (experimental, non-blocking)')
244+
self.assertIn(" if: ${{ github.event_name != 'pull_request' }}\n", experimental)
245+
246+
def test_engine_tests_has_no_dpkt_cell(self) -> None:
247+
"""#1538 P8: its only path, test_dpkt_unit.py, already runs in ``test`` with DPKT."""
248+
section = job('engine-tests')
249+
engines = re.search(r'(?m)^ engine:\n((?: - \w+\n)+)', section)
250+
self.assertIsNotNone(engines)
251+
declared = re.findall(r'- (\w+)', engines.group(1)) # type: ignore[union-attr]
252+
self.assertNotIn('DPKT', declared)
253+
self.assertNotRegex(section, r'(?m)^ +DPKT\)')
254+
227255

228256
def step(section: 'str', name: 'str') -> 'str':
229257
"""The text of the step called ``name`` in ``section``, up to the next step."""
@@ -368,7 +396,7 @@ def run_loop(self, name: 'str', fail: 'str') -> 'tuple[int, list[str]]':
368396
(self.tmp / 'engine-cell.sh').write_text(
369397
'echo "$1" >> "$RUNNER_TEMP/calls"\n[ "$1" != "$FAIL_PY" ]\n', encoding='utf-8')
370398
status = bash(step_script(job('engine-tests'), name),
371-
{'RUNNER_TEMP': str(self.tmp), 'ENGINE': 'DPKT', 'FAIL_PY': fail,
399+
{'RUNNER_TEMP': str(self.tmp), 'ENGINE': 'Scapy', 'FAIL_PY': fail,
372400
'GITHUB_STEP_SUMMARY': str(self.tmp / 'summary')})
373401
calls = self.tmp / 'calls'
374402
return status, calls.read_text(encoding='utf-8').split() if calls.exists() else []

‎tests/test_tier_guard.py‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,7 +1646,7 @@ def test_the_mypy_gate_is_visible_and_dark_on_every_job_that_reaches_it(self) ->
16461646
# collect tests/vendor/ at all -- the same reason they are absent from
16471647
# HAS_VENDOR_DEPS above. engine-tests (#849) no longer reaches it
16481648
# either: each matrix.engine cell now runs an explicit TEST_PATHS list
1649-
# and none of the six names tests/vendor/ -- see the HAS_MYPY
1649+
# and none of the five names tests/vendor/ -- see the HAS_MYPY
16501650
# exclusion's own reason for why that is a real, disclosed change
16511651
# rather than a mistake here.
16521652
for job in ('integration', 'pypcap-parity', 'engine-tests'):
@@ -1706,7 +1706,7 @@ def test_the_isort_gate_is_visible_and_closed_by_the_test_extra(self) -> None:
17061706
'the ignore-shape leg and the whole-suite one collect tests/project/; '
17071707
'integration and pypcap-parity select by fixture tier and never reach it, '
17081708
'the same reason they are absent from HAS_MYPY above. engine-tests (#849) '
1709-
'no longer reaches it either -- none of its six explicit TEST_PATHS lists '
1709+
'no longer reaches it either -- none of its five explicit TEST_PATHS lists '
17101710
'names tests/project/, the same change that dropped it out of HAS_MYPY above')
17111711

17121712
gaps = {(gap.flag, gap.job) for gap in _dependency_gates.dependency_gate_gaps()}
@@ -2020,7 +2020,8 @@ def test_removing_dpkt_is_caught_on_every_job_that_installs_it(self) -> None:
20202020
-- and, separately, engine-tests no longer reaches
20212021
test_runtime_engines.py regardless (see the HAS_RUNTIME exclusion),
20222022
so there is no longer a DPKT-via-engine-tests gap for this doctoring
2023-
to reopen. Four jobs, not five.
2023+
to reopen. Four jobs, not five. (#1538 later dropped that DPKT cell
2024+
outright: it ran only test_dpkt_unit.py, which ``test`` runs too.)
20242025
20252026
"""
20262027
text = _dependency_gates.WORKFLOW.read_text(encoding='utf-8')
@@ -2337,20 +2338,20 @@ def test_a_test_paths_arm_with_no_matching_extra_arm_is_caught(self) -> None:
23372338
23382339
Commenting out one engine's ``EXTRA=`` arm -- rather than renaming it, which the
23392340
new matrix-list-vs-case-arms equality check above would catch first, for a
2340-
different reason -- leaves DPKT declared in the ``engine:`` list and still
2341-
present in ``engine_paths`` (so the equality check sees the same six engines on
2341+
different reason -- leaves Scapy declared in the ``engine:`` list and still
2342+
present in ``engine_paths`` (so the equality check sees the same five engines on
23422343
both sides and stays quiet), but with no ``EXTRA=`` arm of its own: exactly the
23432344
shape the per-engine loop's own check exists for.
23442345
23452346
"""
2346-
doctored = doctored_workflow(self, 'DPKT) EXTRA=DPKT;', '# DPKT) EXTRA=DPKT;')
2347+
doctored = doctored_workflow(self, 'Scapy) EXTRA=Scapy;', '# Scapy) EXTRA=Scapy;')
23472348

23482349
with self.assertRaises(AssertionError) as ctx:
23492350
_dependency_gates.pytest_jobs(doctored)
23502351

23512352
message = str(ctx.exception)
23522353
self.assertIn('engine-tests', message)
2353-
self.assertIn('DPKT', message)
2354+
self.assertIn('Scapy', message)
23542355

23552356
def test_a_commented_out_test_paths_arm_does_not_silently_win(self) -> None:
23562357
"""The relocated blocker the second cross-review flagged, not just commented on.
@@ -2364,17 +2365,20 @@ def test_a_commented_out_test_paths_arm_does_not_silently_win(self) -> None:
23642365
commented-out arm from being read as a live one.
23652366
23662367
"""
2368+
live = ('PyShark) TEST_PATHS="tests/toolkit/test_pyshark_unit.py '
2369+
'tests/foundation/engines/test_pyshark_engine.py" ;;')
23672370
doctored = doctored_workflow(
23682371
self,
2369-
'DPKT) TEST_PATHS="tests/toolkit/test_dpkt_unit.py" ;;',
2370-
'DPKT) TEST_PATHS="tests/toolkit/test_dpkt_unit.py" ;;\n'
2371-
' # DPKT) TEST_PATHS="tests/toolkit/test_pypcap_unit.py" ;;',
2372+
live,
2373+
live + '\n'
2374+
' # PyShark) TEST_PATHS="tests/toolkit/test_pypcap_unit.py" ;;',
23722375
)
23732376

23742377
jobs = _dependency_gates.pytest_jobs(doctored)
2375-
dpkt_jobs = [job for job in jobs if job.name == 'engine-tests' and 'DPKT' in job.extras]
2376-
self.assertEqual(len(dpkt_jobs), 1)
2377-
self.assertEqual(dpkt_jobs[0].paths, ('tests/toolkit/test_dpkt_unit.py',))
2378+
pyshark_jobs = [job for job in jobs if job.name == 'engine-tests' and 'PyShark' in job.extras]
2379+
self.assertEqual(len(pyshark_jobs), 1)
2380+
self.assertEqual(pyshark_jobs[0].paths, ('tests/toolkit/test_pyshark_unit.py',
2381+
'tests/foundation/engines/test_pyshark_engine.py'))
23782382

23792383
def test_the_undoctored_workflow_produces_no_unexplained_gap(self) -> None:
23802384
"""The control: the tests above fail for the doctoring, not by default."""

0 commit comments

Comments
 (0)