Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 06e329c

Browse files
committed
Run extensive tests in CI when relevant files change
Add a CI job with a dynamically calculated matrix that runs extensive jobs on changed files. This makes use of the new `function-definitions.json` file to determine which changed files require full tests for a routine to run.
1 parent c0c33e7 commit 06e329c

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

.github/workflows/main.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,71 @@ jobs:
186186
rustup component add rustfmt
187187
- run: cargo fmt -- --check
188188

189+
calculate_matrix:
190+
name: Calculate job matrix
191+
runs-on: ubuntu-24.04
192+
outputs:
193+
matrix: ${{ steps.script.outputs.matrix }}
194+
steps:
195+
- uses: actions/checkout@v4
196+
with:
197+
fetch-depth: 100
198+
- name: Fetch pull request ref
199+
run: git fetch origin "$GITHUB_REF:$GITHUB_REF"
200+
- run: python3 ci/calculate-exhaustive-matrix.py >> "$GITHUB_OUTPUT"
201+
id: script
202+
203+
extensive:
204+
name: Extensive tests for ${{ matrix.ty }}
205+
needs:
206+
- test
207+
- calculate_matrix
208+
runs-on: ubuntu-24.04
209+
timeout-minutes: 80
210+
strategy:
211+
matrix:
212+
ty: [f16, f32, f64, f128]
213+
# Use the output from `calculate_matrix` to add more `skip` and `changed`
214+
include: ${{ fromJSON(needs.calculate_matrix.outputs.matrix).matrix }}
215+
env:
216+
CHANGED: ${{ matrix.changed }}
217+
steps:
218+
- uses: actions/checkout@v4
219+
if: ${{ !matrix.skip }}
220+
221+
- name: Install Rust
222+
if: ${{ !matrix.skip }}
223+
run: |
224+
rustup update nightly --no-self-update
225+
rustup default nightly
226+
227+
- uses: Swatinem/rust-cache@v2
228+
if: ${{ !matrix.skip }}
229+
230+
- name: Download musl source
231+
if: ${{ !matrix.skip }}
232+
run: ./ci/download-musl.sh
233+
234+
- name: Run extensive tests
235+
if: ${{ !matrix.skip }}
236+
run: |
237+
LIBM_EXTENSIVE_TESTS="$CHANGED" cargo t \
238+
--features test-multiprecision,build-musl,unstable \
239+
--release -- extensive
240+
241+
- name: Print test logs if available
242+
if: ${{ !matrix.skip }}
243+
run: if [ -f "target/test-log.txt" ]; then cat target/test-log.txt; fi
244+
shell: bash
245+
189246
success:
190247
needs:
191248
- test
192249
- builtins
193250
- benchmarks
194251
- msrv
195252
- rustfmt
253+
- extensive
196254
runs-on: ubuntu-24.04
197255
# GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency
198256
# failed" as success. So we have to do some contortions to ensure the job fails if any of its

