Skip to content

Tests

Tests #2

Workflow file for this run

name: Tests
# On-demand CI. Runs only when a user with WRITE access adds the `run-tests`
# label to a PR (or via the manual "Run workflow" button). Nothing runs on
# push or PR-open.
# Repository setup must provide the `run-tests` label before the first run.
#
# Re-running: the label is auto-removed after the run, so re-adding `run-tests`
# re-runs; you can also use "Re-run jobs", or workflow_dispatch below.
on:
pull_request_target:
types: [labeled]
workflow_dispatch:
inputs:
pr:
description: "PR number to test"
required: true
permissions:
contents: read
concurrency:
group: cpu-tests-${{ github.event.pull_request.number || inputs.pr }}-${{ github.event.label.name || 'manual' }}
cancel-in-progress: true
jobs:
authorize:
name: Authorize
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.label.name == 'run-tests'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
statuses: write
outputs:
pr: ${{ steps.ctx.outputs.pr }}
head_sha: ${{ steps.source.outputs.head_sha }}
merge_sha: ${{ steps.source.outputs.merge_sha }}
steps:
# Requirement: only users with WRITE (merge) access may run tests.
# Applying a label needs only "triage", so verify the actor here.
- name: Require write access
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const actor = context.eventName === 'workflow_dispatch'
? context.actor
: context.payload.sender.login;
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner, repo: context.repo.repo, username: actor,
});
if (!['admin', 'write'].includes(data.permission)) {
core.setFailed(`@${actor} lacks write access (has: ${data.permission}).`);
}
- name: Resolve PR number
id: ctx
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const pr = context.eventName === 'workflow_dispatch'
? Number(context.payload.inputs.pr)
: context.payload.pull_request.number;
if (!Number.isInteger(pr) || pr <= 0) {
core.setFailed(`Invalid PR number: ${context.payload.inputs?.pr}`);
return;
}
core.setOutput('pr', pr);
- name: Resolve immutable PR source
id: source
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { data } = await github.rest.pulls.get({
owner: context.repo.owner, repo: context.repo.repo,
pull_number: ${{ steps.ctx.outputs.pr }},
});
if (data.state !== 'open') {
core.setFailed(`PR #${data.number} is ${data.state}; only open PRs can be tested.`);
return;
}
const merge = await github.rest.git.getRef({
owner: context.repo.owner, repo: context.repo.repo,
ref: `pull/${data.number}/merge`,
});
const commit = await github.rest.git.getCommit({
owner: context.repo.owner, repo: context.repo.repo,
commit_sha: merge.data.object.sha,
});
if (!commit.data.parents.some(parent => parent.sha === data.head.sha)) {
core.setFailed('PR changed while its merge commit was being resolved; re-run the test.');
return;
}
core.setOutput('head_sha', data.head.sha);
core.setOutput('merge_sha', merge.data.object.sha);
- name: Mark status pending
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.repos.createCommitStatus({
owner: context.repo.owner, repo: context.repo.repo,
sha: '${{ steps.source.outputs.head_sha }}', state: 'pending',
context: 'cpu-tests', description: 'Running…',
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
test:
name: CPU tests (Python 3.12, no GPU)
needs: authorize
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
# Exercise the PR as merged with the current base branch.
ref: ${{ needs.authorize.outputs.merge_sha }}
allow-unsafe-pr-checkout: true
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
- name: Install CPU torch + dev deps + report tooling
run: |
uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu
uv pip install --system -e ".[dev]" pytest-html
- name: Run CPU-safe test suite
# This is a CPU smoke tier; the following require a GPU/CUDA stack and
# are covered by the GPU tier instead:
#
# Import-time pollution:
# test_placement_group - stubs sys.modules["ray"] at import,
# poisoning the process; run separately below.
# Backends not installed here:
# test_vllm_engine, test_sgl_engine_factory
# Import torchspec.inference.engine -> mooncake store.so, which needs
# the real CUDA driver (libcuda symbols) absent on a GPU-less runner:
# test_connector_slot_mapping, test_eagle3_trainer, test_hf_runner,
# test_mooncake_force_delete, test_sgl_engine_decode, test_hf_engine,
# test_offline_replay, test_save_config, test_dflash
# Genuine GPU ops (.cuda(), CUDA memory stats, inductor):
# test_build_eagle3_block_mask, test_flex_attention
run: |
pytest tests/ -q -p no:cacheprovider \
--ignore=tests/test_placement_group.py \
--ignore=tests/test_vllm_engine.py \
--ignore=tests/test_sgl_engine_factory.py \
--ignore=tests/test_connector_slot_mapping.py \
--ignore=tests/test_eagle3_trainer.py \
--ignore=tests/test_hf_runner.py \
--ignore=tests/test_mooncake_force_delete.py \
--ignore=tests/test_sgl_engine_decode.py \
--ignore=tests/test_hf_engine.py \
--ignore=tests/test_offline_replay.py \
--ignore=tests/test_save_config.py \
--ignore=tests/test_dflash.py \
--ignore=tests/test_build_eagle3_block_mask.py \
--ignore=tests/test_flex_attention.py \
--junit-xml=junit-main.xml \
--html=report-main.html --self-contained-html
- name: Run import-isolating tests in their own process
if: always()
run: |
pytest tests/test_placement_group.py -q -p no:cacheprovider \
--junit-xml=junit-isolated.xml \
--html=report-isolated.html --self-contained-html
- name: Upload HTML + JUnit reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-reports
path: |
report-*.html
junit-*.xml
report:
name: Publish results
needs: [authorize, test]
if: always() && needs.authorize.result == 'success'
runs-on: ubuntu-latest
permissions:
checks: write
statuses: write
pull-requests: write
steps:
# Base checkout (no PR code) gives dorny/test-reporter a git context.
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false
- name: Download reports
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: test-reports
- name: Remove trigger label (re-add it to re-run)
if: github.event_name != 'workflow_dispatch'
continue-on-error: true
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.issues.removeLabel({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: ${{ needs.authorize.outputs.pr }}, name: 'run-tests',
});
- name: Publish JUnit report as a check
continue-on-error: true
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
with:
name: pytest results
path: junit-*.xml
reporter: java-junit
fail-on-error: false
- name: Publish final commit status
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const ok = '${{ needs.test.result }}' === 'success';
await github.rest.repos.createCommitStatus({
owner: context.repo.owner, repo: context.repo.repo,
sha: '${{ needs.authorize.outputs.head_sha }}',
state: ok ? 'success' : 'failure', context: 'cpu-tests',
description: ok ? 'Passed' : 'Failed',
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
if (!ok) core.setFailed('Tests failed');