Run and build tests
JANUS uses pytest for its test suite. Tests are sorted into four tiers by cost, verify physical invariants and pinned reference values, and mirror the source tree. This page covers running the suite and adding a new test. For the full contract (tiers, markers, coverage gates, the linter), see the Testing suite overview.
Prerequisites
Install the develop extras and make sure RAD_DIR and FWL_DATA are set:
pip install -e ".[develop]"
echo $RAD_DIR # should point to your compiled SOCRATES tree
echo $FWL_DATA # should point to your data directory
Even the mocked unit tests import janus, which resolves the SOCRATES
environment at import time, so RAD_DIR must point to a built SOCRATES tree for
collection to succeed. See the installation guide for the
SOCRATES build.
Running the tests
From the root of the JANUS repository:
pytest -m "(unit or smoke) and not skip" # what the PR checks run
pytest -m unit # fast unit tests only
pytest -m smoke # real SOCRATES, low resolution, one step
pytest -m integration # full pipeline coupling (nightly)
pytest -m slow # full physics validation (nightly)
pytest -m "not skip" # everything that should ever run
Run a single file or a single test:
pytest tests/utils/test_phys.py
pytest tests/modules/test_dry_adiabat_setup.py::test_dry_adiabat_conserves_potential_temperature
With coverage:
pytest --cov=janus --cov-report=term -m "not skip"
pytest --cov=janus --cov-report=html -m "not skip" # writes htmlcov/
How the tests are organised
Tiers are selected by a module-level marker at the top of every test file:
import pytest
pytestmark = [pytest.mark.unit, pytest.mark.timeout(30)]
| Tier | Runs | Timeout |
|---|---|---|
unit |
Python logic, SOCRATES and file I/O mocked | 30 s |
smoke |
real SOCRATES binary, one step, low resolution | 60 s |
integration |
full RadConvEqm / MCPA_CBL pipeline |
300 s |
slow |
long sweeps and full validation | 3600 s |
New per-source tests mirror the source path: src/janus/<subdir>/<file>.py maps
to tests/<subdir>/test_<file>.py. The cross-cutting files
(tests/test_constants.py, tests/test_code.py) and the pipeline files
(tests/test_runaway_greenhouse.py, tests/test_instellation.py) are the
documented exceptions.
Adding a test
- Create
tests/<subdir>/test_<file>.pyand give it the module-levelpytestmarkfor its tier. - Write a file-level docstring naming the source under test, and a function-level docstring for every test naming the physical scenario it checks.
- On a physics source, assert at least one physical invariant (conservation or
balance, positivity or boundedness, monotonicity or symmetry, or a pinned
numeric value with a discrimination guard) and tag the test
@pytest.mark.physics_invariant. - Give each physics source at least one
@pytest.mark.reference_pinnedtest that pins against a published benchmark, an analytical limit, or an independent code path, and record the anchor on adocs/Validation/<file>.mdpage. The existing pages under Validation anchors are the template. - Never compare floats with
==; usepytest.approx(value, rel=...)ornp.testing.assert_allclose. On a pinned value, add a follow-up assertion showing the most plausible wrong formula would differ by more than the tolerance. - If the file imports an optional dependency (
hypothesis,mors), callpytest.importorskip('<name>')at the top of the module before importing it.
Local checks before opening a PR
ruff check --fix src/ tests/ && ruff format src/ tests/
bash tools/validate_test_structure.sh # module-level marker validator
python tools/check_test_quality.py --check # anti-happy-path linter (blocking)
python tools/check_test_quality.py --reference-pinned-status
The same steps run in .github/workflows/tests.yaml on every pull request.