Compare commits

...

12 Commits

Author SHA1 Message Date
Kris Austin
137e8e5e92 docs(tests): add a layered guide to the test tree (#14628) 2026-07-23 09:19:37 -03:00
Ian Chua
f1f65342f0 fix: reserve plugin enable state during rescans (#14860)
# Description

Plugin discovery now distinguishes an unavailable or invalid
.install_state.json from a valid sidecar. During transient filesystem
replacement, the existing enabled state is preserved instead of being
reset, preventing plugins from unexpectedly losing their auto-load
behavior during rescans.

Thanks @WeLizard for pointing this out.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-07-23 12:24:36 +08:00
Ian Chua
738af9f4eb Merge branch 'main' into fix/plugin-install-state 2026-07-23 12:24:19 +08:00
Ian Chua
bbe39629ed fix: plugin bugs (#14855)
# Description

Fixes some bugs reported by users and the community

# Fix 1: Latest Version
If the user updates the plugin version on OrcaCloud without a changelog,
the latest version shown on OrcaSlicer won't be accurate.
## Issue
OrcaSlicer uses the changelog returned by OrcaCloud backend as the
source of truth, even though we query a latest version from the backend
as well. The changelog only consists of version entries that have
changelogs so if the latest one, e.g. 1.2.0 has no changelog, but 1.1.0
has, the latest version will be interpreted as 1.1.0.
## Fix
Don't use the changelog for version tracking, just use it for the
plugins dialog changelog tab.

# Fix 2: Host theme script / window.orca bridge leaks into cross-origin
child iframes
If a plugin's embedded app renders its own `<iframe>` (its real
catalog/dashboard UI, or a third-party auth/payment widget), that child
document also gets, uninvited: the host theme `<style>` block fighting
whatever CSS the child page already defines.
## Issue
OrcaSlicer injects host themed scripts and window.orca bridge into every
page, potentially breaking any cross origin child frames or `<iframes>`
in general.
## Fix
Only inject top level frames by checking `if (window.top !==
window.self) return;`.

# Fix 3: Transferring Slicing Pipeline Plugin config when switching
printers (#14832)
Select an entry in the Process tab's "Slicing Pipeline Plugin" picker,
switch the active printer preset, then click **Transfer** in the
"modified settings" dialog — OrcaSlicer crashes immediately and
repeatably. Changing other settings (e.g. a single scalar option) does
not reproduce it; only this picker does.
## Issue
`slicing_pipeline_plugin` is a vector option (`coStrings`) with an empty
default. `deep_diff` diffs vector options per-index, so selecting a
plugin produced a `"slicing_pipeline_plugin#0"` dirty key instead of a
plain one. Unlike genuine per-extruder options, this key wasn't caught
by the printer-switch filter that discards stale per-extruder changes,
so it got cached and replayed through `ConfigBase::apply_only`'s
`'#'`-indexed branch, which calls `ConfigOptionVector::set_at()` on the
freshly-reloaded (and still empty) destination vector. `set_at()`'s only
empty-vector guard is an `assert()`, which is compiled out of Release
builds, so it dereferences `values.front()` on an empty vector —
undefined behavior, matching the reported ACCESS_VIOLATION.
## Fix
Treat `slicing_pipeline_plugin` as a single atomic value in `deep_diff`
(`Preset.cpp`), same as `printable_area`/`thumbnails`/etc.,
since it isn't actually per-extruder data. It's now diffed and replayed
as a whole option (`ConfigOptionVector::set()`, a plain vector
assignment) instead of the index-based `set_at()` path — removing the
crash unconditionally, regardless of whether the two printers share the
same extruder configuration.

# Fix 4: Stale .whl cache
After a .whl was loaded once, if at runtime, the .whl is replaced with a
new one, the plugin system will use the stale .whl cache.
## Fix
Added an option in the context menu to Reload or Delete Cache and Reload
for locally installed plugins. The assumption here is that users
shouldn't be modify cloud plugins, and if they want to develop on a
subscribed cloud plugin, they should create a local copy of it.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-07-23 12:24:06 +08:00
Ian Chua
bc6ae243ca Merge branch 'main' into fix/plugin-bugs 2026-07-22 13:14:03 +08:00
Ian Chua
b121051220 Merge branch 'main' into fix/plugin-bugs 2026-07-21 12:56:52 +08:00
Ian Chua
4c2d9b4b18 Merge branch 'main' into fix/plugin-install-state 2026-07-21 12:55:30 +08:00
Ian Chua
28feb8480e fix: reserve plugin enable state during rescans 2026-07-20 18:40:31 +08:00
Ian Chua
242e100feb fix: stale .whl cache when .whl changes, added option to delete cache and reload 2026-07-20 14:47:26 +08:00
Ian Chua
2a46bd1384 fix: crash transferring Slicing Pipeline Plugin changes across a printer switch 2026-07-20 13:58:57 +08:00
Ian Chua
ddbbc7f7f2 fix: only the top level frame should have the orca syles and bridges injected 2026-07-20 12:48:17 +08:00
Ian Chua
3a48086f5d fix: plugin descriptor should not reply on changelog for latest version 2026-07-20 12:47:18 +08:00
15 changed files with 520 additions and 1081 deletions

View File

@@ -17,7 +17,7 @@ cmake --build . --config %build_type% --target ALL_BUILD -- -m
## Testing
Catch2 framework. Tests in `tests/` directory.
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
```bash
cd build && ctest --output-on-failure # all tests

View File

@@ -48,13 +48,14 @@ std::string plugin_defaults_user_script()
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
// Injected into every page at document start (before the plugin's own scripts).
// Defines window.orca as the only host surface the page may use. It references
// window.wx lazily (at call time) so it never races the backend's deferred
// registration of the "wx" message handler. Guarded against double-injection so
// it is harmless if also prepended.
// Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's
// deferred registration of the "wx" message handler. Guarded against
// double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function send(kind, data) {

View File

@@ -300,7 +300,12 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
add_action("open_folder", "Show in folder", has_local);
add_action("reinstall_plugin", "Reinstall");
if (is_cloud) {
add_action("reinstall_plugin", "Reinstall");
} else {
add_action("reload_plugin", "Reload");
add_action("clear_cache_reload_plugin", "Delete cache and reload");
}
return available_actions;
}
@@ -739,11 +744,13 @@ void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, con
unsubscribe_cloud_plugin(row_data);
} else if (action == "delete_mine_plugin") {
delete_mine_local_and_cloud_plugin(plugin_key);
} else if (action == "reload_plugin") {
reload_local_plugin(plugin_key, /*clear_cache=*/false);
} else if (action == "clear_cache_reload_plugin") {
reload_local_plugin(plugin_key, /*clear_cache=*/true);
} else if (action == "reinstall_plugin") {
if (row_data.is_cloud_plugin())
reinstall_cloud_plugin(row_data);
else
reinstall_local_plugin(plugin_key);
}
}
@@ -1123,7 +1130,7 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
}
void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clear_cache)
{
if (plugin_key.empty())
return;
@@ -1132,11 +1139,34 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
std::pair<bool, std::string> reload_result{false, ""};
try {
reload_result = run_with_dialog_wait(
[plugin_key, was_loaded]() -> std::pair<bool, std::string> {
[plugin_key, was_loaded, clear_cache]() -> std::pair<bool, std::string> {
PluginManager& manager = PluginManager::instance();
boost::filesystem::path cache_dir;
if (clear_cache) {
PluginDescriptor descriptor;
if (!manager.try_get_plugin_descriptor(plugin_key, descriptor))
return {false, "Plugin not found."};
boost::filesystem::path resolved_root;
std::string resolve_error;
if (!resolve_allowed_plugin_root(descriptor, {get_orca_plugins_dir()},
"Refusing to clear a plugin cache outside the local plugin directory.",
resolved_root, resolve_error))
return {false, resolve_error};
cache_dir = resolved_root / "__whl_extracted__";
}
if (!manager.unload_plugin(plugin_key))
return {false, "Failed to unload plugin."};
if (clear_cache) {
boost::system::error_code ec;
boost::filesystem::remove_all(cache_dir, ec);
if (ec)
return {false, "Failed to clear plugin cache: " + ec.message()};
}
manager.load_plugin(plugin_key, false);
std::string error;
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))

View File

@@ -96,7 +96,7 @@ private:
void open_plugin_folder(const Slic3r::PluginDescriptor& plugin);
void delete_local_plugin(const Slic3r::PluginDescriptor& plugin);
void unsubscribe_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void reinstall_local_plugin(const std::string& plugin_key);
void reload_local_plugin(const std::string& plugin_key, bool clear_cache);
void reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void delete_mine_local_and_cloud_plugin(const std::string& plugin_key);

View File

@@ -96,6 +96,9 @@ std::string WebViewHostDialog::document_start_injector(const std::string& markup
const std::string literal = nlohmann::json(markup).dump();
std::string s;
s += "(function(){";
// wxWebView's AddUserScript runs in child frames too (including cross-origin
// frames on WebView2). Host theme state belongs only to the top-level page.
s += "if(window.top!==window.self)return;";
s += prelude;
s += "var css=" + literal + ";";
s += "function inject(){";

View File

@@ -54,7 +54,7 @@ struct PluginDescriptor
std::string description; // Plugin description
std::string author; // Plugin author from manifest, if available
std::string version; // Selected plugin version
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable.
std::string latest_version; // Authoritative latest available cloud version.
std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch.
std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
@@ -66,6 +66,10 @@ struct PluginDescriptor
std::string error; // Blocking error message. Non-empty means the plugin is in an error state.
std::optional<CloudPluginState> cloud; // Extra cloud state layered on top of a normal plugin descriptor.
bool metadata_valid = false; // Manifest/package validity stays separate from the user-facing error field.
// Whether the discovery pass successfully read .install_state.json. This is transient scan
// metadata, used to distinguish an explicit enabled=false from a sidecar that was unavailable
// during a live package replacement.
bool install_state_valid = false;
// Package auto-load flag, read from .install_state.json. Defaults to FALSE: a package with no
// sidecar has never been installed through Orca and carries no auto-load intent, so it must not
// be loaded at startup. Installing a package writes the sidecar with enabled = true.
@@ -113,10 +117,6 @@ struct PluginDescriptor
bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; }
std::string latest_available_version() const
{
for (const PluginChangelog& entry : changelog) {
if (!entry.version.empty())
return entry.version;
}
if (!latest_version.empty())
return latest_version;
return version;

View File

@@ -172,7 +172,7 @@ void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescri
// its error, rather than dropping it silently.
if (entry_path.empty()) {
descriptor.set_error(entry_error);
read_install_state(plugin_dir, descriptor);
descriptor.install_state_valid = read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, plugin_dir);
out.push_back(std::move(descriptor));
BOOST_LOG_TRIVIAL(warning) << "Invalid plugin package: " << plugin_dir.string() << " - " << out.back().error;
@@ -185,7 +185,7 @@ void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescri
read_python_plugin_metadata(entry_path, descriptor, meta_error);
if (!parsed) {
descriptor.set_error(meta_error);
read_install_state(plugin_dir, descriptor);
descriptor.install_state_valid = read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, entry_path);
out.push_back(std::move(descriptor));
BOOST_LOG_TRIVIAL(warning) << (is_wheel ? "Invalid wheel plugin: " : "Invalid .py plugin: ")
@@ -199,7 +199,7 @@ void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescri
// Cloud identity and the package-level auto-load flag. plugin_key is always derived
// below, never read from the sidecar.
read_install_state(plugin_dir, descriptor);
descriptor.install_state_valid = read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, entry_path);
out.push_back(std::move(descriptor));
@@ -735,11 +735,11 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
return true;
}
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
{
PluginInstallState state;
if (!read_install_state(plugin_dir, state))
return;
return false;
// The cloud identity and the persisted installed version are read back. plugin_key
// is always derived by the catalog scan (filename for local, the cloud uuid for
@@ -756,6 +756,7 @@ void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescrip
// capability has no existence — and so no state — until it is materialized, at which point the
// loader seeds the flag onto the capability itself.
entry.enabled = state.enabled;
return true;
}
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)

View File

@@ -156,8 +156,9 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
// Convenience overload: write(dir, entry, /*enabled=*/true, /*capabilities=*/{}).
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry);
// Reads only the cloud identity (uuid) back into the descriptor; plugin_key is always derived.
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry);
// Reads install state back into the descriptor; plugin_key is always derived. Returns whether the
// sidecar was present and valid.
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry);
// Full read of the sidecar; returns false if there is no/invalid sidecar.
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out);

View File

@@ -321,6 +321,8 @@ void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> disco
// A manifest-only rescan has nothing to say about capabilities, so the live module and
// instances are left alone.
if (!descriptor.install_state_valid)
descriptor.enabled = existing->descriptor.enabled;
existing->descriptor = std::move(descriptor);
}

57
tests/AGENTS.md Normal file
View File

@@ -0,0 +1,57 @@
# Test suite rules
Rules for writing tests under `tests/`. [CATCH2.md](CATCH2.md) is the Catch2 reference. Building and running the suites is covered on the wiki, at <https://www.orcaslicer.com/wiki/developer_reference/how_to_test.html>.
## The suites
- `libslic3r`: the core library. Geometry, meshes, file formats, config and presets, Clipper, algorithms, data structures.
- `fff_print`: the FFF slicing pipeline, from a `Model` plus config through `Print` and `PrintObject` to emitted G-code.
- `sla_print`: SLA support-tree and pad geometry, support-point generation, raycast.
- `libnest2d`: 2D nesting and packing.
- `slic3rutils`: the Python plugin system and its slicing-pipeline bindings.
- `filament_group`: filament-to-extruder grouping, checked against golden files.
## Building and running
Tests are off by default, so the build has to be told to include them.
- Windows: `build_release_vs.bat tests`, then `ctest --test-dir build/tests -C Release`
- macOS: `./build_release_macos.sh -s -a arm64 -T`, which builds and runs them
- Linux: `./build_linux.sh -t`, then `ctest --test-dir build/tests`
Rebuild a single suite with `cmake --build build --config Release --target <suite>_tests`. Visual Studio and Xcode are multi-configuration generators, so `ctest` needs `-C` there; on Linux it does not.
## Where a test goes
- Pick the suite by the production code the test exercises, not by how the test is written.
- A property of a class that holds with no `Print` involved belongs in `libslic3r`. Behavior that depends on print settings, or produces or consumes G-code or slicing state, belongs in `fff_print`.
- One file per subsystem, named `test_<subsystem>.cpp`. It owns every test for that subsystem, whether the test reads in-memory state or generated output.
- When you add a file, list it in that suite's `CMakeLists.txt` in the same change.
## Use the existing helpers
Check these before writing your own setup or output-parsing code.
- `tests/test_utils.hpp` is shared by every suite. `load_model()` loads a mesh from `tests/data/`, and `ScopedTemporaryFile` gives a temp path that removes itself.
- `fff_print/test_helpers.hpp` builds and slices a `Print` and parses the emitted G-code. Read it before writing an fff_print test rather than assembling a `Print` by hand.
- The other suites have their own: `sla_print/sla_test_utils.hpp`, `libnest2d/libnest2d_test_utils.hpp`, `slic3rutils/plugin_test_utils.hpp`, `filament_group/fg_test_utils.hpp`. `libslic3r` has none and uses the shared header.
- Test data lives in `tests/data/` and is reached through the `TEST_DATA_DIR` define. Wrap it in `std::string(...)` before joining a path onto it.
## Writing the test
- Name the test case as a plain behavioral sentence in the present tense. No `Subsystem:` prefix.
- Tag it with the subsystem it covers, matching the file, in PascalCase. That tag is what people filter on, so every test needs one.
- Add further tags where they help: a narrower one to slice a large file (`[Rotcalip]`, `[Placer]`), a shared one for something spanning files (`[Python]`, `[H2C]`, `[Regression]`), or `[NotWorking]` / `[.]` to disable or hide a test. Say why in a comment if you disable or hide.
- Prefer a flat `TEST_CASE` per behavior, with `GENERATE` for parameterized cases. Reserve `SCENARIO` / `GIVEN` / `WHEN` / `THEN` for genuine shared setup that branches into a few close variations.
- Set the config keys your test depends on, and derive the expected values from what you set. A 20mm cube sliced at `layer_height` 2 is 10 layers, and the test should state both parts. If a number in your assertion comes from a key you never set, the test is also testing that default.
- Assert the defining property, not an incidental value. "Skirt present" or "at least 2 brim loops" survives a refactor; exact coordinates and byte counts do not.
- Name a regression test for the behavior it protects, never for an issue or PR number.
- When asserting on G-code, match the meaningful token such as `; skirt` rather than whole lines, whitespace or comment wording. Depend on ordering only when ordering is the contract.
## Catch2 rules that cause real breakage
- Never reuse a `SECTION` name inside a loop. Use `DYNAMIC_SECTION` so each iteration is unique.
- Never assert from a spawned thread. Catch2 assertions are not thread-safe. Collect results in the thread and assert on the main thread.
- Never combine conditions with `&&` or `||` inside one assertion. Split them so Catch2 can print both operands on failure.
- Compare floats with `WithinAbs` or `WithinRel`, never `==`. Prefer these over `Approx` in new tests.
- Keep tests self-contained: no shared state, green under `--order rand`.

369
tests/CATCH2.md Normal file
View File

@@ -0,0 +1,369 @@
# 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](AGENTS.md).
OrcaSlicer uses **Catch2 v3.11.0**, vendored in `tests/catch2/`. Include it with the single-header convenience include:
```cpp
#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.
```cpp
// 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.
```cpp
// 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](#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.
```cpp
REQUIRE(a > 0 && b < 10); // WRONG: prints "false"
REQUIRE(a > 0); // CORRECT: each prints its operands
REQUIRE(b < 10);
```
## Test structure
```cpp
#include <catch2/catch_all.hpp>
#include "libslic3r/Point.hpp"
using namespace Slic3r;
TEST_CASE("Behavioral description", "[SubsystemTag]") {
// ...
}
```
## Assertions
```cpp
// 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
```cpp
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
```cpp
#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.
```cpp
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](AGENTS.md)).
```cpp
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
```cpp
// 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
```cpp
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
```cpp
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_ELSE` record a branch condition without counting it as a failure.
- **Benchmarking** (v2.9.0+): `BENCHMARK("name") { return work(); };`, or `BENCHMARK_ADVANCED` when 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.
```cpp
// 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:
```cpp
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](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.
```bash
# 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.
```bash
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
```bash
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.1` yet `Approx(11.1).epsilon(0.1) == 10`.
- **Double-only**: all math is done in `double`, which misbehaves for `float` inputs.
- **Relative by default**: `Approx(0) == X` holds only for `X == 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](#2-assertions-are-not-thread-safe)). The full list of macros that must stay on the main thread:
- **`REQUIRE` family**: throws in a spawned thread with no handler, terminating the process.
- **`CHECK` family**: 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`:
```cpp
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
```cpp
#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).

View File

@@ -1,964 +1 @@
# CLAUDE.md - Testing Guide for OrcaSlicer
This guide provides comprehensive instructions for Claude Code when writing, maintaining, and understanding tests in the OrcaSlicer codebase.
> **Adding or organizing `fff_print` tests?** See
> [fff_print/README.md](fff_print/README.md) for where a test belongs and how to
> name it. This guide covers Catch2 mechanics; that README is the suite's
> organizing contract.
## ⚠️ CRITICAL RULES - MUST FOLLOW
### 1. **SECTIONS IN LOOPS - NEVER REUSE NAMES**
**WRONG**: Will cause unpredictable behavior
```cpp
TEST_CASE("Bad loop sections") {
for (int i = 0; i < 3; ++i) {
SECTION("Same name") { // WRONG! Same name used multiple times
REQUIRE(i >= 0);
}
}
}
```
**CORRECT**: Use DYNAMIC_SECTION or incorporate counter
```cpp
TEST_CASE("Good loop sections") {
for (int i = 0; i < 3; ++i) {
DYNAMIC_SECTION("Section " << i) { // Unique name per iteration
REQUIRE(i >= 0);
}
}
}
```
### 2. **THREAD SAFETY - ASSERTIONS ARE NOT THREAD-SAFE**
**WRONG**: Will cause undefined behavior or crashes
```cpp
TEST_CASE("Multi-threaded test") {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([]() {
REQUIRE(some_calculation() == expected); // NOT THREAD-SAFE!
});
}
}
```
**CORRECT**: Synchronize results, test on main thread
```cpp
TEST_CASE("Multi-threaded test") {
std::vector<std::thread> threads;
std::atomic<int> passed{0};
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&passed]() {
if (some_calculation() == expected) {
passed++;
}
});
}
for (auto& t : threads) t.join();
REQUIRE(passed == 4); // Test results on main thread
}
```
### 3. **EXPRESSION DECOMPOSITION - AVOID BINARY OPERATORS**
**WRONG**: Cannot decompose properly
```cpp
REQUIRE(a > 0 && b < 10); // Shows "false" on failure, not individual values
```
**CORRECT**: Split into separate assertions
```cpp
REQUIRE(a > 0);
REQUIRE(b < 10); // Each shows individual values on failure
```
### 4. **FLOATING POINT - NEVER USE APPROX**
**WRONG**: Approx is deprecated and asymmetric
```cpp
REQUIRE(calculated_value == Catch::Approx(expected)); // Deprecated!
```
**CORRECT**: Use floating point matchers
```cpp
REQUIRE_THAT(calculated_value, WithinAbs(expected, 0.001));
REQUIRE_THAT(calculated_value, WithinRel(expected, 0.01)); // 1% tolerance
REQUIRE_THAT(calculated_value, WithinULP(expected, 4)); // 4 ULPs apart
```
### 5. **TEST ORDERING - ALWAYS USE RANDOM ORDER**
**REQUIRED**: For CI/CD and development
```bash
# Essential flags for running tests
./tests --order rand --warn NoAssertions
# For test sharding (parallel execution), share random seed
./tests --order rand --shard-index 0 --shard-count 3 --rng-seed 0xBEEF
./tests --order rand --shard-index 1 --shard-count 3 --rng-seed 0xBEEF
./tests --order rand --shard-index 2 --shard-count 3 --rng-seed 0xBEEF
```
## Overview of OrcaSlicer's Testing Framework
OrcaSlicer uses **Catch2 v3** (currently v3.11.0, vendored in `tests/catch2/`) as its primary testing framework. The test suite is organized into several modules that mirror the project's architectural components:
> **Note**: Test files include the framework via `#include <catch2/catch_all.hpp>` (the v3 single-header convenience include). All v3 features described in this guide are available.
### Test Structure
```
tests/
├── CMakeLists.txt # Main test configuration
├── catch_main.hpp # Custom test reporter
├── libslic3r/ # Core library tests (21 test files)
├── fff_print/ # FFF printing tests (12 test files)
├── sla_print/ # SLA printing tests (4 test files)
├── libnest2d/ # 2D nesting tests
├── slic3rutils/ # Utility tests
├── data/ # Test data files and meshes
└── catch2/ # Catch2 framework files
```
### Build Integration
- Tests are built using CMake with `catch_discover_tests()` integration
- Each test module creates a separate executable (e.g., `libslic3r_tests`, `fff_print_tests`)
- Test data directory is available via `TEST_DATA_DIR` preprocessor definition
- Custom verbose console reporter provides detailed test output
## Test Suite Organization
### libslic3r Tests
Core slicing engine tests covering:
- **Geometry operations**: Points, polygons, lines, Voronoi diagrams
- **File formats**: STL, 3MF, AMF parsing and validation
- **Algorithms**: Clipper operations, mesh boolean operations, optimization
- **Configuration**: Print settings validation and parsing
- **Utilities**: String processing, time utilities, data structures
### fff_print Tests
Fused Filament Fabrication specific tests:
- **G-code generation**: Writer functionality, cooling, lift/unlift
- **Slicing algorithms**: Layer generation, infill patterns
- **Print mechanics**: Flow calculations, extrusion, support material
- **Model processing**: Print objects, skirt/brim generation
### sla_print Tests
Stereolithography specific tests:
- **SLA print processing**: Layer curing, support generation
- **Raycast operations**: Light path calculations
- **Test utilities**: SLA-specific helper functions
## Writing New Tests - Best Practices
### File Organization
1. **Naming Convention**: `test_<feature>.cpp` (e.g., `test_geometry.cpp`)
2. **Header Structure**: Include `<catch2/catch_all.hpp>` first, then relevant headers
3. **Namespace Usage**: Use `using namespace Slic3r;` for convenience
4. **File Placement**: Add to appropriate test directory and update CMakeLists.txt
### Test Naming and Structure
```cpp
#include <catch2/catch_all.hpp>
#include "libslic3r/Point.hpp"
using namespace Slic3r;
TEST_CASE("Feature description", "[category_tag]") {
// Test implementation
}
```
### Tagging System
Use descriptive tags for test categorization:
- `[Geometry]` - Geometric operations and calculations
- `[GCodeWriter]` - G-code generation functionality
- `[Config]` - Configuration and settings tests
- `[FileFormat]` - File I/O operations (STL, 3MF, etc.)
- `[Algorithm]` - Core algorithms and processing
- `[Performance]` - Performance benchmarks (if applicable)
## Catch2 Features Guide
### Basic Assertions
```cpp
// Primary assertions - stop test on failure
REQUIRE(expression);
REQUIRE_FALSE(expression);
// Continuing assertions - continue test after failure
CHECK(expression);
CHECK_FALSE(expression);
// Non-failing checks - record result but don't fail test
CHECK_NOFAIL(expression); // Useful for assumptions that might be violated
```
### Exception Testing
```cpp
// Verify no exception is thrown
REQUIRE_NOTHROW(function_call());
// Verify any exception is thrown
REQUIRE_THROWS(risky_function());
// Verify specific exception type
REQUIRE_THROWS_AS(function_call(), SpecificException);
// Verify exception message
REQUIRE_THROWS_WITH(function_call(), "Expected error message");
// Verify exception with matchers (for partial matching)
REQUIRE_THROWS_MATCHES(function_call(), SpecificException,
Catch::Matchers::Message("contains this"));
```
### Complex Assertions with Matchers
```cpp
#include <catch2/matchers/catch_matchers.hpp>
// String matchers
using Catch::Matchers::StartsWith;
using Catch::Matchers::EndsWith;
using Catch::Matchers::ContainsSubstring;
using Catch::Matchers::Equals;
using Catch::Matchers::Matches; // Regex matching
REQUIRE_THAT(result_string, StartsWith("Expected prefix"));
REQUIRE_THAT(result_string, ContainsSubstring("middle part"));
REQUIRE_THAT(result_string, Matches(".*pattern.*"));
// Floating point matchers - ALWAYS use these instead of Approx!
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
using Catch::Matchers::WithinULP;
REQUIRE_THAT(float_value, WithinAbs(expected, 0.001)); // Absolute tolerance
REQUIRE_THAT(float_value, WithinRel(expected, 0.01)); // Relative tolerance (1%)
REQUIRE_THAT(float_value, WithinULP(expected, 4)); // ULP difference (requires IEEE-754)
// Combining matchers
REQUIRE_THAT(value, WithinRel(expected, 0.001) || WithinAbs(0.0, 0.000001));
```
### Sections for Test Organization
```cpp
TEST_CASE("Complex feature testing", "[Feature]") {
// Common setup code
SomeObject obj;
SECTION("First scenario") {
// Specific test case
REQUIRE(obj.method1() == expected_value);
}
SECTION("Second scenario") {
// Another test case with same setup
REQUIRE(obj.method2() == other_expected);
}
}
```
### BDD-Style Tests
Use for complex scenarios and user story testing:
> **Note**: BDD macros are aliases for TEST_CASE and SECTION with prefixed names
```cpp
SCENARIO("User performs complex operation", "[UserStory]") {
GIVEN("A specific setup condition") {
GCodeWriter writer;
// Setup code
WHEN("User performs action") {
auto result = writer.some_operation();
THEN("Expected outcome occurs") {
REQUIRE(result.size() > 0);
AND_WHEN("Follow-up action occurs") {
auto next_result = writer.next_operation();
THEN("Final outcome is correct") {
REQUIRE(next_result == expected);
}
}
}
}
}
}
```
### Data Generators for Parameterized Tests
```cpp
TEST_CASE("Function works with various inputs", "[Algorithm]") {
auto test_value = GENERATE(1, 3, 5, 7, 11, 13);
REQUIRE(is_odd(test_value));
REQUIRE(test_value > 0);
}
// Range-based generators
TEST_CASE("Range testing", "[Algorithm]") {
auto i = GENERATE(range(1, 10)); // 1 to 9
REQUIRE(process_value(i) > i);
}
// Using variables in generators (requires GENERATE_COPY or GENERATE_REF)
TEST_CASE("Generator with variables", "[Algorithm]") {
std::vector<int> values = {1, 2, 3, 4, 5};
auto test_value = GENERATE_REF(from_range(values)); // Use GENERATE_REF for references
REQUIRE(test_value > 0);
}
// Custom generators
TEST_CASE("Random values", "[Algorithm]") {
auto random_int = GENERATE(take(100, random(-1000, 1000))); // 100 random values
REQUIRE(process_random_value(random_int));
}
```
### Test Fixtures
```cpp
class GeometryFixture {
public:
Point origin{0, 0};
Point unit_x{1, 0};
Point unit_y{0, 1};
mutable double tolerance = EPSILON; // Use mutable for data that might change
};
// Standard fixture - new instance per test run
TEST_CASE_METHOD(GeometryFixture, "Point operations", "[Geometry]") {
REQUIRE(origin.distance_to(unit_x) == 1.0);
}
// Persistent fixture - single instance for entire test case (v3.2.0+)
TEST_CASE_PERSISTENT_FIXTURE(GeometryFixture, "Persistent operations", "[Geometry]") {
static int call_count = 0;
++call_count;
INFO("This fixture persists across sections, call: " << call_count);
SECTION("First section") {
REQUIRE(origin.distance_to(unit_x) == 1.0);
}
SECTION("Second section") {
REQUIRE(origin.distance_to(unit_y) == 1.0);
// call_count will be 2 here with persistent fixture
}
}
// Template fixtures for type-parameterized tests
template<typename T>
class NumericFixture {
public:
T zero = T{0};
T one = T{1};
};
TEMPLATE_TEST_CASE_METHOD(NumericFixture, "Numeric operations", "[Template]", int, float, double) {
REQUIRE(TestType{} == this->zero);
REQUIRE(TestType{1} == this->one);
}
```
### Advanced Testing Features
#### Logging and Information Macros
```cpp
TEST_CASE("Advanced logging", "[Logging]") {
INFO("This info persists until end of scope");
SECTION("Section A") {
INFO("Section A specific info");
CAPTURE(some_variable, another_var); // Captures variable names and values
CHECK(some_condition);
}
SECTION("Section B") {
UNSCOPED_INFO("This survives beyond its scope"); // v2.7.0+
CHECK(other_condition);
}
}
// Warning and explicit control
TEST_CASE("Explicit test control", "[Control]") {
WARN("This warns but doesn't fail the test");
if (precondition_not_met) {
SKIP("Reason"); // Marks the test as skipped (v3.3.0+, available)
return;
}
if (critical_failure) {
FAIL("Critical condition failed"); // Fails and stops test
}
SUCCEED("Reached successful completion"); // Explicit success marker
}
```
#### Static Assertions (Compile-time Testing)
```cpp
TEST_CASE("Compile-time checks", "[Static]") {
STATIC_REQUIRE(sizeof(int) >= 4); // Checked at compile time
STATIC_REQUIRE_FALSE(std::is_void_v<int>);
// For traits and template metaprogramming
STATIC_CHECK(std::is_trivially_copyable_v<Point>); // v3.0.1+
}
```
#### Conditional Testing
```cpp
TEST_CASE("Conditional blocks", "[Conditional]") {
int value = get_test_value();
// These record the expression but don't count as test failures (v3.0.1+)
CHECKED_IF(value > 0) {
// This block runs if value > 0
REQUIRE(value <= 100);
} CHECKED_ELSE(value > 0) {
// This block runs if value <= 0
REQUIRE(value >= -100);
}
}
```
#### Benchmarking (v2.9.0+)
```cpp
TEST_CASE("Performance testing", "[Benchmark]") {
// Simple benchmarking
BENCHMARK("Algorithm performance") {
return expensive_algorithm();
};
// Advanced benchmarking with setup
BENCHMARK_ADVANCED("Advanced benchmark")(Catch::Benchmark::Chronometer meter) {
std::vector<int> data = setup_test_data(); // Setup not measured
meter.measure([&] {
return process_data(data); // Only this is measured
});
};
}
```
## OrcaSlicer-Specific Testing Patterns
### Geometry Testing
```cpp
TEST_CASE("Line operations", "[Geometry]") {
Line line{{100000, 0}, {0, 0}};
Line parallel{{200000, 0}, {0, 0}};
REQUIRE(line.parallel_to(line));
REQUIRE(line.parallel_to(parallel));
// Test with epsilon tolerance
Line rotated(parallel);
rotated.rotate(0.9 * EPSILON, {0, 0});
REQUIRE(line.parallel_to(rotated));
}
```
### Configuration Testing
```cpp
TEST_CASE("Config loading", "[Config]") {
DynamicPrintConfig config;
std::string config_path = std::string(TEST_DATA_DIR) + "/test_config/sample.ini";
REQUIRE_NOTHROW(config.load_from_ini(config_path));
REQUIRE(config.has("layer_height"));
}
```
### File I/O Testing
```cpp
TEST_CASE("STL file parsing", "[FileFormat]") {
std::string stl_path = std::string(TEST_DATA_DIR) + "/test_stl/20mmbox.stl";
TriangleMesh mesh;
REQUIRE_NOTHROW(mesh.ReadSTLFile(stl_path.c_str()));
REQUIRE(!mesh.empty());
REQUIRE(mesh.volume() > 0);
}
```
### G-code Generation Testing
```cpp
TEST_CASE("G-code writer functionality", "[GCodeWriter]") {
GCodeWriter writer;
// Load test configuration
std::string config_path = std::string(TEST_DATA_DIR) + "/fff_print_tests/test_config.ini";
writer.config.load(config_path, ForwardCompatibilitySubstitutionRule::Disable);
// Test specific G-code generation
std::string result = writer.lift();
REQUIRE(!result.empty());
REQUIRE_THAT(result, Catch::Matchers::ContainsSubstring("G1"));
}
```
### Performance Testing Patterns
```cpp
TEST_CASE("Algorithm performance", "[Performance][Algorithm]") {
// Large test data
std::vector<Point> points = generate_large_point_set(10000);
// Time the operation (manual timing example; the BENCHMARK macro is also available)
auto start = std::chrono::high_resolution_clock::now();
auto result = convex_hull(points);
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
REQUIRE(result.size() > 0);
REQUIRE(duration.count() < 1000); // Should complete in < 1 second
}
```
### Custom String Conversions
#### For Custom Types
```cpp
// Method 1: operator<< overload (preferred)
std::ostream& operator<<(std::ostream& os, const Point& point) {
os << "Point(" << point.x << ", " << point.y << ")";
return os;
}
// Method 2: StringMaker specialization
namespace Catch {
template<>
struct StringMaker<MyCustomType> {
static std::string convert(const MyCustomType& value) {
return "MyCustomType{" + std::to_string(value.data) + "}";
}
};
}
// Method 3: Enum registration (v2.8.0+)
enum class Status { Ready, Processing, Complete, Error };
// Must be at global scope!
CATCH_REGISTER_ENUM(Status, Status::Ready, Status::Processing, Status::Complete, Status::Error);
// Method 4: Exception translation
CATCH_TRANSLATE_EXCEPTION(MyCustomException const& ex) {
return "MyCustomException: " + std::string(ex.what());
}
// Method 5: Disable range iteration for problematic types
namespace Catch {
template<>
struct is_range<ProblematicType> {
static const bool value = false;
};
}
```
## Running and Debugging Tests
### Building Tests
```bash
# Build all tests
cd build && make
# Build specific test suite
cd build && make libslic3r_tests
# Build and run tests
cd build && make && ctest
```
### Running Tests
#### Essential Test Execution Patterns
```bash
# REQUIRED: Random order with assertion warnings (best practice)
cd build && ./tests/libslic3r/libslic3r_tests --order rand --warn NoAssertions
# Run all tests with verbose output via CTest
cd build && ctest --output-on-failure
# Run specific test suite with best practices
cd build && ./tests/libslic3r/libslic3r_tests --order rand --warn NoAssertions
# Filter tests with specific tags
cd build && ./tests/libslic3r/libslic3r_tests "[Geometry]" --order rand
# Filter by test name patterns
cd build && ./tests/libslic3r/libslic3r_tests "*geometry*" --order rand
# Exclude tests (negation)
cd build && ./tests/libslic3r/libslic3r_tests "~[Performance]" --order rand
# Combine filters: (Geometry AND Config) OR Algorithm
cd build && ./tests/libslic3r/libslic3r_tests "[Geometry][Config],[Algorithm]" --order rand
# List available tests, tags, and reporters
cd build && ./tests/libslic3r/libslic3r_tests --list-tests
cd build && ./tests/libslic3r/libslic3r_tests --list-tags
cd build && ./tests/libslic3r/libslic3r_tests --list-reporters
# Debug failing tests
cd build && ./tests/libslic3r/libslic3r_tests --break # Break into debugger on failure
cd build && ./tests/libslic3r/libslic3r_tests --success # Show passing tests too
cd build && ./tests/libslic3r/libslic3r_tests --durations yes # Show timing info
# Abort on first failure
cd build && ./tests/libslic3r/libslic3r_tests --abort
# Test sharding for parallel execution (MUST share random seed)
cd build && ./tests/libslic3r/libslic3r_tests --order rand --shard-index 0 --shard-count 4 --rng-seed 0xBEEF &
cd build && ./tests/libslic3r/libslic3r_tests --order rand --shard-index 1 --shard-count 4 --rng-seed 0xBEEF &
cd build && ./tests/libslic3r/libslic3r_tests --order rand --shard-index 2 --shard-count 4 --rng-seed 0xBEEF &
cd build && ./tests/libslic3r/libslic3r_tests --order rand --shard-index 3 --shard-count 4 --rng-seed 0xBEEF &
wait # Wait for all to complete
```
#### Reporter Options for CI Integration
```bash
# Different output formats for CI systems
cd build && ./tests/libslic3r/libslic3r_tests --reporter console # Default human-readable
cd build && ./tests/libslic3r/libslic3r_tests --reporter compact # Minimal output
cd build && ./tests/libslic3r/libslic3r_tests --reporter xml # Catch2 XML format
cd build && ./tests/libslic3r/libslic3r_tests --reporter junit # JUnit XML (widely supported)
cd build && ./tests/libslic3r/libslic3r_tests --reporter tap # Test Anything Protocol
cd build && ./tests/libslic3r/libslic3r_tests --reporter teamcity # TeamCity integration
cd build && ./tests/libslic3r/libslic3r_tests --reporter sonarqube # SonarQube integration
cd build && ./tests/libslic3r/libslic3r_tests --reporter automake # Automake integration
# Multiple reporters simultaneously (if supported)
cd build && ./tests/libslic3r/libslic3r_tests --reporter console --reporter junit::out=results.xml
```
### Test Output Control
The custom `VerboseConsoleReporter` provides enhanced output:
- Test case start/end notifications with timing
- Section execution tracking
- Color-coded success/failure indicators
- Duration reporting for performance analysis
## Test Data Management
### Using TEST_DATA_DIR
All test data is accessible via the `TEST_DATA_DIR` preprocessor definition:
```cpp
std::string mesh_path = std::string(TEST_DATA_DIR) + "/20mm_cube.obj";
std::string config_path = std::string(TEST_DATA_DIR) + "/test_config/printer.ini";
```
### Available Test Assets
#### 3D Models
- **Basic shapes**: `20mm_cube.obj`, `pyramid.obj`, `sphere.obj`
- **Complex geometry**: `extruder_idler.obj`, `ipadstand.obj`, `bridge.obj`
- **Edge cases**: `cube_with_hole.obj`, `sloping_hole.obj`, `small_dorito.obj`
#### File Format Tests
- **STL variants**: ASCII/binary, different line endings, Unicode names
- **3MF files**: Multi-material, complex assemblies
- **Configuration files**: Various printer/material profiles
#### Test Utilities
The `Test` namespace provides helper functions:
```cpp
using namespace Slic3r::Test;
// Load standard test meshes
TriangleMesh mesh = mesh(TestMesh::cube_20x20x20);
// Standard test configurations
DynamicPrintConfig config = config(TestConfig::PLA_default);
```
## Common Pitfalls and Solutions
### Floating-Point Comparisons
> **CRITICAL**: Never use Approx - it's deprecated due to asymmetry and other issues
**Incorrect**:
```cpp
REQUIRE(calculated_volume == expected_volume); // Exact equality
REQUIRE(calculated_volume == Catch::Approx(expected)); // Deprecated! Asymmetric!
```
**Correct**: Always use floating point matchers
```cpp
// Absolute tolerance - good when values are near zero
REQUIRE_THAT(calculated_volume, WithinAbs(expected_volume, 0.001));
// Relative tolerance - good for values with different magnitudes
REQUIRE_THAT(calculated_volume, WithinRel(expected_volume, 0.01)); // 1% tolerance
// ULP (Units in Last Place) - most precise, requires IEEE-754
REQUIRE_THAT(calculated_volume, WithinULP(expected_volume, 4));
// Combined approach - relative OR absolute
REQUIRE_THAT(calculated_volume,
WithinRel(expected_volume, 0.001) || WithinAbs(0.0, 0.000001));
// Precision control for output
Catch::StringMaker<double>::precision = 15; // Show more decimal places
```
### Why Approx is Problematic:
- **Asymmetric**: `Approx(10).epsilon(0.1) != 11.1` but `Approx(11.1).epsilon(0.1) == 10`
- **Double-only**: All computation done in `double`, causes issues with `float` inputs
- **Default behavior**: Only uses relative comparison, so `Approx(0) == X` only works for `X == 0`
### Path Handling
**Incorrect**:
```cpp
std::string path = TEST_DATA_DIR + "/model.obj"; // May have path separator issues
```
**Correct**:
```cpp
std::string path = std::string(TEST_DATA_DIR) + "/model.obj";
// or use boost::filesystem for complex path operations
```
### Exception Testing
**Incorrect**:
```cpp
bool threw_exception = false;
try {
risky_function();
} catch (...) {
threw_exception = true;
}
REQUIRE(threw_exception);
```
**Correct**:
```cpp
REQUIRE_THROWS(risky_function());
// or for specific exceptions
REQUIRE_THROWS_AS(risky_function(), SpecificException);
```
### Thread Safety
⚠️ **CRITICAL**: Catch2 assertions are **NOT thread-safe** by default!
> **Note**: Catch2 v3.9.0+ has opt-in thread-safe assertions via `CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS`. OrcaSlicer is on v3.11.0 but does not enable this flag, so assertions remain non-thread-safe by default.
**Incorrect**: Will cause undefined behavior or crashes
```cpp
std::thread t([&]() {
REQUIRE(threaded_operation() == expected); // NOT THREAD-SAFE!
CHECK(other_operation()); // NOT THREAD-SAFE!
});
```
**Correct**: Collect results, assert on main thread
```cpp
std::atomic<bool> success{false};
std::atomic<int> error_count{0};
std::thread t([&]() {
// Do work in thread, collect results
bool result1 = (threaded_operation() == expected);
bool result2 = other_operation();
if (result1 && result2) {
success = true;
} else {
error_count++;
}
});
t.join();
// Assert results on main thread
REQUIRE(success);
REQUIRE(error_count == 0);
```
#### Thread Safety Rules:
- **REQUIRE family**: Would terminate process in spawned threads (throws exception with no try-catch)
- **CHECK family**: Not thread-safe, can corrupt internal state
- **SKIP, FAIL, SUCCEED**: Not thread-safe even with v3 thread-safe assertions
- **Message macros**: INFO, CAPTURE, WARN - not thread-safe
- **STATIC_REQUIRE/CHECK**: Not thread-safe (relies on runtime registration)
### Memory Management
Use RAII and smart pointers in tests:
```cpp
TEST_CASE("Resource management", "[Memory]") {
auto model = std::make_unique<Model>();
// Automatic cleanup on test completion/failure
REQUIRE(model->objects.empty());
}
```
## Performance Considerations
### Compilation Optimizations
```cpp
// In CMakeLists.txt or as preprocessor definition
#define CATCH_CONFIG_FAST_COMPILE // 20% faster compilation, disables some features
// For faster test iteration during development
#define CATCH_CONFIG_DISABLE_STRINGIFICATION // Workaround for VS2017 raw string bug
```
### Runtime Performance
```cpp
TEST_CASE("Performance-sensitive test", "[Performance]") {
// Manual timing example (Catch2's built-in BENCHMARK macro is also available)
auto start = std::chrono::high_resolution_clock::now();
auto result = expensive_operation();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
REQUIRE(result.is_valid());
REQUIRE(duration.count() < 1000); // Should complete in < 1 second
INFO("Operation took " << duration.count() << "ms");
}
```
### Memory Leak Detection
```cpp
// For Windows builds - detects memory leaks
#define CATCH_CONFIG_WINDOWS_CRTDBG // Must be defined for whole build
```
## Integration with CMake
### Adding New Test Files
1. Create test file: `test_new_feature.cpp`
2. Add to appropriate `CMakeLists.txt`:
```cmake
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_existing_feature.cpp
test_new_feature.cpp # Add here
)
```
### Advanced Test Discovery
```cmake
# Basic test discovery
catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
# Advanced test discovery with customization
catch_discover_tests(${_TEST_NAME}_tests
TEST_PREFIX "${_TEST_NAME}: "
TEST_SUFFIX " (auto)"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
EXTRA_ARGS --order rand --warn NoAssertions
PROPERTIES
TIMEOUT 300
LABELS "unit;core"
DISCOVERY_MODE PRE_TEST # or POST_BUILD
REPORTER junit
OUTPUT_DIR ${CMAKE_BINARY_DIR}/test-results
OUTPUT_PREFIX "results_"
OUTPUT_SUFFIX ".xml"
)
# Test sharding for parallel execution
include(CatchShardTests) # If available
catch_shard_tests(${_TEST_NAME}_tests
SHARD_COUNT 4
TEST_PREFIX "${_TEST_NAME}_shard: "
)
```
### Conditional Test Compilation
```cmake
# Feature-dependent tests
if (TARGET OpenVDB::openvdb)
target_sources(${_TEST_NAME}_tests PRIVATE test_hollowing.cpp)
endif()
# Platform-specific tests
if(WIN32)
target_sources(${_TEST_NAME}_tests PRIVATE test_windows_specific.cpp)
elseif(UNIX)
target_sources(${_TEST_NAME}_tests PRIVATE test_unix_specific.cpp)
endif()
# Compiler-specific workarounds
if(MSVC)
target_compile_definitions(${_TEST_NAME}_tests PRIVATE CATCH_CONFIG_DISABLE_STRINGIFICATION)
endif()
# Fast compile mode for development
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${_TEST_NAME}_tests PRIVATE CATCH_CONFIG_FAST_COMPILE)
endif()
```
## Known Issues and Workarounds
### Platform-Specific Issues
```cpp
// MinGW/CygWin slow linking workaround
// Use: -fuse-ld=lld flag to speed up linking significantly
// Visual Studio 2017 raw string literal bug
#define CATCH_CONFIG_DISABLE_STRINGIFICATION
// This disables expression stringification but works around the compiler bug
// Visual Studio 2022 spaceship operator issue
// REQUIRE((a <=> b) == 0); // May not compile with MSVC
// Workaround: use clang-cl or avoid spaceship in assertions
// QNX/VxWorks C stdlib issues
#include <cfoo> // Use C++ headers
std::foo_function(); // Always call qualified
// NOT: #include <foo.h> and foo_function();
```
### Catch2 v3 Features Available
```cpp
// OrcaSlicer is on Catch2 v3.11.0 - all of these ARE available:
// SKIP() macro - v3.3.0+
// Opt-in thread-safe assertions - v3.9.0+ (NOT enabled here; see Thread Safety)
// Built-in BENCHMARK / BENCHMARK_ADVANCED - v3.x
// testCasePartial events - v3.0.1+
// Multiple reporters simultaneously - v3.0.1+
// STATIC_CHECK macro - v3.0.1+
// Built-in test sharding (--shard-*) - v3.x
// v3 notes to remember:
// - String matcher is "ContainsSubstring" (v2's "Contains" no longer exists)
// - Sections can still be re-run if a later section fails (unchanged from v2)
```
### Test Organization Best Practices
#### Project Structure Rules
1. **1:1 correspondence**: One test binary per library/module
2. **Hidden tests**: Use `[.]` or `[!benchmark]` tags for tests that shouldn't run by default
3. **Tag hierarchy**: Use consistent tagging scheme across the project
4. **File naming**: Follow `test_<feature>.cpp` pattern
#### CI/CD Integration
```bash
# Essential CI test command
./tests --order rand --warn NoAssertions --reporter junit::out=results.xml
# For coverage analysis
./tests --order rand --warn NoAssertions --reporter console --success
# For performance tracking
./tests --order rand --warn NoAssertions --durations yes
```
This comprehensive guide ensures robust, maintainable, and efficient testing practices for OrcaSlicer development with Claude Code, incorporating all critical knowledge from the official Catch2 documentation.
@AGENTS.md

8
tests/README.md Normal file
View File

@@ -0,0 +1,8 @@
# OrcaSlicer tests
Building, running and writing tests is documented on the wiki, under [How to Test](https://www.orcaslicer.com/wiki/developer_reference/how_to_test.html).
Two files here rather than there, because coding agents only read what is in the repository:
- [AGENTS.md](AGENTS.md) is the same guidance in short form, and is what an agent working under `tests/` picks up.
- [CATCH2.md](CATCH2.md) is the Catch2 reference, including the mistakes that break a test at runtime.

View File

@@ -1,93 +0,0 @@
# fff_print test suite
Component- and pipeline-level tests for FFF slicing: the path from a `Model` plus config, through `Print` / `PrintObject`, to emitted G-code.
For Catch2 mechanics (assertions, generators, matchers, random ordering, thread-safety), see [../CLAUDE.md](../CLAUDE.md). This document is the organizing contract for the suite: where a test goes, and how it is named.
## Organizing principle
**One file per subsystem. A subsystem is usually a single production class (`Flow`, `PrintObject`), but may be a cohesive feature that spans several (skirt/brim lives in `Brim.cpp`, `Print.cpp`, and `GCode.cpp`). That file owns every test for the subsystem: in-memory-state assertions and emitted-G-code assertions alike.**
A test's home is decided by *what production code it exercises*, never by *how it observes the result*. A skirt test that inspects `print.skirt()` and one that greps the G-code for `; skirt` live in the same file.
If you touched code in a subsystem, its test file is where your test goes. If a subsystem has no file yet, add `test_<subsystem>.cpp` and list it in `CMakeLists.txt`.
## File ownership
### Building blocks (one class, exercised through its API)
| File | Source (`src/libslic3r/`) | Covers |
|---|---|---|
| `test_trianglemesh` | `TriangleMesh.{c,h}pp` | mesh stats, transforms, slicing, split/merge/cut |
| `test_flow` | `Flow.{c,h}pp` | extrusion width / area math |
| `test_extrusion_entity` | `ExtrusionEntity.{c,h}pp` | extrusion-collection geometry |
| `test_gcodewriter` | `GCodeWriter.{c,h}pp`, `GCode.cpp` | low-level G-code emit primitives, origin |
| `test_model` | `Model.{c,h}pp` | object / volume / instance construction |
### Slicing pipeline (build a `Print`, then assert state or G-code)
| File | Source (`src/libslic3r/`) | Covers |
|---|---|---|
| `test_printobject` | `PrintObject.cpp` | layer heights, perimeter generation |
| `test_fill` | `Fill/` | infill patterns and infill G-code |
| `test_skirt_brim` | `Brim.cpp`, `Print.cpp` | skirt/brim loop counts, grouping, brim ears, emission order |
| `test_support_material` | `Support/` | support & raft layers, contact distance |
| `test_cooling` | `GCode/CoolingBuffer.cpp` | fan control, speed-marker consumption |
| `test_multifilament` | `GCode/ToolOrdering.cpp` | per-feature and per-object filament routing |
| `test_print` | `Print.{c,h}pp` | `validate()`, solid-shell behavior, sequential printing, custom G-code & config comments, default-slice smoke |
Paths are under `src/libslic3r/`. A trailing `/` is a directory of related files; otherwise it is a single class. `{c,h}pp` means the `.cpp`/`.hpp` pair.
## Naming and tags
- **File:** `test_<subsystem>.cpp`.
- **Test name:** a plain behavioral sentence, present tense, stating the contract the test pins down. No `Subsystem:` prefix (the tag carries that).
- Good: `TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]")`
- Avoid: `TEST_CASE("Print: Skirt generation", "[Print]")`
- **Tags:**
- Exactly one **subsystem** tag, PascalCase, matching the file (`[SkirtBrim]`, `[PrintObject]`, `[Fill]`). This is the grouping / filter key.
- Optional **cross-cutting** tags for a concern that genuinely spans files (`[validate]`, `[Regression]`).
- **Status** tags: `[NotWorking]` marks a test disabled for a known, documented reason; CI excludes it via `~[NotWorking]` (it does not hide itself). Use `[.]` to hide a test from default runs entirely. Either way, say why in a one-line comment.
## Test style
Prefer a flat `TEST_CASE` per behavior, with `GENERATE` for parameterized cases and shared setup factored into helpers. The test name carries the behavior, so the BDD scaffolding is usually redundant. Reserve `SCENARIO` / `GIVEN` / `WHEN` / `THEN` for a test with genuine shared setup that branches into a few closely related variations, and never let a `SCENARIO` accumulate unrelated `WHEN`s: that grab-bag is what this contract exists to prevent (and it hides failures behind a single coarse test case).
## Robust tests
A test should fail only when the behavior it names breaks, not from unrelated changes (the "change-detector" anti-pattern). Test behavior, not incidentals, and aim for one reason to fail. Concretely:
- Don't depend on or assert defaults: set the config keys the behavior needs, and derive expected values from those inputs (a 20mm cube at 0.2mm = 100 layers), not from a default that may change.
- Assert the defining property, not an incidental value: prefer "skirt present", "at least 2 brim loops", or "ears vs none" over exact coordinates, extrusion amounts, line counts, or byte sizes.
- Compare floats with a tolerance (`WithinAbs` / `WithinRel`), never `==`.
- Match the meaningful G-code token (`; skirt`), not whole lines, whitespace, or comment wording.
- Rely on ordering only when it is the contract (as `role_sequence` does).
- Keep tests self-contained: no shared state, green under `--order rand`.
## Helpers
Reuse these instead of building a `Print` or parsing G-code by hand.
- **Global** (`tests/test_utils.hpp`, available to every suite):
- `load_model("file.obj")`: load a `TriangleMesh` from `tests/data/`.
- `ScopedTemporaryFile`: an RAII temp-file path, removed on scope exit.
- **Suite harness** (`fff_print/test_helpers.{hpp,cpp}`):
- Build and run: `init_print(...)`, `init_and_process_print(...)`, `slice(...)` (returns the G-code string), and `gcode(print)`.
- Two-cube placement: `slice_two_cubes_arranged(...)` (arranger-positioned), and `place_two_cubes_apart(...)` / `slice_two_cubes_apart(...)` (a fixed gap, not arranged).
- Meshes: `cube(size)` / `make_cube(...)` for simple shapes; the `TestMesh` enum with `mesh(...)` for named fixtures.
- G-code analysis: `layers_with_role(gcode, role)`, `max_z(gcode)`, `role_passes(gcode, role)`, `role_sequence(gcode, roles)`. Subsystem-specific checks stay local (for example `brim_count` in `test_skirt_brim`).
Promote a helper into the suite harness when it is a general test primitive (not tied to one subsystem's logic), even if only one file uses it today; keep genuinely subsystem-specific helpers local (file-static). Reuse potential, not current usage count, is the test.
## Adding a test (checklist)
1. Find the subsystem's file in the tables; create `test_<subsystem>.cpp` if missing.
2. Build the print with a harness helper; set only the config keys the behavior needs.
3. Assert the behavior, in-memory or via parsed G-code, whichever is clearest.
4. Name it as a behavioral sentence and tag it `[Subsystem]`.
5. For a bug fix, add the regression test in the owning file. Name it for the behavior it protects; the test must stand on its own without relying on an external issue or PR for meaning.
## Running
ctest --test-dir build/tests/fff_print
build/tests/fff_print/<config>/fff_print_tests --order rand "~[NotWorking]"

View File

@@ -52,6 +52,29 @@ print('ok')
} // namespace
TEST_CASE("plugin latest version uses the authoritative catalog field", "[PluginDescriptor]")
{
PluginDescriptor descriptor;
descriptor.version = "1.3.0";
descriptor.latest_version = "1.3.0";
PluginChangelog changelog;
changelog.version = "1.2.0";
descriptor.changelog.push_back(changelog);
CHECK(descriptor.latest_available_version() == "1.3.0");
}
TEST_CASE("plugin latest version falls back to the descriptor version", "[PluginDescriptor]")
{
PluginDescriptor descriptor;
descriptor.version = "1.1.0";
PluginChangelog changelog;
changelog.version = "1.0.0";
descriptor.changelog.push_back(changelog);
CHECK(descriptor.latest_available_version() == "1.1.0");
}
// Regression: update_cloud_metadata() replaces a matched entry's descriptor wholesale with the
// cloud catalog record (`entry = cloud_entry`). Configuration used to ride on the descriptor, so
// that overwrite silently wiped it and plugins fell back to their built-in defaults (found via