Skip to content

Add CESM_NOT_PORTED machine + optimize case creating tests (to not do that) - #271

Open
manishvenu wants to merge 6 commits into
class-mergefrom
cesm-not-ported-case
Open

manishvenu wants to merge 6 commits into
class-mergefrom
cesm-not-ported-case

Conversation

@manishvenu

@manishvenu manishvenu commented Aug 17, 2026

Copy link
Copy Markdown
Member

Descrip:

Lets CrocoDash configure a case on a machine CESM isn't ported to — or with no CESM checkout at all — and makes the test suite roughly twice as fast as a side effect. visualCaseGen already falls back to the CESM_NOT_PORTED placeholder when CIME can't identify the host, but CaseCreator then refuses to create a case.

Changes:

  1. cesmroot is now optional. CESM_NOT_PORTED alone couldn't get us here — reaching that placeholder already requires reading CIME, and it only patches host fields, not the model catalogue. So we skip some steps: _configure_case doesn't run (it's pure validation, and without a checkout there's nothing to validate against), and _NoCesmCIME supplies the little CrocoDash actually needs — comp_classes and get_components_from_compset_lname off the compset long name, cime_output_root/machine off the placeholder.

  2. Deliberate trade, and Case prints it: a typo in a compset long name or a bogus grid name is no longer caught at configure time and will surface when the case is created on a ported machine. A compset alias can't be resolved at all in this mode — alias resolution is the one thing that genuinely needs the catalogue — so a long name is required. Passing a real machine with no cesmroot is an error, not a silent downgrade.

  3. Test suite: one real CESM case instead of ten. create_newcase + case.setup was ~6.5 s of the ~10 s per case, paid ten times over for fixtures that never look at a CIME artifact. Now one ported_case fixture runs CIME and the rest ask for CESM_NOT_PORTED; mutating tests copy the ported case, which needs the original's paths rewritten out or xmlchange appends to the original case's replay.sh. Full suite 121.6 s → 67.8 s, case-heavy files 71.6 s → 35.4 s. $CESMROOT now takes precedence over a hardcoded personal install, and a missing checkout skips rather than fails: 238 of 263 tests need no CESM.

Minor changes:

  • Case.expt raised AttributeError when the case was never created — a real hole for un-ported users, not just tests.
  • session_id was read back out of cvars in four places despite CrocoDash generating it; now a plain attribute the cvar mirrors.
  • is_non_local was re-derived from self.cc at ten call sites; resolved once in __init__.
  • _create_newcase swallowed every exception, so a half-created case looked identical to a working one until something later tripped over a file case.setup never wrote. It now reverts its ccs_config edits and re-raises. This may surface previously-hidden failures loudly, which is the intent.
  • bundle_case won't work from a case CIME never created — CaseBundle.__init__ runs ./xmlquery. Configure and process forcings locally, then recreate on a ported machine via create_case_from_yaml.
  • test_diff_CESM_cases_nodiff is weaker now: two copies of one case make "no differences" closer to a tautology.
  • The un-ported trigger still isn't reachable in tests — unsetting NCAR_HOST isn't enough, because visualCaseGen force-selects casper from the fqdn on a JupyterHub node. test_emulate_not_ported_matches_visualcasegen pins the emulated state against visualCaseGen's own handler instead, so they can't drift.

On a host CESM has no machine definition for, visualCaseGen falls back to the
CESM_NOT_PORTED placeholder machine and CaseCreator refuses to create a case.
CrocoDash previously walked into that wall: _run_create_newcase raised, the
error was swallowed, and every later step that shells out to xmlchange or
appends to a user_nl file failed against a caseroot that was never created.

Treat CESM_NOT_PORTED as "configure, but don't execute" instead. Case derives
self.do_exec from the machine name -- no new public argument, since machine
already defaults to cime.machine -- and threads it through every call site that
would otherwise shell into the case directory. Forcing extraction is untouched:
it never talks to CIME, so the expensive download+regrid work still runs, and
the case itself is recreated later on a ported machine from crocodash_case.yaml.

