12 KiB
Catch2 reference
How to write and structure test code with Catch2 in OrcaSlicer. For where a test belongs, how to name and tag it, and how to build and run the suites, see AGENTS.md.
OrcaSlicer uses Catch2 v3.11.0, vendored in tests/catch2/. Include it with the single-header convenience include:
#include <catch2/catch_all.hpp>
Critical rules
These three mistakes produce undefined behavior, crashes, or useless failure output rather than a normal test failure. Avoid them everywhere.
1. Never reuse a section name inside a loop
A repeated SECTION name in a loop makes Catch2's section tracking behave unpredictably. Use DYNAMIC_SECTION so each iteration is unique.
// WRONG: same name every iteration
for (int i = 0; i < 3; ++i)
SECTION("Same name") { REQUIRE(i >= 0); }
// CORRECT
for (int i = 0; i < 3; ++i)
DYNAMIC_SECTION("Section " << i) { REQUIRE(i >= 0); }
2. Assertions are not thread-safe
Catch2 assertions are not thread-safe by default. A REQUIRE/CHECK from a spawned thread corrupts internal state or terminates the process. Collect results in the thread, assert on the main thread.
// WRONG
std::thread t([&]{ REQUIRE(work() == expected); });
// CORRECT
std::atomic<int> passed{0};
std::thread t([&]{ if (work() == expected) passed++; });
t.join();
REQUIRE(passed == 1);
Catch2 v3.9.0+ has opt-in thread-safe assertions via
CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS. OrcaSlicer does not enable that flag, so assertions remain non-thread-safe. See Thread safety below for the full rule list.
3. Do not combine conditions with binary operators
Catch2 decomposes a single comparison to show both operands on failure. A &&/|| inside one assertion collapses to false with no values. Split it.
REQUIRE(a > 0 && b < 10); // WRONG: prints "false"
REQUIRE(a > 0); // CORRECT: each prints its operands
REQUIRE(b < 10);
Test structure
#include <catch2/catch_all.hpp>
#include "libslic3r/Point.hpp"
using namespace Slic3r;
TEST_CASE("Behavioral description", "[SubsystemTag]") {
// ...
}
Assertions
// Stop the test on failure
REQUIRE(expression);
REQUIRE_FALSE(expression);
// Continue the test after failure (report all failures in the case)
CHECK(expression);
CHECK_FALSE(expression);
// Record the result without failing (for assumptions that may be violated)
CHECK_NOFAIL(expression);
Exceptions
REQUIRE_NOTHROW(function_call());
REQUIRE_THROWS(risky_function());
REQUIRE_THROWS_AS(function_call(), SpecificException);
REQUIRE_THROWS_WITH(function_call(), "Expected error message");
REQUIRE_THROWS_MATCHES(function_call(), SpecificException,
Catch::Matchers::Message("contains this"));
Prefer these over a hand-rolled try/catch with a bool flag.
Matchers
#include <catch2/matchers/catch_matchers.hpp>
// String matchers
using Catch::Matchers::StartsWith;
using Catch::Matchers::EndsWith;
using Catch::Matchers::ContainsSubstring; // v2's "Contains" no longer exists
using Catch::Matchers::Equals;
using Catch::Matchers::Matches; // regex
REQUIRE_THAT(result, StartsWith("Expected prefix"));
REQUIRE_THAT(result, ContainsSubstring("middle part"));
REQUIRE_THAT(result, Matches(".*pattern.*"));
// Float matchers - always prefer these over Approx
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
using Catch::Matchers::WithinULP;
REQUIRE_THAT(v, WithinAbs(expected, 0.001));
REQUIRE_THAT(v, WithinRel(expected, 0.01));
REQUIRE_THAT(v, WithinULP(expected, 4));
// Combine: relative OR absolute (useful when the value can be near zero)
REQUIRE_THAT(v, WithinRel(expected, 0.001) || WithinAbs(0.0, 0.000001));
Sections
Each SECTION re-runs the enclosing TEST_CASE body from the top, so setup declared before the sections is fresh for each one.
TEST_CASE("Complex feature", "[Feature]") {
SomeObject obj; // rebuilt for every section
SECTION("First scenario") { REQUIRE(obj.method1() == expected_value); }
SECTION("Second scenario") { REQUIRE(obj.method2() == other_expected); }
}
BDD-style tests
SCENARIO / GIVEN / WHEN / THEN are aliases for TEST_CASE and SECTION with prefixed names. New tests should prefer a flat TEST_CASE; reserve BDD for genuine shared setup that branches into closely related variations (see the test-design guidance in AGENTS.md).
SCENARIO("User performs an operation", "[UserStory]") {
GIVEN("A setup condition") {
GCodeWriter writer;
WHEN("The user acts") {
auto result = writer.some_operation();
THEN("The outcome holds") {
REQUIRE(result.size() > 0);
}
}
}
}
Generators
// Value list
auto v = GENERATE(1, 3, 5, 7, 11, 13);
// Range
auto i = GENERATE(range(1, 10)); // 1..9
// From a variable (use GENERATE_REF / GENERATE_COPY for captured references)
std::vector<int> values = {1, 2, 3, 4, 5};
auto x = GENERATE_REF(from_range(values));
// Random
auto r = GENERATE(take(100, random(-1000, 1000)));
Fixtures
class GeometryFixture {
public:
Point origin{0, 0};
Point unit_x{1, 0};
};
TEST_CASE_METHOD(GeometryFixture, "Point operations", "[Geometry]") {
REQUIRE(origin.distance_to(unit_x) == 1.0);
}
Persistent (TEST_CASE_PERSISTENT_FIXTURE, one instance for the whole case) and type-parameterized (TEMPLATE_TEST_CASE_METHOD) variants also exist; neither is used in the suite today.
Advanced features
Logging and control
INFO("Persists until end of scope");
UNSCOPED_INFO("Survives beyond its scope"); // v2.7.0+
CAPTURE(some_variable, another_var); // logs names and values
WARN("Warns without failing");
SKIP("Reason"); // marks the test skipped (v3.3.0+)
FAIL("Stops the test");
SUCCEED("Explicit success marker");
Other macros
Available but currently unused in the suite; see the upstream docs for details.
- Compile-time asserts:
STATIC_REQUIRE/STATIC_CHECK(v3.0.1+) check type traits at compile time. - Conditional blocks:
CHECKED_IF/CHECKED_ELSErecord a branch condition without counting it as a failure. - Benchmarking (v2.9.0+):
BENCHMARK("name") { return work(); };, orBENCHMARK_ADVANCEDwhen setup must be excluded from the measurement.
Usage patterns in OrcaSlicer
Concrete shapes for exercising the codebase's own types. Test data is reached through the TEST_DATA_DIR define; always wrap it in std::string(...) before concatenating a path.
// Geometry, with epsilon tolerance
TEST_CASE("Line operations", "[Geometry]") {
Line line{{100000, 0}, {0, 0}};
Line rotated(line);
rotated.rotate(0.9 * EPSILON, {0, 0});
REQUIRE(line.parallel_to(rotated));
}
// Config from an ini
TEST_CASE("Config loading", "[Config]") {
DynamicPrintConfig config;
REQUIRE_NOTHROW(config.load_from_ini(std::string(TEST_DATA_DIR) + "/test_config/sample.ini",
ForwardCompatibilitySubstitutionRule::Disable));
REQUIRE(config.has("layer_height"));
}
// File I/O
TEST_CASE("STL file parsing", "[FileFormat]") {
TriangleMesh mesh;
REQUIRE_NOTHROW(mesh.ReadSTLFile((std::string(TEST_DATA_DIR) + "/test_stl/20mmbox.stl").c_str()));
REQUIRE_FALSE(mesh.empty());
REQUIRE(mesh.volume() > 0);
}
// G-code emission, matched by token (see test_gcodewriter.cpp)
TEST_CASE("z_hop lifts the nozzle", "[GCodeWriter]") {
GCodeWriter writer;
writer.set_extruders({0});
writer.set_extruder(0);
writer.travel_to_z(10.0);
writer.config.z_hop.values = {1.0};
REQUIRE_THAT(writer.eager_lift(LiftType::NormalLift), Catch::Matchers::ContainsSubstring("Z11"));
}
Custom string conversions
Give Catch2 a way to print a custom type on failure. The usual case is an operator<< overload:
std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << "Point(" << p.x << ", " << p.y << ")";
}
When you cannot add operator<<, specialize Catch::StringMaker<T>. Enums can be registered with CATCH_REGISTER_ENUM (at global scope) and exceptions translated with CATCH_TRANSLATE_EXCEPTION; see the upstream docs for those.
Command line
AGENTS.md covers the everyday commands (CTest, per-suite runs, tag filtering as CTest labels). The flags below are Catch2's own, available when you run a suite executable directly.
# Filtering
suite_tests "[Geometry]" # by tag
suite_tests "*geometry*" # by name pattern
suite_tests "~[Performance]" # exclude a tag
suite_tests "[Geometry][Config],[Algorithm]" # (Geometry AND Config) OR Algorithm
# Discovery
suite_tests --list-tests
suite_tests --list-tags
suite_tests --list-reporters
# Debugging a failure
suite_tests --break # break into the debugger on failure
suite_tests --success # show passing assertions too
suite_tests --durations yes # per-test timing
suite_tests --abort # stop at the first failure
Ordering and sharding
Run in random order so tests stay independent. For parallel shards, all shards must share one seed.
suite_tests --order rand --warn NoAssertions
suite_tests --order rand --shard-index 0 --shard-count 4 --rng-seed 0xBEEF
suite_tests --order rand --shard-index 1 --shard-count 4 --rng-seed 0xBEEF
# ...one invocation per shard index
Reporters
suite_tests --reporter console # default, human-readable
suite_tests --reporter compact
suite_tests --reporter xml # Catch2 XML
suite_tests --reporter junit # JUnit XML (CI)
suite_tests --reporter tap
suite_tests --reporter console --reporter junit::out=results.xml # multiple at once
Common pitfalls
Floating-point comparison
Compare floats with the float matchers, never with ==. New tests should prefer the Within* matchers over Approx. Many existing tests still use Approx, which works but is:
- Asymmetric:
Approx(10).epsilon(0.1) != 11.1yetApprox(11.1).epsilon(0.1) == 10. - Double-only: all math is done in
double, which misbehaves forfloatinputs. - Relative by default:
Approx(0) == Xholds only forX == 0.
Use WithinAbs near zero, WithinRel across magnitudes, WithinULP for the tightest check, or combine them. Catch::StringMaker<double>::precision = 15; widens printed precision.
Exception testing
Use REQUIRE_THROWS / REQUIRE_THROWS_AS rather than a try/catch with a bool flag.
Thread safety
Assertions are not thread-safe (see Critical rule 2). The full list of macros that must stay on the main thread:
REQUIREfamily: throws in a spawned thread with no handler, terminating the process.CHECKfamily: can corrupt internal state.SKIP,FAIL,SUCCEED: unsafe even with v3's opt-in thread-safe assertions.- Message macros (
INFO,CAPTURE,WARN): unsafe. STATIC_REQUIRE/STATIC_CHECK: unsafe (rely on runtime registration).
Path handling
Wrap TEST_DATA_DIR in std::string(...) before concatenating, or use boost::filesystem:
std::string path = std::string(TEST_DATA_DIR) + "/model.obj";
Memory
Prefer RAII and smart pointers so a failing assertion cleans up automatically.
Compilation and performance flags
#define CATCH_CONFIG_FAST_COMPILE // ~20% faster compile, disables some features
#define CATCH_CONFIG_DISABLE_STRINGIFICATION // works around the VS2017 raw-string bug
#define CATCH_CONFIG_WINDOWS_CRTDBG // memory-leak detection (whole build)
The test build already defines CATCH_CONFIG_FAST_COMPILE (via test_common in tests/CMakeLists.txt).
Platform-specific workarounds
- MinGW/Cygwin slow linking: build with
-fuse-ld=lld. - Visual Studio 2017 raw-string-literal bug: define
CATCH_CONFIG_DISABLE_STRINGIFICATION(disables expression stringification). - Visual Studio 2022 spaceship operator:
REQUIRE((a <=> b) == 0)may not compile; use clang-cl or avoid<=>in assertions.
Catch2 v3 notes
Available on v3.11.0: SKIP() (v3.3.0+), opt-in thread-safe assertions (v3.9.0+, not enabled here), built-in BENCHMARK, multiple simultaneous reporters (v3.0.1+), STATIC_CHECK (v3.0.1+), built-in sharding (--shard-*).
Two behavior notes: the string matcher is ContainsSubstring (v2's Contains is gone), and a section is re-run when a later sibling section fails (unchanged from v2).