Skip to content

Commit 8ecbd21

Browse files
sbryngelsonclaude
andcommitted
Add gcov-based test pruning with file-level coverage cache
Build a coverage cache mapping each test to the source files it exercises, enabling --only-changes to skip tests unaffected by a PR's changes. Key components: - toolchain/mfc/test/coverage.py: cache build (3-phase: prepare, run, collect), cache load/staleness detection, git diff integration, test filtering - --build-coverage-cache CLI flag for one-time cache generation - --only-changes / --changes-branch CLI flags for coverage-based filtering - CI: rebuild-cache + commit-cache jobs auto-update cache when cases.py changes - Phoenix CI: use GNR nodes (192 cores) with 64-thread parallel test execution - 54 unit tests for coverage module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b5486c9 commit 8ecbd21

File tree

12 files changed

+1502
-10
lines changed

12 files changed

+1502
-10
lines changed

.github/file-filter.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ checkall: &checkall
3636
- *tests
3737
- *scripts
3838
- *yml
39+
40+
cases_py:
41+
- 'toolchain/mfc/test/cases.py'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
# Number of parallel jobs: use SLURM allocation or default to 24.
4+
# Cap at 64 to avoid overwhelming MPI's ORTE daemons with concurrent launches.
5+
NJOBS="${SLURM_CPUS_ON_NODE:-24}"
6+
if [ "$NJOBS" -gt 64 ]; then NJOBS=64; fi
7+
8+
# Build MFC with gcov coverage instrumentation (CPU-only, gfortran).
9+
# -j 8 for compilation (memory-heavy, more cores doesn't help much).
10+
./mfc.sh build --gcov -j 8
11+
12+
# Run all tests in parallel, collecting per-test coverage data.
13+
# Each test gets an isolated GCOV_PREFIX directory so .gcda files
14+
# don't collide. Coverage is collected per-test after all tests finish.
15+
# --gcov is required so the internal build step preserves instrumentation.
16+
./mfc.sh test --build-coverage-cache --gcov -j "$NJOBS"

.github/workflows/phoenix/submit.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ case "$script_basename" in
2424
esac
2525

2626
sbatch_cpu_opts="\
27-
#SBATCH -p cpu-small # partition
28-
#SBATCH --ntasks-per-node=24 # Number of cores per node required
29-
#SBATCH --mem-per-cpu=2G # Memory per core\
27+
#SBATCH -p cpu-gnr # partition (full Granite Rapids node)
28+
#SBATCH --exclusive # exclusive access to all cores
29+
#SBATCH -C graniterapids # constrain to GNR architecture\
3030
"
3131

3232
if [ "$job_type" = "bench" ]; then

.github/workflows/phoenix/test.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ while [ $attempt -le $max_attempts ]; do
5151
attempt=$((attempt + 1))
5252
done
5353

54-
n_test_threads=8
54+
# Use up to 64 parallel test threads on CPU (GNR nodes have 192 cores).
55+
# Cap at 64 to avoid overwhelming MPI's ORTE daemons with concurrent launches.
56+
n_test_threads=$(( SLURM_CPUS_ON_NODE > 64 ? 64 : ${SLURM_CPUS_ON_NODE:-8} ))
5557

5658
if [ "$job_device" = "gpu" ]; then
5759
gpu_count=$(nvidia-smi -L | wc -l) # number of GPUs on node

.github/workflows/test.yml

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ jobs:
5656
file-changes:
5757
name: Detect File Changes
5858
runs-on: 'ubuntu-latest'
59-
outputs:
59+
outputs:
6060
checkall: ${{ steps.changes.outputs.checkall }}
61+
cases_py: ${{ steps.changes.outputs.cases_py }}
6162
steps:
6263
- name: Clone
6364
uses: actions/checkout@v4
@@ -68,10 +69,47 @@ jobs:
6869
with:
6970
filters: ".github/file-filter.yml"
7071

