Testing
Palace comes with two types of tests:
- Unit tests in
test/unit/test individual components in isolation - Regression tests in
test/examples/compare code output against saved references
Both types of tests are run automatically as part of the project's continuous integration (CI) workflows.
Building and running unit tests
The simplest way to build unit tests is using CMake.
First, follow the quick start to build Palace with CMake. From the Palace root directory:
mkdir build && cd build
cmake -DPALACE_MFEM_USE_EXCEPTIONS=yes ..
make -j $(nproc) palace-testsThe PALACE_MFEM_USE_EXCEPTIONS option is necessary to properly capture and test assertions. If you want to also measure test coverage, turn PALACE_BUILD_WITH_COVERAGE on. See Unit test coverage for more details on this.
Once the build completes, the palace-unit-tests executable will be installed in the same bin/ directory as the main palace executable, and you can run tests in two ways:
- Using CTest (recommended for running all tests): Automatically runs all test categories in parallel with proper environment setup
- Using the test executable directly: Provides fine-grained control over which tests run using Catch2's filtering syntax
Running tests with CTest
CTest provides automated test execution with parallel support and proper categorization:
cd palace-build
# If Palace was built with Spack: spack cd -b palace
ctestThis discovers and runs all registered tests. CTest automatically:
- Runs serial tests with a single process
- Runs MPI tests with 2 processes
- Runs GPU tests with proper device configuration
- Prevents concurrent execution of tests that require exclusive resources
To see all the tests available, call:
ctest -NThis will print something like:
Test project palace/build
Test #1: serial-Config Boundary Ports
Test #2: serial-Config Driven Solver
Test #3: serial-FarField
Test #4: serial-EM Constant Check
....If you want to run all tests in parallel (e.g., with 8 processes):
ctest -j8 --output-on-failureCTest will schedule execution of tests trying to use all the 8 processes. In this, CTest handles MPI processes correctly, and ensures that only one GPU test is being run at the time. If you compiled Palace, the total number of processes that could end up used in this example is 8 * OMP_NUM_THREADS.
If you run specific tests categories, you can use regex matching, for example
ctest -R mpi- # Run only MPI tests
ctest -R postoperator # Run tests with postoperator in the nameRunning tests directly with the executable
CTest is powerful, but sometimes we want to have more direct control of how the tests are being run. When this happens, we can use directly the unit-tests executable.
To run all the Serial tests (including the benchmarks):
bin/palace-unit-testsThis runs all Serial tests. For Parallel tests:
mpirun -np 2 bin/palace-unit-testsFor GPU tests:
bin/palace-unit-tests --device cudaYou can use Catch2 filters to run specific subsets:
bin/palace-unit-tests [vector] # Tests tagged [vector]
bin/palace-unit-tests "Vector Sum - Real" # Specific test by name
bin/palace-unit-tests [^postoperator] # Tests not tagged with [postoperator]Benchmarks
The unit test application also includes a small number of benchmarks to compare performance between MFEM's legacy assembly backend, MFEM's partial assembly backend, and the specified libCEED backend (specified with the --backend option, use -h/--help to list all command line options for the palace-unit-tests executable). These can be run using, for example:
bin/palace-unit-tests "[Benchmark]" --benchmark-samples 10These benchmarks can be accelerated using MPI and/or OpenMP parallelism (when configured with PALACE_WITH_OPENMP=ON), but in all cases they are only testing the local operator assembly on each process.
CTest is set up to skip all the benchmarks. More specifically, CTest uses Catch2's --skip-benchmarks flag which skips the BENCHMARK sections within tests but still runs the test logic itself (unlike the ~[Benchmark] tag filter, which would exclude those tests entirely).
Adding unit tests
Palace uses Catch2 for unit testing. Refer to the Catch2 documentation for detailed usage information. If you have never added a test to Palace, start by reading our tutorial on adding a new unit test.
The most important Catch2 feature in the Palace test suite is tags. When creating a test, you provide a name and a series of tags
TEST_CASE("My name", "[MyFirstTag][MyOtherTag][Serial]"){ ... }Catch2 tags are typically used for filtering tests. Palace defines three special tags that control when tests execute based on the runtime environment:
[Serial]tests run only with a single MPI process. Use this for tests that verify single-process behavior.[Parallel]tests run only when multiple MPI processes are available. Use this for tests that verify MPI operations, distributed computations, or inter-process communication.[GPU]tests run only when GPU devices are available. Use this for tests that are meaningful and interesting on GPU hardware.
These tags are inclusive, meaning that a test can be marked with multiple special tags, if the test is meaningful in different contexts (e.g., if a test supports CPU and GPU implementation at the same time). For example, [Serial][Parallel][GPU] indicates a test that should run in every case.
Consider testing this code:
int a = 1, b = 2;
Assert(a + b == 3);This test will pass if run with MPI, but this is uninteresting since every MPI process executes the same code that the Serial test would run (same with GPUs). Therefore, this test should only be marked as [Serial].
For the other tags, we recommend grouping related tests using descriptive tag names like [vector] or [materialoperator] (typically named after files or classes). This enables effective filtering, as described in Building and running unit tests.
Accessing files
Files required for tests (e.g., meshes or configurations) need to be saved inside the test/unit/data folder. This ensures that the files are accessible when Palace is installed in a folder that is not the source folder (e.g., with Spack). The path to the content of test/unit/data maps to PALACE_TEST_DATA_DIR. For example, if you want to access the banana.txt file in test/unit/data, refer to it as
auto path_to_banana = fs::path(PALACE_TEST_DATA_DIR) / "banana.txt"Accessing files without using PALACE_TEST_DATA_DIR will likely result in failing tests on Spack builds (unless you know what you are doing).
Test configuration
The CMakeLists.txt in test/unit/ contains important settings:
- The list of files compiled as part of the tests
- The list of files that need to be compiled with a GPU compiler (
TARGET_SOURCES_DEVICE) - Path variables used in tests, such as
MFEM_DATA_PATHMFEM_DATA_PATHcontains 2D and 3D sample meshes for testing (these meshes come from the MFEM repository).
Unit test coverage
When Palace is built with PALACE_BUILD_WITH_COVERAGE, running the unit tests generates coverage information that tracks which parts of the codebase are exercised by the tests. The scripts/measure-test-coverage script automates the entire coverage measurement process, handling compiler detection, test execution, and report generation.
Make sure you have lcov installed. To check that it works, run
lcov --versionlcov 2.0 or newer is recommended (for --ignore-errors inconsistent support); older versions will still work but may report non-fatal inconsistency warnings. For LLVM, you will also need llvm-profdata and llvm-cov. For GCC, you will need gcov-tool (which comes with GCC).
Also make sure that Palace was build with PALACE_BUILD_WITH_COVERAGE. If not, compile it with
cmake -DPALACE_MFEM_USE_EXCEPTIONS=yes -DPALACE_BUILD_WITH_COVERAGE=yes ..Assuming you correctly built Palace in the build folder, the simplest approach runs tests and generates an HTML report in one command:
cd scripts
./measure-test-coverage report
# With Spack
./measure-test-coverage report $(spack location -b palace)The script takes the build directory as argument (defaulting to ../build). This automatically:
- Detects your compiler type (GCC or LLVM)
- Runs all tests via CTest (serial, MPI, GPU if available)
- Collects and merges coverage data
- Filters to Palace source code only
- Generates an HTML report at
build/coverage_html/index.html
You can control test parallelism with the -j flag:
./measure-test-coverage report -j8Note that MPI tests run with 2 processes each, so -j8 allows up to 4 MPI tests simultaneously. As noted in Running tests with CTest, if Palace was built with OpenMP, account for the additional threads when choosing the parallelism level.
The measure-test-coverage allows for more control, for example:
# Generate coverage data only
./measure-test-coverage generate /path/to/build
# With spack
./measure-test-coverage generate $(spack location -b palace)
# Generate HTML report from existing coverage data
./measure-test-coverage report coverage_filtered.info
# Merge coverage from multiple builds
./measure-test-coverage generate /path/to/build1
./measure-test-coverage generate /path/to/build2
./measure-test-coverage merge build1/coverage_filtered.info build2/coverage_filtered.info
./measure-test-coverage report coverage_filtered.infoUnderstanding the coverage system
Palace supports two coverage implementations:
- LLVM source-based coverage (Clang, AppleClang, IntelLLVM): Provides accurate instrumentation through compiler integration. Coverage data is written to
.profrawfiles during test execution, then merged and converted to LCOV format. - GCC gcov coverage: Uses compile-time annotations (
.gcno) and runtime data (.gcda). Themeasure-test-coveragescript handles the complexity of merging parallel test runs and unmangling filenames.
Both produce LCOV-compatible output for standardized processing and HTML visualization.
Note that measure-test-coverage runs all the tests and produces only one coverage report, merging the results from the serial, parallel, and gpu tests.
LLVM's source-based coverage provides more accurate instrumentation than the gcov-compatible mode, which attempts to emulate gcov behavior but has known reliability issues. The source-based approach offers better precision for complex codebases.
AddressSanitizer and UndefinedBehaviorSanitizer
Palace can be built with AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) to detect memory errors and undefined behavior at runtime. Both sanitizers are enabled together with a single CMake option:
mkdir build && cd build
cmake -DPALACE_BUILD_WITH_SANITIZERS=ON ..
make -j palace-testsThis adds -fsanitize=address,undefined -fno-omit-frame-pointer to the compile and link flags for both the palace library and the unit test executable.
Run the unit tests as usual:
bin/palace-unit-tests --skip-benchmarks
mpirun -np 2 bin/palace-unit-tests --skip-benchmarksThe following environment variables are useful when running under sanitizers:
ASAN_OPTIONS=detect_leaks=0: Disables leak detection, which can produce false positives from MPI runtimes and third-party libraries.LSAN_OPTIONS=suppressions=test/unit/lsan_suppressions.txt: Suppresses known third-party leaks from MPI, hwloc, libevent, and libCEED instead of disabling leak detection entirely.UBSAN_OPTIONS=print_stacktrace=1: Prints a full stack trace when undefined behavior is detected.
AddressSanitizer introduces singificant slowdowns. Use it only for debugging purposes.
GCC does not support AddressSanitizer on macOS with Apple Silicon. Use Apple Clang or Homebrew LLVM/Clang instead when building with sanitizers on macOS.
Regression tests
In addition to unit tests, Palace comes with a series of regression tests. Regression tests based on the provided example applications in the examples/ directory and verify that the code reproduces results in reference files stored in test/examples/.
Tests in CI
Palace runs three tiers of CI checks on every pull request:
- Static analysis (
style.yml): Code formatting (clang-format, Julia formatter), JSON schema validation, and test tag checks. - Unit + regression tests (
build-and-test-linux.yml,build-and-test-macos.yml,spack.yml,singularity.yml): Builds Palace using CMake, Spack, and Singularity with a matrix of compilers (GCC, Clang, Intel), MPI implementations (Open MPI, MPICH, Intel MPI), math libraries (OpenBLAS, ARMPL, MKL), and link/integer options across x86, ARM, macOS, and GPU runners. Each configuration runs the full unit test suite (serial + parallel) and all default regression tests. - Long tests (
long-tests.yml): Expensive test cases (e.g.,transmon/transmon_amr) that are too slow to run on every push.
Long tests are a required check for merging non-trivial pull requests and are not run by default. To run the Long tests, add the trigger-long-tests GitHub label. The typical workflow is:
- Develop and iterate using the default (short) tests that run automatically.
- When the PR is ready to merge, add the
trigger-long-testslabel. - The long tests run and the
long-testsstatus is updated. - If you push new commits while the label is present, the long tests re-run automatically.
Two special cases bypass the long test requirement:
- Trivial changes: If a PR only touches documentation, README files, or other non-code files, the long test status is automatically set to success.
no-long-testslabel: Adding this label bypasses the long test requirement entirely.
Building and running example tests
Prerequisites
- Julia
- Palace executable in PATH or specified via environment variable/command-line argument
Setup
First, instantiate the Julia environment:
julia --project -e "using Pkg; Pkg.instantiate()"You need to do this step only the very first time.
Command Line Arguments
The test runner supports command line arguments for configuration. Each argument can also be set via environment variables as fallbacks.
Key Options:
--palace-test: Path to Palace executable and optional arguments (default: "palace")--num-proc-test: Number of MPI processes (default: number of physical cores)--test-cases: Space-separated list of test cases to run (default: all examples)
Run julia --project runtests.jl --help to see all available options with descriptions and defaults.
Execution
Run all tests:
julia --project runtests.jlRun specific test cases:
julia --project runtests.jl --test-cases "spheres rings"Run with custom Palace executable:
julia --project runtests.jl --palace-test "../../build/bin/palace"Run with custom number of processes:
julia --project runtests.jl --num-proc-test 4You can also use environment variables as fallbacks:
TEST_CASES="spheres rings" julia --project runtests.jl
PALACE_TEST="../../build/bin/palace" julia --project runtests.jlEach test case runs Palace simulations and compares generated CSV files against reference data using configurable tolerances. When Palace behavior changes legitimately (e.g., algorithm improvements), reference data can be updated using the baseline script:
./baseline # Update all reference data
./baseline -e spheres # Update specific example
./baseline --dry-run # Test without updating files
./baseline -np 4 # Use 4 MPI processes