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

Commit fe3cfbc

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 fe3cfbc

File tree

2 files changed

+204
-1
lines changed

2 files changed

+204
-1
lines changed

.github/workflows/main.yml

Lines changed: 59 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,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 the `skip` and `changed` fields
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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 = {ty: [] for ty in TYPES}
112+
for r in sorted(routines):
113+
ret[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+
"skip": len(ty_changed) == 0,
126+
}
127+
ret.append(item)
128+
output = json.dumps({"matrix": ret}, separators=(",", ":"))
129+
eprint(f"output: {output}")
130+
return output
131+
132+
133+
def eprint(*args, **kwargs):
134+
"""Print to stderr."""
135+
print(*args, file=sys.stderr, **kwargs)
136+
137+
138+
def main():
139+
ctx = Context()
140+
output = ctx.make_workflow_output()
141+
print(f"matrix={output}")
142+
143+
144+
if __name__ == "__main__":
145+
main()

0 commit comments

Comments
 (0)