The do_exec plumbing mirrors is_non_local: a BaseConfigurator property sourced
from the registry's live Case, applied to XMLConfigParam/UserNLConfigParam
outputs by the base configure(), so no configurator declares a ctor arg to
carry it. It defaults to True (the opposite of is_non_local's False) so a
configurator with no live Case -- direct construction in tests, deserialize()
-- behaves exactly as it did before. UserNLConfigParam.apply() previously
hardcoded do_exec=True, as did the two batched append_user_nl calls in
ConditionsConfigurator.configure(), which is the one configurator that
deliberately bypasses the base per-param loop to preserve its user_nl banner
formatting -- and the one writing most of user_nl_mom, so missing it would have
defeated the feature.

Also stop swallowing case-creation failures. _create_newcase caught every
exception, printed it, and continued, so a half-created case was
indistinguishable from a working one until something much later tripped over a
file case.setup never wrote (a missing user_nl_cpl surfacing inside CaseBundle,
for instance). It now reverts the ccs_config edits and re-raises.

The two existing is_non_local tests assert an exact xmlchange call signature,
which now carries do_exec, so both are updated alongside five new tests
covering the XML and user_nl paths in both directions.

Note the no-exec path cannot be exercised on a ported machine: init_args_check
validates machine against cime.machines, which won't contain CESM_NOT_PORTED
there. The plumbing is unit-tested via a mocked registry/case; end-to-end
verification needs a genuinely un-ported host.
@github-actions

Copy link
Copy Markdown

📄 Preview your docs here:
👉 https://CROCODILE-CESM.github.io/CrocoDash/pr-271/index.html

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

create_newcase plus case.setup is ~6.5s of the ~10s it takes to stand up a
CrocoDash case, and the default suite was paying it ten times over for fixtures
that never look at a CIME artifact. Configure-without-executing already existed
for un-ported machines, so reuse it: one fixture creates a real case, everything
else skips CIME entirely.

Case gains a do_exec argument. self.do_exec was derived solely from
MACHINE == CESM_NOT_PORTED, and init_args_check rejects that machine name on a
ported host, so the no-exec path was unreachable anywhere it could be tested.
do_exec=False now opts into the same behaviour explicitly, and an un-ported
machine still forces it off, so the two can't disagree. It is excluded from the
state file: crocodash_case.yaml exists to be replayed on a ported machine, and
carrying do_exec=False into the replay would defeat that.

Two things blocked the path once it was reachable. Case.expt read RUNDIR off
self._cime_case, which is None when the case was never created, so it raised
AttributeError -- a real hole for un-ported users too, not just for tests; it
now falls back to the path CIME would have derived. And the no-exec branch of
_create_newcase announced "Machine is CESM_NOT_PORTED" regardless of why it was
taken, which is now simply untrue for a ported machine.

On the test side CrocoDash_case_factory defaults to do_exec=False, and the new
ported_case fixture is the single case that runs CIME. Tests needing artifacts
only CIME writes -- xmlquery, env_*.xml, the default user_nl files, SourceMods/,
README.case -- read that one case, and mutating tests take a copy of it via
ported_case_copy, which costs a directory copy rather than a second
create_newcase.

Copying a caseroot needs the original's own paths rewritten out of it, or the
isolation is a fiction: CIME resolves xmlchange's replay log against CASEROOT in
env_case.xml, so an xmlchange in an unrewritten copy appends to the *original*
case's replay.sh -- the copy never records the change and the shared case gets
polluted. CaseBundle.bundle() has the same problem via user_nl_mom's INPUTDIR.
Doing the rewrite with ./xmlchange CASEROOT= is worse than useless, since that
registers as a non-standard xmlchange and then shows up in every bundle diff, so
_rewrite_paths substitutes the paths directly, skipping symlinks so it cannot
write through ./xmlquery or Tools/ into the CESM installation.

test_case_init_and_create_grid_input no longer asserts on README, which
create_newcase writes and the grid-input step it covers does not;
test_ported_case_has_cime_artifacts covers that instead, so the do_exec=True
path stays tested. test_configure_forcings_invalid_function_overrides gets its
own function-scoped case, having previously called configure_forcings() on a
session-scoped one and left the extract_forcings directory wiped for whoever ran
next. test_configure_forcings had an assertion that could only ever raise
NameError on failure, never fail.

Default suite goes from 119.8s to 66.8s, and the four case-heavy files from
103.4s to 50.1s, with one test added and no change in what passes.

Two ~9s costs remain and are not fixture-level: CaseBundle's
identify_non_standard_case_info builds a fresh reference case to diff against,
which no test-side change can avoid. Note also that test_diff_CESM_cases_nodiff
is weaker than it was, since two copies of one case make "no differences" closer
to a tautology than two independently created cases did.
@manishvenu manishvenu changed the title Configure cases without executing CIME on un-ported machines Configure cases without executing CIME; build one real CESM case in the test suite Aug 19, 2026
@manishvenu
manishvenu marked this pull request as ready for review August 19, 2026 21:32
The previous commit added a public do_exec argument to Case because
init_args_check rejects CESM_NOT_PORTED on a ported host, which made the
un-ported path unreachable anywhere it could be tested. That was the wrong
conclusion: CESM_NOT_PORTED is visualCaseGen's own placeholder, not a CIME
machine, so "machine not in cime.machines" was never the right gate for it and
the argument was working around a self-inflicted restriction.

CIME_interface selects the placeholder in _handle_machine_not_ported() when CIME
cannot identify the host, and from then on it is the only machine the interface
offers. Requesting it on a ported host therefore has to reproduce that state, or
it is only half-applied. Two things go wrong otherwise, both verified rather
than assumed: MACHINE's options are set from the real machine list by
set_launcher_options() during initialize(), so _configure_launch's assignment
dies with "CESM_NOT_PORTED not an option for MACHINE" -- swallowed by
_configure_case, leaving a Case whose caseroot does not exist; and
_is_non_local() compares the interface's real machine against the selected one,
so every xmlchange would wrongly be treated as non-local.

_emulate_not_ported() puts the interface into that state, which also makes
init_args_check pass untouched -- cime.machines becomes [CESM_NOT_PORTED] and
project_required gains the matching entry, so no validation change is needed at
all. do_exec goes back to being derived solely from the machine, and is no
longer part of the state file exclusion list because it is no longer an init arg.

It replicates the handler's three identity assignments rather than calling it,
because the handler also falls back to ~/scratch and ~/inputdata and creates
them -- correct on a laptop with nothing better to use, but the host's own roots
already exist here, and a test run should not create directories in $HOME as a
side effect (it did, once, before this was noticed). Replication can drift from
the original, so test_emulate_not_ported_matches_visualcasegen pins the two
together, driving visualCaseGen's handler with $HOME redirected to tmp_path and
comparing the identity fields. Confirmed to fail if the values are changed.

Requesting the placeholder on a ported host now prints why no case was created.
Without that, replaying a laptop-configured crocodash_case.yaml on Derecho --
which records machine, so it says CESM_NOT_PORTED -- would quietly produce
another case CIME never ran, where before it raised ValueError. The error was
more useful than silence.

The fixtures take a ported flag instead of do_exec, and the un-ported path is
now asserted directly rather than only used: test_not_ported_machine_configures_
without_cime is the complement of test_ported_case_has_cime_artifacts, checking
that the grid inputs and state file are written and that nothing create_newcase
or case.setup writes exists. Note what this changes about the earlier claim that
the path could not be verified without a laptop: the path itself is now
exercised on a ported host. What still cannot be reached here is the *trigger* --
unsetting NCAR_HOST is not enough, because visualCaseGen force-selects casper
from the fqdn on a JupyterHub node before CIME is ever consulted.

Full suite: 246 passed, 16 skipped, 1 pre-existing failure
(test_case_integration_driver, full-suite-only, fails identically at baseline).
get_cesm_root_path hardcoded one personal install and ignored $CESMROOT whenever
it was on glade, so nobody else could run the tests that build a Case, and
pointing them at a different checkout meant editing the fixture. $CESMROOT now
wins, with the per-platform paths as fallbacks, which is what the TODO there
asked for.

A checkout is genuinely required for those tests rather than merely convenient,
and CESM_NOT_PORTED does not change that: CIME_interface asserts <cesmroot>/cime
exists, reads the CIME git tag, imports CIME, and parses compsets, grids and the
machine list out of the tree -- all before _retrieve_machines can conclude the
host is un-ported. Un-ported means CESM is present but this host has no machine
definition, which is what its own alert says ("You can still run visualCaseGen
as normal, but the final step of case creation is disabled"). So there is
nothing to fall back to when the tree is absent, and skipping is the honest
outcome. Making those tests run without a checkout would mean stubbing
CIME_interface, which would stop testing real compset and grid resolution.

Skipping in the session fixture covers every dependent test transitively, so no
per-test markers are needed. Measured with CESMROOT pointed at an empty
directory: 222 passed, 41 skipped, no errors or failures -- 25 tests move to
skipped, and the 238 that never build a Case are untouched. With a checkout
present the suite is unchanged: 246 passed, 16 skipped, plus the pre-existing
test_case_integration_driver failure.

Note 25, not the 63 quoted earlier from counting whole files: most tests in
those five files use mocks or fixtures that never reach CIME.
CESM_NOT_PORTED made an un-ported machine work, but it could not make an absent
CESM work, because it is a conclusion reached from a checkout that has already
been read: CIME_interface asserts <cesmroot>/cime exists, reads CIME's version,
imports CIME, and parses component classes, models, physics, options, grids and
compsets, and only then does _retrieve_machines() decide the host is un-ported.
The placeholder substitutes five host-configuration fields; nothing substitutes
the model catalogue.

But CrocoDash needs far less of that catalogue than the catalogue contains. Of
its non-validation uses, comp_classes is fixed by the compset long-name format,
get_components_from_compset_lname is pure string work, and cime_output_root and
machine are exactly what the placeholder already supplies. Everything else the
catalogue is read for is validation -- checking a compset's physics and options
and a grid name against what this CESM can build. With no checkout there is
nothing to validate against, so skip that step rather than refuse to run.

So cesmroot becomes optional. Omitting it selects _NoCesmCIME, forces machine to
CESM_NOT_PORTED, and skips _configure_case entirely. Passing a real machine
without a cesmroot is an error rather than a silent downgrade, since CIME is what
defines every machine other than the placeholder. Grid input files, the state
file and the forcing configuration are all still produced; forcing extraction
never touched CIME to begin with.

The trade is deliberate and worth being explicit about: a typo in a compset long
name or a bogus grid name is no longer caught at configure time and will instead
surface when the case is created on a ported machine. Case prints that its input
was accepted unvalidated. A compset alias cannot be resolved at all in this mode,
so a long name is required -- alias resolution is the one thing that genuinely
needs the catalogue.

Four things had to move for this to work, each verified by hitting it:

- MB_ATTEMPT_ID was read back out of cvars in four places. It is a uuid CrocoDash
  generates itself, so session_id is now a plain attribute set in __init__ and
  the cvar mirrors it, rather than the reverse. Nothing reads cvars for it.
- is_non_local was re-derived from self.cc at ten call sites. There is no
  CaseCreator without a checkout, so it is resolved once in __init__ and reused;
  with CIME never invoked there is nothing to be non-local to.
- init_args_check validated grid names against cime.domains. The shim reports
  domains as None -- not an empty dict, which would make every grid look invalid
  -- and the three lookups are guarded on it.
- visualCaseGen's xmlchange() and append_user_nl() read cvars["CASEROOT"] before
  they consult do_exec, so they raise rather than no-op with cvars empty. They
  cannot be called at all here. BaseConfigurator gains has_cesm, deliberately
  narrower than do_exec: on an un-ported machine that does have a checkout,
  apply() is still called so it prints the commands for creating the case by
  hand, which is that path's whole purpose. The xmlchanges skipped in case.py all
  derive from init args that the state file records, so a replay applies them.

Full suite: 249 passed, 16 skipped, 1 pre-existing failure
(test_case_integration_driver, full-suite-only, fails identically at baseline).
@manishvenu manishvenu changed the title Configure cases without executing CIME; build one real CESM case in the test suite Configure cases without CIME: un-ported machines, and with no CESM checkout Aug 21, 2026
@manishvenu manishvenu changed the title Configure cases without CIME: un-ported machines, and with no CESM checkout Add CESM_NOT_PORTED machine + optimize case creating tests (to not do that) Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant