Skip to content

ci: skip pre-commit hook on automated cache commit#1319

Closed
sbryngelson wants to merge 11 commits intoMFlowCode:masterfrom
sbryngelson:gcov-test-pruning-v3-rebase
Closed

ci: skip pre-commit hook on automated cache commit#1319
sbryngelson wants to merge 11 commits intoMFlowCode:masterfrom
sbryngelson:gcov-test-pruning-v3-rebase

Conversation

@sbryngelson
Copy link
Copy Markdown
Member

Summary

  • The rebuild-cache CI job's git commit triggers the pre-commit hook, which runs precheck on the entire repo. If any other merged PR introduced a spelling/lint issue, the cache commit fails — even though only test_coverage_cache.json.gz is being committed.
  • Fix: add --no-verify to the automated git commit in the "Commit Cache to Master" step.

Test plan

  • rebuild-cache job succeeds on the next push to master

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 17, 2026 23:19
@github-actions
Copy link
Copy Markdown

Claude Code Review

Head SHA: 04a9f93

Files changed:

  • 15 files (+1927 / -31)
  • .github/workflows/test.yml
  • .github/workflows/common/rebuild-cache.sh (new)
  • toolchain/mfc/test/coverage.py (new, 804 lines)
  • toolchain/mfc/test/test_coverage_unit.py (new, 780 lines)
  • toolchain/mfc/test/test.py
  • toolchain/mfc/test/case.py
  • toolchain/mfc/test/cases.py
  • toolchain/mfc/cli/commands.py
  • CMakeLists.txt
  • toolchain/mfc/test/test_coverage_cache.json.gz (new binary)

Summary:

  • Adds a gcov-based file-level coverage cache system: build once with --gcov, run all tests to record which .fpp files each test exercises, then on PRs run only tests whose covered files overlap with changed files.
  • Adds a rebuild-cache CI job on Phoenix that triggers on cases.py changes or detected Fortran dependency changes, commits the updated cache to master, and uploads it as an artifact for PR jobs to download.
  • Extends the test runner with --build-coverage-cache, --only-changes, and --changes-branch flags.
  • Refactors duplicate post-process output parameter dictionaries from the inline case template into named constants (POST_PROCESS_OUTPUT_PARAMS, etc.) importable by coverage.py.
  • Skips the pre-commit hook (--no-verify) for the automated cache-commit step, which was the immediate motivation for this PR.

Findings:

1. rebuild-cache runs on push to any branch, not just master (test.yml ~line 141)
The rebuild-cache job condition is:

github.event_name == 'push' &&
(needs.file-changes.outputs.cases_py == 'true' || ...)

There is no github.ref == 'refs/heads/master' guard here, only on the inner Commit Cache to Master step. On a push to a non-master branch that modifies cases.py, the full 20-240 min Phoenix SLURM job will run (and the built cache will be silently discarded). Consider adding the ref guard to the job-level if.

2. Rebase-then-push race in Commit Cache to Master (test.yml ~line 187)

git rebase origin/master
git push origin HEAD:refs/heads/master

There is no retry loop around the push. If another commit lands on master between the rebase and the push, the push will fail with a non-fast-forward error and the step will exit non-zero — blocking downstream github and self jobs (since they depend on rebuild-cache and the overall condition checks result == 'success'). A git push --force-with-lease or a retry loop (fetch → rebase → push) would make this robust.

3. New-file coverage gap: no test covers a brand-new .fpp file (coverage.py ~line 1261)
In filter_tests_by_coverage, if a PR adds a new .fpp file (not yet in the coverage cache as a covered file), all tests whose cache entries don't mention it will be skipped — even if the new file is #:included from an existing file that every test covers. The conservative logic only applies to tests that are not in the cache at all; it does not apply to changed files that no test in the cache touches. This is documented as intentional design but is worth flagging since it could silently under-test a new shared utility file.

4. export GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME in submit-slurm-job.sh (line 28)
The environment variable is already in the environment at that point, so export VAR=$VAR is a no-op unless the intent is to ensure it is exported into the sourced subshell. Either way the pattern is redundant and may confuse readers; a comment would help.

