diff --git a/.clang-format b/.clang-format index a5762e0..fcf9718 100644 --- a/.clang-format +++ b/.clang-format @@ -1,7 +1,7 @@ --- Language: Cpp BasedOnStyle: LLVM -Standard: c++23 +Standard: Auto IndentWidth: 4 TabWidth: 4 @@ -14,7 +14,7 @@ IndentGotoLabels: true NamespaceIndentation: None AccessModifierOffset: -4 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: true AlignConsecutiveAssignments: None AlignConsecutiveDeclarations: None AlignConsecutiveMacros: None diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e188a7a..851c084 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [ main ] pull_request: branches: [ main ] + workflow_dispatch: jobs: build: @@ -13,7 +14,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] compiler: [gcc, clang] - build_type: [Release, Debug] + preset: [release-strict, debug-sanitizers] exclude: - os: macos-latest compiler: gcc @@ -31,16 +32,83 @@ jobs: run: conan profile detect --force - name: Install dependencies - run: conan install . --build=missing -s build_type=${{ matrix.build_type }} + run: | + BUILD_TYPE=$(echo "${{ matrix.preset }}" | grep -q "release" && echo "Release" || echo "Debug") + conan install . --build=missing -s build_type=$BUILD_TYPE -s compiler.cppstd=23 - name: Configure CMake - run: cmake --preset conan-${{ matrix.build_type == 'Release' && 'release' || 'debug' }} + run: cmake --preset ${{ matrix.preset }} env: CC: ${{ matrix.compiler == 'gcc' && 'gcc' || 'clang' }} CXX: ${{ matrix.compiler == 'gcc' && 'g++' || 'clang++' }} + - name: Verify C++23 standard + run: | + echo "=== Compiler Information ===" + ${{ matrix.compiler == 'gcc' && 'g++' || 'clang++' }} --version + echo "" + echo "=== Checking C++ standard in compile commands ===" + if [ -f "build/${{ matrix.preset }}/compile_commands.json" ]; then + # Extract and display C++ standard flags + grep -o '\-std=[^ " ]*' "build/${{ matrix.preset }}/compile_commands.json" | sort -u || echo "No -std flag found" + # Verify C++23 is being used + if grep -q '\-std=c++2[3b]' "build/${{ matrix.preset }}/compile_commands.json"; then + echo "✓ C++23 standard verified" + else + echo "✗ C++23 standard NOT found in compile commands" + exit 1 + fi + else + echo "Warning: compile_commands.json not found" + fi + + - name: Build + run: cmake --build --preset ${{ matrix.preset }} + + - name: Test + run: ctest --preset ${{ matrix.preset }} --output-on-failure + + coverage: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Conan + run: pipx install conan + + - name: Install lcov + run: sudo apt-get install -y lcov + + - name: Configure Conan profile + run: conan profile detect --force + + - name: Install dependencies + run: conan install . --build=missing -s build_type=Debug -s compiler.cppstd=23 + + - name: Configure CMake + run: cmake --preset debug-coverage + env: + CC: gcc + CXX: g++ + - name: Build - run: cmake --build --preset conan-${{ matrix.build_type == 'Release' && 'release' || 'debug' }} + run: | + find build -name "*.gcda" -delete 2>/dev/null || true + find build -name "*.gcno" -delete 2>/dev/null || true + cmake --build --preset debug-coverage - name: Test - run: ctest --preset conan-${{ matrix.build_type == 'Release' && 'release' || 'debug' }} --output-on-failure + run: ctest --preset debug-coverage --output-on-failure + + - name: Generate coverage report + run: cmake --build --preset debug-coverage --target coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./build/debug-coverage/coverage/coverage_filtered.info + fail_ci_if_error: true + verbose: true diff --git a/.gitignore b/.gitignore index bf44f88..5ac0855 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ CMakeFiles/ cmake_install.cmake Makefile compile_commands.json +CMakeUserPresets.json # OS files .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 56ea30c..e2ad3d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,11 @@ project( set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS OFF) # Ensures strict C++23 standard + +# Force gtest targets to compile with the same C++23 standard +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +set(BUILD_GTEST ON CACHE BOOL "" FORCE) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(MSVC) @@ -19,10 +23,11 @@ else() endif() include(cmake/ConanSetup.cmake) -include(cmake/LintTargets.cmake) include(cmake/CodeCoverage.cmake) include(cmake/Sanitizers.cmake) enable_testing() add_subdirectory(src) add_subdirectory(tests) + +include(cmake/LintTargets.cmake) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..4ff1928 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,146 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 27, + "patch": 0 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "displayName": "Release", + "description": "Optimized release build", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/Release/generators/conan_toolchain.cmake" + } + }, + { + "name": "debug", + "displayName": "Debug", + "description": "Debug build with full symbols", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/Debug/generators/conan_toolchain.cmake" + } + }, + { + "name": "debug-sanitizers", + "displayName": "Debug with Sanitizers", + "description": "Debug build with AddressSanitizer and UndefinedBehaviorSanitizer", + "inherits": "debug", + "cacheVariables": { + "ENABLE_SANITIZERS": "ON" + } + }, + { + "name": "debug-coverage", + "displayName": "Debug with Coverage", + "description": "Debug build with code coverage instrumentation", + "inherits": "debug", + "cacheVariables": { + "ENABLE_COVERAGE": "ON" + } + }, + { + "name": "debug-full", + "displayName": "Debug Full Check", + "description": "Debug build with sanitizers, coverage, and linting", + "inherits": "debug", + "cacheVariables": { + "ENABLE_SANITIZERS": "ON", + "ENABLE_COVERAGE": "ON", + "ENABLE_LINTING": "ON" + } + }, + { + "name": "release-strict", + "displayName": "Release with Strict Linting", + "description": "Release build with linting enforced during compilation", + "inherits": "release", + "cacheVariables": { + "ENABLE_LINTING": "ON" + } + } + ], + "buildPresets": [ + { + "name": "release", + "configurePreset": "release" + }, + { + "name": "debug", + "configurePreset": "debug" + }, + { + "name": "debug-sanitizers", + "configurePreset": "debug-sanitizers" + }, + { + "name": "debug-coverage", + "configurePreset": "debug-coverage" + }, + { + "name": "debug-full", + "configurePreset": "debug-full" + }, + { + "name": "release-strict", + "configurePreset": "release-strict" + } + ], + "testPresets": [ + { + "name": "release", + "configurePreset": "release", + "output": { + "outputOnFailure": true + } + }, + { + "name": "debug", + "configurePreset": "debug", + "output": { + "outputOnFailure": true + } + }, + { + "name": "debug-sanitizers", + "configurePreset": "debug-sanitizers", + "output": { + "outputOnFailure": true + } + }, + { + "name": "debug-coverage", + "configurePreset": "debug-coverage", + "output": { + "outputOnFailure": true + } + }, + { + "name": "debug-full", + "configurePreset": "debug-full", + "output": { + "outputOnFailure": true + } + }, + { + "name": "release-strict", + "configurePreset": "release-strict", + "output": { + "outputOnFailure": true + } + } + ] +} diff --git a/README.md b/README.md index 29c29b1..defeb71 100644 --- a/README.md +++ b/README.md @@ -111,22 +111,61 @@ For detailed explanations of every command, see the [Build Guide](docs/BUILD.md) conan install . --build=missing # 2. Configure the project -cmake --preset conan-release +cmake --preset release # 3. Build -cmake --build --preset conan-release +cmake --build --preset release # 4. Run -./build/Release/src/cpp_executable_template +./build/release/src/cpp_executable_template # 5. Run tests -ctest --preset conan-release +ctest --preset release ``` For Debug builds, coverage, sanitizers, and linting workflows, see the [Documentation](#documentation) section below. --- +## Build Presets + +This project uses CMake presets to simplify build configuration. All presets are defined in `CMakePresets.json` (committed to the repository). Developers can add personal overrides in `CMakeUserPresets.json` (gitignored). + +| Preset | Description | Use Case | +|--------|-------------|----------| +| `release` | Release build (optimized) | Production builds, performance testing | +| `debug` | Debug build (with debug symbols) | Development, debugging | +| `debug-sanitizers` | Debug + AddressSanitizer + UBSan | Memory error detection, undefined behavior checks | +| `debug-coverage` | Debug + code coverage | Test coverage analysis | +| `debug-full` | Debug + sanitizers + coverage + linting | Comprehensive pre-commit checks | +| `release-strict` | Release + strict linting | CI/CD pipelines, code quality enforcement | + +**Example usage:** + +```bash +# Build with sanitizers enabled +cmake --preset debug-sanitizers +cmake --build --preset debug-sanitizers +ctest --preset debug-sanitizers + +# Build with coverage enabled +cmake --preset debug-coverage +cmake --build --preset debug-coverage +ctest --preset debug-coverage +cmake --build --preset debug-coverage --target coverage + +# Full check (sanitizers + coverage + linting) +cmake --preset debug-full +cmake --build --preset debug-full +ctest --preset debug-full +``` + +**Note:** `CMakePresets.json` is committed to the repository and defines project-wide presets. `CMakeUserPresets.json` is gitignored and can be used for personal overrides. + +For detailed explanations of every command, see the [Build Guide](docs/BUILD.md). + +--- + ## Documentation Detailed guides for each workflow: @@ -136,6 +175,7 @@ Detailed guides for each workflow: | [**Build Guide**](docs/BUILD.md) | Installing Conan, understanding `conan install`, CMake presets, Debug vs Release, build output structure | | [**Linting Guide**](docs/LINTING.md) | clang-format, clang-tidy, pre-commit hooks, naming conventions, enforcing during build, typical workflow | | [**Testing Guide**](docs/TESTING.md) | Running tests, adding new tests, code coverage reports, AddressSanitizer, UndefinedBehaviorSanitizer | +| [**Codecov Guide**](docs/CODECOV.md) | Setting up Codecov, coverage threshold enforcement, viewing coverage reports | --- @@ -144,6 +184,7 @@ Detailed guides for each workflow: ``` cpp-executable-template/ ├── CMakeLists.txt # Root CMake configuration +├── CMakePresets.json # Project-wide CMake presets (release, debug, etc.) ├── conanfile.py # Conan 2.x package manager definition ├── README.md # This file ├── LICENSE # MIT license @@ -172,6 +213,7 @@ cpp-executable-template/ │ ├── LintTargets.cmake # Formatting and linting CMake targets │ ├── CheckPragmaOnce.cmake # Enforces #pragma once in all headers │ ├── CodeCoverage.cmake # Code coverage reporting with lcov +│ ├── CheckCoverageThreshold.cmake # Enforces minimum coverage percentage │ └── Sanitizers.cmake # AddressSanitizer and UndefinedBehaviorSanitizer │ ├── .github/ # GitHub-specific configuration @@ -181,6 +223,10 @@ cpp-executable-template/ ├── scripts/ # Build and utility scripts │ ├── docs/ # Additional project documentation +│ ├── BUILD.md # Build configuration guide +│ ├── LINTING.md # Code quality and formatting guide +│ ├── TESTING.md # Testing, coverage, and sanitizers guide +│ └── CODECOV.md # Codecov setup and integration guide │ └── external/ # Vendored third-party source code ``` @@ -318,6 +364,7 @@ Contains CMake helper modules, custom `Find*.cmake` scripts, and toolchain integ - `LintTargets.cmake` — adds `format-check`, `format-fix`, `lint`, and `check-headers` CMake targets - `CheckPragmaOnce.cmake` — CMake script that verifies all headers contain `#pragma once` - `CodeCoverage.cmake` — adds `coverage` CMake target for generating HTML coverage reports with lcov +- `CheckCoverageThreshold.cmake` — CMake script that enforces minimum coverage percentage (default 80%) - `Sanitizers.cmake` — enables AddressSanitizer and UndefinedBehaviorSanitizer via `-DENABLE_SANITIZERS=ON` - Custom `Find.cmake` modules for dependencies not available through Conan - CMake utility scripts (e.g., code coverage setup, sanitizers configuration) @@ -392,13 +439,18 @@ The root CMake configuration file. It: - Includes sanitizer support from `cmake/Sanitizers.cmake` - Enables CTest and adds `src/` and `tests/` as subdirectories +### `CMakePresets.json` + +Defines project-wide CMake presets for common build configurations. Committed to the repository so all developers use the same presets. Developers can add personal overrides in `CMakeUserPresets.json` (gitignored). See the [Build Presets](#build-presets) section for available presets and usage. + ### `conanfile.py` The Conan 2.x package definition. It: - Declares the package name and version - Specifies standard settings (`os`, `compiler`, `build_type`, `arch`) -- Uses `CMakeDeps` and `CMakeToolchain` generators for seamless CMake integration +- Uses `CMakeDeps` generator for dependency config files +- Uses `CMakeToolchain` in `generate()` with preset generation disabled (presets are managed in `CMakePresets.json`) - Defines `requirements()` where you add dependencies (e.g., `self.requires("fmt/10.2.1")`) - Uses `cmake_layout()` for standardized build directory structure @@ -533,7 +585,7 @@ add_executable(${PROJECT_NAME}_tests test_main.cpp test_config.cpp) ``` 3. Write your tests using Google Test macros (`TEST`, `EXPECT_*`, `ASSERT_*`) -4. Run `ctest --preset conan-release` +4. Run `ctest --preset release` --- @@ -566,37 +618,40 @@ Enforced by `.clang-tidy` via `readability-identifier-naming`: ```bash # Check formatting -cmake --build build/Release --target format-check +cmake --build build/release --target format-check # Auto-fix formatting -cmake --build build/Release --target format-fix +cmake --build build/release --target format-fix # Run linter -cmake --build build/Release --target lint +cmake --build build/release --target lint # Check headers for #pragma once -cmake --build build/Release --target check-headers +cmake --build build/release --target check-headers ``` --- ## Code Coverage & Sanitizers -For detailed instructions on generating code coverage reports and using AddressSanitizer/UndefinedBehaviorSanitizer, see the [Testing Guide](docs/TESTING.md). +For detailed instructions on generating code coverage reports and using AddressSanitizer/UndefinedBehaviorSanitizer, see the [Testing Guide](docs/TESTING.md) and [Codecov Guide](docs/CODECOV.md). + +The project enforces a **minimum 80% line coverage** threshold. The build will fail if coverage falls below this threshold. Quick reference: ```bash # Code coverage (Debug build) -cmake --preset conan-debug -DENABLE_COVERAGE=ON -cmake --build --preset conan-debug -ctest --preset conan-debug -cmake --build build/Debug --target coverage +cmake --preset debug-coverage +cmake --build --preset debug-coverage +ctest --preset debug-coverage +cmake --build --preset debug-coverage --target coverage +# Fails if coverage < 80% # Sanitizers (Debug build) -cmake --preset conan-debug -DENABLE_SANITIZERS=ON -cmake --build --preset conan-debug -ctest --preset conan-debug --output-on-failure +cmake --preset debug-sanitizers +cmake --build --preset debug-sanitizers +ctest --preset debug-sanitizers --output-on-failure ``` --- @@ -608,20 +663,26 @@ This project includes a GitHub Actions workflow (`.github/workflows/ci.yml`) tha **What it does:** - Runs on Linux (Ubuntu) and macOS - Tests with both GCC and Clang compilers -- Builds in both Release and Debug configurations +- Builds with `release-strict` preset (Release + linting enforced) +- Builds with `debug-full` preset (Debug + sanitizers + coverage + linting) - Installs Conan dependencies - Configures, builds, and runs tests via CTest +- Generates coverage reports and uploads to Codecov +- **Fails if coverage falls below 80%** The workflow matrix excludes GCC on macOS since Clang is the default compiler there. **Viewing CI status:** After pushing to GitHub, check the Actions tab to see build status. A green checkmark indicates all configurations passed. +**Viewing coverage:** +After enabling Codecov (see [Codecov Guide](docs/CODECOV.md)), coverage reports are automatically uploaded from the `debug-full` job. + **Customizing the workflow:** Edit `.github/workflows/ci.yml` to: - Add Windows support (requires MSVC setup) -- Enable code coverage reporting -- Add static analysis uploads (e.g., SonarCloud, Codecov) +- Adjust coverage threshold +- Add static analysis uploads (e.g., SonarCloud) - Add deployment/release automation --- diff --git a/cmake/CheckCoverageThreshold.cmake b/cmake/CheckCoverageThreshold.cmake new file mode 100644 index 0000000..e61180e --- /dev/null +++ b/cmake/CheckCoverageThreshold.cmake @@ -0,0 +1,51 @@ +# CheckCoverageThreshold.cmake +# Parses lcov coverage data and fails if coverage is below threshold +# +# Usage: +# cmake -DCOVERAGE_FILE=path/to/coverage.info -DCOVERAGE_THRESHOLD=80 -P CheckCoverageThreshold.cmake + +if(NOT DEFINED COVERAGE_FILE) + message(FATAL_ERROR "COVERAGE_FILE is not defined") +endif() + +if(NOT DEFINED COVERAGE_THRESHOLD) + set(COVERAGE_THRESHOLD 80) +endif() + +if(NOT EXISTS "${COVERAGE_FILE}") + message(FATAL_ERROR "Coverage file not found: ${COVERAGE_FILE}") +endif() + +# Read the coverage file +file(READ "${COVERAGE_FILE}" COVERAGE_CONTENT) + +# Extract line coverage percentage +# lcov format: LH: LF: +string(REGEX MATCH "LF:([0-9]+)" LF_MATCH "${COVERAGE_CONTENT}") +string(REGEX MATCH "LH:([0-9]+)" LH_MATCH "${COVERAGE_CONTENT}") + +if(NOT LF_MATCH OR NOT LH_MATCH) + message(FATAL_ERROR "Could not parse coverage data from ${COVERAGE_FILE}") +endif() + +string(REGEX REPLACE "LF:([0-9]+)" "\\1" LINES_FOUND "${LF_MATCH}") +string(REGEX REPLACE "LH:([0-9]+)" "\\1" LINES_HIT "${LH_MATCH}") + +if(LINES_FOUND EQUAL 0) + message(FATAL_ERROR "No lines found in coverage data") +endif() + +# Calculate percentage +math(EXPR COVERAGE_PERCENT "(${LINES_HIT} * 100) / ${LINES_FOUND}") + +message(STATUS "Coverage: ${LINES_HIT}/${LINES_FOUND} lines (${COVERAGE_PERCENT}%)") +message(STATUS "Threshold: ${COVERAGE_THRESHOLD}%") + +if(COVERAGE_PERCENT LESS COVERAGE_THRESHOLD) + message(FATAL_ERROR + "Code coverage ${COVERAGE_PERCENT}% is below the required threshold of ${COVERAGE_THRESHOLD}%. " + "Please add more tests to increase coverage." + ) +else() + message(STATUS "Coverage check passed: ${COVERAGE_PERCENT}% >= ${COVERAGE_THRESHOLD}%") +endif() diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake index 1a654bf..9f73d83 100644 --- a/cmake/CodeCoverage.cmake +++ b/cmake/CodeCoverage.cmake @@ -1,4 +1,5 @@ option(ENABLE_COVERAGE "Enable code coverage reporting" OFF) +set(COVERAGE_THRESHOLD 80 CACHE STRING "Minimum code coverage percentage (0-100)") if(ENABLE_COVERAGE) if(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") @@ -7,13 +8,42 @@ if(ENABLE_COVERAGE) return() endif() - message(STATUS "Code coverage enabled") + message(STATUS "Code coverage enabled (threshold: ${COVERAGE_THRESHOLD}%)") add_compile_options(--coverage) add_link_options(--coverage) find_program(LCOV_EXECUTABLE NAMES lcov) find_program(GENHTML_EXECUTABLE NAMES genhtml) + + # Find gcov that matches the compiler + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + # Extract GCC version and find matching gcov + execute_process( + COMMAND ${CMAKE_CXX_COMPILER} -dumpversion + OUTPUT_VARIABLE GCC_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + string(REGEX MATCH "^[0-9]+" GCC_VERSION_MAJOR "${GCC_VERSION}") + find_program(GCOV_EXECUTABLE NAMES gcov-${GCC_VERSION_MAJOR} gcov) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + find_program(LLVM_COV_EXECUTABLE NAMES llvm-cov) + if(LLVM_COV_EXECUTABLE) + set(GCOV_EXECUTABLE_WRAPPER "${CMAKE_BINARY_DIR}/llvm-cov-gcov") + file(WRITE "${GCOV_EXECUTABLE_WRAPPER}" "#!/bin/sh\nexec ${LLVM_COV_EXECUTABLE} gcov \"$@\"\n") + execute_process(COMMAND chmod +x "${GCOV_EXECUTABLE_WRAPPER}") + set(GCOV_EXECUTABLE "${GCOV_EXECUTABLE_WRAPPER}") + else() + find_program(GCOV_EXECUTABLE NAMES gcov) + endif() + endif() + + if(NOT GCOV_EXECUTABLE) + message(WARNING "gcov not found. Coverage target will not be available.") + return() + endif() + + message(STATUS "Using gcov: ${GCOV_EXECUTABLE}") if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE) set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage") @@ -21,28 +51,31 @@ if(ENABLE_COVERAGE) add_custom_target(coverage COMMAND ${CMAKE_COMMAND} -E make_directory ${COVERAGE_DIR} COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} - ${LCOV_EXECUTABLE} --capture --directory . --output-file ${COVERAGE_DIR}/coverage.info - --ignore-errors mismatch + ${LCOV_EXECUTABLE} --capture --directory src --directory tests --output-file ${COVERAGE_DIR}/coverage.info + --base-directory ${CMAKE_SOURCE_DIR} + --gcov-tool ${GCOV_EXECUTABLE} + --ignore-errors mismatch,format,unsupported,version,empty COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} ${LCOV_EXECUTABLE} --remove ${COVERAGE_DIR}/coverage.info '/usr/*' '*/tests/*' '*/build/*' '*/external/*' --output-file ${COVERAGE_DIR}/coverage_filtered.info - --ignore-errors unused + --gcov-tool ${GCOV_EXECUTABLE} + --ignore-errors unused,format,unsupported,version,empty COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} ${GENHTML_EXECUTABLE} ${COVERAGE_DIR}/coverage_filtered.info --output-directory ${COVERAGE_DIR}/html --title "${PROJECT_NAME} Code Coverage" --legend --show-details + --ignore-errors empty COMMAND ${CMAKE_COMMAND} -E echo "Coverage report generated at: ${COVERAGE_DIR}/html/index.html" + COMMAND ${CMAKE_COMMAND} + -DCOVERAGE_FILE=${COVERAGE_DIR}/coverage_filtered.info + -DCOVERAGE_THRESHOLD=${COVERAGE_THRESHOLD} + -P ${CMAKE_SOURCE_DIR}/cmake/CheckCoverageThreshold.cmake WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Generating code coverage report..." ) - add_custom_command(TARGET coverage PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} - ${LCOV_EXECUTABLE} --zerocounters --directory . - COMMENT "Resetting coverage counters..." - ) else() message(WARNING "lcov or genhtml not found. Coverage target will not be available.") message(STATUS "Install lcov: apt-get install lcov (Linux) or brew install lcov (macOS)") diff --git a/cmake/LintTargets.cmake b/cmake/LintTargets.cmake index 106b238..4f318dc 100644 --- a/cmake/LintTargets.cmake +++ b/cmake/LintTargets.cmake @@ -59,7 +59,7 @@ if(ENABLE_LINTING) add_dependencies(${PROJECT_NAME} format-check) endif() if(CLANG_TIDY_EXECUTABLE) - set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXECUTABLE} PARENT_SCOPE) + set_target_properties(${PROJECT_NAME} PROPERTIES CXX_CLANG_TIDY ${CLANG_TIDY_EXECUTABLE}) endif() add_dependencies(${PROJECT_NAME} check-headers) endif() diff --git a/conanfile.py b/conanfile.py index f47b615..e9e738b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,10 +6,18 @@ class CppExecutableTemplate(ConanFile): name = "cpp_executable_template" version = "0.1.0" settings = "os", "compiler", "build_type", "arch" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeDeps" + + def configure(self): + self.settings.compiler.cppstd = "23" def requirements(self): - self.requires("gtest/1.14.0") + self.requires("gtest/1.17.0") + + def generate(self): + tc = CMakeToolchain(self) + tc.generate_presets = False + tc.generate() def layout(self): cmake_layout(self) diff --git a/docs/BUILD.md b/docs/BUILD.md index 0885782..a71a194 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -77,7 +77,6 @@ The `.` tells Conan to look in the current directory for `conanfile.py`. Conan t 2. **Downloads packages** from Conan Center (https://conan.io/center/) 3. **Generates CMake integration files** in the build directory: - `conan_toolchain.cmake` — tells CMake where to find dependencies and their headers/libraries - - `CMakePresets.json` — defines `conan-release` and `conan-debug` presets that CMake can use - Package config files (e.g., `GTestConfig.cmake`) for each dependency **`--build=missing`** @@ -114,7 +113,7 @@ conan install . --build=missing -s build_type=Debug The `-s build_type=Debug` flag tells Conan to: - Use Debug compiler flags (`-g -O0` instead of `-O2 -DNDEBUG`) - Download/build Debug versions of dependencies -- Generate `conan-debug` preset instead of `conan-release` +- Generate `debug` preset instead of `release` ### Real Example @@ -141,7 +140,6 @@ gtest/1.14.0: Already installed! ======== Generating toolchain files ======== CMakeToolchain generated: conan_toolchain.cmake CMakeDeps generated: GTestConfig.cmake -CMakePresets generated: CMakePresets.json ``` --- @@ -156,34 +154,50 @@ CMake presets are predefined configuration sets that specify: - Compiler settings - CMake cache variables -Conan generates presets automatically when you run `conan install`. +This project defines presets in `CMakePresets.json` (committed to the repository). Developers can add personal overrides in `CMakeUserPresets.json` (gitignored). ### Available Presets -After running `conan install . --build=missing`, two presets are available: +The following presets are defined in `CMakePresets.json`: -**`conan-release`** -- Build directory: `build/Release` +**`release`** +- Build directory: `build/release` - Build type: `Release` (optimizations enabled, debug symbols stripped) - Compiler flags: `-O2 -DNDEBUG` -**`conan-debug`** -- Build directory: `build/Debug` +**`debug`** +- Build directory: `build/debug` - Build type: `Debug` (no optimizations, full debug symbols) - Compiler flags: `-g -O0` +**`debug-sanitizers`** +- Inherits from: `debug` +- Enables: AddressSanitizer + UndefinedBehaviorSanitizer + +**`debug-coverage`** +- Inherits from: `debug` +- Enables: Code coverage instrumentation + +**`debug-full`** +- Inherits from: `debug` +- Enables: Sanitizers + coverage + linting + +**`release-strict`** +- Inherits from: `release` +- Enables: Linting enforced during build + ### Using Presets **Configure with a preset:** ```bash -cmake --preset conan-release +cmake --preset release ``` This is equivalent to: ```bash -cmake -B build/Release \ +cmake -B build/release \ -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release ``` @@ -191,24 +205,24 @@ cmake -B build/Release \ **Build with a preset:** ```bash -cmake --build --preset conan-release +cmake --build --preset release ``` -This builds everything in `build/Release`. +This builds everything in `build/release`. **Run tests with a preset:** ```bash -ctest --preset conan-release +ctest --preset release ``` -This runs tests from the `build/Release` directory. +This runs tests from the `build/release` directory. ### Real Example ```bash # Configure Release build -cmake --preset conan-release +cmake --preset release # Expected output: # Preset CMake variables: @@ -216,10 +230,10 @@ cmake --preset conan-release # CMAKE_TOOLCHAIN_FILE=/path/to/conan_toolchain.cmake # -- Configuring done # -- Generating done -# -- Build files have been written to: /path/to/build/Release +# -- Build files have been written to: /path/to/build/release # Build -cmake --build --preset conan-release +cmake --build --preset release # Expected output: # [1/3] Building CXX object src/CMakeFiles/cpp_executable_template.dir/main.cpp.o @@ -227,13 +241,85 @@ cmake --build --preset conan-release # [3/3] Built target cpp_executable_template ``` +### Custom Presets (CMakePresets.json) + +The following custom presets are defined in `CMakePresets.json` for common development workflows: + +| Preset | Description | Enabled Features | +|--------|-------------|------------------| +| `debug-sanitizers` | Debug build with sanitizers | AddressSanitizer, UndefinedBehaviorSanitizer | +| `debug-coverage` | Debug build with coverage | Code coverage instrumentation | +| `debug-full` | Comprehensive debug build | Sanitizers + coverage + linting | +| `release-strict` | Release with strict linting | Linting enforced during build | + +These presets inherit from `debug` or `release` and add specific CMake options. + +**Why CMakePresets.json and CMakeUserPresets.json?** + +- `CMakePresets.json` is committed to the repository and defines project-wide presets that all developers use +- `CMakeUserPresets.json` is gitignored and can be used for personal overrides or experimental configurations +- Conan's preset generation is disabled (see `conanfile.py`) to avoid overwriting the committed presets + +**Using custom presets:** + +```bash +# Build with sanitizers (catches memory errors and undefined behavior) +cmake --preset debug-sanitizers +cmake --build --preset debug-sanitizers +ctest --preset debug-sanitizers + +# Real example output: +# -- Sanitizers enabled (AddressSanitizer + UndefinedBehaviorSanitizer) +# -- Configuring done +# -- Generating done +``` + +```bash +# Build with coverage (generates HTML coverage report) +cmake --preset debug-coverage +cmake --build --preset debug-coverage +ctest --preset debug-coverage +cmake --build --preset debug-coverage --target coverage + +# Real example output: +# -- Code coverage enabled +# -- Configuring done +# -- Generating done +# [100%] Generating code coverage report... +# Coverage report generated at: build/coverage/html/index.html +``` + +```bash +# Full check (sanitizers + coverage + linting) +cmake --preset debug-full +cmake --build --preset debug-full +ctest --preset debug-full + +# Real example output: +# -- Sanitizers enabled (AddressSanitizer + UndefinedBehaviorSanitizer) +# -- Code coverage enabled +# -- Configuring done +# -- Generating done +# [ 50%] Checking code formatting... +# [100%] Built target cpp_executable_template +``` + +**When to use each preset:** + +- **`release`**: Production builds, performance testing, deployment +- **`debug`**: Daily development, debugging, running tests +- **`debug-sanitizers`**: Before merging code, catching memory bugs, CI pipelines +- **`debug-coverage`**: Analyzing test coverage, finding untested code paths +- **`debug-full`**: Pre-commit checks, ensuring code quality before pushing +- **`release-strict`**: CI/CD pipelines, enforcing code standards in production builds + --- ## Build Configurations ### Release vs Debug -**Release (`conan-release`)** +**Release (`release`)** - **Use when:** Building for production, performance testing, deployment - **Optimizations:** Enabled (`-O2`) - **Debug symbols:** Stripped @@ -241,7 +327,7 @@ cmake --build --preset conan-release - **Binary size:** Smaller - **Performance:** Faster -**Debug (`conan-debug`)** +**Debug (`debug`)** - **Use when:** Development, debugging, testing, code coverage, sanitizers - **Optimizations:** Disabled (`-O0`) - **Debug symbols:** Full (`-g`) @@ -266,20 +352,16 @@ cmake --build --preset conan-release You can configure both Release and Debug builds side-by-side: ```bash -# Install dependencies for both -conan install . --build=missing -conan install . --build=missing -s build_type=Debug - # Configure both -cmake --preset conan-release -cmake --preset conan-debug +cmake --preset release +cmake --preset debug # Build either -cmake --build --preset conan-release -cmake --build --preset conan-debug +cmake --build --preset release +cmake --build --preset debug ``` -Each configuration lives in its own directory (`build/Release` and `build/Debug`), so they don't interfere with each other. +Each configuration lives in its own directory (`build/release` and `build/debug`), so they don't interfere with each other. --- @@ -291,7 +373,7 @@ The `conanfile.py` uses Conan's `cmake_layout()`, which organizes build artifact ``` build/ -├── Release/ # Release build artifacts +├── release/ # Release build artifacts │ ├── src/ │ │ └── cpp_executable_template # The executable │ ├── tests/ @@ -300,7 +382,7 @@ build/ │ ├── compile_commands.json # For clangd/clang-tidy │ └── ... │ -└── Debug/ # Debug build artifacts +└── debug/ # Debug build artifacts ├── src/ │ └── cpp_executable_template ├── tests/ @@ -319,18 +401,18 @@ build/ - Used by `clangd` (language server) for IDE integration **Executable location** -- Release: `build/Release/src/cpp_executable_template` -- Debug: `build/Debug/src/cpp_executable_template` +- Release: `build/release/src/cpp_executable_template` +- Debug: `build/debug/src/cpp_executable_template` - On Windows: `.exe` extension is added automatically ### Running the Executable ```bash # Release -./build/Release/src/cpp_executable_template +./build/release/src/cpp_executable_template # Debug -./build/Debug/src/cpp_executable_template +./build/debug/src/cpp_executable_template ``` --- @@ -355,19 +437,19 @@ conan install . --build=missing **Step 3: Configure the project** ```bash -cmake --preset conan-release +cmake --preset release ``` **Step 4: Build** ```bash -cmake --build --preset conan-release +cmake --build --preset release ``` **Step 5: Run the executable** ```bash -./build/Release/src/cpp_executable_template +./build/release/src/cpp_executable_template ``` **Expected output:** @@ -383,13 +465,13 @@ Hello, World! conan install . --build=missing -s build_type=Debug # Configure -cmake --preset conan-debug +cmake --preset debug # Build -cmake --build --preset conan-debug +cmake --build --preset debug # Run (with debug symbols for gdb/lldb) -gdb ./build/Debug/src/cpp_executable_template +gdb ./build/debug/src/cpp_executable_template ``` --- @@ -415,10 +497,10 @@ export PATH="$HOME/.local/bin:$PATH" ```bash conan install . --build=missing -cmake --preset conan-release +cmake --preset release ``` -### "Preset 'conan-release' not found" +### "Preset 'release' not found" **Cause:** Conan did not generate `CMakePresets.json`. @@ -451,12 +533,12 @@ conan install . --build=missing # for Release | Command | Purpose | |---|---| | `conan install . --build=missing` | Install dependencies and generate CMake integration files | -| `cmake --preset conan-release` | Configure Release build | -| `cmake --preset conan-debug` | Configure Debug build | -| `cmake --build --preset conan-release` | Build Release configuration | -| `cmake --build --preset conan-debug` | Build Debug configuration | -| `ctest --preset conan-release` | Run tests (Release) | -| `ctest --preset conan-debug` | Run tests (Debug) | +| `cmake --preset release` | Configure Release build | +| `cmake --preset debug` | Configure Debug build | +| `cmake --build --preset release` | Build Release configuration | +| `cmake --build --preset debug` | Build Debug configuration | +| `ctest --preset release` | Run tests (Release) | +| `ctest --preset debug` | Run tests (Debug) | For linting, testing, coverage, and sanitizers, see: - [Linting Guide](LINTING.md) diff --git a/docs/CODECOV.md b/docs/CODECOV.md new file mode 100644 index 0000000..11d954f --- /dev/null +++ b/docs/CODECOV.md @@ -0,0 +1,206 @@ +# Codecov Integration Guide + +This document explains how to set up and use Codecov for code coverage reporting in this project. + +--- + +## What is Codecov? + +[Codecov](https://codecov.io) is a code coverage reporting service that: +- Tracks coverage trends over time +- Shows coverage changes in pull requests +- Provides detailed coverage reports +- Is free for public repositories + +--- + +## Setup Instructions + +### 1. Sign Up for Codecov + +1. Go to [https://codecov.io](https://codecov.io) +2. Click **"Sign Up"** +3. Choose **"Sign up with GitHub"** +4. Authorize Codecov to access your GitHub account + +### 2. Enable Your Repository + +1. After signing in, click **"Add new repository"** +2. Find your repository in the list +3. Click **"Enable"** +4. Codecov will now track coverage for this repository + +### 3. (Optional) Add Codecov Token + +For public repositories, the token is optional but recommended for reliability: + +1. In Codecov, go to your repository settings +2. Copy the **Repository Upload Token** +3. In your GitHub repository, go to **Settings → Secrets and variables → Actions** +4. Click **"New repository secret"** +5. Name: `CODECOV_TOKEN` +6. Value: paste the token from Codecov +7. Click **"Add secret"** + +**Note:** For public repos, the GitHub Action will auto-detect the token, so this step is optional. + +--- + +## How It Works + +### Local Coverage + +When you run the `debug-full` preset locally: + +```bash +cmake --preset debug-full +cmake --build --preset debug-full +ctest --preset debug-full +cmake --build --preset debug-full --target coverage +``` + +The coverage target will: +1. Generate coverage data +2. Create an HTML report at `build/debug-full/coverage/html/index.html` +3. **Check if coverage meets the 80% threshold** (fails if below) + +### CI Coverage + +In GitHub Actions, the `debug-full` preset job will: +1. Build and test with coverage enabled +2. Generate the coverage report +3. **Upload to Codecov** (only on Linux + Clang to avoid duplicates) +4. **Fail if coverage is below 80%** + +--- + +## Coverage Threshold + +The project enforces a **minimum 80% line coverage** threshold. + +### Where It's Enforced + +1. **Locally**: The `cmake --build --preset debug-full --target coverage` command will fail if coverage is below 80% +2. **In CI**: The GitHub Actions job will fail if coverage is below 80% + +### Changing the Threshold + +To change the threshold, modify the `COVERAGE_THRESHOLD` CMake variable: + +**Locally:** +```bash +cmake --preset debug-full -DCOVERAGE_THRESHOLD=90 +``` + +**In CI:** Edit `.github/workflows/ci.yml` and add the variable to the CMake configure step. + +**In CMakePresets.json:** Add to the `debug-full` preset: +```json +{ + "name": "debug-full", + "cacheVariables": { + "ENABLE_SANITIZERS": "ON", + "ENABLE_COVERAGE": "ON", + "ENABLE_LINTING": "ON", + "COVERAGE_THRESHOLD": "90" + } +} +``` + +--- + +## Viewing Coverage Reports + +### Codecov Dashboard + +1. Go to [https://codecov.io](https://codecov.io) +2. Select your repository +3. View: + - **Overall coverage percentage** + - **Coverage trends** (graphs over time) + - **File-by-file coverage** + - **PR coverage changes** + +### Pull Request Comments + +Codecov automatically comments on PRs with: +- Coverage change (e.g., "Coverage increased by 2.3%") +- Files with decreased coverage +- Link to detailed report + +### Local Reports + +Open the HTML report in your browser: + +```bash +# Linux +xdg-open build/debug-full/coverage/html/index.html + +# macOS +open build/debug-full/coverage/html/index.html +``` + +--- + +## Troubleshooting + +### "Coverage check passed" but CI fails + +**Cause:** CI might be using a different coverage file path. + +**Solution:** Check the Codecov upload step in `.github/workflows/ci.yml` to ensure the path matches your build directory. + +### "Could not parse coverage data" + +**Cause:** Coverage file is empty or malformed. + +**Solution:** +1. Ensure tests ran successfully: `ctest --preset debug-full` +2. Check that `.gcda` files were generated in `build/debug-full/` +3. Re-run the coverage target + +### Codecov not showing coverage + +**Cause:** Token not configured or upload failed. + +**Solution:** +1. Check GitHub Actions logs for the "Upload coverage to Codecov" step +2. Ensure the repository is enabled in Codecov +3. (Optional) Add `CODECOV_TOKEN` secret to GitHub + +### Coverage percentage seems wrong + +**Cause:** Coverage excludes tests, external code, and system headers. + +**Solution:** This is intentional. The coverage percentage only measures your production code in `src/`, not test code or dependencies. + +--- + +## Best Practices + +1. **Run coverage locally before pushing:** + ```bash + cmake --build --preset debug-full --target coverage + ``` + +2. **Check PR coverage comments:** Codecov will warn you if your PR decreases coverage + +3. **Aim for meaningful coverage:** Don't just write tests to hit 80% — write tests that verify behavior + +4. **Review coverage reports:** Use the HTML report to find untested code paths + +5. **Update threshold as needed:** If 80% is too easy/hard, adjust it in `CMakePresets.json` + +--- + +## Summary + +| Task | Command | +|---|---| +| Generate coverage locally | `cmake --build --preset debug-full --target coverage` | +| View HTML report | `open build/debug-full/coverage/html/index.html` | +| Check threshold | Automatic (fails if < 80%) | +| View Codecov dashboard | [https://codecov.io](https://codecov.io) | +| Change threshold | `cmake --preset debug-full -DCOVERAGE_THRESHOLD=90` | + +For more information on testing and sanitizers, see [Testing Guide](TESTING.md). diff --git a/docs/LINTING.md b/docs/LINTING.md index 58a3e58..646f2e6 100644 --- a/docs/LINTING.md +++ b/docs/LINTING.md @@ -42,7 +42,7 @@ cmake --build build --target format-check **Real example:** ```bash -cmake --build build/Release --target format-check +cmake --build build/release --target format-check ``` **Expected output (clean):** @@ -74,7 +74,7 @@ cmake --build build --target format-fix **Real example:** ```bash -cmake --build build/Release --target format-fix +cmake --build build/release --target format-fix ``` **Expected output:** @@ -124,7 +124,7 @@ cmake --build build --target lint **Real example:** ```bash -cmake --build build/Release --target lint +cmake --build build/release --target lint ``` **Expected output (clean):** @@ -154,8 +154,8 @@ Each warning includes: The lint target uses `compile_commands.json` from whichever build directory you specify: ```bash -cmake --build build/Release --target lint # uses build/Release/compile_commands.json -cmake --build build/Debug --target lint # uses build/Debug/compile_commands.json +cmake --build build/release --target lint # uses build/release/compile_commands.json +cmake --build build/debug --target lint # uses build/debug/compile_commands.json ``` **Does lint run on both Release and Debug?** @@ -168,16 +168,16 @@ No. You must specify which build directory to use. Lint analyzes source code str ```bash # Configure (generates compile_commands.json) -cmake --preset conan-release +cmake --preset release # Now lint works -cmake --build build/Release --target lint +cmake --build build/release --target lint ``` ### Running clang-tidy Directly ```bash -clang-tidy -p build/Release src/main.cpp +clang-tidy -p build/release src/main.cpp ``` The `-p` flag points to the directory containing `compile_commands.json`. @@ -199,7 +199,7 @@ cmake --build build --target check-headers **Real example:** ```bash -cmake --build build/Release --target check-headers +cmake --build build/release --target check-headers ``` **Expected output (clean):** @@ -347,15 +347,15 @@ pre-commit autoupdate When enabled, the build fails if any formatting or linting issues are detected: ```bash -cmake --preset conan-release -DENABLE_LINTING=ON -cmake --build --preset conan-release +cmake --preset release -DENABLE_LINTING=ON +cmake --build --preset release ``` **Real example:** ```bash -cmake --preset conan-release -DENABLE_LINTING=ON -cmake --build --preset conan-release +cmake --preset release -DENABLE_LINTING=ON +cmake --build --preset release ``` **What happens when `ENABLE_LINTING=ON`:** @@ -407,11 +407,11 @@ Enforced by `.clang-tidy` via `readability-identifier-naming`: 2. **Save files** — your editor may auto-format on save if configured 3. **Run format-fix** to ensure consistent formatting: ```bash - cmake --build build/Release --target format-fix + cmake --build build/release --target format-fix ``` 4. **Run lint** to catch issues early: ```bash - cmake --build build/Release --target lint + cmake --build build/release --target lint ``` 5. **Fix any warnings** reported by lint 6. **Commit** — pre-commit hooks run clang-format as a safety net: @@ -426,19 +426,19 @@ Run all checks to ensure CI will pass: ```bash # Format check -cmake --build build/Release --target format-check +cmake --build build/release --target format-check # Lint -cmake --build build/Release --target lint +cmake --build build/release --target lint # Header check -cmake --build build/Release --target check-headers +cmake --build build/release --target check-headers # Build -cmake --build --preset conan-release +cmake --build --preset release # Test -ctest --preset conan-release +ctest --preset release # Pre-commit on all files pre-commit run --all-files @@ -449,9 +449,9 @@ pre-commit run --all-files In CI, enforce everything: ```bash -cmake --preset conan-release -DENABLE_LINTING=ON -cmake --build --preset conan-release -ctest --preset conan-release --output-on-failure +cmake --preset release -DENABLE_LINTING=ON +cmake --build --preset release +ctest --preset release --output-on-failure ``` --- @@ -499,7 +499,7 @@ clang-tidy --version **Solution:** ```bash -cmake --preset conan-release +cmake --preset release ``` ### "pre-commit: command not found" @@ -519,11 +519,11 @@ pre-commit install | Task | Command | |---|---| -| Check formatting | `cmake --build build/Release --target format-check` | -| Auto-fix formatting | `cmake --build build/Release --target format-fix` | -| Run linter | `cmake --build build/Release --target lint` | -| Check headers | `cmake --build build/Release --target check-headers` | -| Enforce during build | `cmake --preset conan-release -DENABLE_LINTING=ON` | +| Check formatting | `cmake --build build/release --target format-check` | +| Auto-fix formatting | `cmake --build build/release --target format-fix` | +| Run linter | `cmake --build build/release --target lint` | +| Check headers | `cmake --build build/release --target check-headers` | +| Enforce during build | `cmake --preset release -DENABLE_LINTING=ON` | | Install pre-commit | `pip install pre-commit && pre-commit install` | | Run all hooks | `pre-commit run --all-files` | diff --git a/docs/TESTING.md b/docs/TESTING.md index 0ec314a..27fad7b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -11,19 +11,19 @@ This document covers running tests, generating code coverage reports, and using Tests are run using CTest, which is integrated with CMake. The test executable is built as part of the normal build process. ```bash -ctest --preset conan-release +ctest --preset release ``` **Real example:** ```bash -ctest --preset conan-release +ctest --preset release ``` **Expected output:** ``` -Test project /path/to/build/Release +Test project /path/to/build/release Start 1: SanityCheck.TrueIsTrue 1/1 Test #1: SanityCheck.TrueIsTrue ............. Passed 0.00 sec @@ -37,19 +37,19 @@ Total Test time (real) = 0.01 sec See detailed test output (useful when tests fail): ```bash -ctest --preset conan-release --output-on-failure +ctest --preset release --output-on-failure ``` **Real example:** ```bash -ctest --preset conan-release --output-on-failure +ctest --preset release --output-on-failure ``` **Expected output (on failure):** ``` -Test project /path/to/build/Release +Test project /path/to/build/release Start 1: SanityCheck.TrueIsTrue 1/1 Test #1: SanityCheck.TrueIsTrue .............***Failed 0.00 sec Running main() from gtest_main.cc @@ -77,13 +77,13 @@ Expected: true Run only tests matching a pattern: ```bash -ctest --preset conan-release -R "SanityCheck" +ctest --preset release -R "SanityCheck" ``` **Real example:** ```bash -ctest --preset conan-release -R "SanityCheck" +ctest --preset release -R "SanityCheck" ``` ### Debug Build Tests @@ -91,7 +91,7 @@ ctest --preset conan-release -R "SanityCheck" Run tests in Debug configuration (recommended for development): ```bash -ctest --preset conan-debug --output-on-failure +ctest --preset debug --output-on-failure ``` --- @@ -129,19 +129,19 @@ add_executable(${PROJECT_NAME}_tests test_main.cpp test_config.cpp) 3. **Rebuild**: ```bash -cmake --build --preset conan-release +cmake --build --preset release ``` 4. **Run tests**: ```bash -ctest --preset conan-release --output-on-failure +ctest --preset release --output-on-failure ``` **Expected output:** ``` -Test project /path/to/build/Release +Test project /path/to/build/release Start 1: SanityCheck.TrueIsTrue 1/3 Test #1: SanityCheck.TrueIsTrue ............. Passed 0.00 sec Start 2: ConfigTest.DefaultValues @@ -229,14 +229,14 @@ Use Debug build for accurate line-level coverage: ```bash conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_COVERAGE=ON +cmake --preset debug -DENABLE_COVERAGE=ON ``` **Real example:** ```bash conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_COVERAGE=ON +cmake --preset debug -DENABLE_COVERAGE=ON ``` **Expected output:** @@ -250,25 +250,25 @@ cmake --preset conan-debug -DENABLE_COVERAGE=ON **Step 2: Build** ```bash -cmake --build --preset conan-debug +cmake --build --preset debug ``` **Step 3: Run tests** ```bash -ctest --preset conan-debug +ctest --preset debug ``` **Step 4: Generate the HTML coverage report** ```bash -cmake --build build/Debug --target coverage +cmake --build build/debug --target coverage ``` **Real example:** ```bash -cmake --build build/Debug --target coverage +cmake --build build/debug --target coverage ``` **Expected output:** @@ -283,17 +283,17 @@ Using 4 file(s) Overall coverage rate: lines......: 85.7% (42 of 49 lines) functions..: 100.0% (8 of 8 functions) -Coverage report generated at: /path/to/build/Debug/coverage/html/index.html +Coverage report generated at: /path/to/build/debug/coverage/html/index.html ``` **Step 5: Open the report** ```bash # Linux -xdg-open build/Debug/coverage/html/index.html +xdg-open build/debug/coverage/html/index.html # macOS -open build/Debug/coverage/html/index.html +open build/debug/coverage/html/index.html ``` The HTML report shows: @@ -312,24 +312,83 @@ The coverage report automatically excludes: | `*/build/*` | Build artifacts | | `*/external/*` | Vendored third-party code | +### Coverage Threshold + +The project enforces a **minimum 80% line coverage** threshold. When you run the coverage target, it will: + +1. Generate the coverage report +2. Parse the coverage percentage +3. **Fail if coverage is below 80%** + +**Example (coverage passes):** + +```bash +cmake --build build/debug --target coverage +``` + +**Expected output:** + +``` +[1/1] Generating code coverage report... +... +Overall coverage rate: + lines......: 85.7% (42 of 49 lines) + functions..: 100.0% (8 of 8 functions) +Coverage report generated at: /path/to/build/debug/coverage/html/index.html +-- Coverage: 42/49 lines (85%) +-- Threshold: 80% +-- Coverage check passed: 85% >= 80% +``` + +**Example (coverage fails):** + +```bash +cmake --build build/debug --target coverage +``` + +**Expected output:** + +``` +[1/1] Generating code coverage report... +... +Overall coverage rate: + lines......: 65.3% (32 of 49 lines) + functions..: 75.0% (6 of 8 functions) +Coverage report generated at: /path/to/build/debug/coverage/html/index.html +-- Coverage: 32/49 lines (65%) +-- Threshold: 80% +CMake Error: Code coverage 65% is below the required threshold of 80%. Please add more tests to increase coverage. +``` + +**Changing the threshold:** + +```bash +# Set threshold to 90% +cmake --preset debug -DENABLE_COVERAGE=ON -DCOVERAGE_THRESHOLD=90 +``` + +Or modify `CMakePresets.json` to set it permanently for a preset. + +For Codecov integration and viewing coverage trends, see [Codecov Guide](CODECOV.md). + ### Typical Workflow ```bash # 1. Configure with coverage conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_COVERAGE=ON +cmake --preset debug -DENABLE_COVERAGE=ON # 2. Build -cmake --build --preset conan-debug +cmake --build --preset debug # 3. Run tests -ctest --preset conan-debug +ctest --preset debug # 4. Generate report -cmake --build build/Debug --target coverage +cmake --build build/debug --target coverage # 5. View report -open build/Debug/coverage/html/index.html +open build/debug/coverage/html/index.html ``` --- @@ -361,14 +420,14 @@ Use Debug build for best error reports: ```bash conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_SANITIZERS=ON +cmake --preset debug -DENABLE_SANITIZERS=ON ``` **Real example:** ```bash conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_SANITIZERS=ON +cmake --preset debug -DENABLE_SANITIZERS=ON ``` **Expected output:** @@ -382,13 +441,13 @@ cmake --preset conan-debug -DENABLE_SANITIZERS=ON **Step 2: Build** ```bash -cmake --build --preset conan-debug +cmake --build --preset debug ``` **Step 3: Run tests** ```bash -ctest --preset conan-debug --output-on-failure +ctest --preset debug --output-on-failure ``` ### Example: Catching a Buffer Overflow @@ -405,7 +464,7 @@ void process_data() { Without sanitizers, this might "work" (undefined behavior). With ASan: ```bash -ctest --preset conan-debug --output-on-failure +ctest --preset debug --output-on-failure ``` **Expected output:** @@ -454,7 +513,7 @@ With UBSan: Sanitizers can be combined with code coverage and linting: ```bash -cmake --preset conan-debug \ +cmake --preset debug \ -DENABLE_SANITIZERS=ON \ -DENABLE_COVERAGE=ON \ -DENABLE_LINTING=ON @@ -464,10 +523,10 @@ cmake --preset conan-debug \ ```bash conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_SANITIZERS=ON -DENABLE_COVERAGE=ON -cmake --build --preset conan-debug -ctest --preset conan-debug --output-on-failure -cmake --build build/Debug --target coverage +cmake --preset debug -DENABLE_SANITIZERS=ON -DENABLE_COVERAGE=ON +cmake --build --preset debug +ctest --preset debug --output-on-failure +cmake --build build/debug --target coverage ``` This gives you: @@ -482,13 +541,13 @@ All in one build. ```bash # 1. Configure with sanitizers conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_SANITIZERS=ON +cmake --preset debug -DENABLE_SANITIZERS=ON # 2. Build -cmake --build --preset conan-debug +cmake --build --preset debug # 3. Run tests (sanitizers abort on first error) -ctest --preset conan-debug --output-on-failure +ctest --preset debug --output-on-failure # 4. Fix any issues reported by sanitizers # 5. Re-run until all tests pass cleanly @@ -503,13 +562,13 @@ ctest --preset conan-debug --output-on-failure ```bash # Configure Debug build (once) conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug +cmake --preset debug # Build -cmake --build --preset conan-debug +cmake --build --preset debug # Run tests -ctest --preset conan-debug --output-on-failure +ctest --preset debug --output-on-failure ``` ### Before Merging @@ -517,16 +576,16 @@ ctest --preset conan-debug --output-on-failure ```bash # Full check: lint + build + test + coverage + sanitizers conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_SANITIZERS=ON -DENABLE_COVERAGE=ON -DENABLE_LINTING=ON -cmake --build --preset conan-debug -ctest --preset conan-debug --output-on-failure -cmake --build build/Debug --target coverage +cmake --preset debug -DENABLE_SANITIZERS=ON -DENABLE_COVERAGE=ON -DENABLE_LINTING=ON +cmake --build --preset debug +ctest --preset debug --output-on-failure +cmake --build build/debug --target coverage # Also verify Release build works conan install . --build=missing -cmake --preset conan-release -cmake --build --preset conan-release -ctest --preset conan-release --output-on-failure +cmake --preset release +cmake --build --preset release +ctest --preset release --output-on-failure ``` ### CI/CD Pipeline @@ -534,15 +593,15 @@ ctest --preset conan-release --output-on-failure ```bash # Debug with sanitizers conan install . --build=missing -s build_type=Debug -cmake --preset conan-debug -DENABLE_SANITIZERS=ON -DENABLE_LINTING=ON -cmake --build --preset conan-debug -ctest --preset conan-debug --output-on-failure +cmake --preset debug -DENABLE_SANITIZERS=ON -DENABLE_LINTING=ON +cmake --build --preset debug +ctest --preset debug --output-on-failure # Release conan install . --build=missing -cmake --preset conan-release -cmake --build --preset conan-release -ctest --preset conan-release --output-on-failure +cmake --preset release +cmake --build --preset release +ctest --preset release --output-on-failure ``` --- @@ -571,10 +630,10 @@ brew install lcov ```bash # Run tests first to collect coverage data -ctest --preset conan-debug +ctest --preset debug # Then generate report -cmake --build build/Debug --target coverage +cmake --build build/debug --target coverage ``` ### "Sanitizer error: ASan runtime does not come first" @@ -588,7 +647,7 @@ cmake --build build/Debug --target coverage export LD_PRELOAD=$(clang -print-file-name=libclang_rt.asan-x86_64.so) # Then run tests -ctest --preset conan-debug --output-on-failure +ctest --preset debug --output-on-failure ``` ### "Sanitizers not supported on MSVC" @@ -603,12 +662,12 @@ ctest --preset conan-debug --output-on-failure | Task | Command | |---|---| -| Run tests | `ctest --preset conan-release` | -| Run tests (verbose) | `ctest --preset conan-release --output-on-failure` | -| Enable coverage | `cmake --preset conan-debug -DENABLE_COVERAGE=ON` | -| Generate coverage report | `cmake --build build/Debug --target coverage` | -| Enable sanitizers | `cmake --preset conan-debug -DENABLE_SANITIZERS=ON` | -| Combine all | `cmake --preset conan-debug -DENABLE_COVERAGE=ON -DENABLE_SANITIZERS=ON -DENABLE_LINTING=ON` | +| Run tests | `ctest --preset release` | +| Run tests (verbose) | `ctest --preset release --output-on-failure` | +| Enable coverage | `cmake --preset debug -DENABLE_COVERAGE=ON` | +| Generate coverage report | `cmake --build build/debug --target coverage` | +| Enable sanitizers | `cmake --preset debug -DENABLE_SANITIZERS=ON` | +| Combine all | `cmake --preset debug -DENABLE_COVERAGE=ON -DENABLE_SANITIZERS=ON -DENABLE_LINTING=ON` | For build configuration details, see [Build Guide](BUILD.md). For linting and formatting, see [Linting Guide](LINTING.md). diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e63c56d..9f27664 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,7 @@ add_executable(${PROJECT_NAME} main.cpp) +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_23) + target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include ) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 914b439..2b3b842 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,8 @@ find_package(GTest REQUIRED) add_executable(${PROJECT_NAME}_tests test_main.cpp) +target_compile_features(${PROJECT_NAME}_tests PRIVATE cxx_std_23) + target_include_directories(${PROJECT_NAME}_tests PRIVATE ${CMAKE_SOURCE_DIR}/include )