This document describes how to run the automated tests for the ForeFire wildfire simulator. Tests are located within the tests/ directory.
Running the test verification scripts requires:
- Python 3
- Python Libraries:
lxml,xarray,netCDF4(and their C dependencies likelibnetcdf-dev).
Install Python libraries via pip:
pip3 install lxml xarray netCDF4The C++ unit tests exercise propagation and flux models one call at a time, without running a simulation. They are built alongside everything else and run through CTest:
cmake -S . -B build && cmake --build build -j
ctest --test-dir build --output-on-failureThey need no Python and no test data. Configure with
-DFOREFIRE_BUILD_TESTS=OFF to skip building them; wheel builds already do.
tests/unit/README.md describes what they cover and how to add one.
The primary physics regression test, validated in CI by main.yml and macos.yml, is located in tests/runff/. This test verifies core simulation, save/reload functionality, and NetCDF/KML output generation against reference files.
To run this test manually:
- Ensure ForeFire is compiled (e.g., via
install-forefire.sh). - Navigate to the test directory:
cd tests/runff - Execute the test script:
bash ff-run.bash
Test Logic:
The ff-run.bash script:
- Runs an initial simulation (
real_case.ff) generating NetCDF output (ForeFire.0.nc) and a reload file (to_reload.ff). - Runs a second simulation (
reload_case.ff) using the reload file, which generates KML output (real_case.kml). - Uses Python scripts (
compare_kml.py,compare_nc.py) to compare the generated KML and NetCDF files against reference files (*.ref) with numerical tolerance, accounting for minor floating-point variations. - Exits with status 0 on success, non-zero on failure.
The second test validated in CI, by the invariants.yml workflow. Where
runff compares ForeFire against frozen ForeFire output — and so cannot tell a
physics fix from a physics regression — this suite holds no reference data.
Every assertion follows from the published spread equations, so it stays valid
across recalibration.
It asserts, for each propagation model that consumes dead fuel moisture, that
the rate of spread stays finite for any moisture, decreases as moisture rises,
reaches zero at the moisture of extinction, responds to a dynamic
dead-moisture layer, and that DataBroker resolves every property the model
registers.
To run it manually:
- Install the Python package, which builds the
pyforefireextension:python3 -m venv .venv ./.venv/bin/python -m pip install . - Run the suite (add
-vto print every probe's spread rate):./.venv/bin/python tests/python/test_moisture_invariants.py
Restrict it while iterating with --model NAME and --test NAME, both
repeatable. It needs no fixtures — fuel, wind, temperature and moisture layers
are built in memory — and takes well under a minute.
Note that each probe runs in its own interpreter. The C++ core keeps mutable
global state, so a second ForeFire() in one process inherits the first one's
parameters and a parameter sweep silently returns one identical result. Keep
that in mind when writing any new Python test that varies parameters.
Not run in CI, deliberately. On an ordinary interpreter the GIL serialises every call into the extension, so the test skips and would report a green result that proves nothing. It is a reproduction tool, run by hand when working on the shared-state problem in #175.
It runs eight simulations in threads and requires each to reproduce the fire node count it produces when run alone, plus a case that hammers object construction to exercise the shared id counter.
To run it, you need a free-threaded build of CPython (python3.14t or
later) and PYTHON_GIL=0. The variable is required: _pyforefire does not
declare py::mod_gil_not_used(), so importing it switches the GIL back on and
the test skips.
python3.14t -m venv .venv
./.venv/bin/python -m pip install .
PYTHON_GIL=0 ./.venv/bin/python tests/python/test_threading.pyIt does not pass today. On dev it segfaults or returns wrong node counts,
which is the point — it is the failing test the work in #175 has to make pass.
Wiring it into CI belongs with the last step of that issue, once it can pass
for the right reason.
Checks pyforefire.ForeFireRunner, which runs each simulation in its own
worker process (#175). Eight runners driven from threads must each reproduce
the in-process fire node count; exceptions in the worker must reach the
caller, and a killed worker must raise WorkerDied. It needs no free-threaded
interpreter, because the runners are separate processes. CI runs it in the
Model Invariants workflow.
./.venv/bin/python tests/python/test_runner.pyValidated in CI by main.yml. It is the only coverage for
ANNPropagationModel and BMapLoggerForANNTraining: both read a .ffann
network in their constructor and abort without one, so the C++ unit suite
cannot construct them.
It runs the trained network in Rothermel.ffann over the 1424 inputs in
modelrun.csv and checks the root mean squared error against what the
propagation model produced.
To run it manually, after building (ANN_test needs
-DFOREFIRE_BUILD_TOOLS=ON, which is the default outside wheel builds):
cd tests/runANN
bash run.bashAdd print to ANN_test for a per-input dump:
../../bin/ANN_test Rothermel.ffann modelrun.csv printThe tolerance is calibrated between a working network and a useless one:
| RMSE | |
|---|---|
| trained network | 0.0235 |
| predicting the mean of every output | 0.0966 |
| normalisation weights scaled by 1% | 17817 |
| tolerance | 0.05 |
Note the fixture is weak: the expected outputs take four distinct values
spanning 1.1 in 12412, which is why the gap between a working network and a
constant one is so narrow. Replacing modelrun.csv with inputs that produce a
real spread of rates of spread would make this a much stronger check.
Covers tools/preprocessing/genForeFireCase.py, which writes the NetCDF
landscape file a simulation runs on. It builds a landscape, hands it to
ForeFire, ignites it and steps, so it exercises the writer and the reader
together rather than the writer alone. It also pins the field axis order,
where the 3-D and 4-D paths were previously broken.
To run it manually, after installing the Python package as above:
./.venv/bin/python tests/python/test_genforefirecase.pyIt needs numpy and netCDF4, writes only into a temporary directory, and
takes a few seconds. Without a built pyforefire the load-and-simulate case
skips and the rest still run, so it is usable while iterating on the writer.
The test doubles as the worked example for the tool.
tests/run.bash runs every suite that its environment allows: runff and
runANN always, the mnh_* coupled cases when SRC_MESONH is set, and
tests/python/ when PYTHONEXE is set. It is not itself invoked by CI, which
runs the suites individually.
tests/python/test_validate.py covers the forefire-validate landscape
checker. It loads the checker straight from source and exercises its pure
decision logic, so it needs neither the compiled _pyforefire extension nor
netCDF4; the one test that reads a real .nc is skipped when netCDF4 is
absent. run.bash runs it after the examples.
python3 tests/python/test_validate.pytests/python/ also holds idealizedwind.py, farsite_flat.py and
percolation.py. They are examples rather than tests — they produce plots and
assert nothing — and are not run anywhere. They are the closest thing to
worked examples in the repository, so keeping them executable is worthwhile.
-DFOREFIRE_SANITIZE=<list> builds with -fsanitize=<list>, applied to the
compile line, the executables and the shared library. It also switches the
optimisation flags to -g -O1 -fno-omit-frame-pointer, since the default
release set (-O3 -flto -fomit-frame-pointer) makes sanitizer reports hard to
read.
cmake -S . -B build-asan -DFOREFIRE_SANITIZE=address
cmake --build build-asan -j
ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build-asan --output-on-failureaddress is what CI runs, on both the unit suite and runff, as a blocking
check. Other values are passed straight through — undefined, or
address,undefined for both — but only address is currently verified clean.
ForeFire reports zero ASan errors — no use-after-free, no overflow, no double free — on either test path, which is what makes a blocking job possible.
Leaks are blocking for the unit suite and informational for runff. The
unit suite reports none: #159 gave the propagation and flux models, the
propagative layer and two orphaned constant-layer arrays an owner, which took
it from about 4.7 MB to zero. Running the suite with leak detection on is
therefore a check rather than a report:
ASAN_OPTIONS=detect_leaks=1 ctest --test-dir build-asan --output-on-failurerunff still leaks about 200 kB over 783 allocations, on paths a full
simulation reaches and the unit suite does not, so its leak check runs without
gating the build. Closing those is what would let it join the gate; until then
the figure stays visible in the job log.
Note that the sanitizer build writes bin/forefire and lib/libforefireL.so
like any other build, so it replaces a release build in the source tree.
ForeFire's own sources compile with -Wall -Wextra by default. The warnings
are not yet clean, so they are informational rather than fatal; two options
control this:
-DFOREFIRE_ENABLE_WARNINGS=OFF— build quietly.-DFOREFIRE_WARNINGS_AS_ERRORS=ON— fail the build on any warning. Useful on a subset of files while clearing them; not yet usable repository-wide.
The flags apply to libforefireL, the forefire executable and the unit
tests. NetCDF's headers are included as system headers so their warnings do
not appear.
Please see CONTRIBUTING.md for guidelines on contributing to ForeFire, including adding new tests. Report any issues via the repository's issue tracker.