72+
rebuild-cache:
73+
name: Rebuild Coverage Cache
74+
needs: [lint-gate, file-changes]
75+
if: >-
76+
github.event_name == 'pull_request' &&
77+
needs.file-changes.outputs.cases_py == 'true' &&
78+
github.repository == 'MFlowCode/MFC' &&
79+
github.event.pull_request.draft != true
80+
timeout-minutes: 240
81+
runs-on:
82+
group: phoenix
83+
labels: gt
84+
steps:
85+
- name: Clone
86+
uses: actions/checkout@v4
87+
with:
88+
clean: false
89+
90+
- name: Rebuild Cache via SLURM
91+
run: bash .github/workflows/phoenix/submit.sh .github/workflows/phoenix/rebuild-cache.sh cpu none
92+
93+
- name: Print Logs
94+
if: always()
95+
run: cat rebuild-cache-cpu-none.out
96+
97+
- name: Upload Cache Artifact
98+
uses: actions/upload-artifact@v4
99+
with:
100+
name: coverage-cache
101+
path: toolchain/mfc/test/test_coverage_cache.json.gz
102+
retention-days: 1
103+
71104
github:
72105
name: Github
73-
if: needs.file-changes.outputs.checkall == 'true'
74-
needs: [lint-gate, file-changes]
106+
needs: [lint-gate, file-changes, rebuild-cache]
107+
if: >-
108+
always() &&
109+
needs.lint-gate.result == 'success' &&
110+
needs.file-changes.result == 'success' &&
111+
(needs.rebuild-cache.result == 'success' || needs.rebuild-cache.result == 'skipped') &&
112+
needs.file-changes.outputs.checkall == 'true'
75113
strategy:
76114
matrix:
77115
os: ['ubuntu', 'macos']
@@ -98,6 +136,14 @@ jobs:
98136
- name: Clone
99137
uses: actions/checkout@v4
100138

139+
- name: Download Coverage Cache
140+
if: needs.rebuild-cache.result == 'success'
141+
uses: actions/download-artifact@v4
142+
with:
143+
name: coverage-cache
144+
path: toolchain/mfc/test
145+
continue-on-error: true
146+
101147
- name: Setup MacOS
102148
if: matrix.os == 'macos'
103149
run: |
@@ -183,8 +229,15 @@ jobs:
183229

184230
self:
185231
name: "${{ matrix.cluster_name }} (${{ matrix.device }}${{ matrix.interface != 'none' && format('-{0}', matrix.interface) || '' }}${{ matrix.shard != '' && format(' [{0}]', matrix.shard) || '' }})"
186-
if: github.repository == 'MFlowCode/MFC' && needs.file-changes.outputs.checkall == 'true' && github.event.pull_request.draft != true
187-
needs: [lint-gate, file-changes]
232+
needs: [lint-gate, file-changes, rebuild-cache]
233+
if: >-
234+
always() &&
235+
needs.lint-gate.result == 'success' &&
236+
needs.file-changes.result == 'success' &&
237+
(needs.rebuild-cache.result == 'success' || needs.rebuild-cache.result == 'skipped') &&
238+
github.repository == 'MFlowCode/MFC' &&
239+
needs.file-changes.outputs.checkall == 'true' &&
240+
github.event.pull_request.draft != true
188241
continue-on-error: false
189242
timeout-minutes: 480
190243
strategy:
@@ -265,6 +318,14 @@ jobs:
265318
with:
266319
clean: false
267320

