[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/HydroModPy/HydroModPy/dev/tests/README.md [Back]  [Original]

# HydroModPy Test Suite

This directory holds the automated tests for HydroModPy. It is split into
five release tiers plus focused auxiliary tiers with different purposes,
budgets, and selection rules.

## Layout

```
tests/
 conftest.py                  # shared scratch root, update-goldens flag, shared fixtures
 unit/                        # one module under test, < 2 s, no real I/O
 integration/                 # cross-module workflows with shared fixtures, < 10 s
 regression/                  # golden-reference tests for full workflows
    fast/                    # routine non-regression tier
    extensive/               # deeper end-to-end non-regression tier
    reference/               # committed golden JSON files
 validation/                  # scientific benchmarks (analytical, MMS)
    analytical/
       steady/
       transient/
    numerical/
 e2e/                         # subprocess-level command scenarios
 performance/                 # pytest-benchmark storage-wrapper benchmarks
 contract/                    # compatibility contracts across implementations
 validity_frame/              # experimental observability tooling (not run by CI)
 _helpers/                    # pytest-local helpers and shared builders
```

## Tiers

| Tier | Target budget | Purpose | Selection |
|------|---------------|---------|-----------|
| `unit`       |  1 min total,  2 s per test | One module under test, pure-Python logic, no external binaries, no real I/O outside `tmp_path`. | `pytest tests/unit/` |
| `integration` |  10 s per test | Cross-module workflows exercising more than one HydroModPy subpackage via shared fixtures (`tmp_workspace`, `minimal_config`, ). No golden files. | `pytest tests/integration/` or `pytest -m integration` |
| `regression/fast` |  5 min | Full launcher/pipeline workflows on mini fixtures, compared to committed golden signatures. | `pytest tests/regression/fast/` |
| `regression/extensive` |  30 min | Deeper end-to-end golden checks with heavier fixtures. | `pytest tests/regression/extensive/` |
| `validation` |  30 min | Numerical results vs analytical / MMS references with documented tolerances. | `pytest tests/validation/` |

## Markers

Declared in the repository-root `pytest.ini`:

| Marker        | Meaning |
|---------------|---------|
| `regression`  | compares output against a committed reference dataset |
| `intercomparison` | solver-to-solver regression based on compact comparison metrics |
| `validation`  | compares output against an analytical/numerical reference |
| `analytical`  | validation against a closed-form solution |
| `steady`      | steady-state case |
| `transient`   | transient case |
| `fast`        | cheap tier (fast regression or quick validation) |
| `slow`        | long-running test, skipped from fast CI |
| `extensive`   | deeper regression tier |
| `nwt`         | MODFLOW-NWT / MODPATH / MT3DMS |
| `mf6`         | MODFLOW 6 / GWT |
| `petsc`       | Linux PETSc Boussinesq runtime |
| `integration` | cross-module workflow test (auto-applied to `tests/integration/`) |
| `coverage`    | long-running coverage-focused test |
| `solver_sanity` | benchmark built directly on the solver SDK (e.g. flopy); validates the external solver against an analytical reference, **not** the hydromodpy pipeline |
| `e2e`         | end-to-end pipeline scenarios |
| `performance` | performance baseline benchmarks |
| `unit`        | unit-tier tests, auto-applied by path |
| `boussinesq`  | Boussinesq solver specific test |
| `network`     | requires network access |
| `binary`      | requires a solver binary on `PATH` |
| `gpu`         | requires a GPU at runtime |
| `allow_subprocess` | permits subprocess calls inside `unit/` |
| `timeout`     | per-test timeout in seconds |
| `xdist_group` | pytest-xdist worker grouping |

The `fast`/`extensive` markers under `tests/regression/` are auto-applied
based on the file's subdirectory (see `tests/conftest.py`).

## Common commands

```bash
# Unit tests (fastest feedback)
pytest tests/unit/ -q                         # all unit tests
pytest tests/unit/solver -q                   # a subpath
pytest tests/unit/ -q -k metrics_nse          # keyword filter

# Regression - fast tier (parallel)
pytest tests/regression/fast/ -q -n auto
pytest -m "regression and intercomparison" -q -n 1

# Regression - extensive tier (serial)
pytest tests/regression/extensive/ -q -n 1

# Validation - all analytical cases
pytest tests/validation/ -q

# Marker selection
pytest -m "regression and fast" -q
pytest -m "validation and steady" -q
pytest -m nwt -q
pytest -m "not slow" -q

# Parallel execution (xdist)
pytest tests/unit/ -q -n auto
pytest tests/regression/fast/ -q -n auto
```

The `hmp` CLI wraps the most common invocations:

```bash
hmp test unit                #  pytest tests/unit/
hmp test regression --fast   #  pytest tests/regression/fast/
hmp test regression --fast --intercomparison
hmp test regression --extensive
hmp test regression --update-goldens
```

## Environment

Scratch data lands in a repository-external directory to keep the working
tree clean:

| Variable | Default | Purpose |
|----------|---------|---------|
| `HMP_TEST_SCRATCH_ROOT` | `/tmp/hydromodpy_tests/` | Shared scratch root; set this to redirect all test artifacts. |
| `HMP_COVERAGE`          | unset | When `1`, enables coverage collection during regression runs. |
| `PYTEST_DEBUG_TEMPROOT`        | `/pytest` | Points pytest's `tmp_path` generator inside the scratch root. |

The conftest sets `TMPDIR`/`TMP`/`TEMP` to a subdirectory of the scratch root
so spawned subprocesses inherit the same cleanup policy.

## Golden references

Regression tests compare the output of a pipeline run to a committed
**signature** - a statistical summary of fields stored as JSON under
`tests/regression/reference/golden_references/{fast,extensive}/`.

### Reading a failure

When a regression assertion fails:

1. Identify the golden file referenced by the failing test
   (path is printed in the assertion message).
2. Open the corresponding JSON and compare the stored signature to the
   freshly computed one.
3. Decide whether the difference is:
   - **a legitimate code change** - update the golden (see below),
   - **a numerical drift** - investigate the responsible commit,
   - **a platform difference** - document and, if needed, tighten the
     tolerance or mark the test platform-specific.

### Updating goldens

Never edit a JSON file by hand. Use the dedicated CLI switch, which
regenerates the golden from the current output:

```bash
# Refresh every golden referenced by a test run
pytest tests/regression/ -q -n auto --update-goldens

# Refresh the goldens of a single test module
pytest tests/regression/fast/test_simulation_regression_fast_mf6_regression.py \
    -q --update-goldens
```

Then commit the updated JSON files in a dedicated commit. The commit
message must state the scientific or code-level reason the signature
changed.

### Tolerances

Numerical comparisons use `numpy.testing.assert_allclose` (or equivalent
`math.isclose`) with an explicit `rtol`/`atol`. Prefer statistical
signatures (min, p25, p50, p75, p95, max, mean, std, sum) over full-array
dumps so goldens stay compact and cross-platform stable.

Validation tolerances live in `tests/TOLERANCES.md`, with some
case-local files under `validation_cases/**/tolerances.toml` when the
benchmark owns a named envelope. `tests/_helpers/tolerances.py::tol()`
parses the shared table for tests that need a named tolerance.

## Writing new tests

- **unit/** - one importable module under test, one behaviour per test,
  budget  2 s. No external binaries. Use `tmp_path` for any I/O.
- **integration/** - cross-module test (pipeline + catalog, planner +
  adapters, ) backed by shared fixtures from the root conftest
  (`tmp_workspace`, `minimal_config`). Budget  10 s, no golden files.
- **regression/** - exercise a full launcher / pipeline on a fixture, then
  compare a committed signature. Tag with `@pytest.mark.regression` and
  (if solver-specific) `@pytest.mark.nwt` or `@pytest.mark.mf6`.
- **validation/** - exercise a physical case against a known analytical
  solution; document the reference and the tolerance rationale.

The shared fixtures come from `tests/conftest.py`:

- `tmp_workspace(tmp_path)` yields an initialized workspace directory
  (standard `data/`, `projects/`, one `data//` folder per variable)
  ready to back a `SimulationCatalog`.
- `minimal_config(tmp_path)` returns the smallest valid
  `HydroModPyConfig` (synthetic `geographic` + a `workspace` pointed at
  `tmp_path / "project"`). Extend via `.model_copy(update=...)`.

Prefer `@pytest.mark.parametrize` over copy-pasting N near-identical
tests. Keep test files under ~400 LOC: a file that grows beyond that is
usually hiding several independent suites.

## CI

The active GitHub Actions gates are split across `.github/workflows/`:

- **main-ci.yml** - PR + push gate: quality (ruff), secrets, architecture,
  fast / unit / integration / regression-fast tiers, package-smoke, typing.
- **ci-nightly.yml** - nightly extensive regression, validation (not petsc),
  integration with coverage.
- **ci-weekly.yml** - cross-OS unit + fast-regression + analytical, serial
  e2e, full regression.
- **perf.yml** - pytest-benchmark drift gate over `tests/performance/`.
- **petsc-smoke.yml** - Linux PETSc Boussinesq smoke subset (conda env).

Validation, extensive regression, and e2e are not part of the fast
PR-blocking suite; run validation manually before a release with
`pytest tests/validation/ -q`.

Web Proxy Viewer  |  New URL  |  Original Page