5. Fortran source lint / convention — no violations found
No .fpp files are modified. The CMakeLists change is correct: -O1 is now set via CMAKE_Fortran_FLAGS_RELEASE (overriding -O3) rather than appended as a compile option, which cleanly resolves the flag-ordering issue from the previous PR (#1316).

6. gcda_count diagnostic has a shadowing bug (coverage.py ~line 1040)

gcda_count = sum(1 for _, _, fns in os.walk(sample_build) for f in fns if f.endswith(".gcda"))

The loop variable f shadows the outer f from the tuple unpack (which is named fns — so it actually unpacks correctly as _, _, fns). However the inner iterator uses for f in fns where fns is the filename list — this is correct but the outer variable is named fns while standard os.walk convention names it filenames. Low severity, but renaming fnsfilenames would improve clarity.

7. Thread-safety note: cons output interleaving (coverage.py build_coverage_cache)
Phase 2 runs tests via ThreadPoolExecutor, and progress messages (cons.print) are called from the future result loop in the main thread — this is fine. However _prepare_test temporarily redirects cons.raw.file to a StringIO to suppress output; this patch-and-restore is explicitly noted as # NOTE: not thread-safe and Phase 1 is indeed single-threaded, so no issue. Worth confirming this remains so if the code is refactored.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces file-level gcov coverage caching and CI-driven test pruning to speed up PR test runs, and adjusts CI to rebuild/commit the cache automatically (including skipping pre-commit hooks for the automated cache commit).

Changes:

  • Add a file-level gcov coverage cache builder (--build-coverage-cache) and a pruning mode (--only-changes) that selects tests based on changed .fpp files.
  • Extend CI to (a) detect cache-staleness triggers, (b) rebuild/upload/commit the cache, and (c) run pruned test suites on PRs using the downloaded cache.
  • Update build system to improve gcov compatibility (Fypp line markers, disable LTO for gcov, adjust Release optimization for gcov).

Reviewed changes

Copilot reviewed 13 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
toolchain/mfc/test/coverage.py New coverage cache builder + changed-file detection + test filtering logic
toolchain/mfc/test/test.py Wire --only-changes pruning and --build-coverage-cache execution into test runner
toolchain/mfc/cli/commands.py Add CLI flags for building cache and pruning based on changes
.github/workflows/test.yml Add cache rebuild job, artifact flow, gating, and --no-verify commit; enable pruning on PRs
.github/workflows/common/rebuild-cache.sh New script to build gcov-instrumented binaries and generate cache on SLURM runner
.github/workflows/common/test.sh Prune tests on PRs only (keeps full suite on master pushes)
CMakeLists.txt Adjust gcov flags/behavior (Release -O1, disable LTO for gcov, Fypp line markers)
toolchain/mfc/test/case.py Refactor post_process param mods into shared constants/helpers used by cache builder
toolchain/mfc/test/test_coverage_unit.py Add offline unit tests for coverage cache logic
toolchain/mfc/test/test_coverage_cache.json.gz Add committed gzipped cache artifact
.github/file-filter.yml Add common workflow paths + cases_py filter
.github/scripts/submit-slurm-job.sh Pass GITHUB_EVENT_NAME into SLURM job script
.gitignore Ignore legacy raw JSON cache file
toolchain/mfc/test/cases.py Skip 1D_qbmm example (gfortran 12 overflow note)
toolchain/mfc/viz/interactive.py Remove stray whitespace line

Comment on lines +473 to +481
if n_jobs is None:
# Caller should pass n_jobs explicitly on SLURM systems;
# os.cpu_count() may exceed the SLURM allocation.
n_jobs = max(os.cpu_count() or 1, 1)
# Cap Phase 2 test parallelism: each test spawns gcov-instrumented MPI
# processes (~2-5 GB each under gcov). Too many concurrent tests cause OOM.
# Phase 3 gcov workers run at full n_jobs (gcov is lightweight by comparison).
phase2_jobs = min(n_jobs, 16)
cons.print(f"[bold]Building coverage cache for {len(cases)} tests ({phase2_jobs} test workers, {n_jobs} gcov workers)...[/bold]")
Comment on lines +28 to +95
# Create minimal stubs for toolchain modules so coverage.py can be imported
# without the full MFC toolchain being on sys.path.
def _make_stub(name):
mod = types.ModuleType(name)
sys.modules[name] = mod
return mod


for _mod_name in [
"toolchain",
"toolchain.mfc",
"toolchain.mfc.printer",
"toolchain.mfc.common",
"toolchain.mfc.build",
"toolchain.mfc.test",
"toolchain.mfc.test.case",
]:
if _mod_name not in sys.modules:
_make_stub(_mod_name)

# Provide the attributes coverage.py needs from its relative imports
_printer_stub = sys.modules.get("toolchain.mfc.printer", _make_stub("toolchain.mfc.printer"))


class _FakeCons:
def print(self, *args, **kwargs):
pass # suppress output during tests


_printer_stub.cons = _FakeCons()

_common_stub = sys.modules.get("toolchain.mfc.common", _make_stub("toolchain.mfc.common"))
_common_stub.MFC_ROOT_DIR = "/fake/repo"


class _FakeMFCException(Exception):
pass


_common_stub.MFCException = _FakeMFCException

_build_stub = sys.modules.get("toolchain.mfc.build", _make_stub("toolchain.mfc.build"))
_build_stub.PRE_PROCESS = "pre_process"
_build_stub.SIMULATION = "simulation"
_build_stub.POST_PROCESS = "post_process"

_case_stub = sys.modules.get("toolchain.mfc.test.case", _make_stub("toolchain.mfc.test.case"))
_case_stub.input_bubbles_lagrange = lambda case: None
_case_stub.get_post_process_mods = lambda params: {}
_case_stub.POST_PROCESS_3D_PARAMS = {
"fd_order": 1,
"omega_wrt(1)": "T",
"omega_wrt(2)": "T",
"omega_wrt(3)": "T",
}

# Load coverage.py by injecting stubs into sys.modules so relative imports resolve.
_COVERAGE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "coverage.py")