321+
- name: Download Coverage Cache
322+
if: needs.rebuild-cache.result == 'success'
323+
uses: actions/download-artifact@v4
324+
with:
325+
name: coverage-cache
326+
path: toolchain/mfc/test
327+
continue-on-error: true
328+
268329
- name: Build
269330
if: matrix.cluster != 'phoenix'
270331
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3
@@ -299,3 +360,36 @@ jobs:
299360
with:
300361
name: logs-${{ strategy.job-index }}-${{ steps.log.outputs.slug }}
301362
path: ${{ steps.log.outputs.slug }}.out
363+
364+
commit-cache:
365+
name: Commit Coverage Cache
366+
needs: [rebuild-cache]
367+
if: needs.rebuild-cache.result == 'success'
368+
runs-on: ubuntu-latest
369+
permissions:
370+
contents: write
371+
steps:
372+
- name: Clone
373+
uses: actions/checkout@v4
374+
with:
375+
ref: ${{ github.head_ref }}
376+
377+
- name: Download Coverage Cache
378+
uses: actions/download-artifact@v4
379+
with:
380+
name: coverage-cache
381+
path: toolchain/mfc/test
382+
383+
- name: Commit Updated Cache
384+
run: |
385+
git config user.name "github-actions[bot]"
386+
git config user.email "github-actions[bot]@users.noreply.github.com"
387+
git add toolchain/mfc/test/test_coverage_cache.json.gz
388+
if git diff --cached --quiet; then
389+
echo "Coverage cache unchanged."
390+
else
391+
git commit -m "Regenerate gcov coverage cache
392+
393+
Automatically rebuilt because cases.py changed."
394+
git push
395+
fi

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ __pycache__
2222
# Auto-generated version file
2323
toolchain/mfc/_version.py
2424

25+
# Raw coverage cache — legacy, not tracked (the .json.gz version IS committed)
26+
toolchain/mfc/test/test_coverage_cache.json
27+
2528
# Auto-generated toolchain files (regenerate with: ./mfc.sh generate)
2629
toolchain/completions/mfc.bash
2730
toolchain/completions/_mfc

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ macro(HANDLE_SOURCES target useCommon)
381381
--no-folding
382382
--line-length=999
383383
--line-numbering-mode=nocontlines
384+
--line-marker-format=gfortran5
384385
"${fpp}" "${f90}"
385386
DEPENDS "${fpp};${${target}_incs}"
386387
COMMENT "Preprocessing (Fypp) ${fpp_filename}"

toolchain/mfc/cli/commands.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,27 @@
458458
type=str,
459459
default=None,
460460
),
461+
Argument(
462+
name="build-coverage-cache",
463+
help="Run all tests sequentially with gcov instrumentation to build the line-level coverage cache. Requires a prior --gcov build: ./mfc.sh build --gcov -j 8",
464+
action=ArgAction.STORE_TRUE,
465+
default=False,
466+
dest="build_coverage_cache",
467+
),
468+
Argument(
469+
name="only-changes",
470+
help="Only run tests whose covered lines overlap with lines changed since branching from master (uses line-level gcov coverage cache).",
471+
action=ArgAction.STORE_TRUE,
472+
default=False,
473+
dest="only_changes",
474+
),
475+
Argument(
476+
name="changes-branch",
477+
help="Branch to compare against for --only-changes (default: master).",
478+
type=str,
479+
default="master",
480+
dest="changes_branch",
481+
),
461482
],
462483
mutually_exclusive=[
463484
MutuallyExclusiveGroup(arguments=[
@@ -488,13 +509,17 @@
488509
Example("./mfc.sh test -j 4", "Run with 4 parallel jobs"),
489510
Example("./mfc.sh test --only 3D", "Run only 3D tests"),
490511
Example("./mfc.sh test --generate", "Regenerate golden files"),
512+
Example("./mfc.sh test --only-changes -j 4", "Run tests affected by changed lines"),
513+
Example("./mfc.sh build --gcov -j 8 && ./mfc.sh test --build-coverage-cache", "One-time: build line-coverage cache"),
491514
],
492515
key_options=[
493516
("-j, --jobs N", "Number of parallel test jobs"),
494517
("-o, --only PROP", "Run tests matching property"),
495518
("-f, --from UUID", "Start from specific test"),
496519
("--generate", "Generate/update golden files"),
497520
("--no-build", "Skip rebuilding MFC"),
521+
("--build-coverage-cache", "Build line-level gcov coverage cache (one-time)"),
522+
("--only-changes", "Run tests affected by changed lines (requires cache)"),
498523
],
499524
)
500525

0 commit comments

Comments
 (0)