From 5efb96a556805507c77f6c936e01e8b4edf0715f Mon Sep 17 00:00:00 2001 From: Gary Wolfman Date: Thu, 9 Jul 2026 21:46:53 -0400 Subject: [PATCH 1/3] refactor: relocate C bridge to top-level bridge/ (#22) Move frontend/macos/bridge/ to a new top-level bridge/ directory, sibling to libew/ewpresenter/frontend, since the bridge is plain C++20 with no macOS-specific code and is consumed by both the macOS Swift frontend and ewpresenter/tests/test_bridge.cpp. - Root CMakeLists.txt now builds bridge/ unconditionally instead of gating it behind if(APPLE). - ewpresenter/tests/CMakeLists.txt: test_bridge is no longer gated behind if(APPLE) either, since nothing in the bridge or its test depends on macOS APIs. Updated its include path. - frontend/macos/CMakeLists.txt: updated the bridge include paths to point at the new top-level bridge/ directory. - Updated AGENTS.md, README.md, and frontend/macos/README.md to document the new bridge/ location. Co-Authored-By: Oz --- .clang-tidy | 2 +- AGENTS.md | 14 ++++++++++ CMakeLists.txt | 11 ++++---- README.md | 4 ++- .../macos/bridge => bridge}/CMakeLists.txt | 0 .../macos/bridge => bridge}/ewcalc_bridge.cpp | 0 .../macos/bridge => bridge}/ewcalc_bridge.h | 0 ewpresenter/tests/CMakeLists.txt | 27 ++++++++++--------- ewpresenter/tests/test_formatter.cpp | 2 +- frontend/macos/CMakeLists.txt | 7 ++--- frontend/macos/README.md | 19 ++++++++----- 11 files changed, 57 insertions(+), 29 deletions(-) rename {frontend/macos/bridge => bridge}/CMakeLists.txt (100%) rename {frontend/macos/bridge => bridge}/ewcalc_bridge.cpp (100%) rename {frontend/macos/bridge => bridge}/ewcalc_bridge.h (100%) diff --git a/.clang-tidy b/.clang-tidy index 013d9a5..f554fc3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -9,7 +9,7 @@ # This is intentional and low-risk for three reasons: # 1. Every formatted string is < 20 characters and qualifies for SSO in # libc++ and MSVC STL, so no heap allocation occurs in practice. -# The assert in ewcalc_bridge.cpp copy_str() enforces the size bound +# The assert in bridge/ewcalc_bridge.cpp copy_str() enforces the size bound # in debug builds; if that assert ever fires this assumption must be # revisited. # 2. The noexcept annotation communicates "won't throw under normal diff --git a/AGENTS.md b/AGENTS.md index 50fd2c0..a550281 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,11 +102,17 @@ Three strict layers — each layer only depends on layers below it: ``` frontend/{macos,linux,windows} ← platform-native UI (SwiftUI / Qt6 / WinUI 3) ↓ +bridge ← plain-C API over ewpresenter (platform-agnostic) + ↓ ewpresenter ← presenter/viewmodel (platform-agnostic C++20) ↓ libew ← pure calculation library (no UI, no external deps) ``` +`bridge` is optional: only the macOS Swift frontend consumes it today (Swift +cannot import C++ directly). Linux and Windows frontends link `ewpresenter` +directly. + Both `libew` and `ewpresenter` compile to static libs (`build/lib/`). Platform frontends link against them. ### libew @@ -132,6 +138,14 @@ No platform types cross the ewpresenter boundary. Frontends bind to `set_on_chan `formatter.h` / `formatter.cpp` provide shared formatting helpers used by all presenters. +### bridge + +A plain-C API (`bridge/ewcalc_bridge.h/.cpp`) over `ewpresenter`: opaque +handles, value-type output structs with fixed-size string fields, and C +function-pointer callbacks. Lives at the top level (sibling to `libew`, +`ewpresenter`, `frontend`) since it's platform-agnostic and consumed by both +`ewpresenter/tests/test_bridge.cpp` and the macOS Swift frontend. + ### Test framework Tests use a zero-dependency framework in `libew/tests/test_main.h`. Each test file is an independent executable. Key macros: `TEST_MAIN()`, `RUN_TEST(fn)`, `ASSERT_NEAR(actual, expected, tol)`, `ASSERT_TRUE(expr)`. diff --git a/CMakeLists.txt b/CMakeLists.txt index cce1207..e0b2e04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,11 +36,12 @@ endif() add_subdirectory(libew) add_subdirectory(ewpresenter) -# macOS C bridge: build alongside native libs so the Xcode frontend project -# can import it with the correct deployment target (not set by Xcode generator). -if(APPLE) - add_subdirectory(frontend/macos/bridge) -endif() +# C bridge: platform-agnostic plain-C API over ewpresenter, consumed today by +# the macOS Swift frontend (built alongside native libs so the Xcode frontend +# project can import it with the correct deployment target, which the Xcode +# generator does not otherwise propagate) and available to any future +# frontend that needs a C ABI. +add_subdirectory(bridge) # ── GUI frontends ───────────────────────────────────────────────────────────── # Frontends are built by their native toolchains. CMake provides a convenience diff --git a/README.md b/README.md index 8402373..4e8f4fa 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ radar, and spread-spectrum communications — based on the EW101 series by David ## Architecture -Three layers with clean separation: +Layered, with clean separation: - **`libew`** — pure C++20 calculation library, no UI, no external dependencies - **`ewpresenter`** — platform-agnostic presenter/viewmodel layer +- **`bridge/`** — plain-C API over `ewpresenter`, used by the macOS Swift frontend + (Swift cannot import C++ directly); Linux and Windows link `ewpresenter` directly - **`frontend/`** — platform-native UIs: WinUI 3 (Windows), SwiftUI (macOS), Qt6 (Linux) ## libew modules diff --git a/frontend/macos/bridge/CMakeLists.txt b/bridge/CMakeLists.txt similarity index 100% rename from frontend/macos/bridge/CMakeLists.txt rename to bridge/CMakeLists.txt diff --git a/frontend/macos/bridge/ewcalc_bridge.cpp b/bridge/ewcalc_bridge.cpp similarity index 100% rename from frontend/macos/bridge/ewcalc_bridge.cpp rename to bridge/ewcalc_bridge.cpp diff --git a/frontend/macos/bridge/ewcalc_bridge.h b/bridge/ewcalc_bridge.h similarity index 100% rename from frontend/macos/bridge/ewcalc_bridge.h rename to bridge/ewcalc_bridge.h diff --git a/ewpresenter/tests/CMakeLists.txt b/ewpresenter/tests/CMakeLists.txt index 07ebdd2..a63a59e 100644 --- a/ewpresenter/tests/CMakeLists.txt +++ b/ewpresenter/tests/CMakeLists.txt @@ -29,17 +29,20 @@ else() endif() add_test(NAME test_formatter COMMAND test_formatter) -# C bridge integration tests (macOS only — bridge is not built on other platforms) -if(APPLE) - add_executable(test_bridge test_bridge.cpp) - target_link_libraries(test_bridge PRIVATE ewcalc_bridge ewpresenter) - target_include_directories(test_bridge PRIVATE - ${PROJECT_SOURCE_DIR}/libew/tests - ${PROJECT_SOURCE_DIR}/frontend/macos/bridge - ) - set_target_properties(test_bridge PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/tests - ) +# C bridge integration tests. The bridge is plain C++20 with no platform- +# specific code, so it's built and tested on every platform. +add_executable(test_bridge test_bridge.cpp) +target_link_libraries(test_bridge PRIVATE ewcalc_bridge ewpresenter) +target_include_directories(test_bridge PRIVATE + ${PROJECT_SOURCE_DIR}/libew/tests + ${PROJECT_SOURCE_DIR}/bridge +) +set_target_properties(test_bridge PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/tests +) +if(MSVC) + target_compile_options(test_bridge PRIVATE /W4 /permissive-) +else() target_compile_options(test_bridge PRIVATE -Wall -Wextra -Wpedantic) - add_test(NAME test_bridge COMMAND test_bridge) endif() +add_test(NAME test_bridge COMMAND test_bridge) diff --git a/ewpresenter/tests/test_formatter.cpp b/ewpresenter/tests/test_formatter.cpp index 583c35a..485c30d 100644 --- a/ewpresenter/tests/test_formatter.cpp +++ b/ewpresenter/tests/test_formatter.cpp @@ -1,7 +1,7 @@ /// @file test_formatter.cpp /// @brief Size-bound tests for every formatter function. /// -/// Two invariants must hold for the macOS C bridge to remain safe: +/// Two invariants must hold for the C bridge to remain safe: /// 1. Every formatter output fits in the 64-byte internal snprintf buffer /// used inside formatter.cpp (the `fmt()` helper). /// 2. Every formatter output fits in the 80-byte EWP_STR_MAX bridge buffers diff --git a/frontend/macos/CMakeLists.txt b/frontend/macos/CMakeLists.txt index 38c0cfb..8d9b51b 100644 --- a/frontend/macos/CMakeLists.txt +++ b/frontend/macos/CMakeLists.txt @@ -66,7 +66,7 @@ endforeach() add_library(ewcalc_bridge STATIC IMPORTED) set_target_properties(ewcalc_bridge PROPERTIES IMPORTED_LOCATION "${EWCALC_NATIVE_BUILD_DIR}/lib/libewcalc_bridge.a" - INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/bridge" + INTERFACE_INCLUDE_DIRECTORIES "${EWCALC_SRC_ROOT}/bridge" ) # ── Swift app bundle ────────────────────────────────────────────────────────── @@ -109,8 +109,9 @@ add_executable(ewcalc MACOSX_BUNDLE ${SWIFT_SOURCES} ${ICON_FILE}) # libraries that ewcalc_bridge depends on explicitly. target_link_libraries(ewcalc PRIVATE ewcalc_bridge ewpresenter_native libew_native c++) -# Include bridge/ so the bridging header can resolve #include "ewcalc_bridge.h" -target_include_directories(ewcalc PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/bridge") +# Include the top-level bridge/ so the bridging header can resolve +# #include "ewcalc_bridge.h" +target_include_directories(ewcalc PRIVATE "${EWCALC_SRC_ROOT}/bridge") set_target_properties(ewcalc PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/app/Info.plist" diff --git a/frontend/macos/README.md b/frontend/macos/README.md index 9951b67..d7d808d 100644 --- a/frontend/macos/README.md +++ b/frontend/macos/README.md @@ -1,17 +1,13 @@ # ewcalc — macOS Frontend (Phase 4) SwiftUI frontend for ewcalc. Consumes `libew` and `ewpresenter` through a -plain-C bridge (`ewcalc_bridge`) that Swift imports via a bridging header. +plain-C bridge (`ewcalc_bridge`, top-level `bridge/`) that Swift imports via +a bridging header. ## Project structure ``` frontend/macos/ -├── bridge/ -│ ├── ewcalc_bridge.h Plain-C API: opaque handles, value-type output structs, callbacks -│ ├── ewcalc_bridge.cpp Implements the C API on top of ewpresenter C++20 classes -│ └── CMakeLists.txt Built as a static lib by the native CMake step -│ ├── app/ │ ├── BridgingHeader.h Imports ewcalc_bridge.h into Swift │ ├── ewcalcApp.swift App entry point; injects EwCalcStore as environment object @@ -44,6 +40,17 @@ frontend/macos/ └── CMakeLists.txt Configures the Swift app bundle; lists SWIFT_SOURCES explicitly ``` +The bridge itself lives at the repo-top-level `bridge/` (sibling to `libew`, +`ewpresenter`, and `frontend`), since it is platform-agnostic and consumed by +tests as well as this frontend: + +``` +bridge/ +├── ewcalc_bridge.h Plain-C API: opaque handles, value-type output structs, callbacks +├── ewcalc_bridge.cpp Implements the C API on top of ewpresenter C++20 classes +└── CMakeLists.txt Built as a static lib by the root CMake build +``` + ## Build From the repo root: From 90bca7bc902287eb2ea484804d93ec05df62e75e Mon Sep 17 00:00:00 2001 From: Gary Wolfman Date: Thu, 9 Jul 2026 21:54:20 -0400 Subject: [PATCH 2/3] Add MSIX packaging manifest and CI packaging step (#24) Add frontend/windows/ewcalc-winui/ewcalc-winui/Package.appxmanifest declaring package Identity, Properties, Applications, and Capabilities for the single-project MSIX WinUI 3 app. Existing Assets/ logo and splash images already satisfy the referenced sizes. Wire scripts/build-windows.ps1's -Package flow into the build-windows CI job on tagged and manual (workflow_dispatch) runs, uploading the unsigned .msix as a workflow artifact for the release job to attach. Make build-windows.ps1's makeappx invocation pack from the "AppX" staging subfolder when present (per single-project MSIX build output), falling back to the bin root otherwise. Document MSIX packaging and signing in frontend/windows/ewcalc-winui/README.md, including how to generate a matching self-signed certificate for local/test packaging and the requirement for a trusted cert for public distribution. Co-Authored-By: Oz --- .github/workflows/ci.yml | 18 +++++- frontend/windows/ewcalc-winui/README.md | 50 +++++++++++++++++ .../ewcalc-winui/Package.appxmanifest | 55 +++++++++++++++++++ scripts/build-windows.ps1 | 12 +++- 4 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 frontend/windows/ewcalc-winui/ewcalc-winui/Package.appxmanifest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1dcc24..8d55dce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,9 +36,18 @@ jobs: # MSBuild /restore and supply all build-time headers and libs. - name: Build (native libs + frontend) + if: "!(startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')" shell: pwsh run: scripts\build-windows.ps1 -Config Release + # Package as MSIX on tagged/manual runs so the unsigned .msix is available + # as a release artifact (see frontend/windows/ewcalc-winui/README.md for + # signing instructions — CI packaging is intentionally unsigned). + - name: Build and package (release) + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + shell: pwsh + run: scripts\build-windows.ps1 -Config Release -Package + - name: Run tests shell: pwsh run: | @@ -46,8 +55,13 @@ jobs: cmake --build build --config Release ctest --test-dir build --build-config Release --output-on-failure - # MSIX packaging deferred: requires Package.appxmanifest, logo assets, - # and a valid Publisher certificate. Address when preparing for distribution. + - name: Upload MSIX + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: ewcalc-windows-msix + path: build/msix/*.msix + retention-days: 90 # ── macOS ─────────────────────────────────────────────────────────────────── build-macos: diff --git a/frontend/windows/ewcalc-winui/README.md b/frontend/windows/ewcalc-winui/README.md index 877b341..f84b721 100644 --- a/frontend/windows/ewcalc-winui/README.md +++ b/frontend/windows/ewcalc-winui/README.md @@ -88,12 +88,62 @@ Each item's `NoiseFigureDb` and `GainDb` setters call `PushStages()` which rebuilds the full `StageInput[]` and calls `ReceiverAdapter.SetStages()`. Add/Remove buttons are bound to `ICommand` properties. +## MSIX packaging and signing + +`Package.appxmanifest` declares the app's package identity (`Identity`, +`Properties`, `Applications`, `Capabilities`) for MSIX packaging. Logo and +splash assets already exist under `Assets/` at the sizes the manifest +references (`Square44x44Logo`, `Square150x150Logo`, `Wide310x150Logo`, +`Square71x71Logo`, `Square310x310Logo`, `StoreLogo`, `SplashScreen`). + +`scripts/build-windows.ps1` builds, packages, and optionally signs in one +pass: + +```powershell +.\scripts\build-windows.ps1 -Package # build + unsigned .msix +.\scripts\build-windows.ps1 -Package -Sign -CertThumbprint # + sign +``` + +The unsigned `.msix` is written to `build\msix\`. CI (`.github/workflows/ci.yml`, +`build-windows` job) runs the same `-Package` flow on tagged commits and manual +(`workflow_dispatch`) runs, and uploads the `.msix` as a workflow artifact; the +`release` job attaches it to the GitHub Release. CI packaging is unsigned — +signing is a local/distribution step. + +**`Identity/Publisher` must exactly match the Subject of the certificate used +to sign the package**, or installation fails. To generate a matching +self-signed certificate for local/test packaging: + +```powershell +New-SelfSignedCertificate -Type Custom -Subject "CN=Gary Wolfman" ` + -KeyUsage DigitalSignature -FriendlyName "ewcalc test signing" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -TextExtension @("2.5.29.19={text}CA=false", "2.5.29.37={text}1.3.6.1.5.5.7.3.3") +``` + +A self-signed certificate is sufficient for local sideloading and internal +testing (the target machine must trust the cert, e.g. via `Add-AppDevPackage.ps1` +or importing it into the Trusted People store). **Public distribution requires +a trusted certificate** from a CA (standard code-signing or EV), matching how +the macOS build requires a Developer ID Application certificate for +notarization (see `frontend/macos/CMakeLists.txt` and the `build-macos` job in +`.github/workflows/ci.yml`). There is no Store-specific step here — Microsoft +Store submission re-signs the package with its own certificate at ingestion. + ## Current state (v0.8.0) This frontend covers all nine calculator pages (Propagation, Antenna, Link Budget, Receiver, Jamming, Location, Radar, Digital/DSSS, Reference) at v0.6 parity with macOS, with the v0.7.0 and v0.8.0 changes below layered on top. +## Completed in v0.9.0 + +Added `Package.appxmanifest` (Identity/Properties/Applications/Capabilities) +for MSIX packaging, wired `-Package` into the `build-windows` CI job on tagged +and manual runs with `.msix` artifact upload, and documented signing (#24). +The v0.5 changelog entry below has been corrected: the manifest did not exist +until this release. + ## Completed in v0.8.0 All 8 calculator pages (Propagation, Antenna, Link, Receiver, Jamming, Location, Radar, diff --git a/frontend/windows/ewcalc-winui/ewcalc-winui/Package.appxmanifest b/frontend/windows/ewcalc-winui/ewcalc-winui/Package.appxmanifest new file mode 100644 index 0000000..bba4462 --- /dev/null +++ b/frontend/windows/ewcalc-winui/ewcalc-winui/Package.appxmanifest @@ -0,0 +1,55 @@ + + + + + + + + EW Calculator + Gary Wolfman + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 index e880194..a80e1e9 100644 --- a/scripts/build-windows.ps1 +++ b/scripts/build-windows.ps1 @@ -109,7 +109,17 @@ if ($Package) { $MsixOutput = Join-Path $MsixDir "ewcalc-$Platform-$Config.msix" - & $MakeAppx pack /d $FrontendBin /p $MsixOutput /nv /o + # Single-project MSIX (EnableMsixTooling) stages the resolved AppxManifest.xml + # and payload under an "AppX" subfolder of the build output on Build (not just + # Publish); pack from there when present, falling back to the bin root for + # older/alternate layouts. + $PackSource = Join-Path $FrontendBin "AppX" + if (-not (Test-Path (Join-Path $PackSource "AppxManifest.xml"))) { + $PackSource = $FrontendBin + } + Write-Host " Packaging from: $PackSource" -ForegroundColor Gray + + & $MakeAppx pack /d $PackSource /p $MsixOutput /nv /o if ($LASTEXITCODE -ne 0) { throw "makeappx failed (exit $LASTEXITCODE)" } Write-Host " MSIX: $MsixOutput" -ForegroundColor Green From 5ddc741cdb2c12608d0fc0daf1084e08ba071a42 Mon Sep 17 00:00:00 2001 From: Gary Wolfman Date: Thu, 9 Jul 2026 22:07:04 -0400 Subject: [PATCH 3/3] ci: add sanitizer, static-analysis, and coverage CI jobs (#23, #25) Add three independent, parallel jobs to .github/workflows/ci.yml, all running on ubuntu-24.04: - sanitizers: build libew/ewpresenter/tests with ASan+UBSan and run ctest under instrumentation. - static-analysis: run clang-tidy (using .clang-tidy) and cppcheck against libew/ewpresenter sources. - coverage: build with Clang source-based coverage instrumentation, run ctest, generate an HTML report (uploaded as a CI artifact), and fail if libew+ewpresenter line coverage drops below 75%. Add EWCALC_BUILD_COVERAGE CMake option to gate coverage instrumentation flags (GCC --coverage or Clang -fprofile-instr-generate/-fcoverage-mapping) so they aren't hardcoded in the workflow YAML. Fix .clang-tidy: the previous config's Checks value enabled zero checks (verified with --list-checks), so clang-tidy was a silent no-op. Enable clang-diagnostic-*, clang-analyzer-*, bugprone-*, performance-*, and portability-* with three documented suppressions: the existing bugprone-exception-escape (confirmed still warranted), plus new suppressions for bugprone-easily-swappable-parameters (physics functions legitimately take several same-typed double parameters) and portability-avoid-pragma-once (conflicts with the project's #pragma once convention). Fix the one resulting real clang-tidy finding: restrict PresenterBase's default constructor to the Derived CRTP type (bugprone-crtp-constructor-accessibility), and suppress a cppcheck false positive in formatter.cpp's negative-zero normalization (the value == 0.0 / value = 0.0 idiom flips the IEEE-754 sign bit, so it is not a no-op despite cppcheck flagging it as duplicateConditionalAssign). Co-Authored-By: Oz --- .clang-tidy | 35 +++- .github/workflows/ci.yml | 153 +++++++++++++++++- CMakeLists.txt | 21 ++- .../include/ewpresenter/presenter_base.h | 6 + ewpresenter/src/formatter.cpp | 2 + 5 files changed, 212 insertions(+), 5 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 013d9a5..7606c24 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,7 +1,15 @@ # .clang-tidy — root configuration for clang-tidy static analysis. # -# bugprone-exception-escape is suppressed globally. +# Checks enabled: clang-diagnostic-*, clang-analyzer-*, bugprone-*, +# performance-*, portability-*. This favors bug-finding checks over +# style/naming checks (readability-*, modernize-*, cppcoreguidelines-* +# are not enabled here since they mostly flag stylistic preferences — +# e.g. magic numbers in physics formulas, trailing return types — that +# are subjective rather than correctness issues for this codebase). # +# Three checks are suppressed: +# +# 1. bugprone-exception-escape # All eight presenter recompute() / fire() methods, PresenterBase::update_field(), # and format_regime() in formatter.cpp are marked noexcept. They call functions # that return std::string, which can theoretically throw std::bad_alloc. @@ -19,4 +27,27 @@ # 3. Removing noexcept from recompute() / fire() would expose them as # potentially-throwing in the PresenterBase CRTP contract, requiring # cascading changes to update_field() and all eight presenter headers. -Checks: '-bugprone-exception-escape' +# Still warranted: confirmed during v0.9.0 CI hardening (2026-07). +# +# 2. bugprone-easily-swappable-parameters +# libew's physics functions (e.g. jamming.cpp, radar.cpp) legitimately take +# several same-typed double parameters (e.g. signal_erp vs jammer_erp) because +# that mirrors the underlying RF equations. Redesigning these into distinct +# strong-typed parameter structs to satisfy this check is a larger API change +# than warranted for CI hardening; revisit if a real parameter-swap bug +# surfaces. +# +# 3. portability-avoid-pragma-once +# Conflicts with the project convention (AGENTS.md) of using #pragma once +# throughout for all headers. +Checks: > + clang-diagnostic-*, + clang-analyzer-*, + bugprone-*, + performance-*, + portability-*, + -bugprone-exception-escape, + -bugprone-easily-swappable-parameters, + -portability-avoid-pragma-once +WarningsAsErrors: '*' +HeaderFilterRegex: '(libew|ewpresenter)/(include|src)/.*' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1dcc24..060ae04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,158 @@ jobs: path: build/pkg/*.AppImage retention-days: 90 - # ── GitHub Release ─────────────────────────────────────────────────────────── + # ── Sanitizers (ASan/UBSan) ──────────────────────────────────────────────── + sanitizers: + name: Sanitizers (ASan/UBSan) + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build + + - name: Configure (ASan + UBSan) + run: | + cmake -B build -G Ninja \ + -DEWCALC_BUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" + + - name: Build + run: cmake --build build --parallel + + - name: Run tests under sanitizers + env: + ASAN_OPTIONS: detect_leaks=1 + UBSAN_OPTIONS: print_stacktrace=1 + run: ctest --test-dir build --output-on-failure + + # ── Static analysis (clang-tidy + cppcheck) ───────────────────────────────────────── + static-analysis: + name: Static analysis (clang-tidy + cppcheck) + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build clang-tidy cppcheck + + - name: Configure (generate compile_commands.json) + run: | + cmake -B build -G Ninja \ + -DEWCALC_BUILD_TESTS=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + # .clang-tidy (repo root) sets WarningsAsErrors, so clang-tidy exits + # non-zero on any enabled-check violation in libew/ewpresenter sources. + - name: Run clang-tidy + run: | + find libew/src ewpresenter/src -name '*.cpp' -print0 \ + | xargs -0 clang-tidy -p build + + - name: Run cppcheck + run: | + cppcheck --enable=warning,style,performance,portability \ + --std=c++20 --language=c++ --inline-suppr --error-exitcode=1 \ + -I libew/include -I ewpresenter/include \ + libew/src ewpresenter/src + + # ── Code coverage ──────────────────────────────────────────────────────── + coverage: + name: Code coverage + runs-on: ubuntu-24.04 + env: + # Initial baseline: measured ~79.5% line coverage for libew+ewpresenter + # (Clang/llvm-cov, macOS validation build). Set a few points below the + # measured value so the gate gives real regression protection without + # failing on day one; raise it as coverage improves. + COVERAGE_THRESHOLD: "75" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build clang llvm + + - name: Configure (coverage instrumentation) + run: | + cmake -B build -G Ninja \ + -DEWCALC_BUILD_TESTS=ON \ + -DEWCALC_BUILD_COVERAGE=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ + + - name: Build + run: cmake --build build --parallel + + - name: Run tests under instrumentation + run: | + mkdir -p build/profraw + LLVM_PROFILE_FILE="$(pwd)/build/profraw/%p.profraw" \ + ctest --test-dir build --output-on-failure + + - name: Generate coverage report + run: | + llvm-profdata merge -sparse build/profraw/*.profraw -o build/merged.profdata + + BINARIES=() + while IFS= read -r -d '' f; do + BINARIES+=("$f") + done < <(find build/bin -maxdepth 2 -type f -perm -u+x -not -path '*/CMakeFiles/*' -print0) + OBJECT_ARGS=() + for bin in "${BINARIES[@]:1}"; do + OBJECT_ARGS+=(-object "$bin") + done + + llvm-cov report "${BINARIES[0]}" "${OBJECT_ARGS[@]}" \ + -instr-profile=build/merged.profdata \ + -ignore-filename-regex='(/tests/|/harness/|frontend/)' + + llvm-cov show "${BINARIES[0]}" "${OBJECT_ARGS[@]}" \ + -instr-profile=build/merged.profdata \ + -ignore-filename-regex='(/tests/|/harness/|frontend/)' \ + -format=html -output-dir=coverage-html + + llvm-cov export "${BINARIES[0]}" "${OBJECT_ARGS[@]}" \ + -instr-profile=build/merged.profdata \ + -ignore-filename-regex='(/tests/|/harness/|frontend/)' \ + -summary-only > coverage-summary.json + + - name: Enforce coverage threshold + run: | + python3 - <<'EOF' + import json, os, sys + threshold = float(os.environ["COVERAGE_THRESHOLD"]) + with open("coverage-summary.json") as f: + data = json.load(f) + pct = data["data"][0]["totals"]["lines"]["percent"] + print(f"Line coverage: {pct:.2f}% (threshold: {threshold:.2f}%)") + if pct < threshold: + print("::error::Line coverage dropped below threshold") + sys.exit(1) + EOF + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: ewcalc-coverage-html + path: coverage-html/ + retention-days: 30 + + # ── GitHub Release ───────────────────────────────────────────────────────── release: name: Create GitHub Release needs: [ build-windows, build-macos, build-linux ] diff --git a/CMakeLists.txt b/CMakeLists.txt index cce1207..db26d5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,16 +18,33 @@ else() message(STATUS "ewcalc: Platform = Linux") endif() -# ── Options ─────────────────────────────────────────────────────────────────── +# ── Options ────────────────────────────────────────────────────────────── option(EWCALC_BUILD_TESTS "Build libew test suite" ON) option(EWCALC_BUILD_FRONTEND "Build platform GUI frontend" OFF) option(EWCALC_FRONTEND_CONFIG "MSBuild configuration (Windows)" "Release") +option(EWCALC_BUILD_COVERAGE "Instrument libew/ewpresenter/tests for code coverage (GCC+gcov or Clang+llvm-cov)" OFF) -# ── Test support ────────────────────────────────────────────────────────────── +# ── Test support ─────────────────────────────────────────────────────────── if(EWCALC_BUILD_TESTS) enable_testing() endif() +# ── Coverage instrumentation ────────────────────────────────────────────── +# Applied directory-wide (before add_subdirectory(libew/ewpresenter)) so every +# target built afterwards — libraries, harness, and tests — is instrumented. +# Flags are gated here rather than hardcoded in CI workflow YAML. +if(EWCALC_BUILD_COVERAGE) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + add_compile_options(-fprofile-instr-generate -fcoverage-mapping) + add_link_options(-fprofile-instr-generate -fcoverage-mapping) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(--coverage) + add_link_options(--coverage) + else() + message(WARNING "ewcalc: EWCALC_BUILD_COVERAGE is only supported with GCC or Clang — ignoring on ${CMAKE_CXX_COMPILER_ID}.") + endif() +endif() + # ── Core libraries (always built) ──────────────────────────────────────────── # These produce the static libs consumed by all platform frontends: # Windows : ewpresenter.lib + libew.lib → picked up by ewpresenter.net.vcxproj diff --git a/ewpresenter/include/ewpresenter/presenter_base.h b/ewpresenter/include/ewpresenter/presenter_base.h index 25118d1..6e4456b 100644 --- a/ewpresenter/include/ewpresenter/presenter_base.h +++ b/ewpresenter/include/ewpresenter/presenter_base.h @@ -38,6 +38,12 @@ namespace ewpresenter { /// - void fire() noexcept template class PresenterBase { +private: + /// Restricted to Derived: prevents constructing or slicing to + /// PresenterBase directly from outside the CRTP hierarchy. + PresenterBase() = default; + friend Derived; + protected: /// Assign @p value and @p validated to the referenced members, then /// trigger recompute and the on_change callback. diff --git a/ewpresenter/src/formatter.cpp b/ewpresenter/src/formatter.cpp index 2badcd7..0074cc4 100644 --- a/ewpresenter/src/formatter.cpp +++ b/ewpresenter/src/formatter.cpp @@ -8,6 +8,8 @@ namespace { // Format a double with a given number of decimal places and a unit label. std::string fmt(double value, int decimals, const char* unit) { // Normalize IEEE 754 negative zero so it never renders as "-0.0". + // cppcheck-suppress duplicateConditionalAssign + // -0.0 == 0.0 is true, but assigning 0.0 flips the sign bit; not a no-op. if (value == 0.0) value = 0.0; char buf[64]; std::snprintf(buf, sizeof(buf), "%.*f %s",