@@ -276,7 +276,7 @@ jobs:
276276 matrix :
277277 python-version : ["3.11", "3.13"]
278278 os : [ubuntu-latest]
279- shard : [p0, p1, p2]
279+ shard : [p0, p1, p2, fallback ]
280280 include :
281281 - os : macos-latest
282282 python-version : " 3.11"
@@ -287,6 +287,9 @@ jobs:
287287 - os : macos-latest
288288 python-version : " 3.11"
289289 shard : p2
290+ - os : macos-latest
291+ python-version : " 3.11"
292+ shard : fallback
290293 - os : windows-latest
291294 python-version : " 3.11"
292295 shard : p0
@@ -296,6 +299,9 @@ jobs:
296299 - os : windows-latest
297300 python-version : " 3.11"
298301 shard : p2
302+ - os : windows-latest
303+ python-version : " 3.11"
304+ shard : fallback
299305
300306 steps :
301307 - uses : actions/checkout@v4
@@ -381,16 +387,42 @@ jobs:
381387 if [ -n "${DISPATCH_MARKER}" ]; then
382388 EXPR="${DISPATCH_MARKER}"
383389 else
384- # Split by shard for parallel execution
390+ # Split by shard for parallel execution. The fallback shard
391+ # catches any integration test that lacks a p0/p1/p2 marker
392+ # so it can never be silently skipped.
385393 case "${{ matrix.shard }}" in
386394 p0) EXPR="integration and p0" ;;
387395 p1) EXPR="integration and p1" ;;
388396 p2) EXPR="integration and p2" ;;
397+ fallback) EXPR="integration and not (p0 or p1 or p2)" ;;
389398 esac
390399 fi
391400 echo "expr=$EXPR" >> "$GITHUB_OUTPUT"
392401 echo "Selected marker expression: $EXPR"
393402
403+ - name : Fail on unclassified integration tests
404+ if : |
405+ steps.check-integrated.outputs.has_tests == 'true' &&
406+ matrix.shard == 'fallback' &&
407+ matrix.os == 'ubuntu-latest' &&
408+ matrix.python-version == '3.11'
409+ shell : bash
410+ run : |
411+ # Guard: every integration test must carry a priority marker.
412+ # If the fallback shard collects anything, a new unclassified
413+ # test slipped in -- fail loudly instead of silently running
414+ # it outside the three priority shards.
415+ UNCLASSIFIED=$(python -m pytest tests/integration --collect-only -q \
416+ -m "integration and not (p0 or p1 or p2)" 2>/dev/null \
417+ | grep -c "::" || true)
418+ if [ "${UNCLASSIFIED}" -gt 0 ]; then
419+ echo "::error::${UNCLASSIFIED} integration test(s) lack a p0/p1/p2 priority marker. Assign one so the test joins a priority shard."
420+ python -m pytest tests/integration --collect-only -q \
421+ -m "integration and not (p0 or p1 or p2)" 2>/dev/null | grep "::" || true
422+ exit 1
423+ fi
424+ echo "No unclassified integration tests."
425+
394426 - name : Run integrated tests
395427 if : steps.check-integrated.outputs.has_tests == 'true'
396428 shell : bash
@@ -412,16 +444,30 @@ jobs:
412444 pytest tests/integration -v --no-cov \
413445 -n auto --dist=loadscope --timeout=300 \
414446 -m "${{ steps.marker.outputs.expr }}"
415- cp .integration_coverage/integration_subproc \
416- .coverage.integration.${{ matrix.shard }}
417- # `coverage xml` honours fail_under and exits 2 when below;
418- # tolerate that — the combined value is what matters.
419- coverage xml --data-file=.coverage.integration.${{ matrix.shard }} \
420- -o coverage.integration.${{ matrix.shard }}.xml || [ "$?" -eq 2 ]
447+ PYTEST_RC=$?
448+ # exit 5 = no tests collected: expected for the fallback
449+ # shard when every integration test carries a priority
450+ # marker. Any other nonzero code still fails.
451+ if [ "$PYTEST_RC" -ne 0 ] && [ "$PYTEST_RC" -ne 5 ]; then
452+ exit "$PYTEST_RC"
453+ fi
454+ if [ -f .integration_coverage/integration_subproc ]; then
455+ cp .integration_coverage/integration_subproc \
456+ .coverage.integration.${{ matrix.shard }}
457+ # `coverage xml` honours fail_under and exits 2 when below;
458+ # tolerate that — the combined value is what matters.
459+ coverage xml --data-file=.coverage.integration.${{ matrix.shard }} \
460+ -o coverage.integration.${{ matrix.shard }}.xml || [ "$?" -eq 2 ]
461+ fi
421462 else
422463 pytest tests/integration -v \
423464 -n auto --dist=loadscope --timeout=300 \
424- -m "${{ steps.marker.outputs.expr }}"
465+ -m "${{ steps.marker.outputs.expr }}" || {
466+ RC=$?
467+ # exit 5 = no tests collected: expected for the
468+ # fallback shard when nothing is unclassified.
469+ [ "$RC" -eq 5 ] || exit "$RC"
470+ }
425471 fi
426472
427473 - name : Upload integration coverage data
@@ -437,6 +483,10 @@ jobs:
437483 coverage.integration.${{ matrix.shard }}.xml
438484 retention-days : 1
439485 include-hidden-files : true
486+ # The fallback shard produces no data file when every
487+ # integration test carries a priority marker; a missing
488+ # artifact there is expected, not an error.
489+ if-no-files-found : ignore
440490
441491 coverage-report :
442492 name : Coverage Report
@@ -480,13 +530,16 @@ jobs:
480530 - name : Combine all coverage data
481531 shell : bash
482532 run : |
483- # First, combine the three integration shards into one
484- coverage combine --data-file=.coverage.integration \
485- .coverage.integration.p0 \
486- .coverage.integration.p1 \
487- .coverage.integration.p2
488- coverage xml --data-file=.coverage.integration \
489- -o coverage.integration.xml || [ "$?" -eq 2 ]
533+ # First, combine the integration shards into one. The fallback
534+ # shard only produces a data file when it actually ran tests
535+ # (i.e. some integration test lacked a priority marker), so
536+ # include whichever shard files exist.
537+ SHARDS=$(ls .coverage.integration.* 2>/dev/null || true)
538+ if [ -n "$SHARDS" ]; then
539+ coverage combine --data-file=.coverage.integration $SHARDS
540+ coverage xml --data-file=.coverage.integration \
541+ -o coverage.integration.xml || [ "$?" -eq 2 ]
542+ fi
490543
491544 # Then, combine all three tiers: unit, contract, and integration.
492545 coverage combine \
0 commit comments