Skip to content

Commit 2210805

Browse files
sbryngelsonclaude
andcommitted
Fix review findings: dir failure re-raise, FYPP_GCOV_OPTS init, dep_changed safety, SYSCHECK cleanup
- Re-raise OSError in _prepare_test after directory creation failure instead of silently continuing with a broken test directory - Initialize FYPP_GCOV_OPTS to empty before the MFC_GCov block so non-gcov builds don't pass an undefined variable to Fypp - Push dep_changed now defaults to true on git diff failure, matching the safe PR-path behavior - Remove dead SYSCHECK references from test stubs and fix exec fallback replace string to match actual coverage.py imports - Improve docstrings, comments, and cleanup error handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d6f57e commit 2210805

5 files changed

Lines changed: 37 additions & 17 deletions

File tree

.github/workflows/test.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ jobs:
8888
exit 0
8989
}
9090
elif [ "${{ github.event_name }}" = "push" ]; then
91-
DIFF=$(git diff "$BEFORE".."$AFTER" 2>/dev/null || echo "")
91+
DIFF=$(git diff "$BEFORE".."$AFTER" 2>/dev/null) || {
92+
echo "git diff failed for push event — defaulting to dep_changed=true for safety."
93+
echo "dep_changed=true" >> "$GITHUB_OUTPUT"
94+
exit 0
95+
}
9296
else
9397
DIFF=""
9498
fi

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ endif()
113113
# debug builds. These include optimization and debug flags, as well as some that
114114
# are required for a successful build of MFC.
115115