sys.modules.pop("toolchain.mfc.test.coverage", None) # reset if already loaded

_spec = importlib.util.spec_from_file_location("toolchain.mfc.test.coverage", _COVERAGE_PATH, submodule_search_locations=[])
_coverage_mod = importlib.util.module_from_spec(_spec)
_coverage_mod.__package__ = "toolchain.mfc.test"

sys.modules["toolchain.mfc.test"] = types.ModuleType("toolchain.mfc.test")
sys.modules["toolchain.mfc.test"].__package__ = "toolchain.mfc.test"

Comment on lines +178 to +206
def test_parse_single_file(self):
result = _parse_diff_files("src/simulation/m_rhs.fpp\n")
assert result == {"src/simulation/m_rhs.fpp"}

def test_parse_multiple_files(self):
text = "src/simulation/m_rhs.fpp\nsrc/simulation/m_weno.fpp\nREADME.md\n"
result = _parse_diff_files(text)
assert result == {
"src/simulation/m_rhs.fpp",
"src/simulation/m_weno.fpp",
"README.md",
}

def test_parse_empty(self):
assert _parse_diff_files("") == set()
assert _parse_diff_files("\n") == set()

def test_parse_ignores_blank_lines(self):
text = "src/simulation/m_rhs.fpp\n\n\nsrc/simulation/m_weno.fpp\n"
result = _parse_diff_files(text)
assert result == {"src/simulation/m_rhs.fpp", "src/simulation/m_weno.fpp"}

def test_parse_mixed_extensions(self):
text = "src/simulation/m_rhs.fpp\ntoolchain/mfc/test/cases.py\nCMakeLists.txt\n"
result = _parse_diff_files(text)
assert len(result) == 3
assert "toolchain/mfc/test/cases.py" in result
assert "CMakeLists.txt" in result

Comment on lines +107 to +165
rebuild-cache:
name: Rebuild Coverage Cache
needs: [lint-gate, file-changes]
if: >-
github.repository == 'MFlowCode/MFC' &&
(
(github.event_name == 'pull_request' &&
(needs.file-changes.outputs.cases_py == 'true' ||
needs.file-changes.outputs.dep_changed == 'true')) ||
(github.event_name == 'push' &&
(needs.file-changes.outputs.cases_py == 'true' ||
needs.file-changes.outputs.dep_changed == 'true')) ||
github.event_name == 'workflow_dispatch'
)
timeout-minutes: 240
runs-on:
group: phoenix
labels: gt
permissions:
contents: write # Required for Commit Cache to Master on push events
steps:
- name: Clone
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
clean: false

- name: Rebuild Cache via SLURM
run: bash .github/scripts/submit-slurm-job.sh .github/workflows/common/rebuild-cache.sh cpu none phoenix

- name: Print Logs
if: always()
run: cat rebuild-cache-cpu-none.out

- name: Upload Cache Artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: coverage-cache
path: toolchain/mfc/test/test_coverage_cache.json.gz
retention-days: 1

- name: Commit Cache to Master
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add toolchain/mfc/test/test_coverage_cache.json.gz
if git diff --cached --quiet; then
echo "Coverage cache unchanged."
else
git commit --no-verify -m "Regenerate gcov coverage cache [skip ci]"
# Rebase onto latest master in case it advanced during the build
# (which can take 20-240 minutes). Only test_coverage_cache.json.gz
# is changed, so rebase conflicts are essentially impossible.
git fetch origin master
git rebase origin/master
git push origin HEAD:refs/heads/master
fi
# Override Release -O3 with -O1 for gcov: coverage instrumentation is
# inaccurate at -O3, and aggressive codegen (e.g. AVX-512 FP16 on
# Granite Rapids) can emit instructions that older assemblers reject.
set(CMAKE_Fortran_FLAGS_RELEASE "-O1 -DNDEBUG" CACHE STRING "" FORCE)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive coverage cache and test filtering infrastructure for the MFC toolchain. It adds GitHub Actions workflows to build and maintain a gcov-based coverage cache, new CLI options (--build-coverage-cache and --only-changes) to support coverage-driven test selection, CMake support for gcov instrumentation without LTO/IPO conflicts, and a new coverage module that computes file-level coverage data and filters tests based on changed files. The implementation includes workflow orchestration via SLURM, cache artifact management, test pruning logic with fallback mechanisms, and extensive unit test coverage for the new functionality.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly describes the main change: adding --no-verify flag to skip pre-commit hooks on the automated git commit that updates the coverage cache.
Description check ✅ Passed The PR description covers the issue (pre-commit hook failures on cache commits), the fix (--no-verify flag), and a test plan, though the description is brief and the test plan checkbox is unchecked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Important

Merge conflicts detected (Beta)

  • Resolve merge conflict in branch gcov-test-pruning-v3-rebase
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can get early access to new features in CodeRabbit.

Enable the early_access setting to enable early access features such as new models, tools, and more.

@sbryngelson sbryngelson deleted the gcov-test-pruning-v3-rebase branch March 20, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants