Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# .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.
#
# 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
Expand All @@ -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)/.*'
171 changes: 168 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,32 @@ 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: |
cmake -B build -DEWCALC_BUILD_TESTS=ON
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:
Expand Down Expand Up @@ -153,7 +167,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 ]
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)`.
Expand Down
32 changes: 25 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,11 +53,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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions ewpresenter/include/ewpresenter/presenter_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ namespace ewpresenter {
/// - void fire() noexcept
template<typename Derived>
class PresenterBase {
private:
/// Restricted to Derived: prevents constructing or slicing to
/// PresenterBase<Derived> 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.
Expand Down
2 changes: 2 additions & 0 deletions ewpresenter/src/formatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading