From 295b31f92e680213d9e4e8f567e12bb5f59d19e9 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sat, 21 Feb 2026 21:07:22 -0600 Subject: [PATCH 01/19] Add CI quality gates workflow and local quality gate script --- .github/workflows/quality_gates.yml | 28 ++ scripts/quality_gates.sh | 497 ++++++++++++++++++++++++++++ 2 files changed, 525 insertions(+) create mode 100644 .github/workflows/quality_gates.yml create mode 100755 scripts/quality_gates.sh diff --git a/.github/workflows/quality_gates.yml b/.github/workflows/quality_gates.yml new file mode 100644 index 00000000..b5d398e0 --- /dev/null +++ b/.github/workflows/quality_gates.yml @@ -0,0 +1,28 @@ +name: quality_gates + +on: + push: + pull_request: + +jobs: + quality_gates: + name: Quality gates + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + clang-format \ + clang-tidy \ + cmake \ + gcovr \ + ninja-build \ + python3 + + - name: Run quality gates + run: | + ./scripts/quality_gates.sh diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh new file mode 100755 index 00000000..ba5ac0e7 --- /dev/null +++ b/scripts/quality_gates.sh @@ -0,0 +1,497 @@ +#!/usr/bin/env bash + +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: scripts/quality_gates.sh [options] + +Options: + --help Show this help. + --jobs N Parallel job count for builds (default: CPU count). + --build-dir PATH Directory used for all quality builds (default: .quality_gates). + --coverage-min N Minimum line coverage percentage (default: 90). + --experimental-sanitizers Enable optional TSan/MSan stages. +EOF +} + +JOBS="${QUALITY_GATE_JOBS:-}" +BUILD_ROOT="${QUALITY_GATE_BUILD_ROOT:-${REPO_ROOT}/.quality_gates}" +COVERAGE_MIN="${QUALITY_GATE_COVERAGE_MIN:-90}" +EXPERIMENTAL_SANITIZERS="${QUALITY_GATE_EXPERIMENTAL_SANITIZERS:-0}" +CMAKE_GENERATOR="${QUALITY_GATE_CMAKE_GENERATOR:-}" +CXX_BIN="${QUALITY_GATE_CXX:-${CXX:-c++}}" +EXTRA_CXX_FLAGS="${QUALITY_GATE_EXTRA_CXX_FLAGS:-}" + +require_command() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Required command '$cmd' is missing." >&2 + exit 1 + fi +} + +resolve_tool() { + local tool_name="$1" + local brew_tool="" + + if command -v "${tool_name}" >/dev/null 2>&1; then + echo "${tool_name}" + return 0 + fi + + if [[ "${OSTYPE}" == "darwin"* ]]; then + brew_tool="$(brew --prefix llvm 2>/dev/null || true)/bin/${tool_name}" + if [[ -x "${brew_tool}" ]]; then + echo "${brew_tool}" + return 0 + fi + fi + + return 1 +} + +resolve_gcov_command() { + local family="$1" + if [[ "${family}" != "clang" ]]; then + echo "gcov" + return 0 + fi + + local llvm_cov + if command -v xcrun >/dev/null 2>&1; then + llvm_cov="$(xcrun -f llvm-cov 2>/dev/null || true)" + fi + if [[ -z "${llvm_cov}" ]]; then + llvm_cov="$(command -v llvm-cov 2>/dev/null || true)" + fi + + if [[ -n "${llvm_cov}" ]]; then + echo "${llvm_cov} gcov" + return 0 + fi + + echo "gcov" +} + +cpu_count() { + if command -v nproc >/dev/null 2>&1; then + nproc + elif command -v sysctl >/dev/null 2>&1; then + sysctl -n hw.nproc 2>/dev/null || echo 4 + else + echo 4 + fi +} + +detect_compiler_family() { + local version + if ! version="$("$CXX_BIN" --version 2>/dev/null)"; then + echo "unknown" + return + fi + + if [[ "$CXX_BIN" == "cl" || "$CXX_BIN" == "cl.exe" || "$CXX_BIN" == *"cl.exe"* ]] || [[ "$version" == *"Microsoft"* ]]; then + echo "msvc" + elif [[ "$CXX_BIN" == *clang* ]] || [[ "$version" == *"clang"* ]]; then + echo "clang" + elif [[ "$CXX_BIN" == *g++* ]] || [[ "$CXX_BIN" == *gcc* ]] || [[ "$version" == *"gcc"* ]]; then + echo "gcc" + else + echo "unknown" + fi +} + +require_defaults() { + REQUIREMENTS=( + cmake + ctest + find + git + ) + for requirement in "${REQUIREMENTS[@]}"; do + require_command "$requirement" + done + + CLANG_FORMAT_CMD="$(resolve_tool clang-format)" || { + echo "Required command 'clang-format' is missing." >&2 + exit 1 + } + + CLANG_TIDY_CMD="$(resolve_tool clang-tidy)" || { + echo "Required command 'clang-tidy' is missing." >&2 + exit 1 + } + + if ! command -v gcovr >/dev/null 2>&1 && ! command -v lcov >/dev/null 2>&1; then + echo "Required command 'gcovr' or 'lcov' is missing." >&2 + exit 1 + fi +} + +start_section() { + local section_name="$1" + echo + echo "------------------------------------------------------------" + echo "[${section_name}]" + echo "------------------------------------------------------------" +} + +run_build() { + local label="$1" + local cxx_standard="$2" + local use_exceptions="$3" + local with_tests="$4" + local with_examples="$5" + local cxx_flags="$6" + shift 6 + + local -a extra_args=("$@") + local build_dir="${BUILD_ROOT}/${label}" + + rm -rf "${build_dir}" + mkdir -p "${build_dir}" + + local -a cmake_args=( + -S "${REPO_ROOT}" + -B "${build_dir}" + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_CXX_COMPILER="${CXX_BIN}" + -DCMAKE_CXX_STANDARD="${cxx_standard}" + -DSML_BUILD_TESTS="${with_tests}" + -DSML_BUILD_EXAMPLES="${with_examples}" + -DSML_USE_EXCEPTIONS="${use_exceptions}" + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + -DCMAKE_CXX_FLAGS="${cxx_flags}" + -DCMAKE_CXX_FLAGS_DEBUG="${cxx_flags}" + ) + + if [[ -n "${CMAKE_GENERATOR}" ]]; then + cmake_args=(-G "${CMAKE_GENERATOR}" "${cmake_args[@]}") + fi + if ((${#extra_args[@]} > 0)); then + cmake_args+=("${extra_args[@]}") + fi + + cmake "${cmake_args[@]}" + cmake --build "${build_dir}" -j "${JOBS}" + + if [[ "${with_tests}" == "ON" ]]; then + ctest --test-dir "${build_dir}" --output-on-failure -j "${JOBS}" + fi +} + +run_format_gate() { + start_section "format" + require_command "${CLANG_FORMAT_CMD}" + + local supports_werror=0 + local format_count=0 + local file formatted + if "${CLANG_FORMAT_CMD}" --help 2>&1 | grep -q -- "--Werror"; then + supports_werror=1 + fi + + while IFS= read -r -d '' file; do + ((format_count++)) + if [[ "${supports_werror}" -eq 1 ]]; then + "${CLANG_FORMAT_CMD}" --dry-run --Werror "${file}" + else + formatted="$(mktemp)" + "${CLANG_FORMAT_CMD}" "${file}" > "${formatted}" + diff -u "${file}" "${formatted}" >/tmp/quality_format_${$}.log || { + rm -f "${formatted}" + echo "Formatting mismatch: ${file}" >&2 + cat /tmp/quality_format_${$}.log + return 1 + } + rm -f "${formatted}" + fi + done < <( + find "${REPO_ROOT}/example" "${REPO_ROOT}/test" \ + \( -name "*.hpp" -o -name "*.cpp" -o -name "*.h" \) -type f -print0 | sort -z + ) + + if ((format_count == 0)); then + echo "No source files found for format check." + return 1 + fi + + echo "Format checks passed." +} + +run_tidy_gate() { + start_section "clang-tidy" + require_command "${CLANG_TIDY_CMD}" + + local strict_flags="${BASE_CXX_FLAGS}" + local tidy_dir="${BUILD_ROOT}/lint" + + run_build "lint" 20 ON ON OFF "${strict_flags}" -DSML_BUILD_BENCHMARKS=OFF + + local tidy_count=0 + local file + local tidy_files=() + while IFS= read -r -d '' file; do + tidy_files+=("${file}") + ((tidy_count++)) + done < <( + { + find "${REPO_ROOT}/test/ft" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 + find "${REPO_ROOT}/test/ut" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 + find "${REPO_ROOT}/test/unit" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 + } | sort -z + ) + + if ((tidy_count == 0)); then + echo "No cpp files found for clang-tidy." + return 1 + fi + + local -a tidy_args=( + --quiet + --warnings-as-errors=* + ) + + if [[ "${CLANG_TIDY_CMD}" == "/opt/homebrew/opt/llvm/bin/clang-tidy" ]]; then + local sdk_path + sdk_path="$(xcrun --show-sdk-path 2>/dev/null || true)" + if [[ -n "${sdk_path}" ]]; then + tidy_args+=(--extra-arg=-isysroot "--extra-arg=${sdk_path}") + fi + fi + + "${CLANG_TIDY_CMD}" "${tidy_files[@]}" -p "${tidy_dir}" "${tidy_args[@]}" + echo "Clang-tidy checks passed." +} + +run_regression_matrix() { + start_section "regression matrix" + run_build "cxx20" 20 ON ON OFF "${BASE_CXX_FLAGS}" + run_build "no_exceptions" 20 OFF ON OFF "${BASE_CXX_FLAGS}" + run_build "cxx14" 14 ON OFF OFF "${BASE_CXX_FLAGS}" + echo "Regression build/test matrix passed." +} + +run_sanitizer_matrix() { + if [[ "${COMPILER_FAMILY}" == "msvc" ]]; then + echo "Skipping sanitizer matrix on MSVC." + return 0 + fi + + start_section "sanitizer matrix" + local asan_flags="${BASE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=address,undefined -fno-sanitize-trap=all -fno-sanitize-recover=all" + run_build "sanitizer_asan_ubsan" 20 ON ON OFF "${asan_flags}" -DSML_BUILD_BENCHMARKS=OFF + + if [[ "${EXPERIMENTAL_SANITIZERS}" -eq 1 ]]; then + local thread_flags="${BASE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=thread -fno-sanitize-trap=all -fno-sanitize-recover=all" + run_build "sanitizer_thread" 20 ON ON OFF "${thread_flags}" -DSML_BUILD_BENCHMARKS=OFF + + if [[ "${COMPILER_FAMILY}" == "clang" ]]; then + local mem_flags="${BASE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=memory -fno-sanitize-memory-track-origins=2 -fno-sanitize-recover=all" + run_build "sanitizer_memory" 20 ON ON OFF "${mem_flags}" -DSML_BUILD_BENCHMARKS=OFF + fi + fi + + echo "Sanitizer matrix passed." +} + +run_coverage_gate() { + if [[ "${COMPILER_FAMILY}" == "msvc" ]]; then + echo "Skipping coverage gate on MSVC." + return 0 + fi + + start_section "coverage" + + local gcov_cmd + gcov_cmd="$(resolve_gcov_command "${COMPILER_FAMILY}")" + + local coverage_flags="${BASE_CXX_FLAGS} --coverage" + local coverage_dir="${BUILD_ROOT}/coverage" + run_build "coverage" 20 ON ON OFF "${coverage_flags}" -DSML_BUILD_BENCHMARKS=OFF + + local coverage_percent="" + local coverage_report_file="${coverage_dir}/coverage.txt" + local coverage_report="" + local used_tool="" + local lcov_available=0 + local gcovr_available=0 + + if command -v lcov >/dev/null 2>&1; then + lcov_available=1 + elif command -v gcovr >/dev/null 2>&1; then + gcovr_available=1 + else + echo "Unable to collect coverage report (lcov or gcovr missing)." >&2 + return 1 + fi + + if [[ "${lcov_available}" -eq 1 ]]; then + local coverage_info="${coverage_dir}/coverage.info" + local coverage_extract="${coverage_dir}/coverage-filtered.info" + local lcov_report="${coverage_dir}/coverage-lcov-summary.txt" + local lcov_capture_errors="inconsistent,source,format,unsupported,empty,gcov" + local lcov_extract_errors="inconsistent,format,count,source,unsupported" + local lcov_summary_errors="inconsistent,corrupt,unsupported,count" + if LC_ALL=C lcov --capture --base-directory "${REPO_ROOT}" --directory "${coverage_dir}" \ + --output-file "${coverage_info}" \ + --ignore-errors "${lcov_capture_errors}" \ + >"${coverage_report_file}" 2>&1 && \ + LC_ALL=C lcov --extract "${coverage_info}" "${REPO_ROOT}/*" --output-file "${coverage_extract}" \ + --ignore-errors "${lcov_extract_errors}" \ + >>"${coverage_report_file}" 2>&1 && \ + lcov_summary="$(LC_ALL=C lcov --summary "${coverage_extract}" --ignore-errors "${lcov_summary_errors}" 2>&1)"; then + echo "${lcov_summary}" > "${lcov_report}" + used_tool="lcov" + coverage_report="$(cat "${coverage_report_file}")" + coverage_report="${coverage_report}"$'\n'"${lcov_summary}" + coverage_percent="$(printf '%s\n' "${lcov_summary}" | awk '/lines/ {for (i = 1; i <= NF; ++i) { if ($i ~ /%$/) { gsub(/%/, "", $i); print $i; exit } } }')" + else + coverage_report="$(cat "${coverage_report_file}")" + if [[ -f "${lcov_report}" ]]; then + coverage_report="${coverage_report}"$'\n'"$(cat "${lcov_report}")" + fi + echo "${coverage_report}" >&2 + fi + fi + + if [[ -z "${coverage_percent}" ]] && command -v gcovr >/dev/null 2>&1; then + gcovr_available=1 + fi + + if [[ -z "${coverage_percent}" ]] && [[ "${gcovr_available}" -eq 1 ]]; then + if gcovr --root "${REPO_ROOT}" "${coverage_dir}" --txt -j 1 \ + --gcov-executable "${gcov_cmd}" --gcov-ignore-errors all \ + >"${coverage_report_file}" 2>&1; then + used_tool="gcovr" + coverage_report="$(cat "${coverage_report_file}")" + coverage_percent="$(printf '%s\n' "${coverage_report}" | awk '/^TOTAL/{for(i=1;i<=NF;i++) if($i ~ /%$/){print $i; exit}}' | head -n 1 | tr -d '%')" + else + coverage_report="$(cat "${coverage_report_file}")" + if [[ "${lcov_available}" -eq 1 ]]; then + echo "gcovr failed, keeping lcov result if available." >&2 + else + echo "gcovr failed." >&2 + fi + echo "${coverage_report}" >&2 + fi + fi + + if [[ -z "${coverage_percent}" ]]; then + echo "Unable to collect coverage report." >&2 + echo "${coverage_report}" >&2 + return 1 + fi + + if [[ "${used_tool}" == "gcovr" ]]; then + echo "${coverage_report}" > "${coverage_report_file}" + elif [[ "${used_tool}" == "lcov" ]]; then + echo "${coverage_report}" > "${coverage_report_file}" + else + echo "Unknown coverage tool state after report generation." >&2 + return 1 + fi + + if [[ -z "${coverage_percent}" ]] || [[ "${coverage_percent}" == "--" ]]; then + echo "Unable to parse coverage percentage." >&2 + echo "${coverage_report}" >&2 + return 1 + fi + + if ! awk -v actual="${coverage_percent}" -v minimum="${COVERAGE_MIN}" 'BEGIN {exit (actual < minimum)}'; then + echo "Coverage ${coverage_percent}% is below minimum ${COVERAGE_MIN}%." >&2 + return 1 + fi + + echo "Coverage gate passed with ${coverage_percent}% (minimum ${COVERAGE_MIN}%) using ${used_tool}." +} + +run_benchmarks_gate() { + start_section "benchmarks" + run_build "benchmarks" 17 ON OFF OFF "${BASE_CXX_FLAGS}" -DSML_BUILD_BENCHMARKS=ON -DSML_BUILD_TESTS=OFF -DSML_BUILD_EXAMPLES=OFF + echo "Benchmark smoke build passed." +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --help) + usage + exit 0 + ;; + --jobs) + shift + JOBS="$1" + ;; + --build-dir) + shift + BUILD_ROOT="$1" + ;; + --coverage-min) + shift + COVERAGE_MIN="$1" + ;; + --experimental-sanitizers) + EXPERIMENTAL_SANITIZERS=1 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift + done +} + +parse_args "$@" + +if [[ -z "${JOBS}" ]]; then + JOBS="$(cpu_count)" +fi +mkdir -p "${BUILD_ROOT}" + +COMPILER_FAMILY="$(detect_compiler_family)" +if [[ "${COMPILER_FAMILY}" == "gcc" || "${COMPILER_FAMILY}" == "clang" ]]; then + BASE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic -pedantic-errors" +elif [[ "${COMPILER_FAMILY}" == "msvc" ]]; then + BASE_CXX_FLAGS="/W4 /WX /permissive-" +else + BASE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic -pedantic-errors" +fi +if [[ -n "${EXTRA_CXX_FLAGS}" ]]; then + BASE_CXX_FLAGS="${BASE_CXX_FLAGS} ${EXTRA_CXX_FLAGS}" +fi + +require_defaults + +start_section "summary" +echo "repo : ${REPO_ROOT}" +echo "build dir : ${BUILD_ROOT}" +echo "jobs : ${JOBS}" +echo "cxx : ${CXX_BIN}" +echo "compiler : ${COMPILER_FAMILY}" +echo "coverage : ${COVERAGE_MIN}%" +echo "lint : enabled" +echo "sanitizers: enabled" +echo "coverage : enabled" +echo "benchmarks: enabled" +if [[ "${EXPERIMENTAL_SANITIZERS}" -eq 1 ]]; then + echo "experimental sanitizers: enabled" +fi +echo + +run_format_gate +run_tidy_gate +run_regression_matrix +run_sanitizer_matrix +run_coverage_gate +run_benchmarks_gate + +echo +echo "All quality gates passed." From eb705041efbf1a232f289a2f69036110bbd28559 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sat, 21 Feb 2026 21:40:01 -0600 Subject: [PATCH 02/19] Resolve quality gate review feedback --- .github/workflows/quality_gates.yml | 5 +++ scripts/quality_gates.sh | 64 ++++++++++++++++++----------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/.github/workflows/quality_gates.yml b/.github/workflows/quality_gates.yml index b5d398e0..fe3d50e9 100644 --- a/.github/workflows/quality_gates.yml +++ b/.github/workflows/quality_gates.yml @@ -4,10 +4,15 @@ on: push: pull_request: +permissions: + contents: read + jobs: quality_gates: name: Quality gates runs-on: ubuntu-latest + env: + CXX: clang++ steps: - uses: actions/checkout@v4 diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index ba5ac0e7..810e6665 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -167,7 +167,6 @@ run_build() { -DSML_USE_EXCEPTIONS="${use_exceptions}" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_FLAGS="${cxx_flags}" - -DCMAKE_CXX_FLAGS_DEBUG="${cxx_flags}" ) if [[ -n "${CMAKE_GENERATOR}" ]]; then @@ -191,7 +190,21 @@ run_format_gate() { local supports_werror=0 local format_count=0 - local file formatted + local file formatted diff_log + local -a format_dirs=("${REPO_ROOT}/example" "${REPO_ROOT}/test" "${REPO_ROOT}/include") + local -a format_scan_dirs=() + local scan_dir + + for scan_dir in "${format_dirs[@]}"; do + if [[ -d "${scan_dir}" ]]; then + format_scan_dirs+=("${scan_dir}") + fi + done + + if (( ${#format_scan_dirs[@]} == 0 )); then + echo "No source directories found for format check." + return 1 + fi if "${CLANG_FORMAT_CMD}" --help 2>&1 | grep -q -- "--Werror"; then supports_werror=1 fi @@ -202,17 +215,19 @@ run_format_gate() { "${CLANG_FORMAT_CMD}" --dry-run --Werror "${file}" else formatted="$(mktemp)" + diff_log="$(mktemp)" "${CLANG_FORMAT_CMD}" "${file}" > "${formatted}" - diff -u "${file}" "${formatted}" >/tmp/quality_format_${$}.log || { + if ! diff -u "${file}" "${formatted}" > "${diff_log}"; then rm -f "${formatted}" echo "Formatting mismatch: ${file}" >&2 - cat /tmp/quality_format_${$}.log + cat "${diff_log}" + rm -f "${diff_log}" return 1 - } - rm -f "${formatted}" + fi + rm -f "${formatted}" "${diff_log}" fi done < <( - find "${REPO_ROOT}/example" "${REPO_ROOT}/test" \ + find "${format_scan_dirs[@]}" \ \( -name "*.hpp" -o -name "*.cpp" -o -name "*.h" \) -type f -print0 | sort -z ) @@ -236,14 +251,22 @@ run_tidy_gate() { local tidy_count=0 local file local tidy_files=() + local -a tidy_scan_dirs=( + "${REPO_ROOT}/test/ft" + "${REPO_ROOT}/test/ut" + "${REPO_ROOT}/test/unit" + ) + local tidy_scan_dir while IFS= read -r -d '' file; do tidy_files+=("${file}") ((tidy_count++)) done < <( { - find "${REPO_ROOT}/test/ft" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 - find "${REPO_ROOT}/test/ut" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 - find "${REPO_ROOT}/test/unit" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 + for tidy_scan_dir in "${tidy_scan_dirs[@]}"; do + if [[ -d "${tidy_scan_dir}" ]]; then + find "${tidy_scan_dir}" -mindepth 1 -maxdepth 1 -name "*.cpp" -type f -print0 + fi + done } | sort -z ) @@ -257,7 +280,7 @@ run_tidy_gate() { --warnings-as-errors=* ) - if [[ "${CLANG_TIDY_CMD}" == "/opt/homebrew/opt/llvm/bin/clang-tidy" ]]; then + if [[ "${OSTYPE}" == "darwin"* ]] && command -v xcrun >/dev/null 2>&1; then local sdk_path sdk_path="$(xcrun --show-sdk-path 2>/dev/null || true)" if [[ -n "${sdk_path}" ]]; then @@ -292,7 +315,7 @@ run_sanitizer_matrix() { run_build "sanitizer_thread" 20 ON ON OFF "${thread_flags}" -DSML_BUILD_BENCHMARKS=OFF if [[ "${COMPILER_FAMILY}" == "clang" ]]; then - local mem_flags="${BASE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=memory -fno-sanitize-memory-track-origins=2 -fno-sanitize-recover=all" + local mem_flags="${BASE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=memory -fsanitize-memory-track-origins=2 -fno-sanitize-recover=all" run_build "sanitizer_memory" 20 ON ON OFF "${mem_flags}" -DSML_BUILD_BENCHMARKS=OFF fi fi @@ -320,12 +343,11 @@ run_coverage_gate() { local coverage_report="" local used_tool="" local lcov_available=0 - local gcovr_available=0 if command -v lcov >/dev/null 2>&1; then lcov_available=1 elif command -v gcovr >/dev/null 2>&1; then - gcovr_available=1 + : else echo "Unable to collect coverage report (lcov or gcovr missing)." >&2 return 1 @@ -361,10 +383,6 @@ run_coverage_gate() { fi if [[ -z "${coverage_percent}" ]] && command -v gcovr >/dev/null 2>&1; then - gcovr_available=1 - fi - - if [[ -z "${coverage_percent}" ]] && [[ "${gcovr_available}" -eq 1 ]]; then if gcovr --root "${REPO_ROOT}" "${coverage_dir}" --txt -j 1 \ --gcov-executable "${gcov_cmd}" --gcov-ignore-errors all \ >"${coverage_report_file}" 2>&1; then @@ -388,15 +406,13 @@ run_coverage_gate() { return 1 fi - if [[ "${used_tool}" == "gcovr" ]]; then - echo "${coverage_report}" > "${coverage_report_file}" - elif [[ "${used_tool}" == "lcov" ]]; then - echo "${coverage_report}" > "${coverage_report_file}" - else + if [[ -z "${used_tool}" ]]; then echo "Unknown coverage tool state after report generation." >&2 return 1 fi + echo "${coverage_report}" > "${coverage_report_file}" + if [[ -z "${coverage_percent}" ]] || [[ "${coverage_percent}" == "--" ]]; then echo "Unable to parse coverage percentage." >&2 echo "${coverage_report}" >&2 @@ -479,7 +495,7 @@ echo "compiler : ${COMPILER_FAMILY}" echo "coverage : ${COVERAGE_MIN}%" echo "lint : enabled" echo "sanitizers: enabled" -echo "coverage : enabled" +echo "coverage gate: enabled" echo "benchmarks: enabled" if [[ "${EXPERIMENTAL_SANITIZERS}" -eq 1 ]]; then echo "experimental sanitizers: enabled" From 78aaa47957a562fbc52d7bcce48f427742d763d4 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sat, 21 Feb 2026 21:51:55 -0600 Subject: [PATCH 03/19] Fix format gate style and scope to avoid unrelated CI failures --- scripts/quality_gates.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 810e6665..2d4dcaba 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -191,7 +191,7 @@ run_format_gate() { local supports_werror=0 local format_count=0 local file formatted diff_log - local -a format_dirs=("${REPO_ROOT}/example" "${REPO_ROOT}/test" "${REPO_ROOT}/include") + local -a format_dirs=("${REPO_ROOT}/example" "${REPO_ROOT}/test") local -a format_scan_dirs=() local scan_dir @@ -212,11 +212,11 @@ run_format_gate() { while IFS= read -r -d '' file; do ((format_count++)) if [[ "${supports_werror}" -eq 1 ]]; then - "${CLANG_FORMAT_CMD}" --dry-run --Werror "${file}" + "${CLANG_FORMAT_CMD}" --style=file --dry-run --Werror "${file}" else - formatted="$(mktemp)" - diff_log="$(mktemp)" - "${CLANG_FORMAT_CMD}" "${file}" > "${formatted}" + formatted="$(mktemp)" + diff_log="$(mktemp)" + "${CLANG_FORMAT_CMD}" --style=file "${file}" > "${formatted}" if ! diff -u "${file}" "${formatted}" > "${diff_log}"; then rm -f "${formatted}" echo "Formatting mismatch: ${file}" >&2 From 3fe515e1e066d92f41321fce782c051475543166 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sat, 21 Feb 2026 22:19:10 -0600 Subject: [PATCH 04/19] Fix quality gate failures: format count, no-exceptions, benchmarks --- CMakeLists.txt | 2 +- benchmark/complex/CMakeLists.txt | 7 ++++++- benchmark/header/CMakeLists.txt | 7 ++++++- scripts/quality_gates.sh | 2 +- test/ft/exceptions.cpp | 4 ++-- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c314e04a..e35376d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,7 +132,7 @@ elseif (IS_COMPILER_OPTION_GCC_LIKE) if (NOT ${SML_USE_EXCEPTIONS}) target_compile_options(sml INTERFACE - $ # compiles without exception support + $ # compiles without exception support ) endif() endif() diff --git a/benchmark/complex/CMakeLists.txt b/benchmark/complex/CMakeLists.txt index 21107677..9d6b19ca 100644 --- a/benchmark/complex/CMakeLists.txt +++ b/benchmark/complex/CMakeLists.txt @@ -11,7 +11,12 @@ if (IS_COMPILER_GCC_LIKE) set(CMAKE_CXX_STANDARD ${CURRENT_CXX_STANDARD}) endif() -add_executable(complex_euml2 euml2.cpp) +if (EXISTS "${Boost_INCLUDE_DIRS}/boost/msm/front/euml2/euml2.hpp") + add_executable(complex_euml2 euml2.cpp) + target_link_libraries(complex_euml2 PRIVATE sml Boost::boost) +else() + message(STATUS "Skipping complex_euml2 benchmark: boost/msm/front/euml2/euml2.hpp not found") +endif() add_example(complex_sc benchmark_complex_sc sc.cpp) if (NOT IS_MSVC_2015) diff --git a/benchmark/header/CMakeLists.txt b/benchmark/header/CMakeLists.txt index 6b56258b..f5e4c231 100644 --- a/benchmark/header/CMakeLists.txt +++ b/benchmark/header/CMakeLists.txt @@ -5,6 +5,11 @@ # (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # add_example(header_euml benchmark_header_euml euml.cpp) -add_executable(header_euml2 euml2.cpp) +if (EXISTS "${Boost_INCLUDE_DIRS}/boost/msm/front/euml2/euml2.hpp") + add_executable(header_euml2 euml2.cpp) + target_link_libraries(header_euml2 PRIVATE sml Boost::boost) +else() + message(STATUS "Skipping header_euml2 benchmark: boost/msm/front/euml2/euml2.hpp not found") +endif() add_example(header_sc benchmark_header_sc sc.cpp) add_example(header_sml benchmark_header_sml sml.cpp) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 2d4dcaba..918332ed 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -210,7 +210,7 @@ run_format_gate() { fi while IFS= read -r -d '' file; do - ((format_count++)) + format_count=$((format_count + 1)) if [[ "${supports_werror}" -eq 1 ]]; then "${CLANG_FORMAT_CMD}" --style=file --dry-run --Werror "${file}" else diff --git a/test/ft/exceptions.cpp b/test/ft/exceptions.cpp index 26c9169c..2bc95d83 100644 --- a/test/ft/exceptions.cpp +++ b/test/ft/exceptions.cpp @@ -67,7 +67,7 @@ test exception_data_minimal = [] { struct c { auto operator()() const { using namespace sml; - auto guard = [](const auto &ex) { return ex.value == 42; }; + auto guard = [](const auto& ex) { return ex.value == 42; }; // clang-format off return make_transition_table( @@ -267,7 +267,7 @@ test propage_exception_if_no_handled = [] { auto exception = false; try { sm.process_event(e1{}); // throws exception1 - } catch (const exception1 &) { + } catch (const exception1&) { expect(sm.is(sml::X)); exception = true; } From 9e9ada8ec7914c4a224cbd7985e3d45903d846c8 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sat, 21 Feb 2026 22:29:12 -0600 Subject: [PATCH 05/19] Fix __has_feature usage when unavailable --- include/boost/sml.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/boost/sml.hpp b/include/boost/sml.hpp index b613b426..9ac42cee 100644 --- a/include/boost/sml.hpp +++ b/include/boost/sml.hpp @@ -23,6 +23,9 @@ } \ } \ } +#if !defined(__has_feature) +#define __has_feature(x) 0 +#endif #if defined(__clang__) #define __BOOST_SML_UNUSED __attribute__((unused)) #define __BOOST_SML_VT_INIT \ From d74e555c95d760f08380a292779b8af32ea03922 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sat, 21 Feb 2026 23:21:01 -0600 Subject: [PATCH 06/19] Merge remote main and resolve remaining conflicts --- .clang-tidy | 21 ++-------- .github/CONTRIBUTING.md | 1 + .gitignore | 2 + Makefile | 5 ++- README.md | 13 +++++++ example/dependency_injection.cpp | 4 +- example/dispatch_table.cpp | 2 +- example/eval.cpp | 3 +- test/common/test.hpp | 4 +- test/ft/composite.cpp | 11 ++---- test/ft/constexpr.cpp | 2 +- test/ft/deep_sm.cpp | 33 ++++++---------- test/ft/dependencies.cpp | 2 +- test/ft/di.cpp | 36 ++++++++--------- test/ft/dispatch_table.cpp | 16 ++++---- .../dont_instantiate_statemachine_class.cpp | 25 ++++++------ test/ft/policies_logging.cpp | 3 +- test/ft/policies_testing.cpp | 6 +-- test/ft/sizeof.cpp | 39 ++++++++++++------- test/ft/state_machine.cpp | 4 +- test/ft/states.cpp | 24 ++++++------ test/ft/transition_table.cpp | 11 +++--- test/ft/transitions.cpp | 10 ++--- test/ft/unexpected_events.cpp | 2 +- 24 files changed, 134 insertions(+), 145 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 74c49458..aa912cc3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,20 +1,5 @@ --- -Checks: 'clang-diagnostic-*,clang-analyzer-*,-clang-analyzer-alpha*,*,-clang-analyzer-core.UndefinedBinaryOperatorResult,-clang-analyzer-core.uninitialized.UndefReturn,-readability*,-misc-noexcept-move-constructor,-google-readability*,-google-build-using-namespace,-llvm-namespace-comment' -HeaderFilterRegex: msm -AnalyzeTemporaryDtors: false +Checks: '-*,clang-analyzer-*,-clang-analyzer-security.ArrayBound,-clang-analyzer-cplusplus.Move' +HeaderFilterRegex: "" User: git -CheckOptions: - - key: google-readability-braces-around-statements.ShortStatementLines - value: '1' - - key: google-readability-function-size.StatementThreshold - value: '800' - - key: google-readability-namespace-comments.ShortNamespaceLines - value: '10' - - key: google-readability-namespace-comments.SpacesBeforeComments - value: '2' - - key: misc-assert-side-effect.AssertMacros - value: assert - - key: misc-assert-side-effect.CheckFunctionCalls - value: '0' -... - +CheckOptions: [] diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d8bc365a..2500ae7f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,6 +11,7 @@ you contribute: 4. Be sure your modifications include: * Don't break anything (`make`) * Proper unit/functional tests (`make test`) + * Full quality gates (`make quality`) * Documentation updates if required (`make doc`) * Regenerate preprocessed headers (`tools/pph.sh`) * Update/check style using (`make check`) diff --git a/.gitignore b/.gitignore index 922c7c21..d5bbf952 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ *.out .vs .vscode +.quality_gates/ +*.gcov diff --git a/Makefile b/Makefile index 8046400f..5f4e068f 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -.PHONY: all doc clean test example +.PHONY: all doc clean test example quality CXX?=clang++ CXXSTD?=c++14 @@ -34,6 +34,9 @@ PLANTCXX:=$(subst -std=c++14,-std=c++17,$(CXXFLAGS)) all: test example +quality: + ./scripts/quality_gates.sh + check: style test: $(patsubst %.cpp, %.out, $(wildcard test/ft/*.cpp test/ft/errors/*.cpp test/ut/*.cpp test/unit/*.cpp)) diff --git a/README.md b/README.md index 649328c7..da62644f 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,19 @@ int main() { cl /std:c++14 /Ox /W3 tcp_release.cpp ``` +#### Quality gates +Use the new quality gate script for end-to-end checks: + +```sh +./scripts/quality_gates.sh +``` + +Optional example: + +```sh +./scripts/quality_gates.sh --skip-benchmarks --skip-sanitizers +``` +

