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