Skip to content

Commit bada29d

Browse files
authored
Merge pull request #942 from mwcraig/array-api-strict-triage-tooling
Add array-api-strict test backend and array-escape triage tooling
2 parents 9d25eee + d329ac5 commit bada29d

14 files changed

Lines changed: 1363 additions & 4 deletions

.github/workflows/ci_tests.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ jobs:
5858
python: '3.12'
5959
tox_env: 'py312-alldeps-dask'
6060

61+
# Regression gate: fail if a new array-API "escape" (a silent numpy
62+
# coercion of a dask array in library code) appears that is not in
63+
# the checked-in baseline, ccdproc/tests/array_escape_baseline.txt.
64+
# No bottleneck here (as with the dask job above) so the numpy
65+
# fallback paths are exercised.
66+
- name: 'ubuntu-py312-dask-escape-baseline'
67+
os: ubuntu-latest
68+
python: '3.12'
69+
tox_env: 'py312-alldeps-dask-enforce'
70+
6171
- name: 'windows-py312'
6272
os: windows-latest
6373
python: '3.12'
@@ -108,3 +118,77 @@ jobs:
108118
- name: Upload coverage to codecov
109119
if: "endsWith(matrix.tox_env, '-cov')"
110120
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
121+
122+
# The strict array-API job is in its own matrix so that its failures --
123+
# expected until the remaining array-API bugs are fixed -- stay visible
124+
# without cancelling the main test matrix above. The test step uses
125+
# continue-on-error so the job (and the PR checks rollup) stays green;
126+
# the real outcome is reported as a warning annotation, in the step
127+
# summary, and via a per-matrix-entry strict-job-outcome-* artifact that
128+
# strict_status.yml turns into a check run on the PR.
129+
ci-tests-expected-failures:
130+
name: ${{ matrix.name }}
131+
runs-on: ${{ matrix.os }}
132+
if: "!(contains(github.event.head_commit.message, '[skip ci]') || contains(github.event.head_commit.message, '[ci skip]'))"
133+
strategy:
134+
matrix:
135+
include:
136+
- name: 'ubuntu-py313-strict'
137+
os: ubuntu-latest
138+
python: '3.13'
139+
tox_env: 'py313-strict'
140+
141+
steps:
142+
- name: Check out repository
143+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
144+
with:
145+
fetch-depth: 0
146+
- name: Set up Python ${{ matrix.python }}
147+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
148+
with:
149+
python-version: ${{ matrix.python }}
150+
- name: Install base dependencies
151+
run: |
152+
python -m pip install --upgrade pip
153+
python -m pip install tox wheel
154+
- name: Print Python env
155+
run: |
156+
python --version
157+
python -m pip list
158+
- name: Run tests
159+
id: tests
160+
continue-on-error: true
161+
run: |
162+
tox -e ${{ matrix.tox_env }} -- ${{ matrix.toxposargs }}
163+
# always() so an outcome file is recorded even when an earlier step
164+
# failed; a skipped/cancelled tests step is recorded as "error" (an
165+
# infrastructure failure) rather than being conflated with an expected
166+
# test failure. The JSON schema here is the contract with
167+
# strict_status.yml: {"name": "<matrix name>", "state": "success" |
168+
# "failure" | "error"}.
169+
- name: Record test outcome
170+
if: always()
171+
run: |
172+
case "${{ steps.tests.outcome }}" in
173+
success) state='success' ;;
174+
failure) state='failure' ;;
175+
*) state='error' ;;
176+
esac
177+
printf '{"name": "%s", "state": "%s"}\n' '${{ matrix.name }}' "$state" > strict-outcome.json
178+
if [ "$state" = "failure" ]; then
179+
echo "::warning::${{ matrix.tox_env }} tests failed (expected until the remaining array-API bugs are fixed)"
180+
echo ":warning: **${{ matrix.name }}**: tests **failed** (expected until the remaining array-API bugs are fixed)" >> "$GITHUB_STEP_SUMMARY"
181+
elif [ "$state" = "success" ]; then
182+
echo ":tada: **${{ matrix.name }}**: tests **passed** -- the expected-failures carve-out for this job can be retired" >> "$GITHUB_STEP_SUMMARY"
183+
else
184+
echo "::warning::${{ matrix.name }} job hit an infrastructure error before the tests step completed (outcome: ${{ steps.tests.outcome }})"
185+
echo ":x: **${{ matrix.name }}**: job hit an **infrastructure error** before the tests step completed (outcome: ${{ steps.tests.outcome }})" >> "$GITHUB_STEP_SUMMARY"
186+
fi
187+
- name: Upload test outcome
188+
if: always()
189+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0
190+
with:
191+
# Include the matrix entry name so the artifact names stay unique if
192+
# this matrix grows (upload-artifact v4+ errors on duplicate names).
193+
name: strict-job-outcome-${{ matrix.name }}
194+
path: strict-outcome.json
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Turn the outcome of the expected-failures strict array-API jobs into
2+
# check runs on the PR. This runs as a separate workflow_run workflow because
3+
# pull_request runs for fork PRs get a read-only GITHUB_TOKEN and cannot
4+
# create check runs themselves. Each check uses a stable name (the matrix
5+
# entry name); the outcome is carried by the conclusion and output instead.
6+
# These checks are informational only (an expected test failure is reported
7+
# as neutral, so the PR rollup stays green) and are not intended to be
8+
# required checks while the failures are expected.
9+
#
10+
# Note: workflow_run workflows execute from the default branch, so changes to
11+
# this file only take effect once merged.
12+
name: Strict array API status
13+
14+
on:
15+
workflow_run:
16+
workflows: ["CI"]
17+
types: [completed]
18+
19+
permissions:
20+
checks: write
21+
actions: read
22+
23+
jobs:
24+
report:
25+
# Only report on pull_request runs. CI also triggers on push (all
26+
# branches) and schedule, so without this gate a same-repo PR branch gets
27+
# two completed CI runs per push and this workflow would post duplicate
28+
# (possibly contradictory) checks on the same head SHA.
29+
if: github.event.workflow_run.event == 'pull_request'
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Download strict job outcomes
33+
id: download
34+
# The artifacts are missing for CI runs from before the record/upload
35+
# steps became `if: always()` (or when the whole CI run was skipped);
36+
# tolerate that and skip the check runs below. Runs after that change
37+
# should always upload an outcome artifact per matrix entry.
38+
continue-on-error: true
39+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # 7.0.0
40+
with:
41+
pattern: strict-job-outcome-*
42+
merge-multiple: false
43+
path: strict-outcomes
44+
run-id: ${{ github.event.workflow_run.id }}
45+
github-token: ${{ secrets.GITHUB_TOKEN }}
46+
- name: Create check runs
47+
if: steps.download.outcome == 'success'
48+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
49+
with:
50+
script: |
51+
const fs = require('fs');
52+
const path = require('path');
53+
const run = context.payload.workflow_run;
54+
const root = 'strict-outcomes';
55+
// One directory per artifact, i.e. per expected-failures matrix
56+
// entry. Each contains a strict-outcome.json written by ci_tests.yml
57+
// with the schema {"name": "<matrix name>",
58+
// "state": "success" | "failure" | "error"}.
59+
let dirs = [];
60+
try {
61+
dirs = fs.readdirSync(root).filter((d) => d.startsWith('strict-job-outcome-'));
62+
} catch (err) {
63+
core.info(`No outcome artifacts downloaded (${err.message}); nothing to report.`);
64+
return;
65+
}
66+
if (dirs.length === 0) {
67+
core.info('No strict-job-outcome-* artifacts found; nothing to report.');
68+
return;
69+
}
70+
for (const dir of dirs) {
71+
// Fall back to the artifact name and an error state if the JSON
72+
// is missing or malformed, so a broken upload still surfaces.
73+
let name = dir.replace('strict-job-outcome-', '');
74+
let state = 'error';
75+
try {
76+
const data = JSON.parse(fs.readFileSync(path.join(root, dir, 'strict-outcome.json'), 'utf8'));
77+
if (typeof data.name === 'string' && data.name) {
78+
name = data.name;
79+
}
80+
if (['success', 'failure', 'error'].includes(data.state)) {
81+
state = data.state;
82+
} else {
83+
core.warning(`Unrecognized state ${JSON.stringify(data.state)} in ${dir}; reporting an error state.`);
84+
}
85+
} catch (err) {
86+
core.warning(`Could not read outcome JSON in ${dir} (${err.message}); reporting an error state.`);
87+
}
88+
let conclusion;
89+
let title;
90+
let summary;
91+
if (state === 'success') {
92+
conclusion = 'success';
93+
title = 'Strict array-API tests passed';
94+
summary = `The ${name} job in [this CI run](${run.html_url}) passed. ` +
95+
'The expected-failures carve-out for it can be retired.';
96+
} else if (state === 'failure') {
97+
conclusion = 'neutral';
98+
title = 'Strict array-API tests failed (expected)';
99+
summary = `The ${name} job in [this CI run](${run.html_url}) failed. ` +
100+
'This is expected until the remaining array-API bugs are fixed; ' +
101+
'see the run log for details.';
102+
} else {
103+
conclusion = 'failure';
104+
title = 'Strict array-API job broke (infra error)';
105+
summary = `The ${name} job in [this CI run](${run.html_url}) did not ` +
106+
'complete its tests step. This is an infrastructure error in the ' +
107+
'job itself, not a test failure; see the run log for details.';
108+
}
109+
// Stable check name (just the matrix entry name); the outcome
110+
// lives in the conclusion and output so the checks UI does not
111+
// accumulate differently-named checks per outcome.
112+
await github.rest.checks.create({
113+
owner: context.repo.owner,
114+
repo: context.repo.repo,
115+
name,
116+
head_sha: run.head_sha,
117+
status: 'completed',
118+
conclusion,
119+
output: { title, summary },
120+
});
121+
}

CHANGES.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
2.6.0 (unreleased)
2+
------------------
3+
4+
New Features
5+
^^^^^^^^^^^^
6+
7+
- Add ``array-api-strict`` (on a non-default device) as a test backend, a
8+
CPU-only proxy for CuPy device behavior. [#942]
9+
- Add triage tooling for array-API backend failures: ``backend_xfail`` /
10+
``backend_skip`` markers, an escape-site report
11+
(``CCDPROC_TRIAGE_ESCAPES=1``), and an escape logger
12+
(``CCDPROC_LOG_ARRAY_ESCAPES=1``). [#942]
13+
114
2.5.1 (2025-07-05)
215
------------------
316

0 commit comments

Comments
 (0)