Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Critical and moderate issues remain in image-test handling, reporting accuracy, provenance, and automation reliability.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request expands integration-test automation with environment provenance, Markdown reporting, automatic image checking, and weekly cron documentation.
Changes:
- Adds report configuration and per-task environment capture.
- Automates image-checker execution and report generation.
- Documents weekly cron scheduling.
File summaries
| File | Summary |
|---|---|
tests/main_branch_testing/zppy_test.cfg |
Adds report configuration options. |
tests/main_branch_testing/run_integration_test.bash |
Implements environment capture, reporting, and automated image checking. |
docs/source/dev_guide/tests/automated_test.rst |
Documents reports and cron-based execution. |
Review details
Suppressed comments (8)
docs/source/dev_guide/tests/automated_test.rst:91
- The new workflow leaves
tests/main_branch_testing/README.mdinconsistent with this documentation: its Phase 3 section still says the script only prints instructions to runtest_images.pymanually, while this change launches it automatically. Please update that guide in the same change so users do not get contradictory instructions.
* Step 8: Run all Python tests, including the image checker (``pytest tests/integration/test_images.py``), which is now launched automatically on a compute node -- no manual step required.
docs/source/dev_guide/tests/automated_test.rst:337
- The weekly driver copies the runner from
$HOME/ez/zppy, but the runner itself leaves that checkout ontest_zppy_<TAG>during Phase 1. On the next cron run thiscpcan therefore pick up the previous test branch's older runner instead of the latestmainversion, so automation updates to the script are not reliably adopted. Use a separate stable checkout for the driver source or explicitly update that source checkout before copying.
cp "$HOME/ez/zppy/tests/main_branch_testing/run_integration_test.bash" .
tests/main_branch_testing/run_integration_test.bash:1208
- The
|| trueconverts anygit logerror (missingupstream/<branch>, invalid date, or repository failure) into an empty result, which is then rendered asNone. The report can therefore claim that there were no changes when it actually could not inspect the repository; emit an unavailable/error row instead of conflating the two cases.
local commits
commits=$(git -C "$repo_dir" log "upstream/${branch}" --since="${EXPECTED_RESULTS_UPDATED_DATE}" --oneline 2>/dev/null || true)
if [[ -z "$commits" ]]; then
report_append "| [${label}](${repo_url}/commits/${branch}) | None |"
tests/main_branch_testing/run_integration_test.bash:487
- Every
CFGS_ARRAYentry already starts withweekly_(for example,weekly_comprehensive_v3), while the generated output root iszppy_weekly_comprehensive_v3_www. This createszppy_weekly_weekly_comprehensive_v3_wwwand putsenv_description.txtin a new, unused tree instead of alongside the diagnostics.
target_dir="${OUTPUT_WORKSPACE}/zppy_weekly_${cfg}_www/${UNIQUE_ID}/${case}/${task}"
tests/main_branch_testing/run_integration_test.bash:533
- This waits on every job owned by
USER, not the submitted image-checker job. An unrelated long-running job can consume the two-hour timeout, and an earlysqueuesnapshot can miss a newly submitted image job. Poll the submitted job's state directly and handle its terminal result.
wait_for_slurm_jobs 120 7200
tests/main_branch_testing/run_integration_test.bash:1175
- The generated table has eight columns, but this separator row has only four cells. Markdown renderers will treat the failing-only table as malformed; emit eight separators to match the header and data rows.
print("| --- | --- | --- | --- |")
tests/main_branch_testing/run_integration_test.bash:1171
by_taskpreserves the first task occurrence from the cfg-major summary order, which is not sorted by task (the configured task order ise3sm_diags,mpas_analysis,global_time_series, ...). The section is explicitly titled "sorted by task", so iterate oversorted(by_task.items())or otherwise sort the task keys before rendering.
for task, flines in by_task.items():
tests/main_branch_testing/run_integration_test.bash:129
- The manual Perlmutter allocation above includes a one-hour walltime, but this batch directive omits
--time. The automatically submitted checker therefore uses the site's default batch limit instead of the documented limit and may be terminated before the image test completes; keep the batch request consistent with the manual allocation.
SBATCH_DIRECTIVES=$'#SBATCH --qos=interactive\n#SBATCH --time=01:00:00\n#SBATCH --constraint=cpu\n#SBATCH --account=e3sm'
- Files reviewed: 3/3 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Addressed in 2d55a48. |
forsyth2
left a comment
There was a problem hiding this comment.
Did a high-level read-through.
|
Remaining action items:
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate issues affect environment capture, portability, report accuracy, and automated result status.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (7)
tests/integration/image_summary_report.py:53
- The real image-check summary rows are keyed like
comprehensive_v2_e3sm_diags(image_checker.pybuilds<cfg>_<task>), so this split treats the entire string as one token and never matchese3sm_diagsorglobal_time_series. As a result, every failing row is grouped underotherin the generated report; match the task suffix as well.
tokens = {token for token in re.split(r"[^A-Za-z0-9_]+", name) if token}
for task in tasks:
if task in tokens:
tests/main_branch_testing/run_integration_test.bash:607
run_image_checkeris called as the condition ofif ! run_image_checker, so Bash'serrexitis suppressed inside the function. Ifsbatchfails, this assignment leaves an empty job ID and the function proceeds to poll that ID for up to two hours instead of reporting submission failure. Check the submission status explicitly before waiting.
IMAGE_CHECKER_JOB_ID=$(sbatch --parsable "$IMAGE_CHECKER_SBATCH")
log "Image checker job submitted: ${IMAGE_CHECKER_JOB_ID}"
tests/main_branch_testing/run_integration_test.bash:600
- These lines bypass the machine-specific initialization used elsewhere in this script (
source ~/.bashrcplusCONDA_ACTIVATION_CMD). On Compy/Perlmutter, or any setup whereCONDA_PROFILEis not the active installation, the batch job can fail atconda activate, so the automatic image check is not portable across the supported machines. Initialize the batch shell with the same sequence before activatingZPPY_ENV.
source ${CONDA_PROFILE}
conda activate ${ZPPY_ENV}
tests/main_branch_testing/run_integration_test.bash:631
test_images.pywritesearly_test_images_summary.mdbefore re-raising a worker exception (seetests/integration/test_images.py:294-305), but this path only checks the final summary filename. When a worker fails, the generated report loses the partial table and only says the summary is missing; please fall back to the early summary (and clear any stale early file before submission) while retaining the failed status.
if [[ -f "${ZPPY_DIR}/test_images_summary.md" ]]; then
cp "${ZPPY_DIR}/test_images_summary.md" "${SCRIPT_RUN_DIR}/test_images_summary_${TAG}.md"
log_success "Copied test_images_summary.md -> ${SCRIPT_RUN_DIR}/test_images_summary_${TAG}.md"
else
log_warning "test_images_summary.md not found in ${ZPPY_DIR}"
tests/main_branch_testing/run_integration_test.bash:1175
- These bullets are unconditional and do not record any result: the integration invocations above discard exit codes with
|| log_warning, and the unit/status outcomes are not stored here. A passing and failing run therefore produce the same automated-results section, which makes the generated report insufficient for unattended review. Capture the outcomes and render the actual pass/fail status (or failure details) in this section.
report_append "* zppy-interfaces unit tests: see script log for \`Running zppy-interfaces unit tests...\` / \`zppy-interfaces unit tests passed\`"
report_append "* zppy unit tests: see script log for \`Running zppy unit tests...\` / \`zppy unit tests passed\`"
report_append "* Image-checker/report unit tests (tests of the tests): \`tests/images/test_image_checker.py\`, \`tests/images/test_image_severity.py\`, \`tests/test_image_summary_report.py\`"
report_append "* Output directory status files: checked automatically; see \`Checking all status files...\` in the script log"
report_append "* Integration tests run: \`test_last_year.py\`, \`test_bash_generation.py\`, \`test_campaign.py\`, \`test_defaults.py\`, \`test_bundles.py\`"
tests/main_branch_testing/run_integration_test.bash:499
- The generated cfgs place diagnostics under the machine's
user_wwwroot (for example,/lcrc/group/e3sm/public_html/diagnostic_output/$USER/...), whileOUTPUT_WORKSPACEis the separate job-output root. This createsenv_description.txtunder a new.../$USER/zppy_*_wwwtree instead of alongside the diagnostics thattest_images.pyreads, so the promised per-task files are not visible with the results. Derive this target from the same web root used by the generated cfgs.
target_dir="${OUTPUT_WORKSPACE}/zppy_${cfg#test_}_www/${UNIQUE_ID}/${case}/${task}"
mkdir -p "$target_dir" 2>/dev/null || {
log_warning "Could not create ${target_dir}; skipping env description for ${cfg}/${task}"
continue
}
cp "$desc_file" "${target_dir}/env_description.txt"
tests/main_branch_testing/run_integration_test.bash:600
- When Phase 3 is resumed directly with
ZPPY_EXISTING_ENVset, the override at lines 820-822 is never executed, soZPPY_ENVstill has the auto-generated name. The new batch job then tries to activate a nonexistent environment instead of the configured existing one; use the existing-env fallback when generating this command.
conda activate ${ZPPY_ENV}
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
| .. code-block:: | ||
|
|
||
| Running zppy unit tests... | ||
| Running tests of the image checker itself... |
There was a problem hiding this comment.
Updated the docs in 633ed2b so this section now matches the script output more precisely.
Addressed in |
|
I have used this branch to run yesterday's weekly test (9/15 test run. I've identified several edits to make to the test script, which I outlined in that post. |
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
The refactor that extracted `init_conda_base()` moved `set -u`
re-enablement to *before* `conda activate`, `source "${UNIFIED_ENV_CMD}"`,
and the SLURM image-checker's inline conda init — instead of *after*, as
it was previously.
This causes activation scripts that reference unset variables without
defaults (e.g. `cartopy_offline_data-activate.sh`'s `CARTOPY_DATA_DIR`)
to abort with "unbound variable" errors.
Restore `set -u` to fire only after environment activation completes in:
- `activate_env`
- `activate_unified_env`
- the image-checker SBATCH heredoc in `run_image_checker`
Generated with Claude
Fixes errors noted in 9/15 test. Most of this commit was generated by Claude.
Generated by Claude.
Co-authored-by: forsyth2 <30700190+forsyth2@users.noreply.github.com>
…ate tracking - Split ZI_EXPECTED_RESULTS_DATE into ZI_GLOBAL_TIME_SERIES_EXPECTED_RESULTS_DATE and ZI_PCMDI_DIAGS_EXPECTED_RESULTS_DATE. zppy-interfaces bundles two tasks that get refreshed independently (e.g. global_time_series on 8/14 vs. pcmdi_diags on 9/14), so a single date masked whichever task was staler. The Step 2 report table now shows these as two separate rows, each with its own "Since" date and commit list. - Rename E3SM_TO_CMIP_EXPECTED_RESULTS_DATE -> E3SM_TO_CMIP_LAST_TESTED_DATE and ZPPY_EXPECTED_RESULTS_DATE -> ZPPY_LAST_TESTED_DATE. These two dependencies produce no per-task output directory, so there's no "expected results" to track for them -- only whether anything has changed since the last time each was tested. The new name reflects that; detection/fallback behavior (earliest production date across all tasks) is unchanged. - Updated run_integration_test.bash (variable defaults, auto-detection, warning messages, and Markdown report generation/_report_repo_changes, which now takes the relevant date-variable name so its "couldn't determine a date" message points at the right one), zppy_test.cfg, and docs/source/dev_guide/tests/automated_test.rst to match. Generated by Claude.
- Drop step numbers from report headers (out of sync with manual docs page); reword intro line to "See [docs page] for info on setup." - Skip test_images_summary.md's own H1 when embedding it, so it no longer breaks the report's heading hierarchy - Replace repeated per-task "_www" path notes with a single table of each cfg's "_www" prefix - Show actual `grep -v "OK" *status` output for any failing output directory instead of just "failed" - Include commits made on the expected-results date itself when reporting upstream changes (--since now anchors to that day's 00:00:00, not a bare date) Generated by Claude.
- check_status_files: populate STATUS_FILE_ERRORS on every failure
path (missing directory, no *status files, or actual non-OK
entries), not just the last one. Previously a "failed" result
could reach the Markdown report with no explanation whenever the
failure was due to a missing/empty output directory rather than
an actual bad status line, forcing readers to re-run
`grep -v "OK" "${dir}"/*status` themselves to find out why.
- Add any_bundle_cfg_configured() and use it in Phase 3 to only run
test_bundles.py when CFGS_TO_RUN actually includes a *_bundles
cfg. It can't meaningfully pass against jobs that were never
submitted; it's now recorded as
"test_bundles.py: skipped (no _bundles cfg in CFGS_TO_RUN)" and
no longer counts as a failure for partial test runs.
- Update zppy_test.cfg and automated_test.rst comments/docs to
reflect both changes.
Generated by Claude.
…rable check_status_files() previously checked every hard-coded output directory (v2, v3, bundles, and their legacy variants) unconditionally, so the Markdown report listed "directory not found" for cfgs that were never submitted in the first place -- e.g. weekly_comprehensive_v2 being reported missing even though CFGS_TO_RUN didn't include it. The function now takes an optional cfg-name argument and, via a new cfg_was_run() helper, skips the check entirely when that cfg isn't in CFGS_TO_RUN rather than reporting a false failure. All 12 call sites in Phase 2 and Phase 3 now pass their matching cfg name. Also stop hard-coding TEST_SPECIFICS["nco_path"] as "" in the generated utils.py. It's now sourced from a new NCO_PATH cfg variable (default empty, matching current behavior) so runs that need a specific NCO installation can set it. Updated zppy_test.cfg and automated_test.rst accordingly. Generated by Claude.
wait_for_slurm_jobs previously only called scancel when every remaining job in the queue had DependencyNeverSatisfied. In practice, a single failed upstream job (e.g. e3sm_to_cmip) typically leaves only some downstream jobs (e.g. pcmdi_diags) stuck in that state while other, unrelated jobs keep running fine -- so the all-or-nothing check never fired, and the script just polled until it hit the full max_wait timeout (up to 4 hours) with the stuck jobs still sitting in the queue. Now, on each poll, any job seen in DependencyNeverSatisfied is cancelled immediately by job ID, leaving healthy jobs untouched. Once the queue drains, the function returns failure right away if any jobs were cancelled along the way, instead of only failing via the max_wait timeout path. Also documents this fail-fast behavior in automated_test.rst. Generated by Claude.
…g to this run wait_for_slurm_jobs polled squeue unfiltered, so it could pick up stale DependencyNeverSatisfied jobs left over from a prior run and misattribute them to the current one. Worse, its call sites invoked it bare under `set -e`, so any cancellation killed the whole script before phase 2/3 ever ran, even when the current run had actually finished fine. - Scope wait_for_slurm_jobs to only the job IDs a phase just submitted (snapshot before/after submission, diff, pass into squeue/scancel via `-j`), so stale jobs from earlier runs are never touched or counted. - phase_1_setup / phase_2_bundles_part2 now check its return value instead of calling it bare: a cancellation sets SLURM_JOBS_INCOMPLETE=true and logs a warning, but the run continues so phase 3 can report on whatever actually completed. Genuine timeouts (jobs still running) still exit directly, since resuming later is the right call there. - Surface SLURM_JOBS_INCOMPLETE in phase 3's logs and as a new section in the generated Markdown report. - phase_3_validation now records pass/fail in a global (PHASE3_OVERALL_OK) instead of via its own return code, so main() always prints "Integration test automation complete!" and writes the report before exiting 1 on failure -- rather than set -e killing the script at that point. - Update automated_test.rst to match. Generated by Claude.
…DA_INSTALL_LINE Previously, ensure_test_branch() always ran for every dependency (e3sm_to_cmip, e3sm_diags, MPAS-Analysis, zppy-interfaces, zppy), even when a *_EXISTING_ENV override was set. If that env was built against a local branch that was never pushed to GitHub, a later run with a fresh TAG would try to fetch/create a new test branch from upstream and fail -- or switch away from the very branch the existing env was built against. *_EXISTING_ENV was also only honored when the matching *_ENV_TYPE was "dev", so setting an existing env while ENV_TYPE was "unified" silently fell back to the unified env instead. Changes: - ensure_test_branch() now takes an optional existing_env arg and returns immediately (no fetch/checkout) when it's set. - Each dependency's env-selection logic now checks *_EXISTING_ENV first, independent of *_ENV_TYPE: an existing env always wins, since _ENV_TYPE only matters for deciding how to build an env in the first place. - get_env_cmd() now keys off env-name presence instead of env_type, so config generation picks the right activation command for an existing env even when _ENV_TYPE isn't "dev". - All 9 call sites of ensure_test_branch (per-dependency setup, config generation, phase_2, phase_3) updated accordingly. Also add optional *_CONDA_INSTALL_LINE cfg parameters (one per dependency) to pin/override package versions without standing up a dedicated branch or custom env. Each is forwarded to `conda install -n <env> <line> --yes` right after that env is activated, whether freshly built or reused. Ignored with a warning for a dependency using the shared unified env with no existing env set. Updates zppy_test.cfg and automated_test.rst to document both changes. Generated by Claude.
The v2 and bundles legacy cfgs differ from the current cfgs only in a trivial manner. We can therefore delete them, keeping the legacy cfgs only for v3.
When the package(s) being changed by CONDA_INSTALL_LINE ship activate.d/deactivate.d hooks (e.g. geometric_features), `conda install` reactivates the currently active env in-place -- sourcing those hooks in the same shell. The hooks assume nounset is off (they reference conda-internal variables like _CONDA_SET_GEOMETRIC_DATA_DIR with no default), so under this script's `set -u` they abort with "unbound variable" right after the install itself succeeds. init_conda_base/activate_env already disable nounset around `conda activate` for the same reason; apply_conda_install_line needs the same guard around `conda install`. Generated by Claude.
c7dbac3 to
107f7d0
Compare
Summary
Improve test automation by updating the test automation script
tests/main_branch_testing/run_integration_test.bashto:global_time_series/env_description.txtwould include the commit hash ofzppy-interfacesit was using, and a printout of the conda environment package versions.tests/images/test_image_checker.py,tests/images/test_image_severity.py)Additionally, updates
docs/source/dev_guide/tests/automated_test.rstto explain how to auto-launch this test script via a weekly cron job.Issue resolution:
Select one: This pull request is...
Big Change
1. Does this do what we want it to do?
Required:
If applicable:
2. Are the implementation details accurate & efficient?
Required:
If applicable:
zppy/conda, not just animportstatement.3. Is this well documented?
Required:
4. Is this code clean?
Required:
If applicable: