Testing
Running the suite
The runner is a plain script, not a Pkg.test target:
JULIA_NUM_THREADS=4 julia --project=. --heap-size-hint=3G tests/run_tests.jlWith 4 threads it takes four to five minutes and stays below 4 GB, and reports 1404 passing assertions, no failures and no errors.

Assertions per file, counted on one run with every file wrapped in its own test set (in-process group, blue) or from the summary table each subprocess prints (orange). The runner itself scores each subprocess file as one assertion on its exit code, which is why it reports 1404 rather than 1594; two subprocess files check by exit code only.
tests/run_tests.jl has two groups:
In-process files (
test_files). The runnerincludes them one after another inside one@testset "MILEVA unit tests", in one Julia process and one namespace. The first file,test_grid.jl, doesusing Plotsandinclude("include/mileva.jl"); most later files only dousing Testand rely on that, so they run on their own only if you load MILEVA first (julia --project=. -e 'using Plots; include("include/mileva.jl"); include("tests/test_annulus_operator.jl")'). Files run in list order, so put a new file after anything it depends on.Subprocess files (
subprocess_files). These build their own dependency set instead of includinginclude/mileva.jl, and re-includingconfig/SimConfig.jlwith a different set of definitions would redefine a struct, which Julia refuses. The runner launches each in a freshjulia --projectprocess, streams its output, and records a failure if the exit code is non-zero.
Tests that write files put them under output/ (for example tests/test_2d_vcycle_scgs.jl writes its figure to output/tests/), which is git-ignored.
What a test should check
For an incompressible flow
so a test run at constant viscosity cannot distinguish the two operators. Two rules for new tests follow from examples like this one:
Test a property that the correct answer must have. The work-dissipation identity below holds for any Stokes solution at any viscosity field. Agreement between two solvers on one assembled matrix checks the solvers, not the matrix.
Make sure the property responds to what you want to check. Rigid rotation has
, so the term vanishes on it; the null-mode test is useful, but the viscosity-gradient term is checked by the work-dissipation identity at variable viscosity. Where it distinguishes truncation from inconsistency, require the error to shrink under refinement rather than sit below a fixed tolerance.
The invariants the suite guards
These are the tests that carry weight. Any change to the Stokes operator, the temperature solver or the marker transport must keep them green.
| Property | File | Assertion |
|---|---|---|
| Work equals dissipation, | test_annulus_operator.jl | |
| Work equals dissipation, 3-D, radial viscosity ramp | test_stokes_3d_variable_eta.jl | |
| Work equals dissipation under production-like conditions | test_energy_consistency.jl | positive work, identity converging under refinement; the two O(Di) terms (adiabatic and dissipative) balance on the flow they describe |
| Rigid rotation is an exact null mode of | test_annulus_operator.jl | residual < 1e-12 at Nr = 12, 24, 48 |
| Strain rate is exact for | test_annulus_operator.jl | max relative error < 0.02 on a grid with stretch ratio > 3 |
| Nu = 1 on a conducting state, both geometries | test_nusselt_conduction.jl | Nu within 5e-3 of 1 at Nr = 16, 32, 64, error decreasing; Nu_top == Nu_bot to 1e-9 |
| Discrete energy budget closes, with and without internal heating | test_nusselt_conduction.jl | residual < 1e-10 |
| One marker step conserves energy | test_marker_energy.jl | energy delivered to the markers equals the energy the grid solver computed, < 1e-10, in 2-D and 3-D |
| A model given nothing to do does nothing | test_quiescence.jl | an isothermal model stays isothermal and still; uniform heating warms at the prescribed rate without overshoot; a laterally uniform source makes no lateral structure |
| The answer stops moving under refinement | test_solution_convergence.jl | successive differences shrink, peak velocity not amplified by refinement |
| Marker projection does not drive the flow | test_projection_noise.jl | projection noise falls with markers per cell and with refinement |
What each in-process file covers
| File | Covers |
|---|---|
test_grid.jl | 2-D and 3-D grid construction, cell counts and spacings |
test_geometry_metrics.jl | the radial measure |
test_config_matrix.jl | every geometry and compressibility mode runs and is distinguishable; unimplemented flags refuse; solver backends agree; no major physics option is on by default |
test_markers.jl | marker initialisation, marker-grid-marker round trip, advection stays in the domain |
test_diffusion.jl | explicit and Crank-Nicolson diffusion converge and agree; Dirichlet walls; radiogenic heating |
test_advection_consistency.jl | spherical metric factors in 3-D temperature advection; a uniform field is stationary; no overshoot; TVD less diffusive than upwind |
test_conduction_conservation.jl | radial conduction in conservative flux form |
test_annulus_operator.jl | see the invariants table |
test_nusselt_conduction.jl | see the invariants table |
test_marker_energy.jl | see the invariants table; the volatile inventory survives reseeding |
test_composite_rheology.jl | diffusion and dislocation creep against their formulas, the composite, the yield cap, the clamps |
test_stokes_3d_variable_eta.jl | see the invariants table |
test_energy_consistency.jl | see the invariants table |
test_solution_convergence.jl | see the invariants table |
test_projection_noise.jl | see the invariants table |
test_phase_transition_resolution.jl | a phase boundary spans the requested number of cells at every resolution; 2-D and 3-D agree; each transition counted once |
test_config_timeloop.jl | every 2-D configuration, a combined one and 3-D survive the real time loop |
test_quiescence.jl | see the invariants table |
test_spherical_annulus.jl | the |
test_stokes.jl | direct, AMG and 2-D multigrid solvers: free-slip walls, divergence, agreement between backends, high viscosity contrast |
test_melt_conservation.jl | melt mass, composition and energy bookkeeping; the backlog never discards; extraction rate independent of the time step; 3-D deposition |
test_rheology_switches.jl | yield law, depletion stiffening, lower-mantle offset |
test_hpe_partition.jl | heat-producing-element partitioning and the conserved marker field |
test_melt_frame.jl | melting compares the solidus with the real temperature |
test_crust_material.jl | crust density and viscosity as a material |
test_lid_mobility.jl | no-net-rotation frame, lid mobility, the resurfacing detector |
test_2d_vcycle_scgs.jl | 2-D multigrid against the direct solver, at 2, 3 and 4 levels |
test_diagnostics_csv_vrms.jl | diagnostics.csv V_rms in the geometry's own measure |
test_depletion_projection.jl | a painted depletion reaches the grid without melting |
Subprocess group: test_stretched_radial_3d.jl, test_stokes_3d_operator_consistency.jl, test_spherical_operator_nullmode.jl, test_assembly_3d_mirror.jl (serial against distributed 3-D assembly on one rank), test_3d_physics.jl, test_compressible_stokes_ab.jl, test_yinyang_phi_coupling_ab.jl, test_vcycle_var_eta.jl, test_yy_rotation_vs_ks2004.jl (Yin-Yang vector rotation against Kageyama and Sato 2004), test_amr_indicators.jl and test_full_feature_smoke.jl (a 3-D smoke run with every physics flag for two steps).
Outside the runner
These files live in tests/ but run_tests.jl does not run them:
| File | Status |
|---|---|
test_mpi_*.jl | need mpiexec; launch as in the file header, e.g. mpiexec -n 2 julia --project=. tests/test_mpi_2rank_smoke.jl with the mpiexec that MPI.jl returns from MPI.mpiexec() |
test_amr_full.jl | compares the uniform-grid FGMRES iteration count with a stored count of 40 at |
test_mdpi_eta_contrast.jl, test_vcycle_visc_contrast_sweep.jl | 3-D multigrid sweeps over viscosity contrast; they print PASS/FAIL and exit 0 either way, so read the output |
test_arrhenius_viscosity.jl, test_fk_viscosity.jl | print their results rather than asserting them |
smoke_stokes_3d_mg.jl, smoke_chunk_fk_lid.jl, smoke_chunk_arrhenius_lid.jl | coarse 3-D smoke runs (the first is the default of bin/mileva test) |
Tests and benchmarks
The suite guards identities. Agreement with published numbers is checked separately, for the 2-D spherical annulus, by benchmarks/hernlund_tackley_annulus.jl against Hernlund and Tackley (2008) and the annulus column of Fleury et al. (2024); see Benchmarks and verification.
Adding a test
- Create
tests/test_<topic>.jl. Everything ininclude/mileva.jlis already in scope when the runner reaches it; to make it runnable on its own as well, guard the load:
using Test
if !isdefined(Main, :SimConfig)
using Plots; gr()
include(joinpath(@__DIR__, "..", "include", "mileva.jl"))
endAdd it to test_files in tests/run_tests.jl with a one-line comment saying what property it defends. A file that builds its own include set goes in subprocess_files instead and must exit non-zero on failure.
Build configurations from
SimConfig()plus field overrides, never positionally, and usecfg.geometry = "spherical_annulus"for anything 2-D. A planetary configuration that a test needs lives intests/fixtures/(for exampletests/fixtures/Venus_BMO.jl).Keep it small: a few seconds, coarse grids, no output beyond
output/tests/. Coarse is fine for an identity (Nu = 1,, energy closure); it is not fine for a claim about a resolved physical state. Prefer an identity, a conservation law or an exact solution to a golden number. When you compare against a stored value, store the reason with it and expect to update it.
Vary the input that the property depends on: a viscosity test needs a varying viscosity, an energy test a heat source.
Check that the new test fails on code it is meant to protect against (for example by reverting the change locally and running it).