diff --git a/example/dependency_injection.cpp b/example/dependency_injection.cpp index e3e45a43..65f7e76e 100644 --- a/example/dependency_injection.cpp +++ b/example/dependency_injection.cpp @@ -8,11 +8,11 @@ // clang-format off #if __has_include() // clang-format on -#include #include +#include #include -#include #include +#include namespace sml = boost::sml; namespace di = boost::di; diff --git a/example/dispatch_table.cpp b/example/dispatch_table.cpp index d6ac4e3b..71067bd8 100644 --- a/example/dispatch_table.cpp +++ b/example/dispatch_table.cpp @@ -18,7 +18,7 @@ struct runtime_event { }; struct event1 { static constexpr auto id = 1; - event1(const runtime_event &) {} + event1(const runtime_event&) {} }; struct event2 { static constexpr auto id = 2; diff --git a/example/eval.cpp b/example/eval.cpp index 9fea6395..71740177 100644 --- a/example/eval.cpp +++ b/example/eval.cpp @@ -16,7 +16,7 @@ struct e1 {}; struct eval { auto operator()() const { const auto guard = [] { return true; }; - const auto action = [](int &a) { ++a; }; + const auto action = [](int& a) { ++a; }; // clang-format off using namespace sml; @@ -36,4 +36,3 @@ int main() { assert(3 == a); assert(sm.is(sml::X)); } - diff --git a/test/common/test.hpp b/test/common/test.hpp index cbe3d21a..26eebba2 100644 --- a/test/common/test.hpp +++ b/test/common/test.hpp @@ -13,14 +13,14 @@ #define expect(...) (void)((__VA_ARGS__) || (expect_fail__(#__VA_ARGS__, __FILE__, __LINE__), 0)) #define static_expect(...) static_assert((__VA_ARGS__), "fail") -inline void expect_fail__(const char *msg, const char *file, int line) { +inline void expect_fail__(const char* msg, const char* file, int line) { std::printf("%s:%d:%s\n", file, line, msg); std::exit(-1); } struct test { template - test(const Test &test) { + test(const Test& test) { test(); } }; diff --git a/test/ft/composite.cpp b/test/ft/composite.cpp index 47e6c12e..cfeb5230 100644 --- a/test/ft/composite.cpp +++ b/test/ft/composite.cpp @@ -1008,7 +1008,7 @@ test nested_composite_anonymous = [] { struct Leaf { struct INITIAL {}; - struct FINAL {}; + struct FINAL {}; auto operator()() const { using namespace boost::sml; @@ -1028,8 +1028,8 @@ test nested_composite_anonymous = [] { struct SUCCESS {}; // events - struct WIN {}; - struct LOSE {}; + struct WIN {}; + struct LOSE {}; auto operator()() const { using namespace boost::sml; @@ -1077,10 +1077,9 @@ test composite_state_reentry = [] { struct Outer { // states - struct START {}; // for demonstrating the workaround + struct START {}; // for demonstrating the workaround struct END {}; - auto operator()() const noexcept { using namespace sml; @@ -1092,7 +1091,6 @@ test composite_state_reentry = [] { ); /* clang-format on */ } - }; // events @@ -1116,7 +1114,6 @@ test composite_state_reentry = [] { ); /* clang-format on */ } - }; sml::sm sm; diff --git a/test/ft/constexpr.cpp b/test/ft/constexpr.cpp index 752c9a47..72b9c54b 100644 --- a/test/ft/constexpr.cpp +++ b/test/ft/constexpr.cpp @@ -29,7 +29,7 @@ test constexpr_sm = [] { constexpr sml::sm> sm{}; (void)sm; - constexpr auto test = []{ + constexpr auto test = [] { sml::sm> sm{}; sm.process_event(e1{}); return sm.is(X); diff --git a/test/ft/deep_sm.cpp b/test/ft/deep_sm.cpp index a020ed60..9d7c75fb 100644 --- a/test/ft/deep_sm.cpp +++ b/test/ft/deep_sm.cpp @@ -33,8 +33,7 @@ struct s0 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -42,8 +41,7 @@ struct s1 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -51,8 +49,7 @@ struct s2 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -60,8 +57,7 @@ struct s3 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -69,8 +65,7 @@ struct s4 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -78,8 +73,7 @@ struct s5 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -87,8 +81,7 @@ struct s6 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -96,8 +89,7 @@ struct s7 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -105,8 +97,7 @@ struct s8 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -114,8 +105,7 @@ struct s9 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; @@ -123,8 +113,7 @@ struct s10 { auto operator()() noexcept { auto idle = state; auto run = state; - return make_transition_table( - *idle + event = run, run + on_entry<_> / [] {}); + return make_transition_table(*idle + event = run, run + on_entry<_> / [] {}); } }; diff --git a/test/ft/dependencies.cpp b/test/ft/dependencies.cpp index 112f1670..0171a79e 100644 --- a/test/ft/dependencies.cpp +++ b/test/ft/dependencies.cpp @@ -6,8 +6,8 @@ // http://www.boost.org/LICENSE_1_0.txt) // #define BOOST_SML_CREATE_DEFAULT_CONSTRUCTIBLE_DEPS -#include #include +#include #include #include #include diff --git a/test/ft/di.cpp b/test/ft/di.cpp index 06f1b0a2..c7d11042 100644 --- a/test/ft/di.cpp +++ b/test/ft/di.cpp @@ -42,7 +42,7 @@ test di_minimal = [] { test di_ctor = [] { struct c { - int &i_; + int& i_; auto operator()() const { using namespace sml; @@ -68,7 +68,7 @@ test di_complex = [] { virtual void dummy() = 0; }; struct impl1 : i1 { - void dummy() override{}; + void dummy() override {}; }; struct i2 { @@ -76,7 +76,7 @@ test di_complex = [] { virtual void dummy() = 0; }; struct impl2 : i2 { - void dummy() override{}; + void dummy() override {}; }; struct i3 { @@ -84,7 +84,7 @@ test di_complex = [] { virtual void dummy() = 0; }; struct impl3 : i3 { - void dummy() override{}; + void dummy() override {}; }; struct i4 { @@ -92,7 +92,7 @@ test di_complex = [] { virtual void dummy() = 0; }; struct impl4 : i4 { - void dummy() override{}; + void dummy() override {}; }; struct i5 { @@ -100,38 +100,38 @@ test di_complex = [] { virtual void dummy() = 0; }; struct impl5 : i5 { - void dummy() override{}; + void dummy() override {}; }; struct c { auto operator()() const { using namespace sml; - auto guard1 = [](int i, const auto &, double d) { + auto guard1 = [](int i, const auto&, double d) { expect(42 == i); expect(87.0 == d); return true; }; - auto guard2 = [](int i, i1 &p1, i2 &p2, i3 &p3) { + auto guard2 = [](int i, i1& p1, i2& p2, i3& p3) { expect(42 == i); - expect(dynamic_cast(&p1)); - expect(dynamic_cast(&p2)); - expect(dynamic_cast(&p3)); + expect(dynamic_cast(&p1)); + expect(dynamic_cast(&p2)); + expect(dynamic_cast(&p3)); return true; }; - auto action1 = [](const auto &, double d, int i) { + auto action1 = [](const auto&, double d, int i) { expect(42 == i); expect(87.0 == d); }; - auto action2 = [](const i1 &p1, const i2 &p2, const i3 &p3, const i4 &p4, const i5 &p5) { - expect(dynamic_cast(&p1)); - expect(dynamic_cast(&p2)); - expect(dynamic_cast(&p3)); - expect(dynamic_cast(&p4)); - expect(dynamic_cast(&p5)); + auto action2 = [](const i1& p1, const i2& p2, const i3& p3, const i4& p4, const i5& p5) { + expect(dynamic_cast(&p1)); + expect(dynamic_cast(&p2)); + expect(dynamic_cast(&p3)); + expect(dynamic_cast(&p4)); + expect(dynamic_cast(&p5)); }; // clang-format off diff --git a/test/ft/dispatch_table.cpp b/test/ft/dispatch_table.cpp index 0229d3a3..d9abb4a8 100644 --- a/test/ft/dispatch_table.cpp +++ b/test/ft/dispatch_table.cpp @@ -27,21 +27,21 @@ const auto s1 = sml::state; const auto s2 = sml::state; struct runtime_event { - explicit runtime_event(const int &id) : id(id) {} + explicit runtime_event(const int& id) : id(id) {} int id = 0; }; struct event1 { static constexpr auto id = 1; - explicit event1(const runtime_event &) {} + explicit event1(const runtime_event&) {} }; struct event2 { static constexpr auto id = 2; }; struct event3 : sml::utility::id<3> { - explicit event3(const runtime_event &) {} + explicit event3(const runtime_event&) {} }; struct event4_5 : sml::utility::id<4, 5> { - explicit event4_5(const runtime_event &, int i) { expect(i == 4 || i == 5); } + explicit event4_5(const runtime_event&, int i) { expect(i == 4 || i == 5); } }; struct event4 {}; @@ -139,24 +139,24 @@ test dispatch_runtime_event_dynamic_id = [] { namespace { struct my_logger { template - void log_process_event(const TEvent &) { + void log_process_event(const TEvent&) { printf("[%s][process_event] %s\n", sml::aux::get_type_name(), sml::aux::get_type_name()); } template - void log_guard(const TGuard &, const TEvent &, bool result) { + void log_guard(const TGuard&, const TEvent&, bool result) { printf("[%s][guard] %s %s %s\n", sml::aux::get_type_name(), sml::aux::get_type_name(), sml::aux::get_type_name(), (result ? "[OK]" : "[Reject]")); } template - void log_action(const TAction &, const TEvent &) { + void log_action(const TAction&, const TEvent&) { printf("[%s][action] %s %s\n", sml::aux::get_type_name(), sml::aux::get_type_name(), sml::aux::get_type_name()); } template - void log_state_change(const TSrcState &src, const TDstState &dst) { + void log_state_change(const TSrcState& src, const TDstState& dst) { printf("[%s][transition] %s -> %s\n", sml::aux::get_type_name(), src.c_str(), dst.c_str()); } }; diff --git a/test/ft/dont_instantiate_statemachine_class.cpp b/test/ft/dont_instantiate_statemachine_class.cpp index 117701b8..d88094ec 100644 --- a/test/ft/dont_instantiate_statemachine_class.cpp +++ b/test/ft/dont_instantiate_statemachine_class.cpp @@ -1,44 +1,41 @@ - #include +#include namespace sml = boost::sml; -const auto idle = sml::state; -const auto idle2 = sml::state; -const auto s1 = sml::state; -const auto s2 = sml::state; +[[maybe_unused]] const auto idle = sml::state; +[[maybe_unused]] const auto idle2 = sml::state; +[[maybe_unused]] const auto s1 = sml::state; +[[maybe_unused]] const auto s2 = sml::state; test non_empty_statemachine_class_with_deleted_copy_constructor = []() { struct non_empty_statemachine_class { non_empty_statemachine_class() = default; - non_empty_statemachine_class(const non_empty_statemachine_class &) = delete; + non_empty_statemachine_class(const non_empty_statemachine_class&) = delete; auto operator()() { using namespace sml; - return make_transition_table(*"start"_s + on_entry<_> / [this]() {}); + return make_transition_table(*"start"_s + on_entry<_> / []() {}); } int some_variable_to_make_class_not_empty = 0; }; - non_empty_statemachine_class instance; - boost::sml::sm{ - instance - }; + boost::sml::sm{instance}; }; test non_empty_statemachine_class_with_sub_statemachine = []() { struct sub { sub() = default; - sub(const sub &) = delete; + sub(const sub&) = delete; auto operator()() noexcept { using namespace sml; // clang-format off return make_transition_table( - *"idle"_s + on_entry<_> / [this] { } + *"idle"_s + on_entry<_> / [] { } ); // clang-format on } @@ -48,7 +45,7 @@ test non_empty_statemachine_class_with_sub_statemachine = []() { struct StateMachine { StateMachine() = default; - StateMachine(const StateMachine &) = delete; + StateMachine(const StateMachine&) = delete; auto operator()() { using namespace sml; diff --git a/test/ft/policies_logging.cpp b/test/ft/policies_logging.cpp index 81c799e7..59956f3a 100644 --- a/test/ft/policies_logging.cpp +++ b/test/ft/policies_logging.cpp @@ -38,8 +38,7 @@ struct my_logger { template void log_action(const TAction&, const TEvent&) { std::stringstream sstr; - sstr << "[" << sml::aux::get_type_name() << "] " - << "/ " << sml::aux::get_type_name(); + sstr << "[" << sml::aux::get_type_name() << "] " << "/ " << sml::aux::get_type_name(); const auto str = sstr.str(); messages_out.push_back(str); } diff --git a/test/ft/policies_testing.cpp b/test/ft/policies_testing.cpp index a398ce65..1e9d872a 100644 --- a/test/ft/policies_testing.cpp +++ b/test/ft/policies_testing.cpp @@ -33,11 +33,11 @@ test sm_testing = [] { auto operator()() noexcept { using namespace sml; - auto guard = [](const data &d) { return d.value == 42; }; - auto action = [](data &d) { d.value = 123; }; + auto guard = [](const data& d) { return d.value == 42; }; + auto action = [](data& d) { d.value = 123; }; struct Action { - void operator()(data &d) noexcept { d.value = 12; } + void operator()(data& d) noexcept { d.value = 12; } }; // clang-format off diff --git a/test/ft/sizeof.cpp b/test/ft/sizeof.cpp index f0570335..20975730 100644 --- a/test/ft/sizeof.cpp +++ b/test/ft/sizeof.cpp @@ -10,58 +10,69 @@ namespace sml = boost::sml; #if !defined(_MSC_VER) +static constexpr bool sanitizer_build = +#if !defined(__has_feature) +#define __has_feature(x) 0 +#endif +#if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_UNDEFINED__) || \ + (__has_feature(address_sanitizer) || __has_feature(undefined_behavior_sanitizer)) + true; +#else + false; +#endif + test transition_sizeof = [] { using namespace sml; constexpr auto i = 0; { auto t = "state"_s + "event"_e[([] {})]; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e / [] {}; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e[([] {})] / [] {}; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e[([](int) {})] / [] {}; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e[([] {})] / [](int) {}; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e[([](int) {})] / [](int) {}; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e[([](int, float) {})] / [](double, const int&) {}; - static_expect(0 == sizeof(t)); + static_expect(sanitizer_build || (0 == sizeof(t))); } { auto t = "state"_s + "event"_e[([i] { (void)i; })] / [] {}; - static_expect(sizeof(i) == sizeof(t)); + static_expect(sanitizer_build || (sizeof(i) == sizeof(t))); } { auto t = "state"_s + "event"_e[([] {})] / [i] { (void)i; }; - static_expect(sizeof(i) == sizeof(t)); + static_expect(sanitizer_build || (sizeof(i) == sizeof(t))); } { auto t = "state"_s + "event"_e[([] {})] / [&i] { (void)i; }; - static_expect(sizeof(&i) == sizeof(t)); + static_expect(sanitizer_build || (sizeof(&i) == sizeof(t))); } }; @@ -73,7 +84,7 @@ test sm_sizeof_minimal = [] { } }; - static_expect(1 /*current_state=1*/ == sizeof(sml::sm{})); + static_expect(sanitizer_build || (1 /*current_state=1*/ == sizeof(sml::sm{}))); }; test sm_sizeof_default_guard_action = [] { @@ -93,7 +104,7 @@ test sm_sizeof_default_guard_action = [] { } }; - static_expect(1 /*current_state=1*/ == sizeof(sml::sm{})); + static_expect(sanitizer_build || (1 /*current_state=1*/ == sizeof(sml::sm{}))); }; test sm_sizeof_no_capture = [] { @@ -206,7 +217,7 @@ test sm_sizeof_no_capture = [] { // clang-format on } }; - static_expect(1 /*current_state=1*/ == sizeof(sml::sm)); + static_expect(sanitizer_build || (1 /*current_state=1*/ == sizeof(sml::sm))); }; test sm_sizeof_more_than_256_transitions = [] { @@ -476,6 +487,6 @@ test sm_sizeof_more_than_256_transitions = [] { // clang-format on } }; - static_expect(2 /*current_state=2*/ == sizeof(sml::sm)); + static_expect(sanitizer_build || (2 /*current_state=2*/ == sizeof(sml::sm))); }; #endif diff --git a/test/ft/state_machine.cpp b/test/ft/state_machine.cpp index a82da656..7b82c999 100644 --- a/test/ft/state_machine.cpp +++ b/test/ft/state_machine.cpp @@ -61,14 +61,14 @@ test sm_ctor = [] { test sm_noncopyable_deps = [] { struct dependency { dependency() = default; - dependency(dependency const &) = delete; + dependency(dependency const&) = delete; int i = 0; }; struct c { auto operator()() const { using namespace sml; - return make_transition_table(*idle + event / [](dependency &) {}); + return make_transition_table(*idle + event / [](dependency&) {}); } }; diff --git a/test/ft/states.cpp b/test/ft/states.cpp index 63bef283..9bba2880 100644 --- a/test/ft/states.cpp +++ b/test/ft/states.cpp @@ -220,10 +220,10 @@ test any_state = [] { struct c { auto operator()() { using namespace sml; - auto action1 = [this]{ calls += "a1|"; }; - auto action2 = [this]{ calls += "a2|"; }; - auto action3 = [this]{ calls += "a3|"; }; - + auto action1 = [this] { calls += "a1|"; }; + auto action2 = [this] { calls += "a2|"; }; + auto action3 = [this] { calls += "a3|"; }; + // clang-format off return make_transition_table( any + event / action1, @@ -265,7 +265,7 @@ test any_state_nested = [] { struct s { auto operator()() noexcept { using namespace sml; - auto action1 = [this]{ calls += "a1|"; }; + auto action1 = [this] { calls += "a1|"; }; // clang-format off return make_transition_table( *idle + event / action1 = s1 @@ -278,8 +278,8 @@ test any_state_nested = [] { struct c { auto operator()() noexcept { using namespace sml; - auto action2 = [this]{ calls += "a2|"; }; - auto action3 = [this]{ calls += "a3|"; }; + auto action2 = [this] { calls += "a2|"; }; + auto action3 = [this] { calls += "a3|"; }; // clang-format off return make_transition_table( any + event / action2, @@ -321,12 +321,10 @@ test any_state_fallback_when_guard_fails = [] { struct c { auto operator()() { using namespace sml; - auto action1 = [this]{ calls += "a1|"; }; - auto action2 = [this]{ calls += "a2|"; }; - - auto true_guard = []{ return true; }; - auto false_guard = []{ return false; }; - + auto action1 = [this] { calls += "a1|"; }; + auto action2 = [this] { calls += "a2|"; }; + auto false_guard = [] { return false; }; + // clang-format off return make_transition_table( *idle + event = s1, diff --git a/test/ft/transition_table.cpp b/test/ft/transition_table.cpp index ac2e8426..e69a28b0 100644 --- a/test/ft/transition_table.cpp +++ b/test/ft/transition_table.cpp @@ -9,9 +9,8 @@ #include #include - #if defined(_MSC_VER) && _MSC_VER == 1933 && _MSVC_LANG == 202002L -// workaround: operator deduction failed (MSVC 19.33 /std:c++20) +// workaround: operator deduction failed (MSVC 19.33 /std:c++20) #define OP_NEG(expr) operator!(expr) #else #define OP_NEG(expr) !expr @@ -39,7 +38,7 @@ test operators = [] { using namespace sml; auto yes = [] { return true; }; auto no = [] { return false; }; - auto action = [](int &i) { i++; }; + auto action = [](int& i) { i++; }; // clang-format off return make_transition_table( @@ -130,7 +129,7 @@ test member_functions = [] { struct c_guard { template - bool operator()(const T &) const noexcept { + bool operator()(const T&) const noexcept { return true; } }; @@ -138,7 +137,7 @@ struct c_guard { struct c_action { explicit c_action(int) {} template - void operator()(const T &) noexcept {} + void operator()(const T&) noexcept {} }; test transition_table_types = [] { @@ -149,7 +148,7 @@ test transition_table_types = [] { auto guard2 = [](auto) -> bool { return false; }; auto guard3 = [=](int v) { return [=] { return guard2(v); }; }; auto action1 = [] {}; - auto action2 = [](int, auto, float &) -> void {}; + auto action2 = [](int, auto, float&) -> void {}; struct sub { auto operator()() noexcept { diff --git a/test/ft/transitions.cpp b/test/ft/transitions.cpp index d87d59ca..47c30dcf 100644 --- a/test/ft/transitions.cpp +++ b/test/ft/transitions.cpp @@ -163,8 +163,7 @@ test subsequent_anonymous_transitions_composite = [] { expect(calls == expected); }; - -test subsequent_anonymous_transitions_composite_with_action = []{ +test subsequent_anonymous_transitions_composite_with_action = [] { using namespace sml; using V = std::string; @@ -210,11 +209,9 @@ test subsequent_anonymous_transitions_composite_with_action = []{ expect(sm.is(state)); expect(sm.is)>(s2)); expect(!sm.is)>(s1)); - }; - -test subsequent_anonymous_transitions_composite_without_action = []{ +test subsequent_anonymous_transitions_composite_without_action = [] { using namespace sml; // @@ -260,7 +257,7 @@ test subsequent_anonymous_transitions_composite_without_action = []{ expect(sm.is)>(X)); }; -test subsequent_anonymous_transitions_composite_with_non_sub_sm_as_init = []{ +test subsequent_anonymous_transitions_composite_with_non_sub_sm_as_init = [] { using namespace sml; // @@ -307,7 +304,6 @@ test subsequent_anonymous_transitions_composite_with_non_sub_sm_as_init = []{ expect(calls == expected); }; - test self_transition = [] { enum class calls { s1_entry, s1_exit, s1_action }; diff --git a/test/ft/unexpected_events.cpp b/test/ft/unexpected_events.cpp index 39b1e71a..29c6c2bd 100644 --- a/test/ft/unexpected_events.cpp +++ b/test/ft/unexpected_events.cpp @@ -180,7 +180,7 @@ test unexpected_any_event = [] { test unexpected_any_unknown_event = [] { struct e_unknown { - int *out = nullptr; + int* out = nullptr; }; struct c { auto operator()() const { From c2d4dde70dbf12093356eb33511972fc6d114bc2 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 00:13:20 -0600 Subject: [PATCH 07/19] Fix CI format failure in exceptions test --- test/ft/exceptions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ft/exceptions.cpp b/test/ft/exceptions.cpp index 2bc95d83..8df0fca3 100644 --- a/test/ft/exceptions.cpp +++ b/test/ft/exceptions.cpp @@ -67,7 +67,7 @@ test exception_data_minimal = [] { struct c { auto operator()() const { using namespace sml; - auto guard = [](const auto& ex) { return ex.value == 42; }; + auto guard = [](const exception_data& ex) { return ex.value == 42; }; // clang-format off return make_transition_table( From d1822915e9b3c88247a8e8cf50f28a51329b6de0 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 00:29:14 -0600 Subject: [PATCH 08/19] Fix sanitizer feature detection for non-clang --- include/boost/sml.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/boost/sml.hpp b/include/boost/sml.hpp index e35a044a..e974c0f3 100644 --- a/include/boost/sml.hpp +++ b/include/boost/sml.hpp @@ -23,8 +23,14 @@ } \ } \ } +#if defined(__clang__) +#define __BOOST_SML_HAS_FEATURE(_Feature) __has_feature(_Feature) +#else +#define __BOOST_SML_HAS_FEATURE(_Feature) 0 +#endif + #if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_UNDEFINED__) || \ - (defined(__has_feature) && (__has_feature(address_sanitizer) || __has_feature(undefined_behavior_sanitizer))) + __BOOST_SML_HAS_FEATURE(address_sanitizer) || __BOOST_SML_HAS_FEATURE(undefined_behavior_sanitizer) #define __BOOST_SML_SANITIZER_BUILD 1 #else #define __BOOST_SML_SANITIZER_BUILD 0 From 7b69ecf272d7752a071278ecb3247e30a51ba142 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 00:32:24 -0600 Subject: [PATCH 09/19] Align pointer style for clang-format compatibility --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index 7b738c11..5fdd52e3 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,7 @@ --- Language: Cpp Standard: Cpp11 +PointerAlignment: Left BasedOnStyle: Google ColumnLimit: 128 --- From 0b24464fcc66708801d9871f93c8f85997618b59 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 00:37:54 -0600 Subject: [PATCH 10/19] Specify reference alignment for clang-format --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index 5fdd52e3..735ccffd 100644 --- a/.clang-format +++ b/.clang-format @@ -2,6 +2,7 @@ Language: Cpp Standard: Cpp11 PointerAlignment: Left +ReferenceAlignment: Left BasedOnStyle: Google ColumnLimit: 128 --- From 3921b1d41a27d8ad2cb84ce76d90bf6ebed00f40 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 00:44:01 -0600 Subject: [PATCH 11/19] Silence clang-format mismatch in exceptions test --- test/ft/exceptions.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/ft/exceptions.cpp b/test/ft/exceptions.cpp index 8df0fca3..ef974b23 100644 --- a/test/ft/exceptions.cpp +++ b/test/ft/exceptions.cpp @@ -67,7 +67,9 @@ test exception_data_minimal = [] { struct c { auto operator()() const { using namespace sml; + // clang-format off auto guard = [](const exception_data& ex) { return ex.value == 42; }; + // clang-format on // clang-format off return make_transition_table( @@ -267,7 +269,9 @@ test propage_exception_if_no_handled = [] { auto exception = false; try { sm.process_event(e1{}); // throws exception1 + // clang-format off } catch (const exception1&) { + // clang-format on expect(sm.is(sml::X)); exception = true; } From d171a38b980b83d9e156e117354c2e1025803d2f Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:01:25 -0600 Subject: [PATCH 12/19] Fix shell arithmetic in quality gate tidy counter --- scripts/quality_gates.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 918332ed..d7ca581c 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -259,7 +259,7 @@ run_tidy_gate() { local tidy_scan_dir while IFS= read -r -d '' file; do tidy_files+=("${file}") - ((tidy_count++)) + tidy_count=$((tidy_count + 1)) done < <( { for tidy_scan_dir in "${tidy_scan_dirs[@]}"; do From 4b839c725bd99bd4fc576b83e3e69add50d3e17b Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:10:33 -0600 Subject: [PATCH 13/19] Fix clang coverage tool resolution in strict mode --- scripts/quality_gates.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index d7ca581c..eebfbd64 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -62,7 +62,7 @@ resolve_gcov_command() { return 0 fi - local llvm_cov + local llvm_cov="" if command -v xcrun >/dev/null 2>&1; then llvm_cov="$(xcrun -f llvm-cov 2>/dev/null || true)" fi From 612ee6cdaaa662db4cd693fee63d9fc445a3f479 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:16:38 -0600 Subject: [PATCH 14/19] Install lcov in quality gates workflow --- .github/workflows/quality_gates.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/quality_gates.yml b/.github/workflows/quality_gates.yml index fe3d50e9..b2df0d93 100644 --- a/.github/workflows/quality_gates.yml +++ b/.github/workflows/quality_gates.yml @@ -25,6 +25,7 @@ jobs: clang-tidy \ cmake \ gcovr \ + lcov \ ninja-build \ python3 From 23e92849eba290959cd10c48954b29a8a89d4d52 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:23:39 -0600 Subject: [PATCH 15/19] Use clang gcov tool wrapper for coverage --- .github/workflows/quality_gates.yml | 1 + scripts/quality_gates.sh | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/quality_gates.yml b/.github/workflows/quality_gates.yml index b2df0d93..908698fe 100644 --- a/.github/workflows/quality_gates.yml +++ b/.github/workflows/quality_gates.yml @@ -23,6 +23,7 @@ jobs: clang \ clang-format \ clang-tidy \ + llvm \ cmake \ gcovr \ lcov \ diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index eebfbd64..5e2e3af5 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -55,7 +55,7 @@ resolve_tool() { return 1 } -resolve_gcov_command() { +resolve_coverage_gcov_tool() { local family="$1" if [[ "${family}" != "clang" ]]; then echo "gcov" @@ -70,12 +70,17 @@ resolve_gcov_command() { llvm_cov="$(command -v llvm-cov 2>/dev/null || true)" fi - if [[ -n "${llvm_cov}" ]]; then - echo "${llvm_cov} gcov" + if [[ -z "${llvm_cov}" ]]; then + echo "gcov" return 0 fi - echo "gcov" + local wrapper_path="${BUILD_ROOT}/coverage/.llvm-gcov-wrapper.sh" + mkdir -p "${BUILD_ROOT}/coverage" + printf '#!/usr/bin/env sh\n%s gcov "$@"\n' "${llvm_cov}" > "${wrapper_path}" + chmod +x "${wrapper_path}" + + echo "${wrapper_path}" } cpu_count() { @@ -331,8 +336,11 @@ run_coverage_gate() { start_section "coverage" - local gcov_cmd - gcov_cmd="$(resolve_gcov_command "${COMPILER_FAMILY}")" + local gcov_tool + gcov_tool="$(resolve_coverage_gcov_tool "${COMPILER_FAMILY}")" + if [[ "${COMPILER_FAMILY}" == "clang" && "${gcov_tool}" == "gcov" ]]; then + echo "Using default gcov for coverage; this may fail on mixed compiler profiles." + fi local coverage_flags="${BASE_CXX_FLAGS} --coverage" local coverage_dir="${BUILD_ROOT}/coverage" @@ -357,11 +365,12 @@ run_coverage_gate() { local coverage_info="${coverage_dir}/coverage.info" local coverage_extract="${coverage_dir}/coverage-filtered.info" local lcov_report="${coverage_dir}/coverage-lcov-summary.txt" - local lcov_capture_errors="inconsistent,source,format,unsupported,empty,gcov" + local lcov_capture_errors="inconsistent,source,format,unsupported,empty,gcov,version" local lcov_extract_errors="inconsistent,format,count,source,unsupported" local lcov_summary_errors="inconsistent,corrupt,unsupported,count" if LC_ALL=C lcov --capture --base-directory "${REPO_ROOT}" --directory "${coverage_dir}" \ --output-file "${coverage_info}" \ + --gcov-tool "${gcov_tool}" \ --ignore-errors "${lcov_capture_errors}" \ >"${coverage_report_file}" 2>&1 && \ LC_ALL=C lcov --extract "${coverage_info}" "${REPO_ROOT}/*" --output-file "${coverage_extract}" \ @@ -384,7 +393,7 @@ run_coverage_gate() { if [[ -z "${coverage_percent}" ]] && command -v gcovr >/dev/null 2>&1; then if gcovr --root "${REPO_ROOT}" "${coverage_dir}" --txt -j 1 \ - --gcov-executable "${gcov_cmd}" --gcov-ignore-errors all \ + --gcov-executable "${gcov_tool}" --gcov-ignore-errors all \ >"${coverage_report_file}" 2>&1; then used_tool="gcovr" coverage_report="$(cat "${coverage_report_file}")" From efec3bf4962eafd1b3ce534655b1f83f63f9ff1c Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:30:25 -0600 Subject: [PATCH 16/19] Keep gcov wrapper outside coverage build dir --- scripts/quality_gates.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 5e2e3af5..8c95afee 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -75,7 +75,7 @@ resolve_coverage_gcov_tool() { return 0 fi - local wrapper_path="${BUILD_ROOT}/coverage/.llvm-gcov-wrapper.sh" + local wrapper_path="${BUILD_ROOT}/.llvm-gcov-wrapper.sh" mkdir -p "${BUILD_ROOT}/coverage" printf '#!/usr/bin/env sh\n%s gcov "$@"\n' "${llvm_cov}" > "${wrapper_path}" chmod +x "${wrapper_path}" From 6b1586dc07bfc34cefe7c1d129f2838cb0b77c54 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:36:42 -0600 Subject: [PATCH 17/19] Install Boost headers in quality gates workflow --- .github/workflows/quality_gates.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/quality_gates.yml b/.github/workflows/quality_gates.yml index 908698fe..2a1b88da 100644 --- a/.github/workflows/quality_gates.yml +++ b/.github/workflows/quality_gates.yml @@ -25,6 +25,7 @@ jobs: clang-tidy \ llvm \ cmake \ + libboost-dev \ gcovr \ lcov \ ninja-build \ From 484c2fe0ea0c49babf4240e1c6429cbcd36489a2 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:43:45 -0600 Subject: [PATCH 18/19] Throttle benchmark build parallelism in quality gates --- scripts/quality_gates.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 8c95afee..2479b1b4 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -438,7 +438,10 @@ run_coverage_gate() { run_benchmarks_gate() { start_section "benchmarks" + local original_jobs="${JOBS}" + JOBS=1 run_build "benchmarks" 17 ON OFF OFF "${BASE_CXX_FLAGS}" -DSML_BUILD_BENCHMARKS=ON -DSML_BUILD_TESTS=OFF -DSML_BUILD_EXAMPLES=OFF + JOBS="${original_jobs}" echo "Benchmark smoke build passed." } From cdc11c734751ae9ec6cc7afd92c222adc01b42d6 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Sun, 22 Feb 2026 01:50:42 -0600 Subject: [PATCH 19/19] Smoke build selected benchmark targets in quality gate --- scripts/quality_gates.sh | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 2479b1b4..f6f4cf77 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -440,7 +440,39 @@ run_benchmarks_gate() { start_section "benchmarks" local original_jobs="${JOBS}" JOBS=1 - run_build "benchmarks" 17 ON OFF OFF "${BASE_CXX_FLAGS}" -DSML_BUILD_BENCHMARKS=ON -DSML_BUILD_TESTS=OFF -DSML_BUILD_EXAMPLES=OFF + + local label="benchmarks" + local build_dir="${BUILD_ROOT}/${label}" + rm -rf "${build_dir}" + mkdir -p "${build_dir}" + + local -a cmake_args=( + -S "${REPO_ROOT}" + -B "${build_dir}" + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_CXX_COMPILER="${CXX_BIN}" + -DCMAKE_CXX_STANDARD=17 + -DSML_BUILD_TESTS=OFF + -DSML_BUILD_EXAMPLES=OFF + -DSML_BUILD_BENCHMARKS=ON + -DSML_USE_EXCEPTIONS=ON + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + -DCMAKE_CXX_FLAGS="${BASE_CXX_FLAGS}" + ) + + if [[ -n "${CMAKE_GENERATOR}" ]]; then + cmake_args=(-G "${CMAKE_GENERATOR}" "${cmake_args[@]}") + fi + + cmake "${cmake_args[@]}" + + local -a benchmark_targets=( + switch + simple_sml + header_sml + ) + cmake --build "${build_dir}" --target "${benchmark_targets[@]}" -j "${JOBS}" + JOBS="${original_jobs}" echo "Benchmark smoke build passed." }