diff --git a/.clang-format b/.clang-format index dc69733..274fb98 100644 --- a/.clang-format +++ b/.clang-format @@ -78,6 +78,6 @@ SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false -Standard: c++11 +Standard: c++20 TabWidth: 4 UseTab: Never \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy index da93ae8..aa9fc39 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,26 @@ --- # Configure clang-tidy for this project. +# +# cpp-lab is a teaching repository. Examples deliberately show the "before" +# version of a pattern, copy objects so a constructor call becomes visible, read +# moved-from objects to show what happens, and spell types out instead of using +# auto. The following checks fight exactly that and are therefore disabled at +# the end of the Checks list, although they stay valuable in production code +# (note: the Checks list is a YAML folded scalar, so it cannot hold comments): +# +# -misc-use-anonymous-namespace LAB_EXAMPLE declares a static function +# -readability-convert-member-functions-to-static +# -performance-unnecessary-copy-initialization copies are made on purpose +# -performance-unnecessary-value-param pass-by-value is demonstrated +# -bugprone-use-after-move moved-from objects are inspected +# -google-runtime-int short/long are the subject of Fundamental +# -modernize-use-std-numbers the linkage constants are illustrations +# -modernize-use-auto, -modernize-loop-convert explicit code is easier to follow here +# -modernize-avoid-bind std::bind has its own example +# -modernize-use-constraints enable_if is shown next to concepts +# -misc-no-recursion recursion is the lesson (Composite, ...) +# -google-build-using-namespace chrono literals inside one function +# -google-explicit-constructor implicit conversion has its own example # Here is an explanation for why some of the checks are disabled: # @@ -77,7 +98,6 @@ # X update. Checks: > -*, - abseil-*, bugprone-*, google-*, misc-*, @@ -115,12 +135,26 @@ Checks: > -bugprone-implicit-widening-of-multiplication-result, -bugprone-unchecked-optional-access, -bugprone-unused-local-non-trivial-variable, - -bugprone-unused-return-value + -bugprone-unused-return-value, + -misc-use-anonymous-namespace, + -readability-convert-member-functions-to-static, + -performance-unnecessary-copy-initialization, + -performance-unnecessary-value-param, + -bugprone-use-after-move, + -google-runtime-int, + -modernize-use-std-numbers, + -modernize-use-auto, + -modernize-loop-convert, + -modernize-avoid-bind, + -modernize-use-constraints, + -misc-no-recursion, + -google-build-using-namespace, + -google-explicit-constructor # Turn all the warnings from the checks above into errors. WarningsAsErrors: "*" -HeaderFilterRegex: "(google/cloud/|generator/).*\\.h$" +HeaderFilterRegex: "(include/lab|src|tests)/.*\\.h$" CheckOptions: - { key: readability-identifier-naming.NamespaceCase, value: lower_case } diff --git a/.cppcheck-suppressions b/.cppcheck-suppressions new file mode 100644 index 0000000..41fd99d --- /dev/null +++ b/.cppcheck-suppressions @@ -0,0 +1,53 @@ +# cppcheck suppressions for cpp-lab + +# cppcheck --suppressions-list=.cppcheck-suppressions ... ./src ./include + +# Format: or : or :: + +# --- checks that are noise in teaching code --------------------------------- +# Example classes keep their member functions non-static so the examples read +# like ordinary code. +functionStatic +# Explicit loops are often the point of an example; the algorithm version is +# shown where it is the lesson (see core/utils/Algorithm). +useStlAlgorithm + +# --- intentional demonstrations --------------------------------------------- +# "the condition is always true/false" is exactly what these examples show +knownConditionTrueFalse:src/core/datatype/Fundamental.cpp +knownConditionTrueFalse:src/core/datatype/Reference.cpp +knownConditionTrueFalse:src/core/datatype/TypeConversions.cpp +knownConditionTrueFalse:src/core/string/StdString.cpp +knownConditionTrueFalse:src/core/utils/Optional.cpp +knownConditionTrueFalse:src/dp/structural/Adapter.cpp +# pointer/reference rebinding and re-assignment shown on purpose +redundantInitialization:src/core/datatype/Pointer.cpp +redundantAssignment:src/core/function/operator_overloading/AssignmentOperator.cpp +unreadVariable:src/core/datatype/TypeConversions.cpp +constVariableReference:src/core/datatype/Array.cpp +# at(3) must throw here, that is the lesson +containerOutOfBounds:src/core/container/sequence/Array.cpp +# std::deque guarantees that references survive push_front/push_back +invalidContainerReference:src/core/container/sequence/Deque.cpp +# placement new constructs into deliberately raw storage +legacyUninitvar:src/core/function/operator_overloading/AllocationOperator.cpp +# the null check after a move is the point of the example +nullPointerRedundantCheck:src/core/smart_pointer/Unique.cpp +accessMoved:src/core/class/Constructor.cpp +accessMoved:src/core/class/RuleOfThreeFiveZero.cpp +accessMoved:src/core/datatype/Reference.cpp +# classes that show what the compiler generates for them +noCopyConstructor:src/core/class/RuleOfThreeFiveZero.cpp +noOperatorEq:src/core/class/RuleOfThreeFiveZero.cpp +noCopyConstructor:src/core/class/ShallowDeepCopying.cpp +noOperatorEq:src/core/class/ShallowDeepCopying.cpp +noExplicitConstructor:src/core/class/Constructor.cpp +duplInheritedMember:src/core/class/Binding.cpp +virtualCallInConstructor:src/core/class/Binding.cpp +postfixOperator:src/core/function/operator_overloading/InDecOperator.cpp +# writing raw doubles is what binary file I/O looks like +invalidPointerCast:src/core/filehandle/BinaryFileHandling.cpp +# the plugin entry point must have main's signature +constParameter:src/demo/dlopen/sample_app.cpp +# startup code compares the linker-provided section symbols +comparePointers:src/embedded/startup.c diff --git a/.github/workflows/cpp-build-test-coverage.yml b/.github/workflows/cpp-build-test-coverage.yml index c9f71c5..d207131 100644 --- a/.github/workflows/cpp-build-test-coverage.yml +++ b/.github/workflows/cpp-build-test-coverage.yml @@ -3,7 +3,9 @@ # ------------------------------------------------------------- # Purpose: # Runs cppcheck (static analysis), builds your C++ project, -# runs unit tests, and generates a coverage report. +# runs the unit tests and one smoke test per example, +# generates a coverage report, and repeats the tests with +# AddressSanitizer + UndefinedBehaviorSanitizer. # ------------------------------------------------------------- name: C++ Tests and Coverage @@ -45,7 +47,7 @@ jobs: # ------------------------------------------------------- # Step 2: Run Cppcheck (Static Analysis) # - Scans for common C++ issues (style, memory, logic) - # - You can adjust `--enable=` options as needed + # - -I include lets cppcheck understand the LAB_EXAMPLE macro # - https://cppcheck.sourceforge.io/manual.pdf # ------------------------------------------------------- - name: Run static analysis with Cppcheck @@ -56,26 +58,23 @@ jobs: --quiet \ --inline-suppr \ --error-exitcode=1 \ + -I include \ + --suppressions-list=.cppcheck-suppressions \ ./src ./include - + # ------------------------------------------------------- # Step 3: Configure and build the project # ------------------------------------------------------- - - name: Prepare build + - name: Build run: | - rm -rf build - mkdir build - cd build - cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON .. - cmake --build . + cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON + cmake --build build -j "$(nproc)" # ------------------------------------------------------- - # Step 4: Run unit tests + # Step 4: Run unit tests and example smoke tests # ------------------------------------------------------- - name: Run tests - run: | - cd build - ctest --output-on-failure + run: ctest --test-dir build --output-on-failure -j "$(nproc)" # ------------------------------------------------------- # Step 5: Generate code coverage report @@ -97,20 +96,50 @@ jobs: lcov --summary coverageFiltered.info >> $GITHUB_STEP_SUMMARY # # ------------------------------------------------------- - # # Step 7: Run Clang-Tidy (experimenting , not exit) - # # - Modern C++ static analysis - # # - Enforces best practices - # # - Add `--warnings-as-errors=*` to exit code 1 + # # Step 7: Run Clang-Tidy (experimenting, not enforced) + # # - Modern C++ static analysis, configured by .clang-tidy # # ------------------------------------------------------- # - name: Run clang-tidy - # # run: | - # # echo "Running clang-tidy..." - # # clang-tidy \ - # # -checks='clang-analyzer-*,modernize-*,performance-*,readability-*' \ - # # -p build \ - # # $(find ./src -name '*.cpp') # run: | - # echo "Running clang-tidy using .clang-tidy options" - # clang-tidy \ - # -p build \ - # -header-filter='^src/.*' $(find src -name "*.cpp") \ No newline at end of file + # clang-tidy -p build -header-filter='^src/.*' $(find src -name "*.cpp") + + release: + # --------------------------------------------------------- + # A Release build with warnings as errors: optimizations enable extra + # compiler diagnostics that a Debug build never reports. + # --------------------------------------------------------- + runs-on: ubuntu-24.04 + container: + image: urboob21/cpp-lab:latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Build and test in Release + run: | + cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release \ + -DCPPLAB_WARNINGS_AS_ERRORS=ON -DCPPLAB_BUILD_GUI=OFF + cmake --build build-release -j "$(nproc)" + ctest --test-dir build-release --output-on-failure -j "$(nproc)" + + sanitizers: + # --------------------------------------------------------- + # Same tests with AddressSanitizer + UndefinedBehaviorSanitizer: + # memory errors, leaks and undefined behavior fail the job. + # --------------------------------------------------------- + runs-on: ubuntu-24.04 + container: + image: urboob21/cpp-lab:latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Build with sanitizers + run: | + cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DCPPLAB_ENABLE_SANITIZERS=ON -DCPPLAB_BUILD_GUI=OFF + cmake --build build-asan -j "$(nproc)" + + - name: Run tests with sanitizers + run: ctest --test-dir build-asan --output-on-failure -j "$(nproc)" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..cb8f079 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,92 @@ +# ------------------------------------------------------------- +# GitHub Actions Workflow: API documentation (Doxygen) +# ------------------------------------------------------------- +# Purpose: +# Generates the Doxygen site from the sources on every push and +# pull request (Doxygen warnings fail the job), and publishes it +# to GitHub Pages when master moves. +# +# One-time setup: Settings -> Pages -> Build and deployment -> +# Source: "GitHub Actions". See docs/doxygen.md. +# ------------------------------------------------------------- + +name: Docs + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: # lets a maintainer run it by hand from the Actions tab + +# Only one Pages deployment at a time; a newer run waits instead of racing. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + docs: + runs-on: ubuntu-24.04 + + permissions: + contents: read + pages: write # required by actions/deploy-pages + id-token: write # required by actions/deploy-pages (OIDC) + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + # doxygen generates the site, graphviz (dot) draws the class diagrams. + - name: Install Doxygen and Graphviz + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends doxygen graphviz cmake g++ + + # The docs only need the Doxyfile, so the GUI and the tests (which would + # download GoogleTest) stay off: configuring takes a second. + - name: Generate documentation + run: | + cmake -S . -B build-docs \ + -DCPPLAB_BUILD_GUI=OFF \ + -DCPPLAB_BUILD_TESTS=OFF \ + -DCPPLAB_DOCS_WARNINGS_AS_ERRORS=ON + cmake --build build-docs --target docs + + - name: Show Doxygen warnings + if: always() + run: | + echo "## Doxygen warnings" >> "$GITHUB_STEP_SUMMARY" + if [ -s build-docs/docs/doxygen-warnings.log ]; then + sed 's|^| |' build-docs/docs/doxygen-warnings.log >> "$GITHUB_STEP_SUMMARY" + else + echo "None." >> "$GITHUB_STEP_SUMMARY" + fi + + # Always available as a downloadable artifact, also for pull requests. + - name: Upload the site as a build artifact + uses: actions/upload-artifact@v4 + with: + name: doxygen-html + path: build-docs/docs/html + retention-days: 14 + + # --- Publish to GitHub Pages (pushes to master only) --- + - name: Configure GitHub Pages + if: github.event_name != 'pull_request' + uses: actions/configure-pages@v5 + + - name: Upload the Pages artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: build-docs/docs/html + + - name: Deploy to GitHub Pages + id: deployment + if: github.event_name != 'pull_request' + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index e84cd09..34b72fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,21 @@ -*build -*private* -.vscode/ +# Build directories (build/, build-asan/, build-release/, src/embedded/build/, ...) +build/ +build-*/ + +# Coverage reports (scripts/gen_coverage_*.sh) +coverage_gcovr/ +coverage_lcov/ + +# CTest output when ctest runs from the repository root +Testing/ + +# Local-only files +/private/ +.cache/ +__pycache__/ *Identifier -*Testing* -coverage_gcovr -coverage_lcov + +# Editor settings: keep the shared VS Code launch and task configurations +.vscode/* !.vscode/launch.json !.vscode/tasks.json -.cache -__pycache__/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..64adb07 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +A C++20 learning lab: ~100 small, self-contained examples (language features, STL, concurrency, +design patterns, sockets, a PID controller) compiled into one menu-driven executable, plus +standalone programs (GTK4 MVC/MVVM apps, a `dlopen` plugin demo) and a bare-metal ARM example that +is built outside CMake. + +## Build, run, test + +```bash +cmake -S . -B build # Debug by default +cmake --build build -j +./build/bin/cpp_lab_project # interactive menu +ctest --test-dir build -j --output-on-failure +./scripts/run.sh # build + cppcheck + tests + menu +``` + +Command line of the app: `--list [filter]`, `--run `, `--run-all [filter]`, +`--list-ids`, `--plain` (no log prefixes/colors), `--mode dev|uat|prod`, `--version`, `--help`. +Example ids look like `core/smart_pointer/Weak`. + +CMake options: `CPPLAB_BUILD_TESTS` (ON), `CPPLAB_BUILD_DEMOS` (ON), `CPPLAB_BUILD_GUI` +(AUTO/ON/OFF - gtkmm-4.0 is optional), `CPPLAB_BUILD_DOCS` (ON, needs doxygen), +`CPPLAB_DOCS_WARNINGS_AS_ERRORS` (OFF), `CPPLAB_ENABLE_SANITIZERS` (OFF, ASan+UBSan), +`CPPLAB_WARNINGS_AS_ERRORS` (OFF), `ENABLE_COVERAGE` (OFF). + +Tests: `-L unit` are the GoogleTest tests in `tests/`, `-L example` are auto-generated smoke tests +that run every non-interactive example once (`example:`). Both must stay green, also in a +sanitizer build. + +## Architecture: the lab framework + +`include/lab/` + `src/lab/` are a small framework; everything else is examples. + +- `LAB_EXAMPLE("Name", "description" [, lab::kInteractive]) { ... }` (lab/Example.h) declares a + file-local function and registers it in `lab::Registry` from a static initializer. +- The **menu group comes from the file path**: `src/core/smart_pointer/Weak.cpp` becomes the id + `core/smart_pointer/Weak`. `CPPLAB_SOURCE_DIR` (a compile definition) turns `__FILE__` into a + repository-relative path. +- `lab::Registry` keeps examples sorted, rejects duplicate ids and invalid names; `main.cpp` + reports registry errors and exits non-zero. +- `lab::runMenu` (Menu.cpp) builds a folder tree from the ids: numbers navigate, `0` goes back, + text searches, `q` quits. `lab::runExample` / `lab::runAll` (Runner.cpp) print the banner, catch + exceptions and restore `std::cout` formatting. +- Logging: `LOG("text")`, `LOG_S("x = " << x)`, `LOG_FUNC()` (current function signature), + `LOG_SECTION("Title")`. Debug builds prefix `[time][file:line][function]`; Release and `--plain` + print the bare message. +- `lab/version.h` is generated from `include/lab/version.h.in`. + +## Drafts + +`LAB_EXAMPLE(name, description, lab::kDraft)` marks a scaffold: the header comment lists what the +example should teach, `outline()` prints that list, and the menu shows `[draft]`. They still build +and run as ctest smoke tests. `docs/cpp-standards-coverage.md` is the audit of C++11 - C++23 +against the lab and tracks every open draft; update it (and the folder README) when a draft is +finished and `lab::kDraft` is removed. + +## Adding an example + +```bash +./scripts/new_example.sh core/utils Span "std::span: a view over contiguous memory" +``` + +No CMake change is needed: `cpplab_add_example_module()` globs each module folder with +`CONFIGURE_DEPENDS`, and the ctest smoke test is discovered from `--list-ids` at test time. A new +top-level module needs one line in `src/CMakeLists.txt`. Full conventions: +`docs/adding-examples.md`. + +House rules for example code (see also `docs/adding-examples.md`): + +- Put helpers in an anonymous namespace; all examples link into one binary, so global names would + clash (ODR). +- Header comment: what it teaches, key points/pitfalls, a cppreference link. +- Split into small functions, each starting with `LOG_SECTION`. +- Never execute undefined behavior; describe it instead. The smoke tests run under ASan/UBSan. +- Write files only under `std::filesystem::temp_directory_path()` and delete them; restore global + state (e.g. `std::set_terminate`). +- Keep examples fast and non-interactive; mark the ones that need a user or a peer with + `lab::kInteractive`. + +## Static analysis and formatting + +```bash +cppcheck --enable=warning,style,performance,portability --inconclusive --inline-suppr --quiet \ + --error-exitcode=1 -I include --suppressions-list=.cppcheck-suppressions ./src ./include +git ls-files '*.cpp' '*.h' | xargs clang-format -i +clang-tidy -p build $(git ls-files 'src/*.cpp') # configured by .clang-tidy, not enforced in CI +``` + +- `-I include` is required, otherwise cppcheck cannot expand `LAB_EXAMPLE` and reports syntax + errors. Intentional findings (teaching demos) are listed in `.cppcheck-suppressions`; note that a + line containing only `#` breaks that file. +- `.clang-format` is Google-based with `Standard: c++20`. Do not set it back to `c++11`: the + formatter then mangles digit separators such as `1'000'000` and splits `operator<=>`. +- `.clang-tidy` has `WarningsAsErrors: "*"`, and its `Checks:` value is a YAML folded scalar, so + it cannot contain comments - a `#` inside the list silently becomes part of a check name. + Explanations therefore live in the comment block above it. +- Naming (`.clang-tidy`): `lower_case` variables and namespaces, `CamelCase` types, trailing `_` on + private members, `kName` for constants. + +## API documentation + +`cmake --build build --target docs` runs Doxygen (`cmake/Docs.cmake` fills `docs/Doxyfile.in` into +`build/Doxyfile`) and writes `build/docs/html/`. The target exists only when doxygen is installed; +`CPPLAB_DOCS_WARNINGS_AS_ERRORS=ON` (used in CI) turns Doxygen warnings into failures, so keep +`build/docs/doxygen-warnings.log` empty. Doxygen parses the Markdown too: a fenced code block needs +a blank line before it, and a bare `
` in prose is read as an HTML tag - write `` `` ``. +Public headers in `include/lab/` are documented with `///`; examples keep plain `//` comments and +are read through the source browser. + +## CI + +- `.github/workflows/cpp-build-test-coverage.yml` runs on push/PR to `master` inside + `urboob21/cpp-lab:latest`: cppcheck, build with coverage, `ctest`, lcov summary, a Release job + with `CPPLAB_WARNINGS_AS_ERRORS=ON`, and a sanitizer job (`CPPLAB_ENABLE_SANITIZERS=ON`). +- `.github/workflows/docs.yml` builds the Doxygen site (warnings are errors) and deploys it to + GitHub Pages from `master`. `scripts/publish_wiki.sh` refreshes the wiki Home page that links to + it; see `docs/doxygen.md`. + +## Other programs + +- `src/ap/` GTK4 apps (`ap`, `mvc_ap`, `mvvm_ap`), built only when gtkmm-4.0 is found. +- `src/demo/dlopen/` host + plugin; the plugin path is compiled in via `SAMPLE_APP_PATH`, and + `bridge` must stay a SHARED library (see its README). +- `src/embedded/` bare-metal ARM firmware: `cd src/embedded && ./run.sh [gui|debug]` + (needs `gcc-arm-none-eabi` and `qemu-system-arm`). + +## Docs + +`docs/README.md` indexes the per-folder READMEs (`src/**/README.md`), which explain each topic and +embed the draw.io UML diagrams in `docs/uml/`. When adding or renaming an example, update the +README of its folder. `docs/doxygen.md` covers the generated API site. diff --git a/CMakeLists.txt b/CMakeLists.txt index b03e79d..4da8c97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,64 +1,61 @@ -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.16) # Project metadata project(cpp_lab_project # ${PROJECT_NAME} VERSION 1.0.0 - DESCRIPTION "A C/C++ project uses CMake, GoogleTest, gcc, g++, cppcheck, and lcov, integrated with Docker and GitHub Actions for CI/CD." + DESCRIPTION "A C/C++ learning lab built with CMake, GoogleTest, gcc/g++, cppcheck and lcov, integrated with Docker and GitHub Actions for CI/CD." LANGUAGES CXX ) -# Output directories to build/bin -# Executables -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -# Shared libraries -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -# Static libraries -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) - -# Compiler and language configuration -# Require at least C++17 for GoogleTest and modern C++ features +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + +# ---------------------------------------------------------------------------------------- +# Options (pass with -D