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.
This commit is contained in:
parent
05a1f4a982
commit
f8a184e6af
2 changed files with 206 additions and 1 deletions
|
|
@ -14,7 +14,7 @@ env:
|
|||
jobs:
|
||||
test:
|
||||
name: Build and test
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -186,6 +186,62 @@ jobs:
|
|||
rustup component add rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
|
||||
# Determine which extensive tests should be run based on changed files.
|
||||
calculate_extensive_matrix:
|
||||
name: Calculate job matrix
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
matrix: ${{ steps.script.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
- name: Fetch pull request ref
|
||||
run: git fetch origin "$GITHUB_REF:$GITHUB_REF"
|
||||
- run: python3 ci/calculate-exhaustive-matrix.py >> "$GITHUB_OUTPUT"
|
||||
id: script
|
||||
|
||||
extensive:
|
||||
name: Extensive tests for ${{ matrix.ty }}
|
||||
needs:
|
||||
# Wait on `clippy` so we have some confidence that the crate will build
|
||||
- clippy
|
||||
- calculate_extensive_matrix
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 80
|
||||
strategy:
|
||||
matrix:
|
||||
# Use the output from `calculate_extensive_matrix` to calculate the matrix
|
||||
# FIXME: it would be better to run all jobs (i.e. all types) but mark those that
|
||||
# didn't change as skipped, rather than completely excluding the job. However,
|
||||
# this is not currently possible https://github.com/actions/runner/issues/1985.
|
||||
include: ${{ fromJSON(needs.calculate_extensive_matrix.outputs.matrix).matrix }}
|
||||
env:
|
||||
CHANGED: ${{ matrix.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup update nightly --no-self-update
|
||||
rustup default nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Download musl source
|
||||
run: ./ci/download-musl.sh
|
||||
- name: Run extensive tests
|
||||
run: |
|
||||
echo "Changed: '$CHANGED'"
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "No tests to run, exiting."
|
||||
exit
|
||||
fi
|
||||
|
||||
LIBM_EXTENSIVE_TESTS="$CHANGED" cargo t \
|
||||
--features test-multiprecision,unstable \
|
||||
--release -- extensive
|
||||
- name: Print test logs if available
|
||||
run: if [ -f "target/test-log.txt" ]; then cat target/test-log.txt; fi
|
||||
shell: bash
|
||||
|
||||
success:
|
||||
needs:
|
||||
- test
|
||||
|
|
@ -193,6 +249,7 @@ jobs:
|
|||
- benchmarks
|
||||
- msrv
|
||||
- rustfmt
|
||||
- extensive
|
||||
runs-on: ubuntu-24.04
|
||||
# GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency
|
||||
# failed" as success. So we have to do some contortions to ensure the job fails if any of its
|
||||
|
|
|
|||
148
library/compiler-builtins/libm/ci/calculate-exhaustive-matrix.py
Executable file
148
library/compiler-builtins/libm/ci/calculate-exhaustive-matrix.py
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Calculate which exhaustive tests should be run as part of CI.
|
||||
|
||||
This dynamically prepares a list of routines that had a source file change based on
|
||||
git history.
|
||||
"""
|
||||
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from os import getenv
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
GIT = ["git", "-C", REPO_ROOT]
|
||||
|
||||
# Don't run exhaustive tests if these files change, even if they contaiin a function
|
||||
# definition.
|
||||
IGNORE_FILES = [
|
||||
"src/math/support/",
|
||||
"src/libm_helper.rs",
|
||||
"src/math/arch/intrinsics.rs",
|
||||
]
|
||||
|
||||
TYPES = ["f16", "f32", "f64", "f128"]
|
||||
|
||||
|
||||
class FunctionDef(TypedDict):
|
||||
"""Type for an entry in `function-definitions.json`"""
|
||||
|
||||
sources: list[str]
|
||||
type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
gh_ref: str | None
|
||||
changed: list[Path]
|
||||
defs: dict[str, FunctionDef]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.gh_ref = getenv("GITHUB_REF")
|
||||
self.changed = []
|
||||
self._init_change_list()
|
||||
|
||||
with open(REPO_ROOT.joinpath("etc/function-definitions.json")) as f:
|
||||
defs = json.load(f)
|
||||
|
||||
defs.pop("__comment", None)
|
||||
self.defs = defs
|
||||
|
||||
def _init_change_list(self):
|
||||
"""Create a list of files that have been changed. This uses GITHUB_REF if
|
||||
available, otherwise a diff between `HEAD` and `master`.
|
||||
"""
|
||||
|
||||
# For pull requests, GitHub creates a ref `refs/pull/1234/merge` (1234 being
|
||||
# the PR number), and sets this as `GITHUB_REF`.
|
||||
ref = self.gh_ref
|
||||
eprint(f"using ref `{ref}`")
|
||||
if ref is None or "merge" not in ref:
|
||||
# If the ref is not for `merge` then we are not in PR CI
|
||||
eprint("No diff available for ref")
|
||||
return
|
||||
|
||||
# The ref is for a dummy merge commit. We can extract the merge base by
|
||||
# inspecting all parents (`^@`).
|
||||
merge_sha = sp.check_output(
|
||||
GIT + ["show-ref", "--hash", ref], text=True
|
||||
).strip()
|
||||
merge_log = sp.check_output(GIT + ["log", "-1", merge_sha], text=True)
|
||||
eprint(f"Merge:\n{merge_log}\n")
|
||||
|
||||
parents = (
|
||||
sp.check_output(GIT + ["rev-parse", f"{merge_sha}^@"], text=True)
|
||||
.strip()
|
||||
.splitlines()
|
||||
)
|
||||
assert len(parents) == 2, f"expected two-parent merge but got:\n{parents}"
|
||||
base = parents[0].strip()
|
||||
incoming = parents[1].strip()
|
||||
|
||||
eprint(f"base: {base}, incoming: {incoming}")
|
||||
textlist = sp.check_output(
|
||||
GIT + ["diff", base, incoming, "--name-only"], text=True
|
||||
)
|
||||
self.changed = [Path(p) for p in textlist.splitlines()]
|
||||
|
||||
@staticmethod
|
||||
def _ignore_file(fname: str) -> bool:
|
||||
return any(fname.startswith(pfx) for pfx in IGNORE_FILES)
|
||||
|
||||
def changed_routines(self) -> dict[str, list[str]]:
|
||||
"""Create a list of routines for which one or more files have been updated,
|
||||
separated by type.
|
||||
"""
|
||||
routines = set()
|
||||
for name, meta in self.defs.items():
|
||||
# Don't update if changes to the file should be ignored
|
||||
sources = (f for f in meta["sources"] if not self._ignore_file(f))
|
||||
|
||||
# Select changed files
|
||||
changed = [f for f in sources if Path(f) in self.changed]
|
||||
|
||||
if len(changed) > 0:
|
||||
eprint(f"changed files for {name}: {changed}")
|
||||
routines.add(name)
|
||||
|
||||
ret = {}
|
||||
for r in sorted(routines):
|
||||
ret.setdefault(self.defs[r]["type"], []).append(r)
|
||||
|
||||
return ret
|
||||
|
||||
def make_workflow_output(self) -> str:
|
||||
"""Create a JSON object a list items for each type's changed files, if any
|
||||
did change, and the routines that were affected by the change.
|
||||
"""
|
||||
changed = self.changed_routines()
|
||||
ret = []
|
||||
for ty in TYPES:
|
||||
ty_changed = changed.get(ty, [])
|
||||
item = {
|
||||
"ty": ty,
|
||||
"changed": ",".join(ty_changed),
|
||||
}
|
||||
ret.append(item)
|
||||
output = json.dumps({"matrix": ret}, separators=(",", ":"))
|
||||
eprint(f"output: {output}")
|
||||
return output
|
||||
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
"""Print to stderr."""
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
def main():
|
||||
ctx = Context()
|
||||
output = ctx.make_workflow_output()
|
||||
print(f"matrix={output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue