mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Kernel rig: each area runs only when the files its PATHS lists change, every area on the shared core
A plan job (scripts/ci/kernel_rig_plan.py, with its self-test) maps a pull request's changed files to the areas: tests/kernel-cases/<area>/PATHS for each area, tests/kernel-cases/PATHS-ALL for the shared core, the ladder whenever any area runs. Areas come from the folders, so a new one needs no workflow edit. Pushes, merge groups, the release gate and runs by hand run every area. The kernel-rig check counts a skipped build or shard as passed only when the plan selected none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
# The kernel rig and the complexity ladder as a regression gate. A pull request that touches the
|
||||
# kernel surface (the paths below) builds solidcraft-cli once (release, SMLib prebuilt), hands it
|
||||
# to a matrix of shards as an artifact, and each shard runs one area of tests/kernel-cases/ (or
|
||||
# the ladder) in parallel, so the wall time is the build plus the slowest shard. See
|
||||
# tests/kernel-cases/README.md.
|
||||
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
|
||||
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
|
||||
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
|
||||
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes; the ladder runs whenever any
|
||||
# area does. One job then builds solidcraft-cli once (release, SMLib prebuilt), checks that every
|
||||
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
|
||||
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
|
||||
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
|
||||
#
|
||||
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
|
||||
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
|
||||
@@ -10,11 +13,13 @@
|
||||
# - The ladder fails on any case whose status or counts differ from
|
||||
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
|
||||
#
|
||||
# GitHub filters a workflow by whole files, so any change to crates/doc/src/eval*.rs or
|
||||
# crates/engine/src/cmd/features*.rs selects it. Pushes to main and develop that touch the same
|
||||
# paths run it too and commit the compiler cache (a Blacksmith sticky disk) that every other run
|
||||
# mounts read-only. A merge group (once the queue is on) always runs it: it is the last check
|
||||
# before the change lands. The release gate (release-gate.yml) calls it (workflow_call).
|
||||
# The trigger paths are whole folders that hold every area's PATHS (a self-test,
|
||||
# scripts/ci/test_kernel_rig_plan.py, checks they do), so a pull request that touches none of
|
||||
# them (the UI, docs, packaging) starts no runner at all, and a new area needs no edit here: the
|
||||
# plan finds the areas from the folders. Pushes to main and develop on the same paths, merge groups
|
||||
# (once the queue is on), the release gate (release-gate.yml, workflow_call) and a run by hand run
|
||||
# every area; those pushes also commit the compiler cache (a Blacksmith sticky disk) that every
|
||||
# other run mounts read-only.
|
||||
name: Kernel rig
|
||||
|
||||
on:
|
||||
@@ -25,14 +30,17 @@ on:
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features*.rs"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
@@ -42,14 +50,17 @@ on:
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features*.rs"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
merge_group:
|
||||
workflow_call:
|
||||
@@ -67,10 +78,44 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build the CLI
|
||||
plan:
|
||||
name: Select the rig areas
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
|
||||
shards: ${{ steps.plan.outputs.shards }}
|
||||
# Whether the build job, and with it the coverage check, runs.
|
||||
build: ${{ steps.plan.outputs.build }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# A pull request's diff needs its base's history.
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
|
||||
- name: Selection self-test
|
||||
run: python3 -I scripts/ci/test_kernel_rig_plan.py
|
||||
- id: plan
|
||||
name: Map the changed files to areas
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
# Pull requests run the areas their diff reaches; pushes, merge groups, the release
|
||||
# gate and a run by hand run every area.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --github-output "$GITHUB_OUTPUT"
|
||||
else
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build the CLI
|
||||
needs: plan
|
||||
if: needs.plan.outputs.build == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -110,9 +155,15 @@ jobs:
|
||||
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
|
||||
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
|
||||
# job of this workflow.
|
||||
# Every registry command is used by a case or listed in tests/kernel-cases/EXEMPT.
|
||||
- name: Every command has a case
|
||||
run: target/debug/xtask kernel-cases --coverage --cli target/release/solidcraft-cli
|
||||
# A coverage-only run (the registry or the cases changed, no area selected) runs no shard.
|
||||
- name: Pack the binaries
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
run: tar -cf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target release/solidcraft-cli debug/xtask
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}/kernel-rig-bin.tar
|
||||
@@ -123,16 +174,18 @@ jobs:
|
||||
|
||||
shard:
|
||||
name: Kernel rig (${{ matrix.shard }})
|
||||
needs: build
|
||||
needs: [plan, build]
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Every area of tests/kernel-cases/ and the ladder. On the build box (aarch64, 4 cases at
|
||||
# a time) the slowest areas took about 1 minute (booleans, 212 s of cases with one of
|
||||
# 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area needs splitting.
|
||||
shard: [booleans, continuity, crashes, fillets, import, ladder, sheet-metal, shell, sweeps, tessellation, threads]
|
||||
# The areas of tests/kernel-cases/ the plan selected, and the ladder. On the build box
|
||||
# (aarch64, 4 cases at a time) the slowest areas took about 1 minute (booleans, 212 s of
|
||||
# cases with one of 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area
|
||||
# needs splitting.
|
||||
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
@@ -162,14 +215,20 @@ jobs:
|
||||
# One check for the whole gate, for branch protection and the merge queue.
|
||||
kernel-rig:
|
||||
name: kernel-rig
|
||||
needs: [build, shard]
|
||||
needs: [plan, build, shard]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Every shard passed
|
||||
# A skipped build or shard counts as passed only when the plan selected none.
|
||||
- name: Every selected job passed
|
||||
env:
|
||||
PLAN: ${{ needs.plan.result }}
|
||||
WANT_BUILD: ${{ needs.plan.outputs.build }}
|
||||
WANT_SHARDS: ${{ needs.plan.outputs.shards }}
|
||||
BUILD: ${{ needs.build.result }}
|
||||
SHARDS: ${{ needs.shard.result }}
|
||||
run: |
|
||||
test "$BUILD" = success
|
||||
test "$SHARDS" = success
|
||||
echo "plan=$PLAN build=$BUILD shards=$SHARDS selected=$WANT_SHARDS"
|
||||
test "$PLAN" = success
|
||||
if [ "$WANT_BUILD" = true ]; then test "$BUILD" = success; else test "$BUILD" = skipped; fi
|
||||
if [ "$WANT_SHARDS" != '[]' ]; then test "$SHARDS" = success; else test "$SHARDS" = skipped; fi
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Select the kernel rig areas a pull request affects (.github/workflows/kernel-rig.yml).
|
||||
|
||||
Usage: kernel_rig_plan.py (--all | --paths FILE) [--root DIR] [--github-output FILE]
|
||||
|
||||
FILE holds the changed paths relative to the repository root, separated by NUL (git diff -z);
|
||||
"-" reads standard input. --all selects every area (pushes, merge groups, the release gate, a run
|
||||
by hand). --root is the repository (default: two folders up from this script).
|
||||
|
||||
The areas are the folders of tests/kernel-cases/ (the ladder aside), so a new area joins by
|
||||
having a folder with its cases, its KNOWN_FAILING and its PATHS. A changed path selects:
|
||||
- every area, when it matches tests/kernel-cases/PATHS-ALL (the shared core: SMLib, geom, the
|
||||
kernel's core, the rig's grader and runner, the workflow);
|
||||
- an area, when it is under tests/kernel-cases/<area>/ or matches that area's PATHS: one git
|
||||
pathspec glob a line (`*` within a folder, `**` across folders), `#` starts a comment.
|
||||
The ladder spans every operation: it runs whenever any area does, and on a change under
|
||||
tests/kernel-cases/ladder/.
|
||||
|
||||
The coverage check (`cargo xtask kernel-cases --coverage`) runs in the build job. The build runs
|
||||
when a shard does, or when the command registry or the cases may have changed (COVERAGE below),
|
||||
so a new command without a case fails on its own pull request.
|
||||
|
||||
Prints and optionally appends to $GITHUB_OUTPUT:
|
||||
shards=["booleans", ..., "ladder"] the shard matrix, JSON (empty list: no shard runs)
|
||||
build=true|false the build job (and the coverage check) runs
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
CASES = "tests/kernel-cases"
|
||||
LADDER = "ladder"
|
||||
# Changes here may add or remove a registry command, or the case that covers one: the coverage
|
||||
# check must see them even when no area runs.
|
||||
COVERAGE = (
|
||||
"crates/engine/src/cmd/**",
|
||||
"tests/kernel-cases/EXEMPT",
|
||||
"tests/kernel-cases/**/recipe.json",
|
||||
"xtask/src/kernel_cases.rs",
|
||||
)
|
||||
|
||||
|
||||
def glob_regex(glob):
|
||||
"""A git pathspec glob (`:(glob)` magic) as a regular expression over a whole path: `**/`
|
||||
matches any folders (none too), a trailing `/**` everything below, `*` and `?` stay within
|
||||
one folder."""
|
||||
out, i = [], 0
|
||||
while i < len(glob):
|
||||
if glob.startswith("**/", i):
|
||||
out.append("(?:.*/)?")
|
||||
i += 3
|
||||
elif glob.startswith("**", i):
|
||||
out.append(".*")
|
||||
i += 2
|
||||
elif glob[i] == "*":
|
||||
out.append("[^/]*")
|
||||
i += 1
|
||||
elif glob[i] == "?":
|
||||
out.append("[^/]")
|
||||
i += 1
|
||||
else:
|
||||
out.append(re.escape(glob[i]))
|
||||
i += 1
|
||||
return re.compile("".join(out) + r"\Z")
|
||||
|
||||
|
||||
def read_globs(path):
|
||||
"""The globs of a PATHS file: one a line, blank lines and `#` comments skipped."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
lines = [line.strip() for line in f]
|
||||
globs = [line for line in lines if line and not line.startswith("#")]
|
||||
for g in globs:
|
||||
if g.startswith("/") or g.startswith(":") or "\\" in g:
|
||||
raise SystemExit(f"{path}: `{g}`: a glob is relative to the repository root, with / and no pathspec magic")
|
||||
return globs
|
||||
|
||||
|
||||
def areas(root):
|
||||
"""Every area (a folder of tests/kernel-cases/ but the ladder) with its PATHS globs."""
|
||||
base = os.path.join(root, CASES)
|
||||
out = {}
|
||||
for name in sorted(os.listdir(base)):
|
||||
if name == LADDER or not os.path.isdir(os.path.join(base, name)):
|
||||
continue
|
||||
paths = os.path.join(base, name, "PATHS")
|
||||
if not os.path.isfile(paths):
|
||||
raise SystemExit(f"{CASES}/{name}/PATHS is missing: every area lists the source files whose change reruns it")
|
||||
out[name] = read_globs(paths)
|
||||
return out
|
||||
|
||||
|
||||
def matches(globs, path):
|
||||
return any(glob_regex(g).match(path) for g in globs)
|
||||
|
||||
|
||||
def select(root, paths, everything=False):
|
||||
"""Return (shards, build): the areas to run, in order and the ladder last, and whether the
|
||||
build job runs."""
|
||||
by_area = areas(root)
|
||||
if everything:
|
||||
return [*by_area, LADDER], True
|
||||
shared = read_globs(os.path.join(root, CASES, "PATHS-ALL"))
|
||||
hit, ladder = set(), False
|
||||
for path in paths:
|
||||
if matches(shared, path):
|
||||
return [*by_area, LADDER], True
|
||||
if path.startswith(f"{CASES}/{LADDER}/"):
|
||||
ladder = True
|
||||
for area, globs in by_area.items():
|
||||
if path.startswith(f"{CASES}/{area}/") or matches(globs, path):
|
||||
hit.add(area)
|
||||
shards = [a for a in by_area if a in hit]
|
||||
if shards or ladder:
|
||||
shards.append(LADDER)
|
||||
return shards, bool(shards) or any(matches(COVERAGE, p) for p in paths)
|
||||
|
||||
|
||||
def read_paths(src):
|
||||
data = sys.stdin.buffer.read() if src == "-" else open(src, "rb").read()
|
||||
return [p.decode() for p in data.split(b"\0") if p]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
group = ap.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--paths")
|
||||
group.add_argument("--all", action="store_true")
|
||||
ap.add_argument("--root", default=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
ap.add_argument("--github-output")
|
||||
a = ap.parse_args()
|
||||
shards, build = select(a.root, [] if a.all else read_paths(a.paths), everything=a.all)
|
||||
lines = [f"shards={json.dumps(shards)}", f"build={'true' if build else 'false'}"]
|
||||
print("\n".join(lines))
|
||||
if a.github_output:
|
||||
with open(a.github_output, "a", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Self-test for kernel_rig_plan.py. Run: python3 -I scripts/ci/test_kernel_rig_plan.py (from the
|
||||
repo root).
|
||||
|
||||
Uses a small synthetic tests/kernel-cases/, then the real one: every area has its PATHS and
|
||||
KNOWN_FAILING, every glob names files that exist, and the workflow's trigger paths cover them all.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import kernel_rig_plan as plan # noqa: E402
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
WORKFLOW = os.path.join(ROOT, ".github", "workflows", "kernel-rig.yml")
|
||||
|
||||
|
||||
def write(root, rel, text=""):
|
||||
path = os.path.join(root, rel)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
class Glob(unittest.TestCase):
|
||||
def test_star_stays_in_a_folder_and_double_star_crosses(self):
|
||||
self.assertTrue(plan.glob_regex("crates/kernel/src/blend*.rs").match("crates/kernel/src/blend_smlib.rs"))
|
||||
self.assertFalse(plan.glob_regex("crates/kernel/src/*.rs").match("crates/kernel/src/tess/grid.rs"))
|
||||
self.assertTrue(plan.glob_regex("crates/kernel/src/tess/**").match("crates/kernel/src/tess/a/b.rs"))
|
||||
self.assertFalse(plan.glob_regex("crates/kernel/src/tess/**").match("crates/kernel/src/tess.rs"))
|
||||
self.assertTrue(plan.glob_regex("tests/**/recipe.json").match("tests/recipe.json"))
|
||||
self.assertTrue(plan.glob_regex("tests/**/recipe.json").match("tests/a/b/recipe.json"))
|
||||
self.assertFalse(plan.glob_regex("a.rs").match("a.rsx"))
|
||||
self.assertFalse(plan.glob_regex("a.rs").match("b/a.rs"))
|
||||
|
||||
|
||||
class Synthetic(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
r = self.root = self.tmp.name
|
||||
write(r, "tests/kernel-cases/PATHS-ALL", "# shared\ncrates/geom/**\n\n.github/workflows/kernel-rig.yml\n")
|
||||
write(r, "tests/kernel-cases/fillets/PATHS", "# blends\ncrates/kernel/src/blend*.rs\n")
|
||||
write(r, "tests/kernel-cases/shell/PATHS", "crates/kernel/src/offset*.rs\ncrates/kernel/src/blend_smlib.rs\n")
|
||||
write(r, "tests/kernel-cases/ladder/ladder.json", "{}")
|
||||
write(r, "tests/kernel-cases/README.md")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def select(self, *paths):
|
||||
return plan.select(self.root, list(paths))
|
||||
|
||||
def test_an_area_runs_on_its_paths_with_the_ladder(self):
|
||||
self.assertEqual(self.select("crates/kernel/src/blend.rs"), (["fillets", "ladder"], True))
|
||||
self.assertEqual(self.select("crates/kernel/src/blend_smlib.rs"), (["fillets", "shell", "ladder"], True))
|
||||
|
||||
def test_a_change_to_an_areas_cases_runs_it(self):
|
||||
self.assertEqual(self.select("tests/kernel-cases/shell/1-x/expected.json"), (["shell", "ladder"], True))
|
||||
self.assertEqual(self.select("tests/kernel-cases/shell/PATHS"), (["shell", "ladder"], True))
|
||||
self.assertEqual(self.select("tests/kernel-cases/ladder/golden.json"), (["ladder"], True))
|
||||
|
||||
def test_the_shared_core_runs_every_area(self):
|
||||
for p in ["crates/geom/src/brep.rs", ".github/workflows/kernel-rig.yml"]:
|
||||
self.assertEqual(self.select("docs/a.md", p), (["fillets", "shell", "ladder"], True), p)
|
||||
|
||||
def test_unrelated_changes_run_nothing(self):
|
||||
self.assertEqual(self.select("crates/ui-egui/src/app.rs", "docs/a.md", "tests/kernel-cases/README.md"), ([], False))
|
||||
|
||||
def test_a_registry_or_case_change_builds_for_the_coverage_check(self):
|
||||
self.assertEqual(self.select("crates/engine/src/cmd/view_navigation.rs"), ([], True))
|
||||
self.assertEqual(self.select("tests/kernel-cases/EXEMPT"), ([], True))
|
||||
|
||||
def test_everything_runs_every_area(self):
|
||||
self.assertEqual(plan.select(self.root, [], everything=True), (["fillets", "shell", "ladder"], True))
|
||||
|
||||
def test_a_new_area_joins_from_its_folder(self):
|
||||
write(self.root, "tests/kernel-cases/surfaces/PATHS", "crates/kernel/src/surface_*.rs\n")
|
||||
self.assertEqual(self.select("crates/kernel/src/surface_trim.rs"), (["surfaces", "ladder"], True))
|
||||
self.assertEqual(plan.select(self.root, [], everything=True)[0], ["fillets", "shell", "surfaces", "ladder"])
|
||||
|
||||
def test_an_area_without_paths_is_an_error(self):
|
||||
os.makedirs(os.path.join(self.root, "tests/kernel-cases/plastic"))
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
self.select("crates/geom/src/brep.rs")
|
||||
self.assertIn("plastic/PATHS is missing", str(e.exception))
|
||||
|
||||
|
||||
def trigger_globs():
|
||||
"""The pull_request trigger paths of the workflow (the push trigger lists the same)."""
|
||||
with open(WORKFLOW, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
events = {}
|
||||
for event in ("pull_request", "push"):
|
||||
block = re.search(rf"^ {event}:\n((?: .*\n|\s*\n)+)", text, re.M)
|
||||
assert block, event
|
||||
paths = block.group(1).split("paths:", 1)[1]
|
||||
events[event] = re.findall(r'^\s+- "([^"]+)"', paths, re.M)
|
||||
return events
|
||||
|
||||
|
||||
def sample(glob):
|
||||
"""A path the glob matches: each wildcard filled in."""
|
||||
return glob.replace("**", "x/y").replace("*", "x").replace("?", "x")
|
||||
|
||||
|
||||
class Real(unittest.TestCase):
|
||||
def test_every_area_has_its_lists_and_every_glob_names_files(self):
|
||||
# Tracked files and new ones not yet committed (not the ignored ones).
|
||||
ls = ["git", "ls-files", "--cached", "--others", "--exclude-standard"]
|
||||
tracked = subprocess.run(ls, cwd=ROOT, check=True, capture_output=True, text=True).stdout.split("\n")
|
||||
shared = plan.read_globs(os.path.join(ROOT, plan.CASES, "PATHS-ALL"))
|
||||
areas = plan.areas(ROOT)
|
||||
self.assertTrue(areas)
|
||||
for area, globs in {"PATHS-ALL": shared, **areas}.items():
|
||||
if area != "PATHS-ALL":
|
||||
self.assertTrue(os.path.isfile(os.path.join(ROOT, plan.CASES, area, "KNOWN_FAILING")), area)
|
||||
for g in globs:
|
||||
with self.subTest(area=area, glob=g):
|
||||
rx = plan.glob_regex(g)
|
||||
self.assertTrue(any(rx.match(t) for t in tracked), f"{area}: {g} matches no file")
|
||||
|
||||
def test_the_workflow_triggers_on_every_glob(self):
|
||||
events = trigger_globs()
|
||||
self.assertEqual(events["pull_request"], events["push"])
|
||||
triggers = [plan.glob_regex(g) for g in events["pull_request"]]
|
||||
globs = [*plan.read_globs(os.path.join(ROOT, plan.CASES, "PATHS-ALL")), *plan.COVERAGE]
|
||||
for area, own in plan.areas(ROOT).items():
|
||||
globs += [*own, f"{plan.CASES}/{area}/**"]
|
||||
for g in globs:
|
||||
with self.subTest(glob=g):
|
||||
self.assertTrue(any(t.match(sample(g)) for t in triggers), f"{g}: not under the workflow's trigger paths")
|
||||
|
||||
def test_a_ui_change_runs_nothing(self):
|
||||
self.assertEqual(plan.select(ROOT, ["crates/ui-egui/src/app.rs", "docs/kernel-cases.md", "README.md"]), ([], False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,58 @@
|
||||
# The shared core: a change to any of these reruns every area of the kernel rig and the ladder
|
||||
# (scripts/ci/kernel_rig_plan.py). One git pathspec glob a line, relative to the repository root:
|
||||
# `*` stays within a folder, `**` crosses folders. Each area's own sources are in <area>/PATHS.
|
||||
#
|
||||
# SMLib: the bindings, the schema 4 record bridge and the pinned revision (SMLIB_REV,
|
||||
# SMLIB_PREBUILT), and the script that fetches it.
|
||||
crates/smlib/**
|
||||
scripts/fetch-smlib.sh
|
||||
# The geometry every body is made of: B-rep record, curves, surfaces, meshes.
|
||||
crates/geom/**
|
||||
# The kernel's core, which every case goes through: bodies, extrude/revolve/primitives, booleans
|
||||
# and placements, the planar prisms, the SMLib sweeps and booleans, history and provenance, the
|
||||
# native bridge to SMLib, tessellation, and what the grader measures (measure, validity, counts).
|
||||
crates/kernel/Cargo.toml
|
||||
crates/kernel/src/body.rs
|
||||
crates/kernel/src/boolean_record.rs
|
||||
crates/kernel/src/build.rs
|
||||
crates/kernel/src/freeform.rs
|
||||
crates/kernel/src/heal.rs
|
||||
crates/kernel/src/history.rs
|
||||
crates/kernel/src/lib.rs
|
||||
crates/kernel/src/measure.rs
|
||||
crates/kernel/src/native.rs
|
||||
crates/kernel/src/native/**
|
||||
crates/kernel/src/numbered.rs
|
||||
crates/kernel/src/ops.rs
|
||||
crates/kernel/src/par.rs
|
||||
crates/kernel/src/planar_regularize.rs
|
||||
crates/kernel/src/planar_sources.rs
|
||||
crates/kernel/src/plane_fit.rs
|
||||
crates/kernel/src/prism.rs
|
||||
crates/kernel/src/prism_curve.rs
|
||||
crates/kernel/src/prism_measure.rs
|
||||
crates/kernel/src/profile.rs
|
||||
crates/kernel/src/provenance.rs
|
||||
crates/kernel/src/record.rs
|
||||
crates/kernel/src/shell_groups.rs
|
||||
crates/kernel/src/smlib_boolean.rs
|
||||
crates/kernel/src/smlib_sweeps.rs
|
||||
crates/kernel/src/tess/**
|
||||
crates/kernel/src/topo.rs
|
||||
crates/kernel/src/validity.rs
|
||||
crates/kernel/src/watch.rs
|
||||
# Timeline evaluation and the model cache, which every feature is evaluated through.
|
||||
crates/doc/src/eval.rs
|
||||
crates/doc/src/model_cache.rs
|
||||
crates/doc/src/walk.rs
|
||||
# The command registry and the solid feature commands every case starts with (sketches made
|
||||
# into boxes, cylinders, extrusions, revolutions, combines, moves; fillets, holes, shells, sweeps
|
||||
# and lofts live there too).
|
||||
crates/engine/src/cmd/features.rs
|
||||
crates/engine/src/cmd/mod.rs
|
||||
# The rig itself: the grader, the runner, the selection and this gate's workflow.
|
||||
.github/workflows/kernel-rig.yml
|
||||
apps/solidcraft-cli/src/kernel_cases.rs
|
||||
scripts/ci/kernel_rig_plan.py
|
||||
xtask/src/kernel_cases.rs
|
||||
xtask/src/kernel_ladder.rs
|
||||
@@ -0,0 +1,30 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Combine, cuts, holes, patterns, split, mirror, draft, emboss, remove, press/pull, delete face,
|
||||
# coils, text, surface extend/thicken, and bodies moved between components.
|
||||
crates/doc/src/body_transfer.rs
|
||||
crates/doc/src/body_transfer_sheet.rs
|
||||
crates/doc/src/eval_more.rs
|
||||
crates/doc/src/eval_surface.rs
|
||||
crates/doc/src/hole_reference.rs
|
||||
crates/doc/src/thread_catalog.rs
|
||||
crates/engine/src/cmd/component.rs
|
||||
crates/engine/src/cmd/delete.rs
|
||||
crates/engine/src/cmd/features_more.rs
|
||||
crates/engine/src/cmd/surface.rs
|
||||
crates/kernel/src/assemble.rs
|
||||
crates/kernel/src/delete_face.rs
|
||||
crates/kernel/src/draft_corners.rs
|
||||
crates/kernel/src/emboss.rs
|
||||
crates/kernel/src/fill.rs
|
||||
crates/kernel/src/helix.rs
|
||||
crates/kernel/src/intersection.rs
|
||||
crates/kernel/src/offset.rs
|
||||
crates/kernel/src/offset_backend.rs
|
||||
crates/kernel/src/polybool.rs
|
||||
crates/kernel/src/polyhedron.rs
|
||||
crates/kernel/src/sew.rs
|
||||
crates/kernel/src/splitface.rs
|
||||
crates/kernel/src/surface_extend.rs
|
||||
crates/kernel/src/surfaces.rs
|
||||
@@ -0,0 +1,17 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Seams of designed order: fillets and chamfers, patches, stitches and trims, and the continuity
|
||||
# measurement itself (Continuity Analysis).
|
||||
crates/doc/src/eval_surface.rs
|
||||
crates/engine/src/cmd/delete.rs
|
||||
crates/engine/src/cmd/surface.rs
|
||||
crates/engine/src/cmd/surface_analysis.rs
|
||||
crates/kernel/src/blend*.rs
|
||||
crates/kernel/src/continuity.rs
|
||||
crates/kernel/src/continuity_order.rs
|
||||
crates/kernel/src/corner_g2.rs
|
||||
crates/kernel/src/delete_face.rs
|
||||
crates/kernel/src/fill.rs
|
||||
crates/kernel/src/surface_trim.rs
|
||||
crates/kernel/src/surfaces.rs
|
||||
@@ -0,0 +1,21 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Crashes and hangs inside kernel operations: fillets, lofts, patterns, delete face, snap fits,
|
||||
# export, measurement and document inspection.
|
||||
crates/doc/src/plastic.rs
|
||||
crates/engine/src/cmd/delete.rs
|
||||
crates/engine/src/cmd/file.rs
|
||||
crates/engine/src/cmd/inspect.rs
|
||||
crates/engine/src/cmd/measure_sel.rs
|
||||
crates/engine/src/cmd/plastic.rs
|
||||
crates/kernel/src/blend*.rs
|
||||
crates/kernel/src/corner_g2.rs
|
||||
crates/kernel/src/delete_face.rs
|
||||
crates/kernel/src/fill.rs
|
||||
crates/kernel/src/lip.rs
|
||||
crates/kernel/src/loft_blend.rs
|
||||
crates/kernel/src/loft_rails.rs
|
||||
crates/kernel/src/step.rs
|
||||
crates/kernel/src/step_out.rs
|
||||
crates/step/**
|
||||
@@ -0,0 +1,15 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Fillets and chamfers (constant, per-edge, variable, G2 corners), with the drafts, shells, holes
|
||||
# and patterns they round, and the continuity the blends are graded on.
|
||||
crates/doc/src/hole_reference.rs
|
||||
crates/kernel/src/blend*.rs
|
||||
crates/kernel/src/continuity.rs
|
||||
crates/kernel/src/continuity_order.rs
|
||||
crates/kernel/src/corner_g2.rs
|
||||
crates/kernel/src/draft_corners.rs
|
||||
crates/kernel/src/fill.rs
|
||||
crates/kernel/src/offset.rs
|
||||
crates/kernel/src/offset_backend.rs
|
||||
crates/kernel/src/polyhedron.rs
|
||||
@@ -0,0 +1,22 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# STEP and IGES import and export, mesh import (3MF, STL, OBJ, glTF) and mesh-to-solid conversion.
|
||||
crates/engine/src/cmd/file.rs
|
||||
crates/engine/src/cmd/file_import.rs
|
||||
crates/engine/src/cmd/mesh.rs
|
||||
crates/io/src/formats.rs
|
||||
crates/io/src/gltf_in.rs
|
||||
crates/io/src/obj.rs
|
||||
crates/io/src/threemf.rs
|
||||
crates/kernel/src/iges_in.rs
|
||||
crates/kernel/src/iges_in/**
|
||||
crates/kernel/src/iges_out.rs
|
||||
crates/kernel/src/mesh_convert.rs
|
||||
crates/kernel/src/meshbody.rs
|
||||
crates/kernel/src/sew.rs
|
||||
crates/kernel/src/splitface.rs
|
||||
crates/kernel/src/step.rs
|
||||
crates/kernel/src/step_in/**
|
||||
crates/kernel/src/step_out.rs
|
||||
crates/step/**
|
||||
@@ -0,0 +1,15 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Sheet metal: rules, flanges, folds, unfold/refold, flat patterns, convert to sheet metal, with
|
||||
# the parameters that drive them.
|
||||
crates/doc/src/body_transfer_sheet.rs
|
||||
crates/doc/src/params.rs
|
||||
crates/doc/src/sheet.rs
|
||||
crates/doc/src/sheet_convert.rs
|
||||
crates/doc/src/sheet_face_names.rs
|
||||
crates/engine/src/cmd/parameters.rs
|
||||
crates/engine/src/cmd/sheet.rs
|
||||
crates/kernel/src/offset_backend.rs
|
||||
crates/kernel/src/polyhedron.rs
|
||||
crates/kernel/src/sew.rs
|
||||
@@ -0,0 +1,19 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Shell, offset faces, thicken, split face and body, emboss and remove on shelled parts.
|
||||
crates/doc/src/eval_more.rs
|
||||
crates/doc/src/eval_surface.rs
|
||||
crates/doc/src/params.rs
|
||||
crates/engine/src/cmd/delete.rs
|
||||
crates/engine/src/cmd/features_more.rs
|
||||
crates/engine/src/cmd/parameters.rs
|
||||
crates/engine/src/cmd/surface.rs
|
||||
crates/kernel/src/delete_face.rs
|
||||
crates/kernel/src/emboss.rs
|
||||
crates/kernel/src/offset.rs
|
||||
crates/kernel/src/offset_backend.rs
|
||||
crates/kernel/src/polyhedron.rs
|
||||
crates/kernel/src/splitface.rs
|
||||
crates/kernel/src/surface_offset.rs
|
||||
crates/kernel/src/surfaces.rs
|
||||
@@ -0,0 +1,20 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Sweeps, lofts (rails, multi-section blends), pipes, coils, ribs, surface lofts and sweeps, their
|
||||
# 3D sketch paths and the construction planes they run on.
|
||||
crates/doc/src/construct.rs
|
||||
crates/doc/src/eval_more.rs
|
||||
crates/doc/src/eval_surface.rs
|
||||
crates/doc/src/project3d.rs
|
||||
crates/engine/src/cmd/construct.rs
|
||||
crates/engine/src/cmd/features_more.rs
|
||||
crates/engine/src/cmd/sketch_3d.rs
|
||||
crates/engine/src/cmd/surface.rs
|
||||
crates/kernel/src/helix.rs
|
||||
crates/kernel/src/loft_blend.rs
|
||||
crates/kernel/src/loft_rails.rs
|
||||
crates/kernel/src/point_path.rs
|
||||
crates/kernel/src/splitface.rs
|
||||
crates/kernel/src/surfaces.rs
|
||||
crates/kernel/src/sweep_path.rs
|
||||
@@ -0,0 +1,7 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Display meshes (the tessellator itself is in the shared core): which edges the shaded view
|
||||
# smooths over, and mesh bodies.
|
||||
crates/kernel/src/meshbody.rs
|
||||
crates/kernel/src/smooth_edges.rs
|
||||
@@ -0,0 +1,12 @@
|
||||
# The source files whose change reruns this area of the kernel rig (scripts/ci/kernel_rig_plan.py),
|
||||
# besides the shared core in ../PATHS-ALL. One git pathspec glob a line, relative to the
|
||||
# repository root; `#` starts a comment.
|
||||
# Modelled and cosmetic threads, tapped holes, and the thread and pipe-thread catalogues.
|
||||
crates/doc/src/eval_thread.rs
|
||||
crates/doc/src/hole_reference.rs
|
||||
crates/doc/src/pipe_thread_catalog.rs
|
||||
crates/doc/src/pipe_thread_sizes.rs
|
||||
crates/doc/src/thread_catalog.rs
|
||||
crates/doc/src/thread_forms.rs
|
||||
crates/engine/src/cosmetic_threads.rs
|
||||
crates/kernel/src/helix.rs
|
||||
Reference in New Issue
Block a user