From e89b403122d8ac4dbe5f81a28955536b7029d639 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sat, 18 Jul 2026 15:29:41 +0200 Subject: [PATCH 1/6] feat: enable strict compilation with maximum warnings as errors - Add MORPH_ENABLE_STRICT_COMPILATION option (default ON) - Enable -Werror and ~60 additional warning flags across GCC, Clang, MSVC - Move shared diagnostics (Wshadow, -Wconversion, etc.) from Clang-only to the common GNU/Clang baseline - Fix all warnings surfaced by the new flags: - Add missing default: labels in switch statements - Remove empty @par Example paragraphs from doxygen docs - Suppress -Wfloat-equal in tests with intentional float comparisons - Mark bridge-registered helper methods as [[maybe_unused]] - Suppress GCC -Wpedantic on __int128 extension usage - Remove redundant static_cast on size_t in session_auth.hpp - Add missing .roles and .idempotencyKey fields in designated initializers --- .gitignore | 1 + CMakeLists.txt | 4 ++ cmake/compiler_options.cmake | 64 ++++++++++++++++++++++++++-- examples/forms/lab_units.hpp | 2 +- include/morph/action_log.hpp | 1 - include/morph/logger.hpp | 4 +- include/morph/quantity.hpp | 26 ++++------- include/morph/rational.hpp | 7 +++ include/morph/registry.hpp | 1 - include/morph/session_auth.hpp | 2 +- tests/test_bridge_lifetime.cpp | 4 +- tests/test_model.cpp | 8 ++++ tests/test_offline_queue.cpp | 2 +- tests/test_quantity.cpp | 2 +- tests/test_quantity_forms.cpp | 2 +- tests/test_reconnect_coordinator.cpp | 2 +- tests/test_remote_extra.cpp | 2 +- tests/test_security_fixes.cpp | 6 +-- 18 files changed, 102 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 697cd31..711498a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ *.user *.suo .vs/ +bv-clang/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a5705e..55fc5bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,10 @@ endif() option(MORPH_BUILD_QT "Build Qt6 WebSocket backend and tests" OFF) option(MORPH_BUILD_DOCUMENTATION "Build doxygen docs" OFF) option(MORPH_BUILD_CLANG_TIDY "Enable clang-tidy checks (warnings-as-errors)" OFF) +option(MORPH_ENABLE_STRICT_COMPILATION + "Enable strict compilation: treat all warnings as errors with extra diagnostics" + ON +) # Both Qt consumers (the WebSocket backend and the forms QML renderer) need # Qt6 auto-detected on Windows before their find_package(Qt6 ...) calls run. diff --git a/cmake/compiler_options.cmake b/cmake/compiler_options.cmake index 2a74266..e929f34 100644 --- a/cmake/compiler_options.cmake +++ b/cmake/compiler_options.cmake @@ -1,5 +1,6 @@ function(apply_warnings target) target_compile_options(${target} PRIVATE + # ── MSVC ────────────────────────────────────────────────────────────── $<$: /W4 /permissive- @@ -25,6 +26,7 @@ function(apply_warnings target) /w14928 # illegal copy-initialization /wd4068 # suppress: unknown pragma (e.g. clang pragmas in shared headers) > + # ── clang-cl (clang on Windows, MSVC ABI) ──────────────────────────── $<$:$<$: -Wno-c++98-compat -Wno-c++98-compat-pedantic @@ -35,13 +37,11 @@ function(apply_warnings target) -Wno-shadow-uncaptured-local -Wno-unused-command-line-argument >> - + # ── GNU / Clang (common baseline) ──────────────────────────────────── $<$: -Wall -Wextra -Wpedantic - > - $<$: -Wshadow -Wnon-virtual-dtor -Wold-style-cast @@ -55,6 +55,62 @@ function(apply_warnings target) -Wimplicit-fallthrough > ) + + # ── Strict mode: warnings-as-errors + maximum diagnostics ────────────── + # Controlled by MORPH_ENABLE_STRICT_COMPILATION (default ON in CI). + if(MORPH_ENABLE_STRICT_COMPILATION) + target_compile_options(${target} PRIVATE + $<$:-Werror> + $<$:/WX> + + # ── GNU / Clang (common strict) ────────────────────────────────── + $<$: + -Wcast-qual + -Wformat=2 + -Wredundant-decls + -Winit-self + -Wmissing-include-dirs + -Wundef + -Wswitch-default + -Wswitch-enum + -Wctor-dtor-privacy + -Wpacked + -Wdouble-promotion + -Wformat-security + -Wformat-nonliteral + -Wmissing-declarations + -Warray-bounds + > + + # ── GCC-only (strict) ──────────────────────────────────────────── + $<$: + -Wduplicated-cond + -Wduplicated-branches + -Wlogical-op + -Wuseless-cast + -Wrestrict + -Warray-bounds=2 + -Walloc-zero + -Wstringop-overflow=4 + -Wstringop-truncation + -Wstrict-overflow=5 + -Wsuggest-override + -Wnoexcept + -Wsubobject-linkage + -Wtrampolines + -Wconditionally-supported + > + + # ── Clang-only (strict) ────────────────────────────────────────── + $<$: + -Wheader-hygiene + -Wdocumentation + -Wcomma + -Wdeprecated + -Wshorten-64-to-32 + > + ) + endif() endfunction() function(apply_sanitizers target mode) @@ -81,4 +137,4 @@ function(apply_coverage target) -fprofile-instr-generate -fcoverage-mapping -g -O0) target_link_options(${target} PRIVATE -fprofile-instr-generate) -endfunction() +endfunction() \ No newline at end of file diff --git a/examples/forms/lab_units.hpp b/examples/forms/lab_units.hpp index 6849cbf..046c955 100644 --- a/examples/forms/lab_units.hpp +++ b/examples/forms/lab_units.hpp @@ -46,8 +46,8 @@ struct morph::units::UnitTraits { case lab::Unit::g: return {"g", "g", 1}; case lab::Unit::t: return {"t", "t", 4}; case lab::Unit::l: return {"l", "L", 1}; + default: return {"?", "?", 3}; } - return {"?", "?", 3}; } /// @brief Within-dimension conversion ratios (mass g/t ↔ kg, volume L ↔ m³). diff --git a/include/morph/action_log.hpp b/include/morph/action_log.hpp index c9d45cb..a2312aa 100644 --- a/include/morph/action_log.hpp +++ b/include/morph/action_log.hpp @@ -211,7 +211,6 @@ inline void setActionLog(std::shared_ptr log) { /// case's sink never leaks into the next) and for applications that need to /// temporarily redirect auto-attached logging within a scope. /// -/// @par Example /// @code /// { /// morph::journal::ScopedActionLog guard{std::make_shared()}; diff --git a/include/morph/logger.hpp b/include/morph/logger.hpp index 8f92f09..f040680 100644 --- a/include/morph/logger.hpp +++ b/include/morph/logger.hpp @@ -41,8 +41,9 @@ constexpr std::string_view levelName(LogLevel level) noexcept { return "ERROR"; case LogLevel::off: return "OFF "; + default: + return "? "; } - return "? "; } /// @brief Sink function type used internally for log output. @@ -220,7 +221,6 @@ void logError(std::format_string fmt, Args&&... args) { /// custom sink into other tests. Thread-safe construction and destruction — /// the global mutex is acquired briefly during each. /// -/// @par Example /// @code /// { /// std::vector captured; diff --git a/include/morph/quantity.hpp b/include/morph/quantity.hpp index 8a7530d..cfbdc77 100644 --- a/include/morph/quantity.hpp +++ b/include/morph/quantity.hpp @@ -242,15 +242,13 @@ struct UnitAlternative { template struct UnitTraits; -/// @brief Concept: an enum with a `UnitTraits` specialisation. -/// @tparam E Candidate enum type. +/// @brief Concept: an enum with a `UnitTraits` specialisation (template param `E`). template concept UnitEnum = std::is_enum_v && requires(E unit) { { UnitTraits::meta(unit) } -> std::convertible_to; }; -/// @brief Concept: `UnitTraits` also declares within-dimension `relations`. -/// @tparam E Candidate enum type. +/// @brief Concept: `UnitTraits` also declares within-dimension `relations` (template param `E`). template concept HasUnitRelations = requires { { UnitTraits::relations }; @@ -307,7 +305,7 @@ struct RatioResult { /// @param value A strictly-positive rational. /// @return `1 / value`. [[nodiscard]] consteval morph::math::Rational reciprocal(const morph::math::Rational& value) { - requirePositiveRatio(value); + static_cast(requirePositiveRatio(value)); // compile error on a bad ratio return morph::math::Rational{morph::math::Numerator{value.denominator}, morph::math::Denominator{value.numerator}, value.decimalPlaces}; } @@ -346,7 +344,7 @@ template bool touches = false; if (relation.from == current) { neighbour = relation.to; - requirePositiveRatio(relation.fromTo); // compile error on a bad ratio + static_cast(requirePositiveRatio(relation.fromTo)); // compile error on a bad ratio edge = relation.fromTo; touches = true; } else if (relation.to == current) { @@ -426,9 +424,7 @@ template template inline constexpr auto kUnitAlternatives = makeAlternatives(); -/// @brief Concept: two enumerators of the same enum with distinct values. -/// @tparam A First enumerator. -/// @tparam B Second enumerator. +/// @brief Concept: two enumerators of the same enum with distinct values (template params `A`, `B`). template concept SameEnumDistinct = std::same_as && (A != B); @@ -440,9 +436,7 @@ template ::meta requires UnitEnum struct Quantity; -/// @brief Concept: the auto-generated ratio `convert` applies to `From → To`. -/// @tparam From Source unit enumerator. -/// @tparam To Target unit enumerator. +/// @brief Concept: the auto-generated ratio `convert` applies to `From → To` (template params `From`, `To`). template concept RatioConvertible = detail::SameEnumDistinct && UnitEnum && HasUnitRelations && detail::conversionRatio(From, To).found; @@ -462,18 +456,14 @@ template void convert(const Quantity& in, Quantity& out); /// @brief Concept: the application supplied a `UnitTraits::convert` static -/// for this pair (a non-ratio override, e.g. °C → °F). -/// @tparam From Source unit enumerator. -/// @tparam To Target unit enumerator. +/// for this pair (a non-ratio override, e.g. °C → °F) (template params `From`, `To`). template concept HasUserConvert = requires(Quantity from, Quantity& to) { UnitTraits::convert(from, to); }; /// @brief Concept: `From → To` is convertible — a user override takes -/// precedence over an auto-generated ratio path. -/// @tparam From Source unit enumerator. -/// @tparam To Target unit enumerator. +/// precedence over an auto-generated ratio path (template params `From`, `To`). template concept Convertible = detail::SameEnumDistinct && (HasUserConvert || RatioConvertible); diff --git a/include/morph/rational.hpp b/include/morph/rational.hpp index 2462917..0fa3ab3 100644 --- a/include/morph/rational.hpp +++ b/include/morph/rational.hpp @@ -192,7 +192,14 @@ struct U128 { /// @brief Multiplies two u64 exactly into 128 bits (for exact fraction comparison). [[nodiscard]] constexpr U128 mulU64(std::uint64_t lhs, std::uint64_t rhs) noexcept { #ifdef __SIZEOF_INT128__ +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wpedantic" +# endif auto const product = static_cast(lhs) * rhs; +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +# endif return U128{.hi = static_cast(product >> 64), .lo = static_cast(product)}; #else // Portable 32-bit limb multiplication (MSVC has no __int128). diff --git a/include/morph/registry.hpp b/include/morph/registry.hpp index 092ceed..7494348 100644 --- a/include/morph/registry.hpp +++ b/include/morph/registry.hpp @@ -78,7 +78,6 @@ concept HasValidate = requires(const A& act) { /// and keeping the predicate next to the action keeps the GUI side oblivious /// to model internals. /// -/// @par Example — member function /// @code /// struct FormAction { /// double a; diff --git a/include/morph/session_auth.hpp b/include/morph/session_auth.hpp index b412071..ffdd611 100644 --- a/include/morph/session_auth.hpp +++ b/include/morph/session_auth.hpp @@ -58,7 +58,7 @@ inline std::string sha256(std::string_view data) { 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; std::vector msg(data.begin(), data.end()); - const uint64_t bitLen = static_cast(msg.size()) * 8; + const uint64_t bitLen = msg.size() * 8; msg.push_back(0x80); while (msg.size() % 64 != 56) { msg.push_back(0x00); diff --git a/tests/test_bridge_lifetime.cpp b/tests/test_bridge_lifetime.cpp index db2004f..3a20d5e 100644 --- a/tests/test_bridge_lifetime.cpp +++ b/tests/test_bridge_lifetime.cpp @@ -100,7 +100,7 @@ class BackendShim : public morph::backend::detail::IBackend { // and does not throw, so the fake backend can build the result once; the throw // only fires when executeVia's `.then` closure moves it into the typed state. struct ThrowOnMove { - ThrowOnMove() = default; + ThrowOnMove() = default; // NOLINT (used by model factory) ThrowOnMove(const ThrowOnMove&) = default; ThrowOnMove& operator=(const ThrowOnMove&) = default; // NOLINTNEXTLINE(performance-noexcept-move-constructor) @@ -147,7 +147,7 @@ struct morph::model::ActionTraits { using Result = ThrowOnMove; static constexpr std::string_view typeId() { return "BL_ThrowAction"; } static std::string toJson(const ThrowAction&) { return "{}"; } - static ThrowAction fromJson(std::string_view) { return {}; } + [[maybe_unused]] static ThrowAction fromJson(std::string_view) { return {}; } static std::string resultToJson(const ThrowOnMove&) { return "{}"; } static ThrowOnMove resultFromJson(std::string_view) { return {}; } }; diff --git a/tests/test_model.cpp b/tests/test_model.cpp index f7c354c..52a9e3e 100644 --- a/tests/test_model.cpp +++ b/tests/test_model.cpp @@ -4,6 +4,12 @@ #include #include +// This test compares floats with == intentionally; -Wfloat-equal is a false positive here. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" + struct Foo { int x = 0; @@ -57,3 +63,5 @@ TEST_CASE("morph::exec::detail::ModelId equality and hash", "[model]") { // Different values should (almost always) hash differently REQUIRE(hasher(idA) != hasher(idC)); } +#pragma clang diagnostic pop +#pragma GCC diagnostic pop diff --git a/tests/test_offline_queue.cpp b/tests/test_offline_queue.cpp index 901854a..73ecfd6 100644 --- a/tests/test_offline_queue.cpp +++ b/tests/test_offline_queue.cpp @@ -97,7 +97,7 @@ namespace { struct MinimalQueue : morph::offline::IOfflineQueue { uint64_t enqueue(std::string payload) override { const uint64_t itemId = ++nextId; - items.push_back(morph::offline::QueueItem{.id = itemId, .payload = std::move(payload)}); + items.push_back(morph::offline::QueueItem{.id = itemId, .payload = std::move(payload), .idempotencyKey = {}}); return itemId; } std::vector drain() override { return items; } diff --git a/tests/test_quantity.cpp b/tests/test_quantity.cpp index 67b9766..22e2d64 100644 --- a/tests/test_quantity.cpp +++ b/tests/test_quantity.cpp @@ -56,8 +56,8 @@ struct morph::units::UnitTraits { case qt::U::tonne: return {"t", "t", 3}; case qt::U::celsius: return {"celsius", "C", 1}; case qt::U::fahrenheit: return {"fahrenheit", "F", 1}; + default: return {"?", "?", 3}; } - return {"?", "?", 3}; } static constexpr std::array, 3> relations{{ diff --git a/tests/test_quantity_forms.cpp b/tests/test_quantity_forms.cpp index 7f26aad..ff94102 100644 --- a/tests/test_quantity_forms.cpp +++ b/tests/test_quantity_forms.cpp @@ -44,8 +44,8 @@ struct morph::units::UnitTraits { case QFUnit::m3: return {.id = "m3", .display = "m³", .defaultDecimals = 3}; case QFUnit::kg_per_m3: return {.id = "kg_per_m3", .display = "kg/m³", .defaultDecimals = 1}; case QFUnit::g: return {.id = "g", .display = "g", .defaultDecimals = 1}; + default: return {.id = "?", .display = "?", .defaultDecimals = 3}; } - return {.id = "?", .display = "?", .defaultDecimals = 3}; } static constexpr std::array, 1> relations{ diff --git a/tests/test_reconnect_coordinator.cpp b/tests/test_reconnect_coordinator.cpp index 7e32181..0c0a25f 100644 --- a/tests/test_reconnect_coordinator.cpp +++ b/tests/test_reconnect_coordinator.cpp @@ -103,7 +103,7 @@ struct Fakes { return -1; } - [[nodiscard]] int count(const std::string& name) const { + [[nodiscard, maybe_unused]] int count(const std::string& name) const { int n = 0; for (const auto& e : events) { if (e == name) { diff --git a/tests/test_remote_extra.cpp b/tests/test_remote_extra.cpp index bb4b496..83dfdad 100644 --- a/tests/test_remote_extra.cpp +++ b/tests/test_remote_extra.cpp @@ -331,7 +331,7 @@ TEST_CASE( // expiresAtMs must be strictly positive — a zero expiry is now treated as // already-expired (never eternal), so mint with a far-future real expiry. req.session.token = morph::session::TokenIssuer{secret}.issue( - morph::session::SessionToken{.principal = "real-verified-user", .expiresAtMs = 9'999'999'999'999}); + morph::session::SessionToken{.principal = "real-verified-user", .expiresAtMs = 9'999'999'999'999, .roles = {}}); WaitReply waiter; server->handle(morph::wire::encode(req), std::ref(waiter)); diff --git a/tests/test_security_fixes.cpp b/tests/test_security_fixes.cpp index 2699466..474cc23 100644 --- a/tests/test_security_fixes.cpp +++ b/tests/test_security_fixes.cpp @@ -43,10 +43,10 @@ template <> struct morph::model::ActionTraits { using Result = std::string; static constexpr std::string_view typeId() { return "SEC_Echo"; } - static std::string toJson(const SecEcho&) { return "{}"; } + [[maybe_unused]] static std::string toJson(const SecEcho&) { return "{}"; } static SecEcho fromJson(std::string_view) { return {}; } static std::string resultToJson(const std::string& res) { return "\"" + res + "\""; } - static std::string resultFromJson(std::string_view json) { + [[maybe_unused]] static std::string resultFromJson(std::string_view json) { std::string out; (void)glz::read_json(out, json); return out; @@ -209,7 +209,7 @@ TEST_CASE("RemoteServer::dispatchExecute: verifying authorizer still stamps the // expiresAtMs must be strictly positive — a zero expiry is now treated as // already-expired (never eternal), so mint with a far-future real expiry. req.session.token = morph::session::TokenIssuer{secret}.issue( - morph::session::SessionToken{.principal = "verified-user", .expiresAtMs = 9'999'999'999'999}); + morph::session::SessionToken{.principal = "verified-user", .expiresAtMs = 9'999'999'999'999, .roles = {}}); morph::testing::WaitReply waiter; server->handle(morph::wire::encode(req), std::ref(waiter)); From c66d12588a5f365645a7649fe6738df5dfd3aa95 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 07:30:33 +0200 Subject: [PATCH 2/6] fix: CI strict-compilation failures on GCC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - datetime.hpp: rename 'text'→'str' to avoid -Wshadow on glz::text - test_model.cpp: guard #pragma clang diagnostic with __clang__ - remote.hpp: noexcept on non-throwing lambda (-Wnoexcept) - src/main.cpp: noexcept on non-throwing lambda (-Wnoexcept) --- include/morph/datetime.hpp | 6 +++--- include/morph/remote.hpp | 2 +- src/main.cpp | 2 +- tests/test_model.cpp | 8 ++++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/morph/datetime.hpp b/include/morph/datetime.hpp index 54d286e..22d2d00 100644 --- a/include/morph/datetime.hpp +++ b/include/morph/datetime.hpp @@ -301,12 +301,12 @@ template <> struct from { template static void op(auto&& value, is_context auto&& ctx, auto&& iter, auto end) { - std::string text{}; - parse::op(text, ctx, iter, end); + std::string str{}; + parse::op(str, ctx, iter, end); if (bool(ctx.error)) [[unlikely]] { return; } - if (auto parsed = morph::time::DateTime::fromIso8601(text); parsed.has_value()) { + if (auto parsed = morph::time::DateTime::fromIso8601(str); parsed.has_value()) { value = *parsed; return; } diff --git a/include/morph/remote.hpp b/include/morph/remote.hpp index fe04628..e6c8131 100644 --- a/include/morph/remote.hpp +++ b/include/morph/remote.hpp @@ -112,7 +112,7 @@ class RemoteServer : public std::enable_shared_from_this { // canonical decode-error reply (avoids duplicating that path here). } std::string reply; - std::function capture = [&reply](std::string out) { reply = std::move(out); }; + std::function capture = [&reply](std::string out) noexcept { reply = std::move(out); }; dispatchMessage(msg, capture); return reply; } diff --git a/src/main.cpp b/src/main.cpp index f60e4ed..5b5bc1c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,7 +59,7 @@ void runScenario(morph::bridge::Bridge& bridge, morph::exec::MainThreadExecutor& } }); - handler.execute(FailingAction{}).then([](int) {}); + handler.execute(FailingAction{}).then([](int) noexcept {}); gui.runFor(std::chrono::milliseconds(500)); std::cout << "[main] " << successCount.load() << " increments completed\n"; diff --git a/tests/test_model.cpp b/tests/test_model.cpp index 52a9e3e..511bf36 100644 --- a/tests/test_model.cpp +++ b/tests/test_model.cpp @@ -5,8 +5,10 @@ #include // This test compares floats with == intentionally; -Wfloat-equal is a false positive here. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wfloat-equal" +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wfloat-equal" +#endif #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" @@ -63,5 +65,7 @@ TEST_CASE("morph::exec::detail::ModelId equality and hash", "[model]") { // Different values should (almost always) hash differently REQUIRE(hasher(idA) != hasher(idC)); } +#if defined(__clang__) #pragma clang diagnostic pop +#endif #pragma GCC diagnostic pop From dc1988a2679b1ab04c2ef31f7bb286da35f26e20 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 07:50:59 +0200 Subject: [PATCH 3/6] fix: CI strict-compilation failures on both Clang and GCC - backend.hpp: rename lambda capture h to avoid -Wshadow on structured binding - compiler_options.cmake: drop -Wnoexcept (GCC 14 ICE on glaze templates + false positives on every std::function test lambda) - offline_queue.hpp: suppress -Woverloaded-virtual on enqueue two-arg overload - session_auth.hpp: explicit uint8_t cast for xor result (-Wconversion) - test_subscription.cpp: rename lambda param to avoid -Wshadow --- cmake/compiler_options.cmake | 1 - include/morph/backend.hpp | 4 ++-- include/morph/offline_queue.hpp | 3 +++ include/morph/session_auth.hpp | 2 +- tests/test_subscription.cpp | 12 ++++++------ 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cmake/compiler_options.cmake b/cmake/compiler_options.cmake index e929f34..10cefb8 100644 --- a/cmake/compiler_options.cmake +++ b/cmake/compiler_options.cmake @@ -95,7 +95,6 @@ function(apply_warnings target) -Wstringop-truncation -Wstrict-overflow=5 -Wsuggest-override - -Wnoexcept -Wsubobject-linkage -Wtrampolines -Wconditionally-supported diff --git a/include/morph/backend.hpp b/include/morph/backend.hpp index 4a0c6eb..af87db6 100644 --- a/include/morph/backend.hpp +++ b/include/morph/backend.hpp @@ -210,8 +210,8 @@ class LocalBackend : public detail::IBackend { } } for (auto& [modelId, holder] : sinks) { - _strand.post(modelId, [holder = std::move(holder)]() mutable { - auto* sink = dynamic_cast<::morph::model::detail::IBackendChangedSink*>(holder.get()); + _strand.post(modelId, [h = std::move(holder)]() mutable { + auto* sink = dynamic_cast<::morph::model::detail::IBackendChangedSink*>(h.get()); if (sink != nullptr) { sink->onBackendChanged(); } diff --git a/include/morph/offline_queue.hpp b/include/morph/offline_queue.hpp index a6fb9ea..c1c79cd 100644 --- a/include/morph/offline_queue.hpp +++ b/include/morph/offline_queue.hpp @@ -74,11 +74,14 @@ struct IOfflineQueue { /// @param payload Serialised action to persist. /// @param idempotencyKey Stable dedup token for the logical op; may be empty. /// @return A stable id that can be passed to `markDone()`. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverloaded-virtual" virtual uint64_t enqueue(std::string payload, std::string idempotencyKey) { const uint64_t itemId = enqueue(std::move(payload)); setIdempotencyKey(itemId, std::move(idempotencyKey)); return itemId; } +#pragma GCC diagnostic pop /// @brief Returns all pending items in enqueue order without removing them. /// diff --git a/include/morph/session_auth.hpp b/include/morph/session_auth.hpp index ffdd611..0ea0796 100644 --- a/include/morph/session_auth.hpp +++ b/include/morph/session_auth.hpp @@ -159,7 +159,7 @@ inline bool constantTimeEquals(std::string_view lhs, std::string_view rhs) { } uint8_t diff = 0; for (std::size_t i = 0; i < lhs.size(); ++i) { - diff |= static_cast(lhs[i]) ^ static_cast(rhs[i]); + diff = uint8_t(diff | (uint8_t(lhs[i]) ^ uint8_t(rhs[i]))); } return diff == 0; } diff --git a/tests/test_subscription.cpp b/tests/test_subscription.cpp index 1b8c43f..083d2d9 100644 --- a/tests/test_subscription.cpp +++ b/tests/test_subscription.cpp @@ -88,13 +88,13 @@ BRIDGE_REGISTER_ACTION(FormModel, FlakyAction, "Test_FlakyAction") BRIDGE_REGISTER_ACTION(FormModel, MixedAction, "Test_MixedAction") BRIDGE_REGISTER_ACTION(FormModel, SlowAction, "Test_SlowAction") -BRIDGE_REGISTER_VALIDATOR(FormAction, [](const FormAction& action) { - return action.a != 0.0 && action.b != 0.0 && action.c != 0.0; +BRIDGE_REGISTER_VALIDATOR(FormAction, [](const FormAction& a) { + return a.a != 0.0 && a.b != 0.0 && a.c != 0.0; }) -BRIDGE_REGISTER_VALIDATOR(ThrowAction, [](const ThrowAction& action) { return action.trigger != 0; }) -BRIDGE_REGISTER_VALIDATOR(FlakyAction, [](const FlakyAction& action) { return action.mode >= 0; }) -BRIDGE_REGISTER_VALIDATOR(MixedAction, [](const MixedAction& action) { - return !action.name.empty() && action.inner.n != 0 && action.count != 0; +BRIDGE_REGISTER_VALIDATOR(ThrowAction, [](const ThrowAction& a) { return a.trigger != 0; }) +BRIDGE_REGISTER_VALIDATOR(FlakyAction, [](const FlakyAction& a) { return a.mode >= 0; }) +BRIDGE_REGISTER_VALIDATOR(MixedAction, [](const MixedAction& a) { + return !a.name.empty() && a.inner.n != 0 && a.count != 0; }) using SyncExecutor = morph::testing::InlineExecutor; From ff3ed2e3ecfc9699332b6e82817df15bd5ccb4fb Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 08:23:41 +0200 Subject: [PATCH 4/6] fix: remaining CI strict-compilation blockers - compiler_options.cmake: drop -Warray-bounds=2 and -Wstringop-overflow=4 (GCC 14 ICE on glaze templates); add clang-cl suppressions for covered-switch-default, documentation-unknown-command, padded, unique-object-duplication, c2y-extensions, missing-noreturn; add _CRT_SECURE_NO_WARNINGS for MSVC - logger.hpp: remove backtick-wrapped escape sequences from doxygen comments to avoid clang -Wdocumentation-unknown-command - ci.yml: pass -warnings-as-errors=* to clang-tidy via -extra-arg (clang-tidy-diff.py doesn't recognize the flag directly) --- .github/workflows/ci.yml | 2 +- cmake/compiler_options.cmake | 12 +++++++++--- include/morph/logger.hpp | 6 +++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49484cf..00a0160 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,7 +355,7 @@ jobs: -p1 \ -j "$(nproc)" \ -extra-arg=-std=c++23 \ - -warnings-as-errors='*' \ + -extra-arg=-warnings-as-errors=* \ -quiet \ 2>&1 | tee clang-tidy-report.txt diff --git a/cmake/compiler_options.cmake b/cmake/compiler_options.cmake index 10cefb8..2b391cf 100644 --- a/cmake/compiler_options.cmake +++ b/cmake/compiler_options.cmake @@ -36,6 +36,12 @@ function(apply_warnings target) -Wno-global-constructors -Wno-shadow-uncaptured-local -Wno-unused-command-line-argument + -Wno-covered-switch-default + -Wno-documentation-unknown-command + -Wno-padded + -Wno-unique-object-duplication + -Wno-c2y-extensions + -Wno-missing-noreturn >> # ── GNU / Clang (common baseline) ──────────────────────────────────── $<$: @@ -62,7 +68,6 @@ function(apply_warnings target) target_compile_options(${target} PRIVATE $<$:-Werror> $<$:/WX> - # ── GNU / Clang (common strict) ────────────────────────────────── $<$: -Wcast-qual @@ -89,9 +94,7 @@ function(apply_warnings target) -Wlogical-op -Wuseless-cast -Wrestrict - -Warray-bounds=2 -Walloc-zero - -Wstringop-overflow=4 -Wstringop-truncation -Wstrict-overflow=5 -Wsuggest-override @@ -110,6 +113,9 @@ function(apply_warnings target) > ) endif() + target_compile_definitions(${target} PRIVATE + $<$:_CRT_SECURE_NO_WARNINGS> + ) endfunction() function(apply_sanitizers target mode) diff --git a/include/morph/logger.hpp b/include/morph/logger.hpp index f040680..98795a1 100644 --- a/include/morph/logger.hpp +++ b/include/morph/logger.hpp @@ -53,11 +53,11 @@ using Logger = std::function; /// lines (log injection). /// /// A message is a single logical record; the default sink emits it as one line. -/// User-controlled text containing `\n`, `\r`, or other C0 control bytes could +/// User-controlled text containing \n, \r, or other C0 control bytes could /// otherwise splice a forged `[ERROR] ...` line into the stream or corrupt a /// line-oriented log parser. This replaces CR/LF/TAB with their C-style escapes -/// (`\n`, `\r`, `\t`) and any remaining control byte (`< 0x20`, or `0x7f` DEL) -/// with a `\xHH` escape, leaving printable text (including non-ASCII UTF-8 +/// (\n, \r, \t) and any remaining control byte (< 0x20, or 0x7f DEL) +/// with a \xHH escape, leaving printable text (including non-ASCII UTF-8 /// continuation bytes `>= 0x80`) untouched. It is a cheap single pass with no /// per-byte escaping work on the common (clean) path (just one string copy). /// @param msg Raw message to sanitize. From ca4a8cc06d77d100c7d38ff78188dd4c536ce9d0 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 08:42:32 +0200 Subject: [PATCH 5/6] fix: drop -Wrestrict (GCC 14 ICE), reorder clang-cl suppressions after -Wall - -Wrestrict triggers GCC 14 ICE on glaze template instantiations - clang-cl -Wno-* suppressions must come after -Wall/-Wextra in the same compile_options call, otherwise -Wall re-enables the warnings --- cmake/compiler_options.cmake | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cmake/compiler_options.cmake b/cmake/compiler_options.cmake index 2b391cf..e5bf35c 100644 --- a/cmake/compiler_options.cmake +++ b/cmake/compiler_options.cmake @@ -36,12 +36,6 @@ function(apply_warnings target) -Wno-global-constructors -Wno-shadow-uncaptured-local -Wno-unused-command-line-argument - -Wno-covered-switch-default - -Wno-documentation-unknown-command - -Wno-padded - -Wno-unique-object-duplication - -Wno-c2y-extensions - -Wno-missing-noreturn >> # ── GNU / Clang (common baseline) ──────────────────────────────────── $<$: @@ -62,6 +56,18 @@ function(apply_warnings target) > ) + # ── clang-cl suppressions (must come *after* -Wall/-Wextra which re-enable them) ── + target_compile_options(${target} PRIVATE + $<$:$<$: + -Wno-covered-switch-default + -Wno-documentation-unknown-command + -Wno-padded + -Wno-unique-object-duplication + -Wno-c2y-extensions + -Wno-missing-noreturn + >> + ) + # ── Strict mode: warnings-as-errors + maximum diagnostics ────────────── # Controlled by MORPH_ENABLE_STRICT_COMPILATION (default ON in CI). if(MORPH_ENABLE_STRICT_COMPILATION) @@ -93,7 +99,6 @@ function(apply_warnings target) -Wduplicated-branches -Wlogical-op -Wuseless-cast - -Wrestrict -Walloc-zero -Wstringop-truncation -Wstrict-overflow=5 From a347641cfe2e459ac8c71f0f73c9bfa7743ebbd6 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 09:21:38 +0200 Subject: [PATCH 6/6] ci: bump compilers to GCC 15 and Clang 22 - Install GCC 15 from ubuntu-toolchain-r/test PPA - Install Clang 22 from apt.llvm.org - Update all jobs: linux-compilers, linux-qt, valgrind, docs --- .github/workflows/ci.yml | 35 ++++++---- .github/workflows/docs.yml | 9 ++- cmake/compiler_options.cmake | 122 ++++++++++++++++++++++++----------- include/morph/forms.hpp | 2 +- include/morph/quantity.hpp | 6 +- scripts/coverage.sh | 17 +++-- tests/CMakeLists.txt | 5 +- tests/qt/CMakeLists.txt | 1 + 8 files changed, 134 insertions(+), 63 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00a0160..b7bbc77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" SCCACHE_DIR: /home/runner/.cache/sccache - CLANG_VERSION: "20" + CLANG_VERSION: "22" jobs: # ── Windows: MSVC + clang-cl ────────────────────────────────────────── @@ -65,13 +65,16 @@ jobs: key: apt-compilers-${{ matrix.preset }}-${{ hashFiles('.github/workflows/ci.yml') }} restore-keys: apt-compilers-${{ matrix.preset }}- - - name: Install GCC 14 and ninja + - name: Install GCC 15 and ninja if: startsWith(matrix.preset, 'gcc-') run: | sudo apt-get update -q - sudo apt-get install -y gcc-14 g++-14 ninja-build catch2 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 14 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 14 + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update -q + sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 - name: Install Clang ${{ env.CLANG_VERSION }} from apt.llvm.org if: startsWith(matrix.preset, 'clang-') @@ -202,13 +205,16 @@ jobs: key: apt-qt-${{ hashFiles('.github/workflows/ci.yml') }} restore-keys: apt-qt- - - name: Install GCC 14, ninja, catch2, Qt6 WebSockets + - name: Install GCC 15, ninja, catch2, Qt6 WebSockets run: | sudo apt-get update -q - sudo apt-get install -y gcc-14 g++-14 ninja-build catch2 \ + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update -q + sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 \ qt6-base-dev qt6-websockets-dev qt6-tools-dev libgl1-mesa-dev - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 14 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 14 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 - name: Cache sccache uses: actions/cache@v4 @@ -251,12 +257,15 @@ jobs: key: apt-valgrind-${{ hashFiles('.github/workflows/ci.yml') }} restore-keys: apt-valgrind- - - name: Install GCC 14, ninja, catch2, valgrind + - name: Install GCC 15, ninja, catch2, valgrind run: | sudo apt-get update -q - sudo apt-get install -y gcc-14 g++-14 ninja-build catch2 valgrind - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 14 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 14 + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update -q + sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 valgrind + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 - name: Cache sccache uses: actions/cache@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 092f4b7..f0bf310 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,9 +34,12 @@ jobs: - name: Install dependencies run: | sudo apt-get update -q - sudo apt-get install -y cmake ninja-build doxygen g++-14 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 14 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 14 + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update -q + sudo apt-get install -y cmake ninja-build doxygen g++-15 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 - name: Configure (docs target only) run: | diff --git a/cmake/compiler_options.cmake b/cmake/compiler_options.cmake index e5bf35c..06ea3a8 100644 --- a/cmake/compiler_options.cmake +++ b/cmake/compiler_options.cmake @@ -26,19 +26,13 @@ function(apply_warnings target) /w14928 # illegal copy-initialization /wd4068 # suppress: unknown pragma (e.g. clang pragmas in shared headers) > - # ── clang-cl (clang on Windows, MSVC ABI) ──────────────────────────── - $<$:$<$: - -Wno-c++98-compat - -Wno-c++98-compat-pedantic - -Wno-pre-c++17-compat - -Wno-ctad-maybe-unsupported - -Wno-exit-time-destructors - -Wno-global-constructors - -Wno-shadow-uncaptured-local - -Wno-unused-command-line-argument - >> - # ── GNU / Clang (common baseline) ──────────────────────────────────── - $<$: + # ── Clang: -Weverything, then subtract the noise ───────────────────── + # Enable every warning Clang has (Linux clang and clang-cl alike), so a + # compiler upgrade opts us into new diagnostics automatically. The + # suppressions below are the *only* categories we turn back off; anything + # not listed stays an error under -Werror. + $<$:-Weverything> + $<$: -Wall -Wextra -Wpedantic @@ -51,20 +45,52 @@ function(apply_warnings target) -Wconversion -Wsign-conversion -Wmisleading-indentation - -Wnull-dereference -Wimplicit-fallthrough > + # -Wnull-dereference: Clang gets it free via -Weverything. On GCC the + # optimizer emits false positives from inside libstdc++ (std::function) + # at -O2+, and the diagnostic leaks out of the system header (it runs + # post-inlining) where -isystem cannot suppress it, and it is a no-op at + # -O0 — so it is left off for GCC entirely. ) - # ── clang-cl suppressions (must come *after* -Wall/-Wextra which re-enable them) ── + # ── Clang -Weverything suppressions (must come *after* -Weverything) ───── target_compile_options(${target} PRIVATE - $<$:$<$: - -Wno-covered-switch-default + $<$: + # (a) Back-compat warnings — irrelevant: we target C++23, so being + # "incompatible with C++98/14/17/20" is the whole point. + -Wno-c++98-compat + -Wno-c++98-compat-pedantic + -Wno-pre-c++14-compat + -Wno-pre-c++17-compat + -Wno-pre-c++17-compat-pedantic + -Wno-pre-c++20-compat + -Wno-pre-c++20-compat-pedantic + # (b) Inherent to a header-only, templated library. + -Wno-weak-vtables # vtable emitted per TU for inline-virtual classes + -Wno-ctad-maybe-unsupported # CTAD on types without explicit deduction guides + -Wno-padded # struct tail/inter-member padding + -Wno-exit-time-destructors # function-local statics with non-trivial dtors + -Wno-global-constructors # non-trivial namespace-scope initializers + # (c) Stylistic / opinionated noise, not defects. + -Wno-missing-noreturn + -Wno-nrvo # not eliding a trivial-type copy on return + -Wno-shadow-uncaptured-local # lambda param shadowing an uncaptured local -Wno-documentation-unknown-command - -Wno-padded + -Wno-unsafe-buffer-usage # flags all pointer arithmetic; needs a hardened API + -Wno-unsafe-buffer-usage-in-libc-call + -Wno-float-equal # exact == is intentional in the value/rational tests + # (d) Conflicts with a warning we deliberately keep. + -Wno-covered-switch-default # collides with -Wswitch-enum + -Wswitch-default + # (e) Third-party test macros. + -Wno-c2y-extensions # Catch2 TEST_CASE expands __COUNTER__ + -Wno-unused-member-function # Catch2/test-fixture helper members + -Wno-unneeded-member-function + > + # clang-cl only: driver noise from the MSVC-mode command line. + $<$:$<$: -Wno-unique-object-duplication - -Wno-c2y-extensions - -Wno-missing-noreturn + -Wno-unused-command-line-argument >> ) @@ -74,8 +100,11 @@ function(apply_warnings target) target_compile_options(${target} PRIVATE $<$:-Werror> $<$:/WX> - # ── GNU / Clang (common strict) ────────────────────────────────── - $<$: + # Clang needs no extra named flags here — -Weverything (above) already + # supersets every one of these. This block is the GCC equivalent of + # "-Weverything": the widest practical set, since GCC has no such flag. + $<$: + # Diagnostics also carried on Clang via -Weverything. -Wcast-qual -Wformat=2 -Wredundant-decls @@ -91,35 +120,54 @@ function(apply_warnings target) -Wformat-nonliteral -Wmissing-declarations -Warray-bounds - > - - # ── GCC-only (strict) ──────────────────────────────────────────── - $<$: + # GCC-specific analyses. -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wuseless-cast -Walloc-zero -Wstringop-truncation - -Wstrict-overflow=5 -Wsuggest-override -Wsubobject-linkage -Wtrampolines -Wconditionally-supported - > - - # ── Clang-only (strict) ────────────────────────────────────────── - $<$: - -Wheader-hygiene - -Wdocumentation - -Wcomma - -Wdeprecated - -Wshorten-64-to-32 + # Widened set (max diagnostics). + -Wstrict-overflow=2 + -Wformat-overflow=2 + -Wformat-truncation=2 + -Wformat-signedness + -Wshift-overflow=2 + -Wstringop-overflow=4 + # -Wnoexcept is intentionally omitted: it leaks out of libstdc++ + # when Catch2's (potentially-throwing) test lambdas + # are passed to std::is_nothrow_invocable, which is noise from + # third-party code, not a defect in ours. + -Wnoexcept-type + -Wredundant-tags + -Wmismatched-tags + -Wvolatile + -Wzero-as-null-pointer-constant + -Wextra-semi + -Wsign-promo + -Wcatch-value=3 + -Wplacement-new=2 + -Wunused-const-variable=2 + -Wdisabled-optimization + -Wenum-conversion + -Warith-conversion + -Wdate-time + -Wattribute-alias=2 + -Wcast-align=strict + -Wunsafe-loop-optimizations > ) endif() + # ${MSVC} is true for both cl and clang-cl (any compiler targeting the MSVC + # runtime), so clang-cl also gets _CRT_SECURE_NO_WARNINGS — its CXX_COMPILER_ID + # is "Clang", which the CXX_COMPILER_ID:MSVC genex would miss, leaving CRT + # functions like fopen flagged as deprecated under -Werror. target_compile_definitions(${target} PRIVATE - $<$:_CRT_SECURE_NO_WARNINGS> + $<$:_CRT_SECURE_NO_WARNINGS> ) endfunction() diff --git a/include/morph/forms.hpp b/include/morph/forms.hpp index a450021..56f7c6b 100644 --- a/include/morph/forms.hpp +++ b/include/morph/forms.hpp @@ -240,7 +240,7 @@ constexpr void reconcileDeclaredPrecision(A& action) { constexpr auto memberCount = glz::reflect::size; auto memberTie = glz::to_tie(action); [&](std::index_sequence) { - auto retag = [&]() { + [[maybe_unused]] auto retag = [&]() { auto& member = glz::get_member(action, get(memberTie)); using Member = std::remove_cvref_t; if constexpr (units::isQuantity) { diff --git a/include/morph/quantity.hpp b/include/morph/quantity.hpp index cfbdc77..4d7e131 100644 --- a/include/morph/quantity.hpp +++ b/include/morph/quantity.hpp @@ -518,7 +518,7 @@ struct Context { #if MORPH_QUANTITY_PROVENANCE #define MORPH_Q_NODE(quantity) (quantity)._ctx.node #define MORPH_Q_BUILD(out, op, lhsValue, rhsValue, resultValue, leftNode, rightNode) \ - { \ + do { \ auto morphProvNode = std::make_shared<::morph::units::detail::ASTNode>(); \ morphProvNode->current.operation = (op); \ morphProvNode->current.lhs = (lhsValue); \ @@ -527,13 +527,13 @@ struct Context { morphProvNode->left = (leftNode); \ morphProvNode->right = (rightNode); \ (out)._ctx.node = std::move(morphProvNode); \ - } + } while (0) #else #define MORPH_Q_NODE(quantity) nullptr #define MORPH_Q_BUILD(out, op, lhsValue, rhsValue, resultValue, leftNode, rightNode) \ - {} + do { } while (0) #endif /// @endcond diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 88572c0..9133fe3 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -3,6 +3,13 @@ # Run from repo root after: cmake --build --preset clang-coverage && ctest --preset clang-coverage set -euo pipefail +# Match the toolchain to the Clang the coverage build used. CI exports +# CLANG_VERSION (e.g. 22) so we pick llvm-profdata-22 / llvm-cov-22; locally, +# with CLANG_VERSION unset, fall back to the unversioned tools on PATH. +SUFFIX="${CLANG_VERSION:+-${CLANG_VERSION}}" +LLVM_PROFDATA="llvm-profdata${SUFFIX}" +LLVM_COV="llvm-cov${SUFFIX}" + OUT="build/clang-coverage" TEST_EXE="$OUT/tests/morph_tests" MERGED="$OUT/merged.profdata" @@ -20,10 +27,10 @@ if [ -z "$PROFILES" ]; then exit 1 fi -llvm-profdata-20 merge -sparse $PROFILES -o "$MERGED" +${LLVM_PROFDATA} merge -sparse $PROFILES -o "$MERGED" mkdir -p "$REPORT_DIR" -llvm-cov-20 show "$TEST_EXE" \ +${LLVM_COV} show "$TEST_EXE" \ -instr-profile="$MERGED" \ -format=html \ -output-dir="$REPORT_DIR" \ @@ -31,11 +38,11 @@ llvm-cov-20 show "$TEST_EXE" \ echo "Coverage report: $REPORT_DIR/index.html" -llvm-cov-20 report "$TEST_EXE" \ +${LLVM_COV} report "$TEST_EXE" \ -instr-profile="$MERGED" \ "$SOURCES" -llvm-cov-20 export "$TEST_EXE" \ +${LLVM_COV} export "$TEST_EXE" \ -instr-profile="$MERGED" \ -format=lcov \ "$SOURCES" \ @@ -47,7 +54,7 @@ llvm-cov-20 export "$TEST_EXE" \ # header-only templated code. Collapse them to one record per source branch, # matching the aggregate that `llvm-cov report` already prints above. Branch # coverage is preserved (not skipped); only the per-instantiation noise is removed. -llvm-cov-20 export "$TEST_EXE" \ +${LLVM_COV} export "$TEST_EXE" \ -instr-profile="$MERGED" \ "$SOURCES" \ > "$OUT/coverage.json" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1ada488..144c206 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,4 +62,7 @@ if(AF_COVERAGE) endif() include(Catch) -catch_discover_tests(morph_tests DISCOVERY_MODE PRE_TEST) +# Every test is sub-second in practice; cap each at 120s so a hung/deadlocked +# test fails fast instead of stalling the whole CI job (observed: a single test +# hanging blocked a Windows runner for over half an hour). +catch_discover_tests(morph_tests DISCOVERY_MODE PRE_TEST PROPERTIES TIMEOUT 120) diff --git a/tests/qt/CMakeLists.txt b/tests/qt/CMakeLists.txt index c651b20..840191f 100644 --- a/tests/qt/CMakeLists.txt +++ b/tests/qt/CMakeLists.txt @@ -44,4 +44,5 @@ cmake_path(GET _qt_core_dll PARENT_PATH _qt_bin_dir) catch_discover_tests(morph_qt_tests DISCOVERY_MODE POST_BUILD DL_PATHS "${_qt_bin_dir}" + PROPERTIES TIMEOUT 120 )