ci/calculate-exhaustive-matrix.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env python3
2+
"""Calculate which exhaustive tests should be run as part of CI.
3+
4+
This dynamically prepares a list of TODO
5+
"""
6+
7+
import subprocess as sp
8+
import sys
9+
import json
10+
from dataclasses import dataclass
11+
from os import getenv
12+
from pathlib import Path
13+
14+
15+
REPO_ROOT = Path(__file__).parent.parent
16+
GIT = ["git", "-C", REPO_ROOT]
17+
18+
IGNORE_FILES = [
19+
"src/math/support/",
20+
"src/libm_helper.rs",
21+
"src/math/arch/intrinsics.rs",
22+
]
23+
"""Don't run exhaustive tests if these files change, even if they contaiin a function
24+
definition."""
25+
26+
27+
@dataclass
28+
class Context:
29+
gh_ref: str | None
30+
changed: list[Path]
31+
defs: dict[str, dict[str, list[str] | str]]
32+
"""A list of all available functions by name."""
33+
34+
def __init__(self) -> None:
35+
self.gh_ref = getenv("GITHUB_REF")
36+
self.changed = []
37+
self._init_change_list()
38+
39+
with open(REPO_ROOT.joinpath("etc/function-definitions.json")) as f:
40+
defs = json.load(f)
41+
42+
defs.pop("__comment", None)
43+
self.defs = defs
44+
45+
def _init_change_list(self):
46+
"""Create a list of files that have been changed. This uses GITHUB_REF if
47+
available, otherwise a diff between `HEAD` and `master`."""
48+
49+
# For pull requests, GitHub creates a ref `refs/pull/1234/merge` (1234 being
50+
# the PR number), and sets this as `GITHUB_REF`.
51+
ref = self.gh_ref
52+
if ref is not None:
53+
eprint(f"using ref `{ref}`")
54+
if "merge" not in ref:
55+
# If the ref is not for `merge` then we are not in PR CI
56+
eprint("No diff available for ref")
57+
return
58+
59+
# The ref is for a dummy merge commit. We can extract the merge base by
60+
# inspecting all parents (`^@`).
61+
merge_sha = sp.check_output(
62+
GIT + ["show-ref", "--hash", ref], text=True
63+
).strip()
64+
merge_log = sp.check_output(GIT + ["log", "-1", merge_sha], text=True)
65+
eprint(f"Merge:\n{merge_log}\n")
66+
67+
parents = (
68+
sp.check_output(GIT + ["rev-parse", f"{merge_sha}^@"], text=True)
69+
.strip()
70+
.splitlines()
71+
)
72+
assert len(parents) == 2, f"expected two-parent merge but got:\n{parents}"
73+
base = parents[0]
74+
incoming = parents[1]
75+
else:
76+
# When running locally, allow providing a rev via `MERGE_BASE`. Otherwise
77+
# assume `HEAD -> MASTER`
78+
base = getenv("MERGE_BASE")
79+
if base is None:
80+
base = sp.check_output(
81+
GIT + ["merge-base", "HEAD", "master"], text=True
82+
)
83+
incoming = "HEAD"
84+
85+
base = base.strip()
86+
incoming = incoming.strip()
87+
88+
eprint(f"base: {base}, incoming: {incoming}")
89+
textlist = sp.check_output(
90+
GIT + ["diff", base, incoming, "--name-only"], text=True
91+
)
92+
self.changed = [Path(p) for p in textlist.splitlines()]
93+
94+
def changed_routines(self) -> dict[str, list[str]]:
95+
"""Create a list of routines for which one or more files have been updated,
96+
separated by type.
97+
"""
98+
routines = set()
99+
for name, meta in self.defs.items():
100+
def_list = meta["sources"]
101+
add = False
102+
for fname in def_list:
103+
# Skip if the file was not changed
104+
if Path(fname) not in self.changed:
105+
continue
106+
107+
# Don't update if changes to the file should be ignored
108+
if any(fname.startswith(pfx) for pfx in IGNORE_FILES):
109+
continue
110+
111+
add = True
112+
113+
if add:
114+
routines.add(name)
115+
116+
ret = {}
117+
for r in sorted(routines):
118+
ret.setdefault(self.defs[r]["type"], []).append(r)
119+
120+
return ret
121+
122+
def make_workflow_output(self) -> str:
123+
changed = self.changed_routines()
124+
ret = []
125+
for ty in ["f16", "f32", "f64", "f128"]:
126+
ty_changed = changed.get(ty, [])
127+
item = {
128+
"ty": ty,
129+
"changed": ",".join(ty_changed),
130+
"skip": len(ty_changed) == 0,
131+
}
132+
ret.append(item)
133+
output = json.dumps({"matrix": ret}, separators=(",", ":"))
134+
eprint(f"output: {output}")
135+
return output
136+
137+
138+
def eprint(*args, **kwargs):
139+
"""Print to stderr."""
140+
print(*args, file=sys.stderr, **kwargs)
141+
142+
143+
def main():
144+
ctx = Context()
145+
output = ctx.make_workflow_output()
146+
print(f"matrix={output}")
147+
148+
149+
if __name__ == "__main__":
150+
main()

0 commit comments

Comments
 (0)