Skip to content
This repository was archived by the owner on Oct 31, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 83 additions & 52 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
# otherwise would turn a real gate into a skipped one. See AGENTS.md.
name: Tests

# Once per change: pushes to main and pull requests. Documentation-only changes
# Once per change: pushes to main and pull requests, each checked for what it
# changes (tools/scoped_tests.py --ci). Documentation-only changes
# skip it; the two package READMEs under lib/ are not skipped, because tests
# read them and the wheels carry them. So does a release pull request, which
# changes only VERSION and CHANGELOG.md, and its merge: the code it releases
Expand All @@ -15,20 +16,25 @@ name: Tests
on:
push:
branches: [main]
paths-ignore: ['docs/**', 'README.md', 'ROADMAP.md', 'CHANGELOG.md', 'AGENTS.md', 'LICENSE', 'VERSION']
paths-ignore: ['docs/**', 'README.md', 'ROADMAP.md', 'CHANGELOG.md', 'AGENTS.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'LICENSE', 'VERSION']
pull_request:
# `labeled` too: adding the `lifecycle: all classes` label re-runs the
# pull request with the whole lifecycle matrix (see the test step).
types: [opened, synchronize, reopened, labeled]
paths-ignore: ['docs/**', 'README.md', 'ROADMAP.md', 'CHANGELOG.md', 'AGENTS.md', 'LICENSE', 'VERSION']
paths-ignore: ['docs/**', 'README.md', 'ROADMAP.md', 'CHANGELOG.md', 'AGENTS.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'LICENSE', 'VERSION']
# The full matrix on demand, for a branch that should see every Python
# before it merges rather than after.
workflow_dispatch:
# Everything, every Python, every class and node, once a week: the scoped
# runs above trust a mapping from files to tests, and this is the run that
# would catch a dependency the mapping does not know about.
schedule:
- cron: '23 6 * * 1'

# A new push to a pull request cancels the run it supersedes: only the newest
# commit's result is ever read. Pushes to main are never cancelled or queued
# behind one another (each gets a group of its own), so every commit on main
# keeps its own full-matrix record.
# keeps its own record.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Expand All @@ -39,8 +45,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
# Every supported Python on main and on a manual run; on a pull
# request only 3.13, the Python the other two platforms run. A failure
# Every supported Python on main, on a manual run and weekly; on a
# pull request only 3.13, the Python the other two platforms run. A failure
# only another Python shows surfaces on the push to main, or sooner
# from a manual run on the branch.
python: ${{ fromJSON(github.event_name == 'pull_request' && '["3.13"]' || '["3.11", "3.12", "3.13", "3.14"]') }}
Expand All @@ -53,30 +59,15 @@ jobs:
- os: macos-latest
python: "3.13"
runs-on: ${{ matrix.os }}
env:
# The lifecycle matrices (quick mode). All 45 classes take about 4.4
# CPU hours, over an hour on this runner, and the 13 audiodsp nodes
# under them half an hour more on one worker, so a pull request runs
# only what its change can move: the class or node whose own code
# changed, and when a shared file changed (the base class, the
# matrix, the pin) a fixed sample of each, nine classes in about 13
# minutes and four nodes in about five (SAMPLE in
# tests/test_lifecycle_matrix.py, NODE_SAMPLE in
# tests/test_node_matrix.py). A push to main runs every class and
# every node, and so does a manual run (workflow_dispatch) or a pull
# request labelled `lifecycle: all classes`.
LIFECYCLE_CHANGED_FROM: "${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'lifecycle: all classes') && format('origin/{0}', github.base_ref) || '' }}"
steps:
# A pull request fetches its history, so the matrices can ask git what
# the change touches against origin/<base> (LIFECYCLE_CHANGED_FROM).
# Written `!= ... && 1 || 0` because `== ... && 0 || 1` is always 1:
# 0 is false in an expression, so the `||` took over and every pull
# request checked out at depth 1. With no base to diff from, the
# matrix then fell back to all 45 classes, and a pull request ran
# 1 h 43 min. A base git cannot diff from is now an error instead.
# Full history, so the scope and the matrices can ask git what the
# change touches: against origin/<base> for a pull request, against
# the commit main moved from for a push. A base git cannot diff from
# is an error, not a silent fall back to everything (a pull request
# once ran 1 h 43 min that way, checked out at depth 1).
- uses: actions/checkout@v7
with:
fetch-depth: ${{ github.event_name != 'pull_request' && 1 || 0 }}
fetch-depth: 0
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
Expand Down Expand Up @@ -141,6 +132,61 @@ jobs:
# a test. The tests would generate them too; this makes it explicit.
- run: python tools/effect_probes/make_probes.py --ensure

# What this run checks. A pull request: what it changes against its
# base branch. A push to main: what that push changes, from the commit
# main moved from (github.event.before), so a merge is checked for the
# whole pull request it brings in. Everything when that commit is
# missing (a new branch, a force push that dropped it), on a manual
# run and on the weekly run.
#
# The lifecycle matrices (quick mode). All 45 classes take about 4.4
# CPU hours, over an hour on this runner, and the 13 audiodsp nodes
# under them half an hour more on one worker, so a change runs only
# what it can move: the class or node whose own code changed. When a
# shared file changed (the base class, the matrix, the pin), a pull
# request runs a fixed sample of each, nine classes in about 13
# minutes and four nodes in about five (SAMPLE in
# tests/test_lifecycle_matrix.py, NODE_SAMPLE in
# tests/test_node_matrix.py), and its push to main runs every class
# and every node. So do a manual run (workflow_dispatch), the weekly
# run and a pull request labelled `lifecycle: all classes`. This
# step sets LIFECYCLE_CHANGED_FROM for them; empty means all.
- name: Scope
id: scope
shell: bash
env:
EVENT: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
BEFORE: ${{ github.event.before }}
ALL_CLASSES: "${{ contains(github.event.pull_request.labels.*.name, 'lifecycle: all classes') }}"
run: |
base=
if [[ "$EVENT" == pull_request ]]; then
base="origin/$BASE_REF"
elif [[ "$EVENT" == push && -n "$BEFORE" && ! "$BEFORE" =~ ^0+$ ]]; then
if git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then
base="$BEFORE"
else
echo "the commit main moved from ($BEFORE) is not in the history: everything runs"
fi
fi
if [[ -z "$base" ]]; then
files=full
else
files=$(python tools/scoped_tests.py --ci "$base")
fi
# The matrices pick their own classes and nodes from the same base.
# A push whose change is shared runs all of them; a pull request
# runs the sample unless it is labelled for all.
lifecycle="$base"
if [[ "$EVENT" == push && "$files" == full ]]; then lifecycle=; fi
if [[ "$ALL_CLASSES" == true ]]; then lifecycle=; fi
echo "LIFECYCLE_CHANGED_FROM=$lifecycle" >> "$GITHUB_ENV"
if [[ "$files" == full ]]; then files=tests; fi
echo "files=$files" >> "$GITHUB_OUTPUT"
echo "base: ${base:-none, everything}"
echo "running: $files"

# What the two matrices will run, in the log where a reader can check
# it, and a scope git cannot work out fails here by name.
- name: Show the lifecycle scope
Expand All @@ -152,31 +198,16 @@ jobs:
print('classes:', ' '.join(classes()) or 'none');
print('nodes:', ' '.join(names()) or 'none')"

# A pull request runs only the tests its change can move: the test
# files it edits, the tests of each effect class whose module it edits,
# and the kit, the contract and the two lifecycle matrices, which
# every pull request runs. A change to shared code (the kit, the base
# class, tests/support, the probes, the pin, this workflow) or to a
# file with no tests of its own runs the whole suite. So do pushes to
# main and manual runs, on every Python. tools/scoped_tests.py --ci
# holds the mapping and prints why it chose what it chose.
- name: Choose the tests
id: scope
shell: bash
run: |
if [[ "${{ github.event_name }}" == pull_request ]]; then
files=$(python tools/scoped_tests.py --ci "origin/${{ github.base_ref }}")
else
files=full
fi
if [[ "$files" == full ]]; then
files=tests
fi
echo "files=$files" >> "$GITHUB_OUTPUT"
echo "running: $files"

# The same tests `python -m unittest discover -s tests -p "test_*.py"`
# finds (or the pull request's share of them, above), spread across
# The tests the Scope step chose. A change runs the test files it
# edits, the tests of each effect or instrument whose module it edits
# (with the tests that name it and the ones that go through every
# class), the tests that import a helper it edits, and the kit, the
# contract and the two lifecycle matrices, which every change runs. A
# change to shared code (the kit, the base classes, the probes, the
# pin, this workflow) or to a file with no tests of its own runs the
# whole suite: the same tests `python -m unittest discover -s tests -p
# "test_*.py"` finds. tools/scoped_tests.py --ci holds the mapping and
# prints why it chose what it chose. They are spread across
# every core the runner has: serially the suite
# takes over an hour. A test that writes a file writes it into its own
# temporary directory, so no two workers collide. loadscope keeps each
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ python tests/parity/effects_library_smoke.py
python -m flake8
```

CI doesn't run all of the first line on every change. It runs the tests
the change can move, chosen by `tools/scoped_tests.py --ci` from the diff:
for a pull request against its base, for a push to main against the commit
main moved from. A change to shared code (the base classes, the kit, the
probes, `AUDIODSP_PIN`, the workflow) runs everything. The run's "Scope"
step prints what it chose and why. The whole suite runs on every Python on
a manual run of the Tests workflow and every Monday.

Most work does not need the whole suite. [`tools/scoped_tests.py`](tools/scoped_tests.py) picks the
files a change can break:

Expand Down
13 changes: 8 additions & 5 deletions tests/support/KIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,14 @@ CI doesn't run all of it on every pull request:
(`NODE_SAMPLE`) when it changes a shared file: `AUDIODSP_PIN`, the base
class, `lifecycle.py`, the class matrix test, or a line of the adapters
outside every node's own.
- A push to main runs all 45 classes and every node, and so does a manual
run of the workflow or a pull request labelled `lifecycle: all classes`.

To tell what changed, CI diffs the pull request against its base branch,
so the checkout fetches the history. A base git can't diff from fails the
- A push to main runs what that push changes, the same way, except that a
shared file runs all 45 classes and every node rather than the sample.
So do a manual run of the workflow, the weekly run on Monday and a pull
request labelled `lifecycle: all classes`.

To tell what changed, CI diffs a pull request against its base branch and
a push against the commit main moved from, so the checkout fetches the
history. A base git can't diff from fails the
run by name rather than quietly running everything: a depth-1 checkout once
did that, and a pull request's run took 1 h 43 min. The workflow prints
what each matrix will run in its "Show the lifecycle scope" step.
Expand Down
5 changes: 3 additions & 2 deletions tests/test_lifecycle_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,9 @@ def effect_class(name):
#: (LowPass), Rack, Saturation (the recentre path) and Distortion (the graph
#: rebuild). Measured on CPython at audiodsp v0.6.6rc8: 3 114 CPU seconds,
#: about 13 minutes on a 4-core runner, where all 45 take 4.4 CPU hours.
#: A push to main runs all 45, and so does a manual run or a pull request
#: labelled `lifecycle: all classes` (.github/workflows/tests.yml).
#: A push to main that changes a shared file runs all 45, and so do a
#: manual run, the weekly run and a pull request labelled `lifecycle: all
#: classes` (.github/workflows/tests.yml).
SAMPLE = ("Bitcrusher", "Tremolo", "DigitalDelay", "Reverb", "Octaver",
"LowPass", "Rack", "Saturation", "Distortion")

Expand Down
5 changes: 3 additions & 2 deletions tests/test_node_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@
#: (NodeSplitMix) and a free-running phase (NodeShimmerLoop, cheaper than
#: NodeFlanger's LFO by minutes). Measured on CPython at audiodsp
#: v0.6.6rc9: 86 s, where all 13 take 541 s (about 30 minutes on CI).
#: A push to main runs every node, and so does a manual run or a pull
#: request labelled `lifecycle: all classes` (.github/workflows/tests.yml).
#: A push to main that changes a shared file runs every node, and so do a
#: manual run, the weekly run and a pull request labelled `lifecycle: all
#: classes` (.github/workflows/tests.yml).
NODE_SAMPLE = ("NodeBiquad", "NodeEcho", "NodeSplitMix", "NodeShimmerLoop")


Expand Down
Loading
Loading