From a6ca87b582447d9d21c475e62db2bc4d122574c3 Mon Sep 17 00:00:00 2001 From: ZhangZheng <67276816+LuckZAE@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:26:51 +0800 Subject: [PATCH 01/28] fix(login): stop token extraction at first '?', '&' or '#' in callback URL (#741) --- src/slic3r/GUI/WebSMUserLoginDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/WebSMUserLoginDialog.cpp b/src/slic3r/GUI/WebSMUserLoginDialog.cpp index 9b23e17535b..1300b5be95b 100644 --- a/src/slic3r/GUI/WebSMUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebSMUserLoginDialog.cpp @@ -184,7 +184,7 @@ void SMUserLogin::OnNavigationRequest(wxWebViewEvent &evt) std::string token; start += std::string("token=").size(); // 跳过"token="的长度 - size_t end = tmpUrl.find("?", start); + size_t end = tmpUrl.find_first_of("?&#", start); if (end != std::string::npos) { token = tmpUrl.substr(start, end - start).ToStdString(); } else { From dba88591e3be1910f5ca78ba9d3d913b37c3b1d9 Mon Sep 17 00:00:00 2001 From: zhouzengping <44285325+zhouzengping@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:16:58 +0800 Subject: [PATCH 02/28] Fix the issue where device 1 was not mounted, resulting in no model execution of consumable data synchronization. The synchronization was set to 0 consumables and then adding new consumables triggered a crash. (#740) --- src/slic3r/GUI/Plater.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 24905270715..de319b1dff4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9249,6 +9249,10 @@ void Sidebar::show_sync_filament_dialog() std::vector syncedData = dlg.getSyncDataList(); size_t effective_size = syncedData.size(); + // The number of filaments cannot be reduced to zero. + if (effective_size == 0) + return; + size_t combo_Size = p->combos_filament.size(); if (effective_size != combo_Size) { if (effective_size > combo_Size && From 0d8f2f5b151f3f40a2e92bc66856b930c642e5b7 Mon Sep 17 00:00:00 2001 From: Kenshin627 Date: Wed, 19 Aug 2026 20:40:50 -0700 Subject: [PATCH 03/28] fix: stay on Preview when re-dropping the same G-code file (#732) * fix: stay on Preview when re-dropping the same G-code file Re-dropping the same G-code file onto the window stranded the UI on the (empty) 3D editor, so the already-loaded G-code preview looked like it had disappeared. PlaterDropTarget::OnDropFiles unconditionally switched to the 3D editor before calling load_files. On a repeat drop, Plater::load_gcode then early-returns via its same-file guard (m_last_loaded_gcode == filename && m_only_gcode) and never reaches its select_tab(tpPreview), leaving the user on the 3D editor with an empty bed. Fix in two parts: - Only force the 3D editor when not already in only-gcode mode, so a repeat G-code drop does not yank the user off the Preview tab. Model drops and the first G-code drop are unaffected (only_gcode_mode() is false for them). - After load_files, if we are in only-gcode mode, ensure the Preview tab is selected. No-op on a fresh load (load_gcode already switches), but guarantees we land on Preview when load_gcode no-ops on a repeat drop. * fix: move same-file G-code view restore into load_gcode guard Follow-up to 21a68ae945 ("fix: stay on Preview when re-dropping the same G-code file"). That commit's post-load check in PlaterDropTarget::OnDropFiles keyed off only_gcode_mode() after load_files() returned, but that flag does not mean "we just handled a G-code drop": on the 3MF "Load Geometry Only" path (open_3mf_file -> priv::load_files) nothing resets m_only_gcode, so the flag stays stale-true. Dropping a 3MF in only-gcode mode ended with priv::load_files() switching to the 3D editor to show the loaded model, only for the stale-flag check to yank the user back to the empty Preview tab. The model became invisible, and since m_only_gcode was still set, clicking Prepare offered the "will be closed before creating a new model" confirm whose new_project() then discarded the model. Restructure so each load path owns its final view: - Plater::load_gcode: split the compound early-return guard. The not-a-G-code branch still returns silently, but the same-file guard (m_last_loaded_gcode == filename && m_only_gcode) now selects the Preview tab and preview panel before returning, mirroring the normal load path (select_tab(tpPreview) + set_current_panel(preview, true) + render()). "Re-opening the already-loaded G-code leaves you looking at it" is now an invariant of load_gcode itself, so every caller (drag & drop, File > Open G-code, file association) benefits. - PlaterDropTarget::OnDropFiles: remove the post-load select_view_3D("Preview") check, which was the regression source (it also never ran for the early-returning SVG branch). The pre-load 3D-editor switch stays gated on !only_gcode_mode() to avoid flashing the empty 3D editor; its comment is updated because load_gcode's guard now does switch back to Preview. Behavior in only-gcode mode after this change: repeat-dropping the same G-code stays on Preview; dropping a different G-code lands on Preview; dropping a 3MF as geometry lands on the 3D editor with the model visible (previously bounced back to Preview); dropping an STL still shows "Cannot add models when in preview mode!" and stays on Preview. --- src/slic3r/GUI/Plater.cpp | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index de319b1dff4..3ab3e095670 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10473,9 +10473,16 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi #endif // WIN32 m_mainframe.Raise(); - m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); - if (wxGetApp().is_editor()) - m_plater.select_view_3D("3D"); + // Do not force the 3D editor before we even know what was dropped. When a + // G-code is already loaded (only-gcode mode) the user is on the Preview + // tab: load_gcode()'s same-file guard switches straight back to Preview + // on a repeat drop, and the other load paths select their own final view, + // so switching here would only flash the (empty) 3D editor. + if (!m_plater.only_gcode_mode()) { + m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); + if (wxGetApp().is_editor()) + m_plater.select_view_3D("3D"); + } // When only one .svg file is dropped on scene if (filenames.size() == 1) { @@ -18789,11 +18796,22 @@ void Plater::load_gcode(const wxString& filename) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " entry and filename: " << filename; BOOST_LOG_TRIVIAL(info) << __FUNCTION__; - if (! is_gcode_file(into_u8(filename)) - || (m_last_loaded_gcode == filename && m_only_gcode) - ) + if (! is_gcode_file(into_u8(filename))) return; + if (m_last_loaded_gcode == filename && m_only_gcode) { + // The same G-code is already loaded: reloading would be a no-op, so + // just make sure the user is looking at its preview — callers may + // have left the UI on another view (e.g. the 3D editor after a + // drag & drop). + wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + p->set_current_panel(p->preview, true); + GLCanvas3D* canvas = p->get_current_canvas3D(); + if (canvas) + canvas->render(); + return; + } + // Reject a missing / inaccessible file up front. Without this check the // code below would walk through process_file -> parse_file_raw_internal, // which used to crash on a NULL FILE* (now it just returns false), and From f5c6da5151a5ccd2258e4fffe4c27a4547c12999 Mon Sep 17 00:00:00 2001 From: lhx <48871316+fire2wind@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:51:17 +0800 Subject: [PATCH 04/28] Fix ini bug from sentry (#750) * fix empty hint bug * fix crash when slicering --- src/slic3r/GUI/DailyTips.cpp | 8 +++++--- src/slic3r/GUI/HintNotification.cpp | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/DailyTips.cpp b/src/slic3r/GUI/DailyTips.cpp index 8dcc32d03bc..3e60d1a51a2 100644 --- a/src/slic3r/GUI/DailyTips.cpp +++ b/src/slic3r/GUI/DailyTips.cpp @@ -165,9 +165,11 @@ void DailyTipsDataRenderer::render_text(const ImVec2& start_pos, const ImVec2& s imgui.text(title_line); bool is_zh = false; - for (int i = 0; i < content_lines.size() - 1; i += 2) { - if ((content_lines[i] & 0x80) && (content_lines[i + 1] & 0x80)) - is_zh = true; + if(!content_lines.empty()){ + for (int i = 0; i < content_lines.size() - 1; i += 2) { + if ((content_lines[i] & 0x80) && (content_lines[i + 1] & 0x80)) + is_zh = true; + } } if (!is_zh) { // problem in Chinese with spaces diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index fa26cd17a2f..f62c94cf5b5 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -317,8 +317,12 @@ void HintDatabase::init() } void HintDatabase::init_random_hint_id() { - srand(time(NULL)); - m_hint_id = rand() % m_loaded_hints.size(); + if (m_loaded_hints.empty()) { + m_hint_id = 0; + return; + } + srand(time(NULL)); + m_hint_id = rand() % m_loaded_hints.size(); } void HintDatabase::load_hints_from_file(const boost::filesystem::path& path) { From 4f4feb6ff35ca1c04e047fba5a95207e903c7309 Mon Sep 17 00:00:00 2001 From: zhangzhend0ng Date: Fri, 21 Aug 2026 14:20:16 +0800 Subject: [PATCH 05/28] test: upgrade test framework to Catch2 v3 and add CI pipelines (#749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: upgrade test framework to Catch2 v3 and add CI pipelines - Migrate from vendored Catch2 v2 (tests/catch2/catch.hpp) to FetchContent Catch2 v3 via cmake/catch2.cmake; update 43 test files to v3 headers - Unify test CMake: TEST_LINK_LIBS, force-include catch_main.hpp, enable sla_print tests, fix headless linking with nanosvg_impl OBJECT library - Add test-linux / sanitizer (ASan+UBSan) / coverage workflows, all manual trigger for now so they don't gate main or release branches - Add cmake/sanitizers.cmake + cmake/coverage.cmake (opt-in, default OFF); SLIC3R_ASAN forwarded to ENABLE_ASAN with deprecation warning - Add test tooling: run_tests.py, run_release_tests.bat, junit-to-html.js - Add CTestConfig.cmake and a tests aggregate target Kept out: production packaging changes (expat/NSIS) carried by the stale ci-test base; dev tooling (.clangd / .clang-tidy / compile_commands export) kept for a separate PR; GMP/MPFR link fix preserved in tests/fff_print. * fix(tests): make Catch2 v3 suites build and run on macOS The v3 upgrade commit missed three test files added later via upstream merges (#599/#702) that still include the v2 header, and passed the GNU-ld-only -Wl,--no-as-needed flag unconditionally, which Apple ld64 rejects. Test binaries also never configured libslic3r's runtime environment, so resources_dir() stayed empty (bed-temperature suites read an empty nozzle_info.json) and boost log lines polluted catch_discover_tests' stdout-based test enumeration with bogus, always-failing test entries. - Migrate test_mixed_filament_color_golden / test_profile_load_util / test_bed_temperature to - Guard -Wl,--no-as-needed with if(UNIX AND NOT APPLE) in the five test CMakeLists (Linux behaviour unchanged; macOS skips, the test_nanosvg_impl OBJECT library still links unconditionally) - Bootstrap the test runtime in tests/catch_main.hpp with two mechanisms of opposite timing requirements: a prioritised constructor(101) raises the log level before any static initialiser runs (writes only a constant-initialised enum + the boost::log singleton; MSVC has no prioritised ctors and falls back to a plain static object with unspecified ordering), and a Catch2 testRunStarting listener sets resources_dir(TEST_RESOURCES_DIR) after all static init — assigning the non-trivial std::string any earlier gets wiped by its own constructor (verified in lldb) - Drop fff_print's per-suite TestResources static initializer, which suffered exactly that static-init-order wipe - Refresh the stale TestResources reference in sanitizer-tests.yml Verified on macOS (GUI=OFF, BUILD_TESTS=ON, arm64): 356 tests register cleanly (no log-line pollution), 353/356 pass. The 3 bed-temperature failures are a pre-existing product bug, not a regression: PrintConfigDef's constructor never calls init_filament_option_keys(), so DynamicPrintConfig::set_num_filaments() is a silent no-op and multi-extruder configs collapse to one extruder. Linux CI verification still pending (workflows are manual-dispatch). --- .github/workflows/coverage.yml | 177 + .github/workflows/sanitizer-tests.yml | 253 + .github/workflows/test-linux.yml | 212 + CMakeLists.txt | 41 +- CTestConfig.cmake | 16 + cmake/catch2.cmake | 63 + cmake/coverage.cmake | 120 + cmake/modules/Catch2/Catch.cmake | 175 - cmake/modules/Catch2/CatchAddTests.cmake | 106 - .../Catch2/ParseAndAddCatchTests.cmake | 225 - cmake/sanitizers.cmake | 91 + scripts/junit-to-html.js | 236 + scripts/run_release_tests.bat | 64 + scripts/run_tests.py | 136 + tests/CLAUDE.md | 38 +- tests/CMakeLists.txt | 75 +- tests/catch2/LICENSE.txt | 23 - tests/catch2/VERSION.txt | 2 - tests/catch2/catch.hpp | 17937 ---------------- tests/catch2/catch_reporter_automake.hpp | 62 - tests/catch2/catch_reporter_tap.hpp | 253 - tests/catch2/catch_reporter_teamcity.hpp | 220 - tests/catch_main.hpp | 130 +- tests/fff_print/CMakeLists.txt | 17 +- tests/fff_print/fff_print_tests.cpp | 16 +- tests/fff_print/test_bed_temperature.cpp | 2 +- tests/fff_print/test_data.cpp | 32 +- tests/fff_print/test_data.hpp | 6 +- tests/fff_print/test_extrusion_entity.cpp | 2 +- tests/fff_print/test_fill.cpp | 6 +- tests/fff_print/test_flow.cpp | 42 +- tests/fff_print/test_gcode.cpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 34 +- tests/fff_print/test_model.cpp | 19 +- tests/fff_print/test_print.cpp | 41 +- tests/fff_print/test_printgcode.cpp | 69 +- tests/fff_print/test_printobject.cpp | 23 +- tests/fff_print/test_skirt_brim.cpp | 90 +- tests/fff_print/test_support_material.cpp | 23 +- tests/fff_print/test_trianglemesh.cpp | 6 +- tests/libnest2d/CMakeLists.txt | 14 +- tests/libnest2d/libnest2d_tests_main.cpp | 55 +- tests/libslic3r/CMakeLists.txt | 24 +- tests/libslic3r/libslic3r_tests.cpp | 8 +- tests/libslic3r/test_3mf.cpp | 16 +- tests/libslic3r/test_aabbindirect.cpp | 24 +- tests/libslic3r/test_clipper_offset.cpp | 23 +- tests/libslic3r/test_clipper_utils.cpp | 17 +- tests/libslic3r/test_config.cpp | 64 +- tests/libslic3r/test_custom_gcode.cpp | 4 +- .../test_elephant_foot_compensation.cpp | 2 +- tests/libslic3r/test_geometry.cpp | 4 +- tests/libslic3r/test_hollowing.cpp | 2 +- tests/libslic3r/test_indexed_triangle_set.cpp | 3 +- .../test_local_z_order_optimizer.cpp | 2 +- tests/libslic3r/test_marchingsquares.cpp | 16 +- tests/libslic3r/test_meshboolean.cpp | 4 +- tests/libslic3r/test_mixed_filament.cpp | 39 +- .../test_mixed_filament_color_golden.cpp | 2 +- tests/libslic3r/test_mutable_polygon.cpp | 2 +- .../libslic3r/test_mutable_priority_queue.cpp | 4 +- tests/libslic3r/test_optimizers.cpp | 2 +- tests/libslic3r/test_placeholder_parser.cpp | 65 +- tests/libslic3r/test_png_io.cpp | 6 +- tests/libslic3r/test_polygon.cpp | 2 +- tests/libslic3r/test_profile_load_util.cpp | 2 +- tests/libslic3r/test_stl.cpp | 2 +- tests/libslic3r/test_timeutils.cpp | 2 +- tests/libslic3r/test_triangle_selector.cpp | 2 +- tests/libslic3r/test_voronoi.cpp | 7 +- tests/nanosvg_impl.cpp | 21 + tests/sla_print/CMakeLists.txt | 33 +- tests/sla_print/sla_print_tests.cpp | 14 +- tests/sla_print/sla_print_tests_main.cpp | 5 + tests/sla_print/sla_raycast_tests.cpp | 15 +- tests/sla_print/sla_supptgen_tests.cpp | 2 +- tests/sla_print/sla_test_utils.cpp | 16 +- tests/sla_print/sla_test_utils.hpp | 7 +- tests/slic3rutils/CMakeLists.txt | 30 +- tests/slic3rutils/slic3rutils_tests_main.cpp | 6 +- 80 files changed, 2128 insertions(+), 19525 deletions(-) create mode 100644 .github/workflows/coverage.yml create mode 100644 .github/workflows/sanitizer-tests.yml create mode 100644 .github/workflows/test-linux.yml create mode 100644 CTestConfig.cmake create mode 100644 cmake/catch2.cmake create mode 100644 cmake/coverage.cmake delete mode 100644 cmake/modules/Catch2/Catch.cmake delete mode 100644 cmake/modules/Catch2/CatchAddTests.cmake delete mode 100644 cmake/modules/Catch2/ParseAndAddCatchTests.cmake create mode 100644 cmake/sanitizers.cmake create mode 100644 scripts/junit-to-html.js create mode 100644 scripts/run_release_tests.bat create mode 100644 scripts/run_tests.py delete mode 100644 tests/catch2/LICENSE.txt delete mode 100644 tests/catch2/VERSION.txt delete mode 100644 tests/catch2/catch.hpp delete mode 100644 tests/catch2/catch_reporter_automake.hpp delete mode 100644 tests/catch2/catch_reporter_tap.hpp delete mode 100644 tests/catch2/catch_reporter_teamcity.hpp create mode 100644 tests/nanosvg_impl.cpp diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000000..a3a770e1522 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,177 @@ +name: Code Coverage + +# Disabled by default to avoid running the coverage build on every push and PR. +# Trigger manually from the Actions tab (workflow_dispatch), or re-enable +# push/pull_request triggers when you want CI coverage again. +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + coverage: + name: Coverage (ubuntu-22.04) + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + lfs: 'true' + + - name: Restore deps tarball + id: cache_deps + uses: actions/cache@v4 + with: + # Cache a SINGLE zstd tarball instead of the multi-million-file + # destdir tree (archiving the raw OCCT/OpenCV/boost headers never + # completed, so deps rebuilt every run). A single tarball restores/ + # saves reliably; the key is repo-scoped and shared with the + # test-linux / sanitizer workflows (first to build serves the rest). + path: ${{ github.workspace }}/deps-destdir.tar.zst + key: ubuntu-22.04-deps-tarball-${{ hashFiles('deps/CMakeLists.txt', 'deps/*.cmake', 'deps/*/*.cmake', 'deps/*/*.patch', 'build_linux.sh') }} + + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.28.0" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake git g++ build-essential libgl1-mesa-dev m4 \ + libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules pkgconf \ + libglu1-mesa-dev libcairo2-dev libgtk-3-dev libsoup2.4-dev libwebkit2gtk-4.1-dev \ + libgstreamer1.0-dev libgstreamer-plugins-good1.0-dev libgstreamer-plugins-base1.0-dev \ + gstreamer1.0-plugins-bad wget sudo autoconf curl libunwind-dev texinfo ccache lcov libblosc-dev + + - name: Install dependencies from build_linux.sh + shell: bash + run: sudo ./build_linux.sh -ur + + - name: Fix permissions + shell: bash + run: sudo chown $USER -R ./ + + - name: Free disk space + shell: bash + run: | + echo "Before cleanup:" && df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/share/swift /usr/local/share/powershell /usr/local/share/chromium /usr/local/lib/node_modules 2>/dev/null || true + echo "After cleanup:" && df -h / + + - name: Setup swap (coverage builds need extra memory) + shell: bash + run: | + SWAPFILE="/swapfile-$$" + sudo swapoff "$SWAPFILE" 2>/dev/null || true + sudo rm -f "$SWAPFILE" + sudo fallocate -l 4G "$SWAPFILE" + sudo chmod 600 "$SWAPFILE" + sudo mkswap "$SWAPFILE" + sudo swapon "$SWAPFILE" + # Clean up stale swap from previous runs on reused runners + sudo swapoff /swapfile 2>/dev/null || true + sudo rm -f /swapfile + + - name: Extract deps tarball (cache hit) + if: steps.cache_deps.outputs.cache-hit == 'true' + shell: bash + run: | + mkdir -p deps/build + tar --zstd -x -f deps-destdir.tar.zst -C deps/build + rm -f deps-destdir.tar.zst + + - name: Build dependencies (cache miss) + if: steps.cache_deps.outputs.cache-hit != 'true' + shell: bash + run: | + mkdir -p deps/build + mkdir -p deps/build/destdir + ./build_linux.sh -dr + sudo chown $USER -R ./ + # Pack the freshly built destdir into a single tarball for reliable caching. + tar --zstd -c -f deps-destdir.tar.zst -C deps/build destdir + + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ubuntu-22.04-coverage + append-timestamp: false + - name: Configure CMake with coverage + shell: bash + run: cmake -S . -B build_cov -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_COVERAGE=ON -DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/deps/build/destdir/usr/local" -DSLIC3R_STATIC=ON -DOPENVDB_USE_STATIC_LIBS=ON -DSLIC3R_GUI=OFF -DBUILD_TESTS=ON -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache + + - name: Build tests + id: build-tests + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + run: | + cmake --build build_cov --target tests --parallel 1 + test -f build_cov/tests/libslic3r/libslic3r_tests \ + || test -f build_cov/tests/fff_print/fff_print_tests \ + || test -f build_cov/tests/libnest2d/libnest2d_tests \ + || { echo "ERROR: No test binaries found in build_cov/tests/"; exit 1; } + + - name: Run tests with JUnit output + if: success() + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + # -E '\[trace\]': drop ghost test names created by Catch2 v3 test + # discovery. The test binaries emit a boost::log trace line + # ("Initializing StaticPrintConfigs") to stdout during discovery; that + # line is captured as a (bogus) test name and then fails at runtime with + # "No test cases matched". The leak originates in production static-init + # logging and cannot be suppressed without touching production sources, + # so we exclude those pseudo-tests here. Real tests never contain a + # literal "[trace]" token in their names. + run: ctest --test-dir build_cov --output-on-failure --output-junit "${{ github.workspace }}/build_cov/test-results.xml" --timeout 3600 -E '\[trace\]' + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-results-coverage + path: build_cov/test-results.xml + if-no-files-found: warn + + - name: Generate coverage report + if: always() && steps.build-tests.outcome == 'success' + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + run: | + cmake --build build_cov --target coverage 2>&1 | tee build_cov/coverage-summary.txt + + - name: Upload coverage report + if: always() && steps.build-tests.outcome == 'success' + uses: actions/upload-artifact@v5 + with: + name: coverage-report + path: build_cov/coverage/ + if-no-files-found: warn + + - name: Upload coverage summary + if: always() && steps.build-tests.outcome == 'success' + uses: actions/upload-artifact@v5 + with: + name: coverage-summary + path: build_cov/coverage-summary.txt + if-no-files-found: warn + + - name: Generate HTML report + if: always() + shell: bash + run: node scripts/junit-to-html.js build_cov/test-results.xml -t "OrcaSlicer Coverage Tests" -o build_cov/test-report.html + + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-report-${{ matrix.os }} + path: build_cov/test-report.html + if-no-files-found: warn diff --git a/.github/workflows/sanitizer-tests.yml b/.github/workflows/sanitizer-tests.yml new file mode 100644 index 00000000000..3703101db45 --- /dev/null +++ b/.github/workflows/sanitizer-tests.yml @@ -0,0 +1,253 @@ +name: Sanitizer Tests (ASan + UBSan) + +# Disabled by default to avoid running the expensive ASan/UBSan matrix on every +# push and PR. Trigger manually from the Actions tab (workflow_dispatch), or +# re-enable push/pull_request triggers when you want CI coverage again. +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + sanitizers: + name: ASan+UBSan (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # ubuntu-24.04 removed: under RelWithDebInfo + ASan + UBSan the four + # print.process()-heavy fff_print tests (init_print, Extrusion width, + # Model construction, Perimeter generation) hang for the full 1h + # per-test timeout each (>6h total) on GCC 13, while passing in ~0.6s + # on 22.04's GCC 11. Re-add when switching to clang or upstream fixes + # the instrumentation slowdown. + os: [ubuntu-22.04] + env: + # detect_leaks=0: LeakSanitizer is disabled. The test binaries set + # program-lifetime globals (e.g. Slic3r::set_resources_dir() at + # src/libslic3r/utils.cpp:222, called from the TestFrameworkBootstrap + # Catch2 listener in tests/catch_main.hpp) that LSan flags as leaks. + # Those reports make catch_discover_tests' build-time test enumeration + # exit non-zero (failing the build) AND fail every test process at exit. + # They are false positives (intentional program-lifetime state), not + # real leaks. + # ASan (heap/stack memory errors) and UBSan (undefined behavior) — the + # critical detections — remain fully active. Leak detection can be + # re-enabled later with a curated LSAN_OPTIONS suppression file. + ASAN_OPTIONS: detect_leaks=0:strict_string_checks=1:detect_stack_use_after_return=1:detect_container_overflow=1:symbolize=1:halt_on_error=1 + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=0 + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + lfs: "true" + + - name: Restore deps tarball + id: cache_deps + uses: actions/cache@v4 + with: + # Cache a SINGLE zstd tarball instead of the multi-million-file + # destdir tree (archiving the raw OCCT/OpenCV/boost headers never + # completed, so deps rebuilt every run). A single tarball restores/ + # saves reliably; the key is repo-scoped and shared with the + # test-linux / coverage workflows (first to build serves the rest). + path: ${{ github.workspace }}/deps-destdir.tar.zst + key: ${{ matrix.os }}-deps-tarball-${{ hashFiles('deps/CMakeLists.txt', 'deps/*.cmake', 'deps/*/*.cmake', 'deps/*/*.patch', 'build_linux.sh') }} + + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.28.0" + + - name: Setup swap (sanitizer builds need extra memory) + shell: bash + run: | + SWAPFILE="/swapfile-$$" + sudo swapoff "$SWAPFILE" 2>/dev/null || true + sudo rm -f "$SWAPFILE" + sudo fallocate -l 8G "$SWAPFILE" + sudo chmod 600 "$SWAPFILE" + sudo mkswap "$SWAPFILE" + sudo swapon "$SWAPFILE" + # Clean up stale swap from previous runs on reused runners + sudo swapoff /swapfile 2>/dev/null || true + sudo rm -f /swapfile + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake git g++ build-essential libgl1-mesa-dev m4 \ + libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules pkgconf \ + libglu1-mesa-dev libcairo2-dev libgtk-3-dev libsoup2.4-dev libwebkit2gtk-4.1-dev \ + libgstreamer1.0-dev libgstreamer-plugins-good1.0-dev libgstreamer-plugins-base1.0-dev \ + gstreamer1.0-plugins-bad wget sudo autoconf curl libunwind-dev texinfo llvm ccache libblosc-dev + + - name: Resolve llvm-symbolizer path + # ASan/UBSan need llvm-symbolizer to produce human-readable stack + # traces. Resolve it dynamically instead of hardcoding /usr/bin/..., + # which may not match the installed llvm package layout. + shell: bash + run: | + SYM="$(command -v llvm-symbolizer || true)" + [ -n "$SYM" ] || SYM="/usr/bin/llvm-symbolizer" + echo "ASAN_SYMBOLIZER_PATH=$SYM" >> "$GITHUB_ENV" + echo "UBSAN_SYMBOLIZER_PATH=$SYM" >> "$GITHUB_ENV" + echo "Resolved llvm-symbolizer: $SYM" + + - name: Install dependencies from build_linux.sh + shell: bash + run: sudo ./build_linux.sh -ur + + - name: Fix permissions + shell: bash + run: sudo chown $USER -R ./ + + - name: Free disk space + shell: bash + run: | + echo "Before cleanup:" && df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/share/swift /usr/local/share/powershell /usr/local/share/chromium /usr/local/lib/node_modules 2>/dev/null || true + echo "After cleanup:" && df -h / + + - name: Extract deps tarball (cache hit) + if: steps.cache_deps.outputs.cache-hit == 'true' + shell: bash + run: | + mkdir -p deps/build + tar --zstd -x -f deps-destdir.tar.zst -C deps/build + rm -f deps-destdir.tar.zst + + - name: Build dependencies (cache miss) + if: steps.cache_deps.outputs.cache-hit != 'true' + shell: bash + run: | + mkdir -p deps/build + mkdir -p deps/build/destdir + ./build_linux.sh -dr + sudo chown $USER -R ./ + # Pack the freshly built destdir into a single tarball for reliable caching. + tar --zstd -c -f deps-destdir.tar.zst -C deps/build destdir + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-sanitizers + append-timestamp: false + restore-keys: | + ${{ matrix.os }}-release + + - name: Configure CMake with ASan+UBSan + shell: bash + run: cmake -S . -B build_san -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_ASAN=ON -DENABLE_UBSAN=ON -DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/deps/build/destdir/usr/local" -DSLIC3R_STATIC=ON -DOPENVDB_USE_STATIC_LIBS=ON -DSLIC3R_GUI=OFF -DBUILD_TESTS=ON -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache + + - name: Build tests + id: build-tests + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + run: | + cmake --build build_san --target tests --parallel $(nproc) + test -f build_san/tests/libslic3r/libslic3r_tests \ + || test -f build_san/tests/fff_print/fff_print_tests \ + || test -f build_san/tests/libnest2d/libnest2d_tests \ + || { echo "ERROR: No test binaries found in build_san/tests/"; exit 1; } + + - name: Detect changed modules + id: changed + uses: tj-actions/changed-files@v46 + with: + files_yaml: | + core: + - src/libslic3r/** + gui: + - src/slic3r/** + cmake: + - "**/CMakeLists.txt" + - cmake/** + test_infra: + - tests/*.hpp + - tests/*.cpp + - tests/data/** + test_libslic3r: + - tests/libslic3r/** + test_fff_print: + - tests/fff_print/** + test_sla_print: + - tests/sla_print/** + test_slic3rutils: + - tests/slic3rutils/** + test_libnest2d: + - tests/libnest2d/** + ci_config: + - .github/workflows/** + + - name: Determine test filter + id: filter + shell: bash + run: | + if [ "${{ steps.changed.outputs.core_any_changed }}" = "true" ] || \ + [ "${{ steps.changed.outputs.cmake_any_changed }}" = "true" ] || \ + [ "${{ steps.changed.outputs.test_infra_any_changed }}" = "true" ] || \ + [ "${{ steps.changed.outputs.ci_config_any_changed }}" = "true" ]; then + echo "filter=" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.gui_any_changed }}" = "true" ]; then + echo "filter=slic3rutils" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_libslic3r_any_changed }}" = "true" ]; then + echo "filter=libslic3r" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_fff_print_any_changed }}" = "true" ]; then + echo "filter=fff_print" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_sla_print_any_changed }}" = "true" ]; then + echo "filter=sla_print" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_slic3rutils_any_changed }}" = "true" ]; then + echo "filter=slic3rutils" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_libnest2d_any_changed }}" = "true" ]; then + echo "filter=libnest2d" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "filter=__SKIP__" >> "$GITHUB_OUTPUT" + + - name: Run tests + if: success() && steps.filter.outputs.filter != '__SKIP__' + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + run: | + if [ -n "${{ steps.filter.outputs.filter }}" ]; then + ctest --test-dir build_san --output-on-failure --output-junit "${{ github.workspace }}/build_san/test-results.xml" --timeout 3600 -R "${{ steps.filter.outputs.filter }}" -E '\[trace\]' + else + ctest --test-dir build_san --output-on-failure --output-junit "${{ github.workspace }}/build_san/test-results.xml" --timeout 3600 -E '\[trace\]' + fi + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-results-sanitizer-${{ matrix.os }} + path: build_san/test-results.xml + if-no-files-found: warn + + - name: Generate HTML report + if: always() + shell: bash + run: node scripts/junit-to-html.js build_san/test-results.xml -t "OrcaSlicer Sanitizer Tests" -o build_san/test-report.html + + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-report-${{ matrix.os }} + path: build_san/test-report.html + if-no-files-found: warn diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml new file mode 100644 index 00000000000..eb7969b6ae7 --- /dev/null +++ b/.github/workflows/test-linux.yml @@ -0,0 +1,212 @@ +name: Linux Tests + +# Disabled by default: only manual trigger (Actions tab -> Run workflow). +# Re-enable push/pull_request triggers once the test pipeline is stable and +# green on main, and keep the release/* branches off the required-checks path. +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test_linux: + name: Build & Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + lfs: 'true' + + - name: Restore deps tarball + id: cache_deps + uses: actions/cache@v4 + with: + # Cache a SINGLE zstd tarball instead of the multi-million-file + # destdir tree. Archiving the raw tree (OCCT/OpenCV/boost headers) + # never completed in actions/cache, so deps rebuilt on every run. + # A single tarball restores/saves reliably. The key is repo-scoped, + # so all three test workflows share it (the first to build serves + # the others); build_linux.sh is in the key so a build-script change + # busts the cache. + path: ${{ github.workspace }}/deps-destdir.tar.zst + key: ${{ matrix.os }}-deps-tarball-${{ hashFiles('deps/CMakeLists.txt', 'deps/*.cmake', 'deps/*/*.cmake', 'deps/*/*.patch', 'build_linux.sh') }} + + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.28.0" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake git g++ build-essential libgl1-mesa-dev m4 \ + libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules pkgconf \ + libglu1-mesa-dev libcairo2-dev libgtk-3-dev libsoup2.4-dev libwebkit2gtk-4.1-dev \ + libgstreamer1.0-dev libgstreamer-plugins-good1.0-dev libgstreamer-plugins-base1.0-dev \ + gstreamer1.0-plugins-bad wget sudo autoconf curl libunwind-dev texinfo libblosc-dev ccache + + - name: Install dependencies from build_linux.sh + shell: bash + run: sudo ./build_linux.sh -ur + + - name: Fix permissions + shell: bash + run: sudo chown $USER -R ./ + + - name: Free disk space + shell: bash + run: | + echo "Before cleanup:" && df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /usr/share/swift /usr/local/share/powershell /usr/local/share/chromium /usr/local/lib/node_modules 2>/dev/null || true + echo "After cleanup:" && df -h / + + - name: Extract deps tarball (cache hit) + if: steps.cache_deps.outputs.cache-hit == 'true' + shell: bash + run: | + mkdir -p deps/build + tar --zstd -x -f deps-destdir.tar.zst -C deps/build + rm -f deps-destdir.tar.zst + + - name: Build dependencies (cache miss) + if: steps.cache_deps.outputs.cache-hit != 'true' + shell: bash + run: | + mkdir -p deps/build + mkdir -p deps/build/destdir + ./build_linux.sh -dr + sudo chown $USER -R ./ + # Pack the freshly built destdir into a single tarball for reliable caching. + tar --zstd -c -f deps-destdir.tar.zst -C deps/build destdir + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-release + append-timestamp: false + + - name: Configure CMake + shell: bash + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/deps/build/destdir/usr/local" -DSLIC3R_STATIC=ON -DOPENVDB_USE_STATIC_LIBS=ON -DSLIC3R_GUI=OFF -DBUILD_TESTS=ON -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache + + - name: Build tests + id: build-tests + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + run: | + cmake --build build --target tests --parallel 1 + test -f build/tests/libslic3r/libslic3r_tests \ + || test -f build/tests/fff_print/fff_print_tests \ + || test -f build/tests/libnest2d/libnest2d_tests \ + || { echo "ERROR: No test binaries found in build/tests/"; exit 1; } + + - name: Detect changed modules + id: changed + uses: tj-actions/changed-files@v46 + with: + files_yaml: | + core: + - src/libslic3r/** + gui: + - src/slic3r/** + cmake: + - '**/CMakeLists.txt' + - cmake/** + test_infra: + - tests/*.hpp + - tests/*.cpp + - tests/data/** + test_libslic3r: + - tests/libslic3r/** + test_fff_print: + - tests/fff_print/** + test_sla_print: + - tests/sla_print/** + test_slic3rutils: + - tests/slic3rutils/** + test_libnest2d: + - tests/libnest2d/** + ci_config: + - .github/workflows/** + + - name: Determine test filter + id: filter + shell: bash + run: | + if [ "${{ steps.changed.outputs.core_any_changed }}" = 'true' ] || \ + [ "${{ steps.changed.outputs.cmake_any_changed }}" = 'true' ] || \ + [ "${{ steps.changed.outputs.test_infra_any_changed }}" = 'true' ] || \ + [ "${{ steps.changed.outputs.ci_config_any_changed }}" = 'true' ]; then + echo "filter=" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.gui_any_changed }}" = 'true' ]; then + echo "filter=slic3rutils" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_libslic3r_any_changed }}" = 'true' ]; then + echo "filter=libslic3r" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_fff_print_any_changed }}" = 'true' ]; then + echo "filter=fff_print" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_sla_print_any_changed }}" = 'true' ]; then + echo "filter=sla_print" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_slic3rutils_any_changed }}" = 'true' ]; then + echo "filter=slic3rutils" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.changed.outputs.test_libnest2d_any_changed }}" = 'true' ]; then + echo "filter=libnest2d" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "filter=__SKIP__" >> "$GITHUB_OUTPUT" + + - name: Run tests + if: success() && steps.filter.outputs.filter != '__SKIP__' + shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/deps/build/destdir/usr/local/lib:$LD_LIBRARY_PATH + run: | + if [ -n "${{ steps.filter.outputs.filter }}" ]; then + echo "Running filtered tests: -R ${{ steps.filter.outputs.filter }}" + ctest --test-dir build --output-on-failure --output-junit "${{ github.workspace }}/build/test-results.xml" --timeout 3600 -R "${{ steps.filter.outputs.filter }}" -E '\[trace\]' + else + echo "Running all tests" + ctest --test-dir build --output-on-failure --output-junit "${{ github.workspace }}/build/test-results.xml" --timeout 3600 -E '\[trace\]' + fi + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-results-${{ matrix.os }} + path: build/test-results.xml + if-no-files-found: warn + + - name: Generate HTML report + if: always() + shell: bash + run: node scripts/junit-to-html.js build/test-results.xml -t "OrcaSlicer Linux Tests" -o build/test-report.html + + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v5 + with: + name: test-report-${{ matrix.os }} + path: build/test-report.html + if-no-files-found: warn diff --git a/CMakeLists.txt b/CMakeLists.txt index 6755b4d2e6b..dd901e050f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,7 +107,14 @@ option(SLIC3R_PCH "Use precompiled headers" 1) option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1) option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1) option(SLIC3R_PERL_XS "Compile XS Perl module and enable Perl unit and integration tests" 0) -option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0) + +# ── Sanitizers (legacy compat) ────────────────────────────────────────────── +# ENABLE_ASAN / ENABLE_UBSAN are declared in cmake/sanitizers.cmake. +# SLIC3R_ASAN is the legacy name; forward with a deprecation warning. +if(DEFINED SLIC3R_ASAN) + message(WARNING "SLIC3R_ASAN is deprecated — use ENABLE_ASAN instead") + set(ENABLE_ASAN ${SLIC3R_ASAN} CACHE BOOL "Enable AddressSanitizer (ASan)" FORCE) +endif() # Sentry crash reporting - enabled only on Windows by default if (WIN32 OR APPLE) @@ -391,24 +398,11 @@ if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMP endif() -if (SLIC3R_ASAN) - # ASAN should be available on MSVC starting with Visual Studio 2019 16.9 - # https://devblogs.microsoft.com/cppblog/address-sanitizer-for-msvc-now-generally-available/ - add_compile_options(-fsanitize=address) +# ── Sanitizers (ASan / UBSan) ────────────────────────────────────────────── +include(cmake/sanitizers.cmake) - if (NOT MSVC) - add_compile_options(-fno-omit-frame-pointer) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address") - set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fsanitize=address") - else() - add_compile_definitions(_DISABLE_STRING_ANNOTATION=1 _DISABLE_VECTOR_ANNOTATION=1) - endif () - - if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lasan") - endif () -endif () +# ── Code Coverage ─────────────────────────────────────────────────────────── +include(cmake/coverage.cmake) if (APPLE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=partial-availability -Werror=unguarded-availability -Werror=unguarded-availability-new") @@ -893,6 +887,17 @@ if(BUILD_TESTS) add_subdirectory(tests) endif() +if(BUILD_TESTS) + if(NOT TARGET tests) + add_custom_target(tests) + endif() + foreach(_t IN ITEMS libnest2d_tests libslic3r_tests slic3rutils_tests fff_print_tests sla_print_tests) + if(TARGET ${_t}) + add_dependencies(tests ${_t}) + endif() + endforeach() +endif() + if (NOT WIN32 AND NOT APPLE) set(SLIC3R_APP_CMD "snapmaker-orca") configure_file(${LIBDIR}/dev-utils/platform/unix/build_appimage.sh.in ${CMAKE_CURRENT_BINARY_DIR}/build_appimage.sh USE_SOURCE_PERMISSIONS @ONLY) diff --git a/CTestConfig.cmake b/CTestConfig.cmake new file mode 100644 index 00000000000..328f381f77a --- /dev/null +++ b/CTestConfig.cmake @@ -0,0 +1,16 @@ +# CTestConfig.cmake — Dashboard and reporting configuration +# +# Enables ctest XML output for CI dashboards. +# Usage: +# ctest --test-dir build --output-junit build/test-results.xml +# ctest --test-dir build -D Experimental + +set(CTEST_PROJECT_NAME "Snapmaker_Orca") +set(CTEST_NIGHTLY_START_TIME "03:00:00 UTC") + +# Output settings +set(CTEST_OUTPUT_ON_FAILURE ON) + +# Coverage settings (used with ENABLE_COVERAGE=ON) +set(CTEST_COVERAGE_COMMAND "gcov") +set(CTEST_COVERAGE_EXTRA_FLAGS "--preserve-paths --relative-only") diff --git a/cmake/catch2.cmake b/cmake/catch2.cmake new file mode 100644 index 00000000000..0551f60f157 --- /dev/null +++ b/cmake/catch2.cmake @@ -0,0 +1,63 @@ +# cmake/catch2.cmake — FetchContent configuration for Catch2 v3 +# +# This module downloads Catch2 v3.x at configure time and makes the following +# targets available: +# Catch2::Catch2 — header-only library (no main) +# Catch2::Catch2WithMain — header-only library with main() +# +# It also provides the catch_discover_tests() CMake function for CTest +# integration, replacing the old vendored cmake/modules/Catch2/ scripts. +# +# Usage (from tests/CMakeLists.txt): +# include(../cmake/catch2.cmake) +# target_link_libraries(test_common INTERFACE Catch2::Catch2) +# +# Offline / air-gapped builds +# --------------------------- +# The clone happens once at configure time (only when BUILD_TESTS=ON) and is +# cached under /_deps/catch2-*. Fresh build directories need +# network access unless one of these built-in FetchContent overrides is used: +# - Point FetchContent at a local checkout instead of downloading: +# cmake -DFETCHCONTENT_SOURCE_DIR_CATCH2=/path/to/Catch2 ... +# - Reuse previously downloaded content and skip the git update entirely: +# cmake -DFETCHCONTENT_FULLY_DISCONNECTED=ON ... + +include(FetchContent) + +# Prevent Catch2 from building its own tests, examples, and benchmarks +set(CATCH_BUILD_TESTING OFF CACHE INTERNAL "") +set(CATCH_BUILD_EXAMPLES OFF CACHE INTERNAL "") +set(CATCH_BUILD_EXTRA_TESTS OFF CACHE INTERNAL "") +set(CATCH_INSTALL_DOCS OFF CACHE INTERNAL "") +set(CATCH_INSTALL_HELPERS OFF CACHE INTERNAL "") + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + # Pinned by full commit hash, not the (mutable) tag: tags can be deleted or + # retargeted upstream, hashes cannot. This is the exact commit of the + # v3.7.1 release the test suites were verified against. To bump the + # version, resolve the new commit with: + # git ls-remote https://github.com/catchorg/Catch2.git "refs/tags/vX.Y.Z^{}" + GIT_TAG fa43b77429ba76c462b1898d6cd2f2d7a9416b14 # v3.7.1 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) + +# FetchContent_MakeAvailable is CMake 3.14+; use manual populate for 3.13 compat +FetchContent_GetProperties(Catch2) +if(NOT Catch2_POPULATED) + FetchContent_Populate(Catch2) + add_subdirectory( + ${catch2_SOURCE_DIR} ${catch2_BINARY_DIR} + EXCLUDE_FROM_ALL + ) +endif() + +include(${catch2_SOURCE_DIR}/extras/Catch.cmake) + +if(WIN32 AND TARGET Catch2WithMain) + target_compile_definitions(Catch2WithMain PRIVATE DO_NOT_USE_WMAIN) +endif() + +message(STATUS "Catch2 v3: using FetchContent (${catch2_SOURCE_DIR})") diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake new file mode 100644 index 00000000000..d2a9960a085 --- /dev/null +++ b/cmake/coverage.cmake @@ -0,0 +1,120 @@ +# cmake/coverage.cmake — Code coverage with gcov/lcov (GCC/Clang) +# +# Provides ENABLE_COVERAGE option and a 'coverage' target. +# +# Usage: +# include(cmake/coverage.cmake) +# cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON +# cmake --build build --target tests +# ctest --test-dir build +# cmake --build build --target coverage +# +# On MSVC: coverage is not supported; enabling emits a warning and is a no-op. + +option(ENABLE_COVERAGE "Enable code coverage instrumentation (GCC/Clang only)" OFF) + +if(NOT ENABLE_COVERAGE) + return() +endif() + +# ── MSVC ──────────────────────────────────────────────────────────────────────── +if(MSVC) + message(WARNING "Code coverage (ENABLE_COVERAGE) is not supported on MSVC — ignored") + return() +endif() + +# ── GCC / Clang ───────────────────────────────────────────────────────────────── +message(STATUS "Code coverage (gcov/lcov) enabled") + +# Compiler and linker flags for coverage instrumentation +add_compile_options(--coverage) +add_link_options(--coverage) + +# Find required tools +find_program(LCOV_EXECUTABLE lcov) +find_program(GENHTML_EXECUTABLE genhtml) + +# Auto-detect the correct gcov for the active compiler. +# GCC → system gcov; Clang → llvm-cov gcov (or gcov if present). +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + find_program(GCOV_EXECUTABLE NAMES "llvm-cov") + if(GCOV_EXECUTABLE) + set(GCOV_TOOL "${GCOV_EXECUTABLE} gcov") + else() + set(GCOV_TOOL "gcov") + endif() +else() + set(GCOV_TOOL "gcov") +endif() + +if(NOT LCOV_EXECUTABLE) + message(WARNING "lcov not found — 'coverage' target will not be available.") + message(WARNING "Install with: sudo apt-get install lcov") +endif() + +if(NOT GENHTML_EXECUTABLE) + message(WARNING "genhtml not found — 'coverage' target will not be available.") + message(WARNING "Install with: sudo apt-get install lcov") +endif() + +# ── Coverage target ───────────────────────────────────────────────────────────── +if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE) + set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage") + set(COVERAGE_INFO "${COVERAGE_DIR}/coverage.info") + + add_custom_target(coverage + + # Clean previous run + COMMAND ${CMAKE_COMMAND} -E remove_directory ${COVERAGE_DIR} + COMMAND ${CMAKE_COMMAND} -E make_directory ${COVERAGE_DIR} + + # Reset counters (in case of incremental builds) + COMMAND ${LCOV_EXECUTABLE} --directory . --zerocounters --quiet + + # Run tests (re-run to collect fresh coverage data) + # -E \[trace\]: skip ghost test names created by Catch2 v3 discovery + # stdout pollution (see .github/workflows/coverage.yml "Run tests" step). + # VERBATIM ensures the regex is properly quoted in generated Makefile + # rules (without it, the backslashes are consumed by the shell). + COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} --output-on-failure -E "\\[trace\\]" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + VERBATIM + + # Capture coverage data + COMMAND ${LCOV_EXECUTABLE} + --directory . + --capture + --output-file ${COVERAGE_INFO}.raw + --gcov-tool ${GCOV_TOOL} + --quiet + + # Strip system headers and third-party deps + COMMAND ${LCOV_EXECUTABLE} + --remove ${COVERAGE_INFO}.raw + '/usr/*' + '*/deps/*' + '*/deps_src/*' + '*/build/*' + '*/tests/*' + --output-file ${COVERAGE_INFO} + --quiet + + # Generate HTML report + COMMAND ${GENHTML_EXECUTABLE} + ${COVERAGE_INFO} + --output-directory ${COVERAGE_DIR} + --title "${PROJECT_NAME} Coverage" + --legend + --show-details + --quiet + + # Print summary + COMMAND ${LCOV_EXECUTABLE} + --list ${COVERAGE_INFO} + --quiet + + COMMENT "Code coverage report: ${COVERAGE_DIR}/index.html" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + USES_TERMINAL + ) +endif() diff --git a/cmake/modules/Catch2/Catch.cmake b/cmake/modules/Catch2/Catch.cmake deleted file mode 100644 index 0ffe978dc59..00000000000 --- a/cmake/modules/Catch2/Catch.cmake +++ /dev/null @@ -1,175 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -#[=======================================================================[.rst: -Catch ------ - -This module defines a function to help use the Catch test framework. - -The :command:`catch_discover_tests` discovers tests by asking the compiled test -executable to enumerate its tests. This does not require CMake to be re-run -when tests change. However, it may not work in a cross-compiling environment, -and setting test properties is less convenient. - -This command is intended to replace use of :command:`add_test` to register -tests, and will create a separate CTest test for each Catch test case. Note -that this is in some cases less efficient, as common set-up and tear-down logic -cannot be shared by multiple test cases executing in the same instance. -However, it provides more fine-grained pass/fail information to CTest, which is -usually considered as more beneficial. By default, the CTest test name is the -same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. - -.. command:: catch_discover_tests - - Automatically add tests with CTest by querying the compiled test executable - for available tests:: - - catch_discover_tests(target - [TEST_SPEC arg1...] - [EXTRA_ARGS arg1...] - [WORKING_DIRECTORY dir] - [TEST_PREFIX prefix] - [TEST_SUFFIX suffix] - [PROPERTIES name1 value1...] - [TEST_LIST var] - ) - - ``catch_discover_tests`` sets up a post-build command on the test executable - that generates the list of tests by parsing the output from running the test - with the ``--list-test-names-only`` argument. This ensures that the full - list of tests is obtained. Since test discovery occurs at build time, it is - not necessary to re-run CMake when the list of tests changes. - However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set - in order to function in a cross-compiling environment. - - Additionally, setting properties on tests is somewhat less convenient, since - the tests are not available at CMake time. Additional test properties may be - assigned to the set of tests as a whole using the ``PROPERTIES`` option. If - more fine-grained test control is needed, custom content may be provided - through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` - directory property. The set of discovered tests is made accessible to such a - script via the ``_TESTS`` variable. - - The options are: - - ``target`` - Specifies the Catch executable, which must be a known CMake executable - target. CMake will substitute the location of the built executable when - running the test. - - ``TEST_SPEC arg1...`` - Specifies test cases, wildcarded test cases, tags and tag expressions to - pass to the Catch executable with the ``--list-test-names-only`` argument. - - ``EXTRA_ARGS arg1...`` - Any extra arguments to pass on the command line to each test case. - - ``WORKING_DIRECTORY dir`` - Specifies the directory in which to run the discovered test cases. If this - option is not provided, the current binary directory is used. - - ``TEST_PREFIX prefix`` - Specifies a ``prefix`` to be prepended to the name of each discovered test - case. This can be useful when the same test executable is being used in - multiple calls to ``catch_discover_tests()`` but with different - ``TEST_SPEC`` or ``EXTRA_ARGS``. - - ``TEST_SUFFIX suffix`` - Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of - every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may - be specified. - - ``PROPERTIES name1 value1...`` - Specifies additional properties to be set on all tests discovered by this - invocation of ``catch_discover_tests``. - - ``TEST_LIST var`` - Make the list of tests available in the variable ``var``, rather than the - default ``_TESTS``. This can be useful when the same test - executable is being used in multiple calls to ``catch_discover_tests()``. - Note that this variable is only available in CTest. - -#]=======================================================================] - -#------------------------------------------------------------------------------ -function(catch_discover_tests TARGET) - cmake_parse_arguments( - "" - "" - "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST" - "TEST_SPEC;EXTRA_ARGS;PROPERTIES" - ${ARGN} - ) - - if(NOT _WORKING_DIRECTORY) - set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") - endif() - if(NOT _TEST_LIST) - set(_TEST_LIST ${TARGET}_TESTS) - endif() - - ## Generate a unique name based on the extra arguments - string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}") - string(SUBSTRING ${args_hash} 0 7 args_hash) - - # Define rule to generate test list for aforementioned test executable - set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") - set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") - get_property(crosscompiling_emulator - TARGET ${TARGET} - PROPERTY CROSSCOMPILING_EMULATOR - ) - add_custom_command( - TARGET ${TARGET} POST_BUILD - BYPRODUCTS "${ctest_tests_file}" - COMMAND "${CMAKE_COMMAND}" - -D "TEST_TARGET=${TARGET}" - -D "TEST_EXECUTABLE=$" - -D "TEST_EXECUTOR=${crosscompiling_emulator}" - -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" - -D "TEST_SPEC=${_TEST_SPEC}" - -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" - -D "TEST_PROPERTIES=${_PROPERTIES}" - -D "TEST_PREFIX='${_TEST_PREFIX}'" - -D "TEST_SUFFIX='${_TEST_SUFFIX}'" - -D "TEST_LIST=${_TEST_LIST}" - -D "CTEST_FILE=${ctest_tests_file}" - -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" - VERBATIM - ) - - file(WRITE "${ctest_include_file}" - "if(EXISTS \"${ctest_tests_file}\")\n" - " include(\"${ctest_tests_file}\")\n" - "else()\n" - " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" - "endif()\n" - ) - - if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") - # Add discovered tests to directory TEST_INCLUDE_FILES - set_property(DIRECTORY - APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" - ) - else() - # Add discovered tests as directory TEST_INCLUDE_FILE if possible - get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) - if (NOT ${test_include_file_set}) - set_property(DIRECTORY - PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" - ) - else() - message(FATAL_ERROR - "Cannot set more than one TEST_INCLUDE_FILE" - ) - endif() - endif() - -endfunction() - -############################################################################### - -set(_CATCH_DISCOVER_TESTS_SCRIPT - ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake -) diff --git a/cmake/modules/Catch2/CatchAddTests.cmake b/cmake/modules/Catch2/CatchAddTests.cmake deleted file mode 100644 index ca5ebc17e59..00000000000 --- a/cmake/modules/Catch2/CatchAddTests.cmake +++ /dev/null @@ -1,106 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -set(prefix "${TEST_PREFIX}") -set(suffix "${TEST_SUFFIX}") -set(spec ${TEST_SPEC}) -set(extra_args ${TEST_EXTRA_ARGS}) -set(properties ${TEST_PROPERTIES}) -set(script) -set(suite) -set(tests) - -function(add_command NAME) - set(_args "") - foreach(_arg ${ARGN}) - if(_arg MATCHES "[^-./:a-zA-Z0-9_]") - set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument - else() - set(_args "${_args} ${_arg}") - endif() - endforeach() - set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) -endfunction() - -macro(_add_catch_test_labels LINE) - # convert to list of tags - string(REPLACE "][" "]\\;[" tags ${line}) - - add_command( - set_tests_properties "${prefix}${test}${suffix}" - PROPERTIES - LABELS "${tags}" - ) -endmacro() - -macro(_add_catch_test LINE) - set(test ${line}) - # use escape commas to handle properly test cases with commans inside the name - string(REPLACE "," "\\," test_name ${test}) - # ...and add to script - add_command( - add_test "${prefix}${test}${suffix}" - ${TEST_EXECUTOR} - "${TEST_EXECUTABLE}" - "${test_name}" - ${extra_args} - ) - - add_command( - set_tests_properties "${prefix}${test}${suffix}" - PROPERTIES - WORKING_DIRECTORY "${TEST_WORKING_DIR}" - ${properties} - ) - list(APPEND tests "${prefix}${test}${suffix}") -endmacro() - -# Run test executable to get list of available tests -if(NOT EXISTS "${TEST_EXECUTABLE}") - message(FATAL_ERROR - "Specified test executable '${TEST_EXECUTABLE}' does not exist" - ) -endif() -execute_process( - COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-tests - OUTPUT_VARIABLE output - RESULT_VARIABLE result -) -# Catch --list-test-names-only reports the number of tests, so 0 is... surprising -if(${result} EQUAL 0) - message(WARNING - "Test executable '${TEST_EXECUTABLE}' contains no tests!\n" - ) -elseif(${result} LESS 0) - message(FATAL_ERROR - "Error running test executable '${TEST_EXECUTABLE}':\n" - " Result: ${result}\n" - " Output: ${output}\n" - ) -endif() - -string(REPLACE "\n" ";" output "${output}") -set(test) -set(tags_regex "(\\[([^\\[]*)\\])+$") - -# Parse output -foreach(line ${output}) - # lines without leading whitespaces are catch output not tests - if(${line} MATCHES "^[ \t]+") - # strip leading spaces and tabs - string(REGEX REPLACE "^[ \t]+" "" line ${line}) - - if(${line} MATCHES "${tags_regex}") - _add_catch_test_labels(${line}) - else() - _add_catch_test(${line}) - endif() - endif() -endforeach() - -# Create a list of all discovered tests, which users may use to e.g. set -# properties on the tests -add_command(set ${TEST_LIST} ${tests}) - -# Write CTest script -file(WRITE "${CTEST_FILE}" "${script}") diff --git a/cmake/modules/Catch2/ParseAndAddCatchTests.cmake b/cmake/modules/Catch2/ParseAndAddCatchTests.cmake deleted file mode 100644 index 925d9328196..00000000000 --- a/cmake/modules/Catch2/ParseAndAddCatchTests.cmake +++ /dev/null @@ -1,225 +0,0 @@ -#==================================================================================================# -# supported macros # -# - TEST_CASE, # -# - SCENARIO, # -# - TEST_CASE_METHOD, # -# - CATCH_TEST_CASE, # -# - CATCH_SCENARIO, # -# - CATCH_TEST_CASE_METHOD. # -# # -# Usage # -# 1. make sure this module is in the path or add this otherwise: # -# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # -# 2. make sure that you've enabled testing option for the project by the call: # -# enable_testing() # -# 3. add the lines to the script for testing target (sample CMakeLists.txt): # -# project(testing_target) # -# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # -# enable_testing() # -# # -# find_path(CATCH_INCLUDE_DIR "catch.hpp") # -# include_directories(${INCLUDE_DIRECTORIES} ${CATCH_INCLUDE_DIR}) # -# # -# file(GLOB SOURCE_FILES "*.cpp") # -# add_executable(${PROJECT_NAME} ${SOURCE_FILES}) # -# # -# include(ParseAndAddCatchTests) # -# ParseAndAddCatchTests(${PROJECT_NAME}) # -# # -# The following variables affect the behavior of the script: # -# # -# PARSE_CATCH_TESTS_VERBOSE (Default OFF) # -# -- enables debug messages # -# PARSE_CATCH_TESTS_NO_HIDDEN_TESTS (Default OFF) # -# -- excludes tests marked with [!hide], [.] or [.foo] tags # -# PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME (Default ON) # -# -- adds fixture class name to the test name # -# PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME (Default ON) # -# -- adds cmake target name to the test name # -# PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS (Default OFF) # -# -- causes CMake to rerun when file with tests changes so that new tests will be discovered # -# # -# One can also set (locally) the optional variable OptionalCatchTestLauncher to precise the way # -# a test should be run. For instance to use test MPI, one can write # -# set(OptionalCatchTestLauncher ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${NUMPROC}) # -# just before calling this ParseAndAddCatchTests function # -# # -# The AdditionalCatchParameters optional variable can be used to pass extra argument to the test # -# command. For example, to include successful tests in the output, one can write # -# set(AdditionalCatchParameters --success) # -# # -# After the script, the ParseAndAddCatchTests_TESTS property for the target, and for each source # -# file in the target is set, and contains the list of the tests extracted from that target, or # -# from that file. This is useful, for example to add further labels or properties to the tests. # -# # -#==================================================================================================# - -if (CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.8) - message(FATAL_ERROR "ParseAndAddCatchTests requires CMake 2.8.8 or newer") -endif() - -option(PARSE_CATCH_TESTS_VERBOSE "Print Catch to CTest parser debug messages" OFF) -option(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS "Exclude tests with [!hide], [.] or [.foo] tags" OFF) -option(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME "Add fixture class name to the test name" ON) -option(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME "Add target name to the test name" ON) -option(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS "Add test file to CMAKE_CONFIGURE_DEPENDS property" OFF) - -function(ParseAndAddCatchTests_PrintDebugMessage) - if(PARSE_CATCH_TESTS_VERBOSE) - message(STATUS "ParseAndAddCatchTests: ${ARGV}") - endif() -endfunction() - -# This removes the contents between -# - block comments (i.e. /* ... */) -# - full line comments (i.e. // ... ) -# contents have been read into '${CppCode}'. -# !keep partial line comments -function(ParseAndAddCatchTests_RemoveComments CppCode) - string(ASCII 2 CMakeBeginBlockComment) - string(ASCII 3 CMakeEndBlockComment) - string(REGEX REPLACE "/\\*" "${CMakeBeginBlockComment}" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "\\*/" "${CMakeEndBlockComment}" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "${CMakeBeginBlockComment}[^${CMakeEndBlockComment}]*${CMakeEndBlockComment}" "" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "\n[ \t]*//+[^\n]+" "\n" ${CppCode} "${${CppCode}}") - - set(${CppCode} "${${CppCode}}" PARENT_SCOPE) -endfunction() - -# Worker function -function(ParseAndAddCatchTests_ParseFile SourceFile TestTarget) - # If SourceFile is an object library, do not scan it (as it is not a file). Exit without giving a warning about a missing file. - if(SourceFile MATCHES "\\\$") - ParseAndAddCatchTests_PrintDebugMessage("Detected OBJECT library: ${SourceFile} this will not be scanned for tests.") - return() - endif() - # According to CMake docs EXISTS behavior is well-defined only for full paths. - get_filename_component(SourceFile ${SourceFile} ABSOLUTE) - if(NOT EXISTS ${SourceFile}) - message(WARNING "Cannot find source file: ${SourceFile}") - return() - endif() - ParseAndAddCatchTests_PrintDebugMessage("parsing ${SourceFile}") - file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) - - # Remove block and fullline comments - ParseAndAddCatchTests_RemoveComments(Contents) - - # Find definition of test names - string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^\)]+\\)+[ \t\n]*{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") - - if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) - ParseAndAddCatchTests_PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") - set_property( - DIRECTORY - APPEND - PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} - ) - endif() - - foreach(TestName ${Tests}) - # Strip newlines - string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") - - # Get test type and fixture if applicable - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") - string(REGEX REPLACE "${TestType}\\([ \t]*" "" TestFixture "${TestTypeAndFixture}") - - # Get string parts of test definition - string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") - - # Strip wrapping quotation marks - string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") - string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") - - # Validate that a test name and tags have been provided - list(LENGTH TestStrings TestStringsLength) - if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) - message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") - endif() - - # Assign name and tags - list(GET TestStrings 0 Name) - if("${TestType}" STREQUAL "SCENARIO") - set(Name "Scenario: ${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND TestFixture) - set(CTestName "${TestFixture}:${Name}") - else() - set(CTestName "${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) - set(CTestName "${TestTarget}:${CTestName}") - endif() - # add target to labels to enable running all tests added from this target - set(Labels ${TestTarget}) - if(TestStringsLength EQUAL 2) - list(GET TestStrings 1 Tags) - string(TOLOWER "${Tags}" Tags) - # remove target from labels if the test is hidden - if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") - list(REMOVE_ITEM Labels ${TestTarget}) - endif() - string(REPLACE "]" ";" Tags "${Tags}") - string(REPLACE "[" "" Tags "${Tags}") - else() - # unset tags variable from previous loop - unset(Tags) - endif() - - list(APPEND Labels ${Tags}) - - set(HiddenTagFound OFF) - foreach(label ${Labels}) - string(REGEX MATCH "^!hide|^\\." result ${label}) - if(result) - set(HiddenTagFound ON) - break() - endif(result) - endforeach(label) - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_LESS "3.9") - ParseAndAddCatchTests_PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") - else() - ParseAndAddCatchTests_PrintDebugMessage("Adding test \"${CTestName}\"") - if(Labels) - ParseAndAddCatchTests_PrintDebugMessage("Setting labels to ${Labels}") - endif() - - # Escape commas in the test spec - string(REPLACE "," "\\," Name ${Name}) - - # Add the test and set its properties - add_test(NAME "\"${CTestName}\"" COMMAND ${OptionalCatchTestLauncher} $ ${Name} ${AdditionalCatchParameters}) - # Old CMake versions do not document VERSION_GREATER_EQUAL, so we use VERSION_GREATER with 3.8 instead - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_GREATER "3.8") - ParseAndAddCatchTests_PrintDebugMessage("Setting DISABLED test property") - set_tests_properties("\"${CTestName}\"" PROPERTIES DISABLED ON) - else() - set_tests_properties("\"${CTestName}\"" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" - LABELS "${Labels}") - endif() - set_property( - TARGET ${TestTarget} - APPEND - PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") - set_property( - SOURCE ${SourceFile} - APPEND - PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") - endif() - - - endforeach() -endfunction() - -# entry point -function(ParseAndAddCatchTests TestTarget) - ParseAndAddCatchTests_PrintDebugMessage("Started parsing ${TestTarget}") - get_target_property(SourceFiles ${TestTarget} SOURCES) - ParseAndAddCatchTests_PrintDebugMessage("Found the following sources: ${SourceFiles}") - foreach(SourceFile ${SourceFiles}) - ParseAndAddCatchTests_ParseFile(${SourceFile} ${TestTarget}) - endforeach() - ParseAndAddCatchTests_PrintDebugMessage("Finished parsing ${TestTarget}") -endfunction() diff --git a/cmake/sanitizers.cmake b/cmake/sanitizers.cmake new file mode 100644 index 00000000000..d9b62b05e6d --- /dev/null +++ b/cmake/sanitizers.cmake @@ -0,0 +1,91 @@ +# cmake/sanitizers.cmake — AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) +# +# Provides options ENABLE_ASAN and ENABLE_UBSAN that add the appropriate +# compiler and linker flags for GCC, Clang, and MSVC. +# +# Both options default to OFF and are strictly opt-in. Including this +# module therefore has no effect on any build unless the caller passes +# -DENABLE_ASAN=ON / -DENABLE_UBSAN=ON. This keeps release/production +# and normal Debug builds byte-for-byte unaffected. +# +# Usage: +# include(cmake/sanitizers.cmake) +# cmake -S . -B build -DENABLE_ASAN=ON -DENABLE_UBSAN=ON +# +# On GCC / Clang, ASan and UBSan can coexist: both -fsanitize=address +# and -fsanitize=undefined are passed. +# +# On MSVC, only ASan is supported (/fsanitize=address). UBSan is not +# available; enabling UBSAN on MSVC emits a warning and is a no-op. +# +# Sanitizer builds always add debug info (-g / /Zi) alongside -fno-omit-frame-pointer +# so that ASan/UBSan reports carry symbolized stack traces even in Release builds. + +option(ENABLE_ASAN "Enable AddressSanitizer (ASan) — detect memory errors" OFF) +option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer (UBSan) — detect undefined behavior" OFF) + +include(CheckCXXCompilerFlag) + +# Note on global flag application: sanitizer instrumentation is added via +# add_compile_options / add_link_options (directory-wide) rather than per-target +# target_*() calls. This is deliberate: every translation unit and the final +# executable must be instrumented for ASan/UBSan to be effective, so a +# directory-global application is the correct intent here (not the uncontrolled +# "global CMake" anti-pattern, which concerns flags that should be scoped but +# are not). These options are only active when ENABLE_ASAN/ENABLE_UBSAN is ON, +# which is opt-in and never set for release/production builds. + +# ── ASan ────────────────────────────────────────────────────────────────────── +if(ENABLE_ASAN) + # ASan is available on MSVC starting with Visual Studio 2019 16.9 + # https://devblogs.microsoft.com/cppblog/address-sanitizer-for-msvc-now-generally-available/ + if(MSVC) + add_compile_options(/fsanitize=address /Zi) + add_compile_definitions(_DISABLE_STRING_ANNOTATION=1 _DISABLE_VECTOR_ANNOTATION=1) + else() + add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g) + add_link_options(-fsanitize=address) + + # GCC needs explicit -lasan for static linking in some configurations + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_link_options(-lasan) + endif() + endif() + + message(STATUS "AddressSanitizer (ASan) enabled") +endif() + +# ── UBSan ───────────────────────────────────────────────────────────────────── +if(ENABLE_UBSAN) + if(MSVC) + message(WARNING "UndefinedBehaviorSanitizer (UBSan) is not supported on MSVC — ignored") + else() + add_compile_options( + -fsanitize=undefined + -fsanitize=signed-integer-overflow + -fsanitize-recover=undefined + -g + ) + add_link_options(-fsanitize=undefined) + + # -fsanitize=implicit-conversion requires GCC 7+ but full support varies; + # probe at configure-time so the build doesn't break on older toolchains. + check_cxx_compiler_flag(-fsanitize=implicit-conversion HAS_IMPLICIT_CONVERSION) + if(HAS_IMPLICIT_CONVERSION) + add_compile_options(-fsanitize=implicit-conversion) + else() + message(STATUS "UBSan: -fsanitize=implicit-conversion not supported — skipped") + endif() + + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_link_options(-lubsan) + endif() + + message(STATUS "UndefinedBehaviorSanitizer (UBSan) enabled") + endif() +endif() + +# ── LeakSanitizer note ──────────────────────────────────────────────────────── +# ASan includes LeakSanitizer (LSan) by default on Linux x86_64. +# To disable LSan at runtime: ASAN_OPTIONS=detect_leaks=0 ./your_binary +# To run with extra UBSan checks at runtime: UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./your_binary \ No newline at end of file diff --git a/scripts/junit-to-html.js b/scripts/junit-to-html.js new file mode 100644 index 00000000000..4c2f46f78ca --- /dev/null +++ b/scripts/junit-to-html.js @@ -0,0 +1,236 @@ +#!/usr/bin/env node +/** + * scripts/junit-to-html.js — Convert JUnit XML test results to a self-contained HTML report + * + * Reads JUnit XML from one or more files (or stdin) and writes a single HTML file + * with Mermaid charts, per-module tables, and pass/fail summary. + * + * Usage: + * node scripts/junit-to-html.js test-results.xml > report.html + * node scripts/junit-to-html.js build/test-results.xml -o report.html + * ctest --test-dir build --output-junit results.xml && node scripts/junit-to-html.js results.xml -o report.html + * + * Options: + * -o, --output Write HTML to file (default: stdout) + * -t, --title Report title (default: "Test Results") + * -c, --commit Commit SHA to display (default: none) + * -r, --run CI run URL to link (default: none) + * --help Show this help + */ + +const fs = require("node:fs"); +const path = require("node:path"); + +// ── CLI argument parsing ──────────────────────────────────────────────────────── +const args = process.argv.slice(2); +let outputFile = null; +let title = "Test Results"; +let commitSha = null; +let runUrl = null; +const inputFiles = []; + +for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === "-o" || a === "--output") { outputFile = args[++i]; } + else if (a === "-t" || a === "--title") { title = args[++i]; } + else if (a === "-c" || a === "--commit") { commitSha = args[++i]; } + else if (a === "-r" || a === "--run") { runUrl = args[++i]; } + else if (a === "--help") { console.log("Usage: node junit-to-html.js [options] [file ...]\n\nOptions:\n -o, --output Write HTML to file (default: stdout)\n -t, --title Report title\n -c, --commit Commit SHA\n -r, --run CI run URL\n --help Show this help\n\nIf no input files, reads from stdin."); process.exit(0); } + else if (a.startsWith("-")) { console.error("Unknown option: " + a); process.exit(1); } + else { inputFiles.push(a); } +} + +// ── XML parsing ────────────────────────────────────────────────────────────────── +function parseJUnit(xml) { + const cases = []; + const re = //g; + let m; + while ((m = re.exec(xml)) !== null) { + const name = m[1]; + const className = m[2]; + const time = parseFloat(m[3]) || 0; + const status = m[4] || "run"; + const endTag = xml.indexOf("", m.index); + const seg = endTag !== -1 ? xml.substring(m.index + m[0].length, endTag) : ""; + let assertions = 0; + const am = seg.match(/(\d+)\s+assertions?/); + if (am) assertions = parseInt(am[1], 10); + let failureDetail = ""; + let isFailure = false; + if (seg.includes("]*message="([^"]*)"/); + failureDetail = fm ? fm[1] : "unknown failure"; + } else if (seg.includes("]*message="([^"]*)"/); + failureDetail = em ? em[1] : "unknown error"; + } else if (status !== "run") { + isFailure = true; + failureDetail = "Status: " + status; + } + const module = className.split(":")[0] || "other"; + const shortName = name.startsWith(module + ":") ? name.substring(module.length + 1) : name; + cases.push({ name, shortName, module, time, assertions, status, isFailure, failureDetail }); + } + return cases; +} + +// ── Aggregation ────────────────────────────────────────────────────────────────── +function buildStats(cases) { + const byModule = {}; + for (const c of cases) { + if (!byModule[c.module]) byModule[c.module] = { tests: 0, failed: 0, time: 0, assertions: 0 }; + byModule[c.module].tests++; + if (c.isFailure) byModule[c.module].failed++; + byModule[c.module].time += c.time; + byModule[c.module].assertions += c.assertions; + } + return byModule; +} + +function totalTime(stats) { return Object.values(stats).reduce((a, b) => a + b.time, 0); } +function totalAssertions(stats) { return Object.values(stats).reduce((a, b) => a + b.assertions, 0); } + +// ── HTML generation ────────────────────────────────────────────────────────────── +function esc(s) { + return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +function modTable(stats) { + return Object.entries(stats).sort(([a], [b]) => a.localeCompare(b)).map(([mod, s]) => + "" + esc(mod) + "" + s.tests + "" + s.time.toFixed(1) + "s" + s.assertions + "" + s.failed + "" + ).join(""); +} + +function slowRows(cases, n) { + return [...cases].sort((a, b) => b.time - a.time).slice(0, n).map(c => + '' + esc(c.shortName) + "" + esc(c.module) + "" + c.time.toFixed(2) + "s" + c.assertions + "" + ).join(""); +} + +function topCards(cases) { + return [...cases].sort((a, b) => b.assertions - a.assertions).slice(0, 4).map(c => + '
' + esc(c.module) + '
' + c.assertions + '
' + esc(c.shortName) + '
' + c.time.toFixed(2) + "s
" + ).join(""); +} + +function failureRows(cases) { + const failed = cases.filter(c => c.isFailure); + if (failed.length === 0) return ""; + return failed.map(c => + '' + esc(c.shortName) + "" + esc(c.module) + "" + c.time.toFixed(2) + "s" + esc(c.failureDetail) + "" + ).join(""); +} + +function caseRows(arr, max) { + const slice = arr.slice(0, max); + const extra = arr.length > max ? '... and ' + (arr.length - max) + " more tests" : ""; + return slice.map(c => + '' + esc(c.shortName) + "" + c.time.toFixed(3) + "s" + c.assertions + "" + ).join("") + extra; +} + +function pieData(stats) { + return Object.entries(stats).sort(([a], [b]) => a.localeCompare(b)).map(([mod, s]) => + ' "' + mod + '" : ' + s.tests + ).join("\n"); +} + +function barData(stats) { + const entries = Object.entries(stats).sort(([a], [b]) => a.localeCompare(b)); + return { + axis: entries.map(([mod]) => '"' + mod + '"').join(", "), + values: entries.map(([, s]) => s.time.toFixed(1)).join(", ") + }; +} + +function generateHtml(allCases, titleText, commit, run) { + const stats = buildStats(allCases); + const total = allCases.length; + const failed = allCases.filter(c => c.isFailure).length; + const passed = total - failed; + const tTime = totalTime(stats).toFixed(1); + const tAsserts = totalAssertions(stats); + const bar = barData(stats); + + const byMod = {}; + for (const c of allCases) { + if (!byMod[c.module]) byMod[c.module] = []; + byMod[c.module].push(c); + } + for (const mod of Object.keys(byMod)) { + byMod[mod].sort((a, b) => b.time - a.time); + } + + const modTables = Object.entries(byMod).sort(([a], [b]) => a.localeCompare(b)).map(([mod, cases]) => { + const s = stats[mod]; + const maxShow = mod === "libslic3r" ? 30 : cases.length; + return '\n

' + esc(mod) + " — " + s.tests + " Tests" + (s.failed ? ' (' + s.failed + " failed)" : "") + '

\n \n \n ' + caseRows(cases, maxShow) + '\n
TestTimeAssertions
'; + }).join(""); + + const failSection = failed > 0 ? '\n

Failed Tests

\n \n \n ' + failureRows(allCases) + '\n
TestModuleTimeError
' : ""; + + const slowSection = allCases.length > 0 ? '\n

Slowest 15 Tests

\n \n \n ' + slowRows(allCases, 15) + '\n
TestModuleTimeAssertions
' : ""; + + const topSection = allCases.length > 0 ? '\n

Top Assertion-Heavy Tests

\n
' + topCards(allCases) + '
' : ""; + + const badgeHtml = failed === 0 + ? '
ALL ' + total + ' TESTS PASSED
' + : '
' + failed + ' OF ' + total + ' TESTS FAILED
'; + + const subtitle = []; + if (commit) subtitle.push('Commit ' + esc(commit.substring(0, 8)) + ''); + if (run) { + const runId = run.split("/").pop(); + subtitle.push('CI ' + esc(runId) + ''); + } + subtitle.push(new Date().toISOString().split("T")[0]); + + return '\n\n\n\n\n' + esc(titleText) + '\n' + css() + '\n\n\n
\n

' + esc(titleText) + '

\n

' + subtitle.join(" · ") + '

\n' + badgeHtml + '\n\n

Summary

\n
\n
Total Tests
' + total + '
\n
Passed
' + passed + '
\n
Failed / Errors
' + failed + '
\n
Total Time
' + tTime + 's
\n
Assertions
' + tAsserts + '
\n
\n' + (total > 0 ? '\n

Module Breakdown

\n\n \n ' + modTable(stats) + '\n \n
ModuleTestsTimeAssertionsFailed
Total' + total + '' + tTime + 's' + tAsserts + '' + failed + '
\n\n
\n
\npie showData\n    title Test Cases by Module (' + total + ' total)\n' + pieData(stats) + '\n
\n
\n\n
\n
\n%%{init: {"theme": "dark"}}%%\nxychart-beta\n    title "Execution Time by Module"\n    x-axis [' + bar.axis + ']\n    y-axis "Seconds" 0 --> ' + (Math.ceil(totalTime(stats)) + 1) + '\n    bar [' + bar.values + ']\n
\n
\n' : '
🔍

No test cases found in input.

Check that ctest --output-junit produced valid XML.

\n') + '\n' + failSection + '\n' + slowSection + '\n' + topSection + '\n' + modTables + '\n
Generated ' + new Date().toISOString().replace("T", " ").substring(0, 19) + ' · OrcaSlicer test suite
\n
\n