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

Commit 1e01833

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 46ddb9c commit 1e01833

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

.github/workflows/main.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ env:
1414
jobs:
1515
test:
1616
name: Build and test
17-
timeout-minutes: 20
17+
timeout-minutes: 25
1818
strategy:
1919
fail-fast: false
2020
matrix:
@@ -186,13 +186,66 @@ 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+
# Wait on `clippy` so we have some confidence that the crate will build
207+
- clippy
208+
- calculate_matrix
209+
runs-on: ubuntu-24.04
210+
timeout-minutes: 80
211+
strategy:
212+
matrix:
213+
# Use the output from `calculate_matrix` to calculate the matrix
214+
include: ${{ fromJSON(needs.calculate_matrix.outputs.matrix).matrix }}
215+
env:
216+
CHANGED: ${{ matrix.changed }}
217+
steps:
218+
- uses: actions/checkout@v4
219+
220+
- name: Install Rust
221+
run: |
222+
rustup update nightly --no-self-update
223+
rustup default nightly
224+
225+
- uses: Swatinem/rust-cache@v2
226+
227+
- name: Download musl source
228+
run: ./ci/download-musl.sh
229+
230+
- name: Run extensive tests
231+
run: |
232+
echo "running $CHANGED"
233+
LIBM_EXTENSIVE_TESTS="$CHANGED" cargo t \
234+
--features test-multiprecision,build-musl,unstable \
235+
--release -- extensive
236+
237+
- name: Print test logs if available
238+
run: if [ -f "target/test-log.txt" ]; then cat target/test-log.txt; fi
239+
shell: bash
240+
189241
success:
190242
needs:
191243
- test
192244
- builtins
193245
- benchmarks
194246
- msrv
195247
- rustfmt
248+
- extensive
196249
runs-on: ubuntu-24.04
197250
# GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency
198251
# 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: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
from typing import TypedDict
14+
15+
16+
REPO_ROOT = Path(__file__).parent.parent
17+
GIT = ["git", "-C", REPO_ROOT]
18+
19+
# Don't run exhaustive tests if these files change, even if they contaiin a function
20+
# definition.
21+
IGNORE_FILES = [
22+
"src/math/support/",
23+
"src/libm_helper.rs",
24+
"src/math/arch/intrinsics.rs",
25+
]
26+
27+
TYPES = ["f16", "f32", "f64", "f128"]
28+
29+
30+
class FunctionDef(TypedDict):
31+
"""Type for an entry in `function-definitions.json`"""
32+
33+
sources: list[str]
34+
type: str
35+
36+
37+
@dataclass
38+
class Context:
39+
gh_ref: str | None
40+
changed: list[Path]
41+
defs: dict[str, FunctionDef]
42+
43+
def __init__(self) -> None:
44+
self.gh_ref = getenv("GITHUB_REF")
45+
self.changed = []
46+
self._init_change_list()
47+
48+
with open(REPO_ROOT.joinpath("etc/function-definitions.json")) as f:
49+
defs = json.load(f)
50+
51+
defs.pop("__comment", None)
52+
self.defs = defs
53+
54+
def _init_change_list(self):
55+
"""Create a list of files that have been changed. This uses GITHUB_REF if
56+
available, otherwise a diff between `HEAD` and `master`.
57+
"""
58+
59+
# For pull requests, GitHub creates a ref `refs/pull/1234/merge` (1234 being
60+
# the PR number), and sets this as `GITHUB_REF`.
61+
ref = self.gh_ref
62+
eprint(f"using ref `{ref}`")
63+
if ref is None or "merge" not in ref:
64+
# If the ref is not for `merge` then we are not in PR CI
65+
eprint("No diff available for ref")
66+
return
67+
68+
# The ref is for a dummy merge commit. We can extract the merge base by
69+
# inspecting all parents (`^@`).
70+
merge_sha = sp.check_output(
71+
GIT + ["show-ref", "--hash", ref], text=True
72+
).strip()
73+
merge_log = sp.check_output(GIT + ["log", "-1", merge_sha], text=True)
74+
eprint(f"Merge:\n{merge_log}\n")
75+
76+
parents = (
77+
sp.check_output(GIT + ["rev-parse", f"{merge_sha}^@"], text=True)
78+
.strip()
79+
.splitlines()
80+
)
81+
assert len(parents) == 2, f"expected two-parent merge but got:\n{parents}"
82+
base = parents[0].strip()
83+
incoming = parents[1].strip()
84+
85+
eprint(f"base: {base}, incoming: {incoming}")
86+
textlist = sp.check_output(
87+
GIT + ["diff", base, incoming, "--name-only"], text=True
88+
)
89+
self.changed = [Path(p) for p in textlist.splitlines()]
90+
91+
@staticmethod
92+
def _ignore_file(fname: str) -> bool:
93+
return any(fname.startswith(pfx) for pfx in IGNORE_FILES)
94+
95+
def changed_routines(self) -> dict[str, list[str]]:
96+
"""Create a list of routines for which one or more files have been updated,
97+
separated by type.
98+
"""
99+
routines = set()
100+
for name, meta in self.defs.items():
101+
# Don't update if changes to the file should be ignored
102+
sources = (f for f in meta["sources"] if not self._ignore_file(f))
103+
104+
# Select changed files
105+
changed = [f for f in sources if Path(f) in self.changed]
106+
107+
if len(changed) > 0:
108+
eprint(f"changed files for {name}: {changed}")
109+
routines.add(name)
110+
111+
ret = {}
112+
for r in sorted(routines):
113+
ret.setdefault(self.defs[r]["type"], []).append(r)
114+
115+
return ret
116+
117+
def make_workflow_output(self) -> str:
118+
changed = self.changed_routines()
119+
ret = []
120+
for ty in TYPES:
121+
ty_changed = changed.get(ty, [])
122+
item = {
123+
"ty": ty,
124+
"changed": ",".join(ty_changed),
125+
}
126+
ret.append(item)
127+
output = json.dumps({"matrix": ret}, separators=(",", ":"))
128+
eprint(f"output: {output}")
129+
return output
130+
131+
132+
def eprint(*args, **kwargs):
133+
"""Print to stderr."""
134+
print(*args, file=sys.stderr, **kwargs)
135+
136+
137+
def main():
138+
ctx = Context()
139+
output = ctx.make_workflow_output()
140+
print(f"matrix={output}")
141+
142+
143+
if __name__ == "__main__":
144+
main()

0 commit comments

Comments
 (0)