Testing¶
The suites in the table are registered with ctest; the few that are not are run by hand, as the
sections below say. A test that is built but not registered never runs, in CI or locally: after
adding one, check that ctest --test-dir build -N lists it.
| Suite | Source | ctest entries | Registered when |
|---|---|---|---|
| Library unit tests | the files of TEST_SOURCES in tests/ |
one per test case | BUILD_TESTING (on by default) |
| Component unit tests | components/<name>/.../tests/ |
wmtk_test_<name>, one per component |
BUILD_TESTING and WMTK_COMPONENT_TESTS (both on by default) |
| Memory route | tests/test_component_io.cpp |
one per test case | as the component tests |
| Integration tests | tests/integration_tests.cpp, configs in data/integration_tests |
Integration_Tests |
WMTK_BUILD_INTEGRATION_TESTS=ON |
| Python | tests/python/ |
python_memory_io, python_tracking, python_tracking_correspondences |
the Python module is built and its interpreter has NumPy; the last in non-Debug builds only |
| Documentation examples | docs/examples/ |
doc_example_cpp_<name>, doc_examples_python_json |
WMTK_BUILD_DOC_EXAMPLES=ON, non-Debug builds only |
Library unit tests¶
wmtk_tests is one Catch2 executable built from the files listed in TEST_SOURCES in
tests/CMakeLists.txt:
a new test file goes into that list. catch_discover_tests registers every test case as a ctest
entry of its own, under its name. The tests link wmtk::data, so they read the data repository
through WMTK_DATA_DIR:
Small meshes built in code are in
src/wmtk/utils/examples/
(TriMesh_examples.hpp: single_triangle(), two_triangles(), ...).
-DWMTK_DEBUG_BRUTE_FORCE_OPS=ON compiles a check into TriMesh's split and collapse that
compares each result with one rebuilt from the whole mesh. It makes every operation linear in
the mesh size: for tests on small meshes only.
Component unit tests¶
Each component's tests/CMakeLists.txt calls add_component_test(<name>)
(components/cmake/add_component_test.cmake),
which builds wmtk_test_<name> and registers it as one ctest entry:
add_test(NAME ${COMPONENT_TEST_NAME} COMMAND ${COMPONENT_TEST_NAME} --allow-running-no-tests --durations yes)
--durations yes prints the time of every case, which is how a case too slow for the Debug jobs
shows up in the CI log. To run one case, run the executable with its name or a tag:
ctest --test-dir build -R wmtk_test_qslim --output-on-failure
build/components/qslim/wmtk/components/qslim/tests/wmtk_test_qslim "[boundary]"
wmtk_component_io_tests
(tests/test_component_io.cpp,
built from components/CMakeLists.txt) tests the memory route: components called on named
meshes, and the memory and file routes giving the same result. Each component's cases are
compiled only when the component is (#ifdef WMTK_TEST_<name>).
Integration tests¶
With -DWMTK_BUILD_INTEGRATION_TESTS=ON, wmtk_integration_tests
(tests/integration_tests.cpp)
runs every config listed in data/integration_tests/integration_tests.json, the way wmtk_app
does (the JSON, input_dir set to its directory, the component from the generated map):
for (const auto& input_file : input_files) {
const path& f = integration_tests_dir / input_file;
CHECK(fs::exists(f));
CHECK_NOTHROW(wmtk_wrapper(f));
}
That is the whole check: a config passes if it runs without throwing. No output is compared
with a stored one. A config therefore asks its component to throw when the result is wrong:
throw_on_fail (the output misses what the component promises: the target vertex count, the
stop energy), max_expected_iterations in tetwild
and triwild (more optimization iterations than expected), throw_on_nonconvergence in
topological_offset. All the configs run in one ctest entry, Integration_Tests, with a timeout of
7200 s.
The other cases in the file are hidden ([.]): Catch2 does not run them unless asked, and
catch_discover_tests does not register them. Each reads its own list from
data/integration_tests:
build/tests/wmtk_integration_tests "[challenging]" # challenging_low_stop_energy_models.json: minutes to hours each
build/tests/wmtk_integration_tests "[offset]" # topological_offset_models.json
build/tests/wmtk_integration_tests "[manifold]" # manifold_extraction_models.json
A new config goes into the data repository and its list, and the data pin moves to the data2 commit that has it (writing a component).
Python tests¶
The tests in tests/python
run the bindings. Three are registered in tests/CMakeLists.txt, when the module is built
(WMTK_PYBIND, on by default) and the interpreter CMake found imports NumPy:
| ctest entry | File | What it runs |
|---|---|---|
python_memory_io |
test_memory_io.py |
the memory route through the bindings, NumPy only |
python_tracking |
test_tracking.py |
histories as bytes and files, wm.Tracking, wm.CurveTracking, wm.VolumeTracking |
python_tracking_correspondences |
test_tracking_correspondences.py |
the tracking tutorial's pipeline (docs/examples/python/tracking_correspondences.py): the dense bunny remeshed, tetrahedralized, and every vertex mapped through both histories and back; its counts are ranges, since a run with threads changes slightly from one run to the next; non-Debug builds only, timeout 1800 s |
The others are run by hand or elsewhere:
test_docs_examples.py: the documentation's examples, below.test_smoke.py: imports the module and its error paths; the wheel builds run it withtest_memory_io.pyandtest_tracking.pyon every wheel (test-commandinpyproject.toml).test_integration.py: the integration configs through the bindings, compared byte for byte withwmtk_app's output when the config is serial (python -m pytest tests/python; see itsREADME.md).
python/tests holds the tests of wildmeshing.polyfem_ops, run with python -m pytest
python/tests and gmsh installed.
Documentation examples¶
With -DWMTK_BUILD_DOC_EXAMPLES=ON, every docs/examples/cpp/<name>.cpp is built as
wmtk_doc_example_<name>, linked with every component, and in a non-Debug build registered as
doc_example_cpp_<name>; doc_examples_python_json (tests/python/test_docs_examples.py) runs
the Python scripts and, through wmtk_app -j, the JSON files. They check that each example runs
without an error. Writing documentation has the conventions.
Slow cases¶
Debug builds run 10 to over 100 times slower than Release. A case that takes more than a fraction
of a second in Debug is tagged release_only: hidden in Debug builds, run as usual in Release.
The pattern, at the top of the test file:
// Cases too slow for the Debug CI jobs, Windows Debug above all: Release builds only.
#ifdef NDEBUG
const std::string release_only = "";
#else
const std::string release_only = "[.]";
#endif
and on each slow case:
Commit e8494ffb4f
introduced it for the 95 cases over 0.3 s in a macOS Debug build or 5 s on
Windows Debug, taking a macOS Debug ctest to 27.5 s; tetwild's and simwild's whole runs were
already hidden in Debug. A case that should not run anywhere by default, a benchmark on a large
model say, gets a hidden tag of its own ([.slow]), and the reason in a comment.
Running the tests locally¶
cmake -B build -DCMAKE_BUILD_TYPE=Release -DWMTK_BUILD_INTEGRATION_TESTS=ON
cmake --build build -j
ctest --test-dir build -N # what is registered
ctest --test-dir build -E Integration_Tests --output-on-failure
ctest --test-dir build -R Integration_Tests --output-on-failure
The first build clones the data repository into data/.
In CI¶
.github/workflows/continuous.yml
runs on every pull request and every push to main, in eight jobs:
| Job (check name) | Runner | Configs |
|---|---|---|
Linux |
ubuntu-latest |
Debug, Release |
Linux arm64 |
ubuntu-24.04-arm |
Debug, Release |
macos-latest |
macos-latest |
Debug, Release |
Windows 2025 (VS 2026) |
windows-2025-vs2026 |
Debug, Release |
A check is named after its job and config, as in Linux (Release).
Each job configures with CMAKE_BUILD_TYPE set to its config, builds with two jobs (make -j2;
Ninja on Windows) behind a compiler cache (ccache; sccache on Windows), and runs
ctest --verbose --output-on-failure, one test at a time (CTEST_PARALLEL_LEVEL: 1). So:
- every job runs the library, component and memory-route tests, with the
release_onlycases in the Release jobs only; - the Release jobs add
-DWMTK_BUILD_INTEGRATION_TESTS=ONand-DWMTK_BUILD_DOC_EXAMPLES=ON, and run every listed integration config and the documentation's C++ examples; - the Linux jobs install
python3-numpy, so they run the registered Python tests, and in Release the Python and JSON examples; the other jobs run them if the interpreter CMake finds has NumPy.
When the tests fail on Linux or macOS, a last step re-runs the component test executables under
gdb or lldb and prints the backtrace of every thread, for a crash that leaves only a signal in
the log. A manual run of the workflow (workflow_dispatch) with the debug_ssh input opens an
SSH session on a Linux or macOS runner when a step fails.
Two other workflows run on every pull request: pre-commit.yml checks the formatting
(contributing), and docs.yml checks the specs
(gen_reference.py --check) and builds the site with --strict
(writing documentation).