116+
set(FYPP_GCOV_OPTS "")
117+
116118
if (CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
117119
add_compile_options(
118120
$<$<COMPILE_LANGUAGE:Fortran>:-ffree-line-length-none>

toolchain/mfc/cli/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@
460460
),
461461
Argument(
462462
name="build-coverage-cache",
463-
help="Run all tests with gcov instrumentation to build the file-level coverage cache. Requires a prior --gcov build: ./mfc.sh build --gcov -j 8",
463+
help="Run all tests with gcov instrumentation to build the file-level coverage cache. Pass --gcov to enable coverage instrumentation in the internal build step.",
464464
action=ArgAction.STORE_TRUE,
465465
default=False,
466466
dest="build_coverage_cache",

toolchain/mfc/test/coverage.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
covered file set. Only tests that touch at least one changed file run.
99
1010
Workflow:
11-
./mfc.sh build --gcov -j 8 # one-time: build with coverage
12-
./mfc.sh test --build-coverage-cache # one-time: populate the cache
13-
./mfc.sh test --only-changes -j 8 # fast: run only affected tests
11+
./mfc.sh test --build-coverage-cache --gcov -j 8 # one-time: populate the cache
12+
./mfc.sh test --only-changes -j 8 # fast: run only affected tests
1413
"""
1514

1615
import io
@@ -234,6 +233,9 @@ def _collect_single_test_coverage( # pylint: disable=too-many-locals
234233
"""
235234
build_subdir = os.path.join(test_gcda, "build")
236235
if not os.path.isdir(build_subdir):
236+
# No .gcda files produced — test may not have run or GCOV_PREFIX
237+
# was misconfigured. Return empty list; the sanity check at the end
238+
# of build_coverage_cache will catch systemic failures.
237239
return uuid, []
238240

239241
gcno_copies = []
@@ -367,13 +369,17 @@ def _prepare_test(case, root_dir: str) -> dict: # pylint: disable=unused-argume
367369
Prepare a test for direct execution: create directory, generate .inp
368370
files, and resolve binary paths. All Python/toolchain overhead happens
369371
here (single-threaded) so the parallel phase is pure subprocess calls.
372+
373+
Operates on a shallow copy of case.params to avoid mutating the
374+
original case object.
370375
"""
371376
try:
372377
case.delete_output()
373378
case.create_directory()
374379
except OSError as exc:
375380
cons.print(f"[yellow]Warning: Failed to prepare test directory for "
376381
f"{case.get_uuid()}: {exc}[/yellow]")
382+
raise
377383

378384
# Lagrange bubble tests need input files generated before running.
379385
if case.params.get("bubbles_lagrange", 'F') == 'T':
@@ -383,35 +389,39 @@ def _prepare_test(case, root_dir: str) -> dict: # pylint: disable=unused-argume
383389
cons.print(f"[yellow]Warning: Failed to generate Lagrange bubble input "
384390
f"for {case.get_uuid()}: {exc}[/yellow]")
385391

392+
# Work on a copy so we don't permanently mutate the case object.
393+
params = dict(case.params)
394+
386395
# Apply post_process output params so simulation writes data files that
387396
# post_process reads. Mirrors the generated case.py logic that normally
388397
# runs via ./mfc.sh run (see POST_PROCESS_OUTPUT_PARAMS in case.py).
389-
case.params.update(get_post_process_mods(case.params))
398+
params.update(get_post_process_mods(params))
390399

391400
# Run only one timestep: we only need to know which source files are
392401
# *touched*, not verify correctness. A single step exercises the key
393402
# code paths across all three executables while preventing heavy 3D tests
394403
# from timing out under gcov instrumentation (~10x slowdown).
395-
case.params['t_step_stop'] = 1
404+
params['t_step_stop'] = 1
396405

397406
# Adaptive-dt tests: post_process computes n_save = int(t_stop/t_save)+1
398407
# and iterates over that many save indices. But with small t_step_stop
399408
# the simulation produces far fewer saves. Clamp t_stop so post_process
400409
# only reads saves that actually exist.
401-
if case.params.get('cfl_adap_dt', 'F') == 'T':
402-
t_save = float(case.params.get('t_save', 1.0))
403-
case.params['t_stop'] = t_save # n_save = 2: indices 0 and 1
410+
if params.get('cfl_adap_dt', 'F') == 'T':
411+
t_save = float(params.get('t_save', 1.0))
412+
params['t_stop'] = t_save # n_save = 2: indices 0 and 1
404413

405414
# Heavy 3D tests: remove vorticity output (omega_wrt + fd_order) for
406415
# 3D QBMM tests. Normal test execution never runs post_process (only
407416
# PRE_PROCESS + SIMULATION, never POST_PROCESS), so post_process on
408417
# heavy 3D configs is untested. Vorticity FD computation on large grids
409418
# with many QBMM variables causes post_process to crash (exit code 2).
410-
if (int(case.params.get('p', 0)) > 0 and
411-
case.params.get('qbmm', 'F') == 'T'):
419+
if (int(params.get('p', 0)) > 0 and
420+
params.get('qbmm', 'F') == 'T'):
412421
for key in POST_PROCESS_3D_PARAMS:
413-
case.params.pop(key, None)
422+
params.pop(key, None)
414423

424+
case.params = params
415425
test_dir = case.get_dirpath()
416426
input_file = case.to_input_file()
417427

@@ -466,6 +476,8 @@ def build_coverage_cache( # pylint: disable=too-many-locals,too-many-statements
466476
strip = _compute_gcov_prefix_strip(root_dir)
467477

468478
if n_jobs is None:
479+
# Caller should pass n_jobs explicitly on SLURM systems;
480+
# os.cpu_count() may exceed the SLURM allocation.
469481
n_jobs = max(os.cpu_count() or 1, 1)
470482
# Cap Phase 2 test parallelism: each test spawns gcov-instrumented MPI
471483
# processes (~2-5 GB each under gcov). Too many concurrent tests cause OOM.
@@ -568,7 +580,11 @@ def build_coverage_cache( # pylint: disable=too-many-locals,too-many-statements
568580
if completed % 50 == 0 or completed == len(test_results):
569581
cons.print(f" [{completed:3d}/{len(test_results):3d}] tests processed")
570582
finally:
571-
shutil.rmtree(gcda_dir, ignore_errors=True)
583+
try:
584+
shutil.rmtree(gcda_dir)
585+
except OSError as exc:
586+
cons.print(f"[yellow]Warning: Failed to clean up temp directory "
587+
f"{gcda_dir}: {exc}[/yellow]")
572588

573589
# Sanity check: at least some tests should have non-empty coverage.
574590
tests_with_coverage = sum(1 for v in cache.values() if v)

toolchain/mfc/test/test_coverage_unit.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ class _FakeMFCException(Exception):
6868
_build_stub.PRE_PROCESS = "pre_process"
6969
_build_stub.SIMULATION = "simulation"
7070
_build_stub.POST_PROCESS = "post_process"
71-
_build_stub.SYSCHECK = "syscheck"
7271

7372
_case_stub = sys.modules.get("toolchain.mfc.test.case", _make_stub("toolchain.mfc.test.case"))
7473
_case_stub.input_bubbles_lagrange = lambda case: None
@@ -123,7 +122,6 @@ class _FakeMFCException(Exception):
123122
"PRE_PROCESS": "pre_process",
124123
"SIMULATION": "simulation",
125124
"POST_PROCESS": "post_process",
126-
"SYSCHECK": "syscheck",
127125
}
128126
with open(_COVERAGE_PATH, encoding="utf-8") as _f:
129127
_src = _f.read()
@@ -133,7 +131,7 @@ class _FakeMFCException(Exception):
133131
.replace("from ..printer import cons", "cons = _globals['cons']")
134132
.replace("from .. import common", "")
135133
.replace("from ..common import MFCException", "MFCException = _globals['MFCException']")
136-
.replace("from ..build import PRE_PROCESS, SIMULATION, POST_PROCESS, SYSCHECK", "")
134+
.replace("from ..build import PRE_PROCESS, SIMULATION, POST_PROCESS", "")
137135
.replace("from .case import (input_bubbles_lagrange, get_post_process_mods,\n"
138136
" POST_PROCESS_3D_PARAMS)",
139137
"input_bubbles_lagrange = lambda case: None\n"

0 commit comments

Comments
 (0)