From 72e65373fc310f58a7aa9df4d71ecf0783092909 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Thu, 16 Jul 2026 20:29:00 +0100 Subject: [PATCH 1/3] Professionalize the C++ exercise repository --- .gitattributes | 10 ++ .github/FUNDING.yml | 3 +- .github/ISSUE_TEMPLATE/bug_report.md | 27 ++--- .github/PULL_REQUEST_TEMPLATE.md | 21 ++++ .github/dependabot.yml | 10 ++ .github/workflows/ci.yml | 58 +++++++---- .github/workflows/coverage-comment.yml | 119 ++++++++++++++++++++++ .gitignore | 24 +++++ CHANGELOG.md | 25 +++++ CONTRIBUTING.md | 46 +++++++++ Makefile | 59 ++++++++++- PRIVACY.md | 7 ++ README.md | 133 ++++++++++++++++--------- SECURITY.md | 17 ++++ SUPPORT.md | 15 +++ common.mk | 8 +- fold-left-fold-right/Makefile | 2 +- integer-factorization/Makefile | 2 +- map-with-unknown-key/Makefile | 2 +- multithread-sum/Makefile | 2 +- oop-with-exception/Makefile | 2 +- parallel-integral-pi/Makefile | 2 +- pi-monte-carlo/Makefile | 2 +- pi-monte-carlo/tests.sh | 14 +-- pi/Makefile | 2 +- pi/tests.sh | 13 ++- prefix-sum/Makefile | 2 +- ranges-find-in-vector-string/Makefile | 2 +- rot47/Makefile | 2 +- smart-ptr/Makefile | 2 +- smart-ptr/main.cpp | 6 +- sudoku-solver/Makefile | 2 +- sudoku-solver/main.cpp | 14 +-- thread-safe-queue/Makefile | 2 +- unique-ptr-basics/Makefile | 2 +- views-zip-enumerate/Makefile | 2 +- virtual-interface/Makefile | 2 +- 37 files changed, 529 insertions(+), 134 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/coverage-comment.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 PRIVACY.md create mode 100644 SECURITY.md create mode 100644 SUPPORT.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f1b2460 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto eol=lf + +*.cpp text eol=lf +*.h text eol=lf +*.hpp text eol=lf +*.mk text eol=lf +Makefile text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a27b041..802cecb 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,5 @@ github: doctorlai patreon: doctorlai buy_me_a_coffee: y0BtG5R -thanks_dev: doctorlai +custom: + ["https://www.buymeacoffee.com/y0BtG5R", "https://paypal.me/doctorlai/5"] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..81a9c9f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,29 +10,20 @@ assignees: '' **Describe the bug** A clear and concise description of what the bug is. -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +**To reproduce** +List the example, command, arguments, and sanitizer configuration that reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Environment** +- OS and version: +- Compiler and version: +- Sanitizer (`address`, `thread`, `undefined`, or none): +- Commit SHA: -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] +**Output** +Include the complete compiler error, sanitizer report, or program output. **Additional context** Add any other context about the problem here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..da2bce6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +## Summary + +Describe the problem and the approach taken. + +## Validation + +- [ ] `make lint` +- [ ] Relevant example tests pass +- [ ] `make check` passes, or any unavailable check is explained below +- [ ] `make coverage` remains at or above the required threshold + +## Checklist + +- [ ] The change is focused and follows the existing C++ style +- [ ] New behavior has meaningful tests +- [ ] Documentation and `CHANGELOG.md` are updated when appropriate +- [ ] No generated binaries or coverage files are included + +## Additional context + +Include sanitizer output, performance notes, or follow-up work when relevant. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9365f05 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + labels: + - dependencies + - github-actions + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe2b422..465858e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,37 +6,53 @@ on: pull_request: branches: [main] +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - build-and-run: + check: runs-on: ubuntu-latest env: - ASAN_OPTIONS: detect_leaks=1 # automatically detect memory leaks + ASAN_OPTIONS: detect_leaks=1 steps: - # 1. Checkout repo - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - # 2. Install build tools - name: Install build tools run: | sudo apt-get update - sudo apt-get install -y build-essential g++-14 clang clang-format libtbb-dev + sudo apt-get install -y g++-14 make clang-format libtbb-dev - # 3. Clang-format check - - name: Clang-format Check - run: | - ./clang-check.sh *.cpp *.hpp *.c *.h + - name: Run all checks + run: make check SANITIZERS="address thread undefined" - # 4. Build and run all examples dynamically - - name: Build and Run + coverage: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install coverage tools run: | - for san in address thread undefined; do - echo "=== Building all examples with sanitizer $san ===" - make SANITIZE=$san - echo "=== Running all examples with sanitizer $san ===" - make run - done - - - name: Clean up build artifacts + sudo apt-get update + sudo apt-get install -y g++-14 make gcovr jq libtbb-dev + + - name: Generate coverage report + run: make coverage COVERAGE_MIN=0 COVERAGE_FUNCTION_MIN=0 COVERAGE_BRANCH_MIN=0 + + - name: Upload coverage report + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: coverage-report + path: coverage/ + retention-days: 14 + + - name: Enforce coverage thresholds run: | - make clean + jq --exit-status \ + '.line_percent >= 80 and .function_percent >= 80 and .branch_percent >= 70' \ + coverage/summary.json diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml new file mode 100644 index 0000000..3035351 --- /dev/null +++ b/.github/workflows/coverage-comment.yml @@ -0,0 +1,119 @@ +name: Coverage Comment + +on: + workflow_run: + workflows: [C++ CI] + types: [completed] + +permissions: + actions: read + contents: read + pull-requests: write + +concurrency: + group: coverage-comment-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + +jobs: + comment: + if: ${{ github.event.workflow_run.event == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Download coverage summary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: coverage-report + path: coverage-data + repository: ${{ github.repository }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create or update coverage comment + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + LINE_COVERAGE_THRESHOLD: '80' + FUNCTION_COVERAGE_THRESHOLD: '80' + BRANCH_COVERAGE_THRESHOLD: '70' + with: + script: | + const fs = require('fs'); + const summary = JSON.parse(fs.readFileSync('coverage-data/summary.json', 'utf8')); + + function readMetric(prefix, label, threshold) { + const percent = Number(summary[`${prefix}_percent`]); + const covered = Number(summary[`${prefix}_covered`]); + const total = Number(summary[`${prefix}_total`]); + const valid = Number.isFinite(percent) + && Number.isInteger(covered) + && Number.isInteger(total) + && percent >= 0 + && percent <= 100 + && covered >= 0 + && total >= covered; + + if (!valid) { + throw new Error(`Invalid ${label} coverage summary`); + } + + return { label, percent, covered, total, threshold }; + } + + const metrics = [ + readMetric('line', 'Lines', Number(process.env.LINE_COVERAGE_THRESHOLD)), + readMetric('function', 'Functions', Number(process.env.FUNCTION_COVERAGE_THRESHOLD)), + readMetric('branch', 'Branches', Number(process.env.BRANCH_COVERAGE_THRESHOLD)), + ]; + const rows = metrics.map((metric) => { + const status = metric.percent >= metric.threshold ? 'PASS' : 'FAIL'; + return `| ${status} | ${metric.label} | ${metric.percent.toFixed(2)}% (${metric.threshold}% target) | ${metric.covered} / ${metric.total} |`; + }); + const run = context.payload.workflow_run; + const associatedPulls = await github.paginate( + github.rest.repos.listPullRequestsAssociatedWithCommit, + { + ...context.repo, + commit_sha: run.head_sha, + per_page: 100, + }, + ); + const pull = associatedPulls.find((candidate) => + candidate.state === 'open' && candidate.head.sha === run.head_sha); + if (!pull) { + core.notice(`No open pull request currently points to ${run.head_sha}`); + return; + } + + const marker = ''; + const body = [ + marker, + '## Coverage Report', + '', + '| Status | Category | Percentage | Covered / Total |', + '|:--:|---|---:|---:|', + ...rows, + '', + `[Generated in workflow #${run.run_number}](${run.html_url}) for commit \`${run.head_sha.slice(0, 7)}\` by the C++ coverage workflow.`, + ].join('\n'); + + const issue_number = pull.number; + const comments = await github.paginate(github.rest.issues.listComments, { + ...context.repo, + issue_number, + per_page: 100, + }); + const previous = comments.find((comment) => + comment.user.type === 'Bot' && comment.body.includes(marker)); + + if (previous) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: previous.id, + body, + }); + } else { + await github.rest.issues.createComment({ + ...context.repo, + issue_number, + body, + }); + } diff --git a/.gitignore b/.gitignore index 5a0b7ba..e396328 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,34 @@ # Object files *.o +*.d +*.gcda +*.gcno # Executable thread_safe_queue unique_ptr_basics *.out +fold-left-fold-right/fold-left-fold-right +integer-factorization/integer-factorization +map-with-unknown-key/map-with-unknown-key +multithread-sum/multithread-sum +oop-with-exception/oop-with-exception +parallel-integral-pi/parallel-integral-pi +parallel-transform/parallel-transform +pi-monte-carlo/pi-monte-carlo +pi/pi +prefix-sum/prefix-sum +ranges-find-in-vector-string/ranges-find-in-vector-string +rot47/rot47 +smart-ptr/smart-ptr +sudoku-solver/sudoku-solver +thread-safe-queue/thread-safe-queue +unique-ptr-basics/unique-ptr-basics +views-zip-enumerate/views-zip-enumerate +virtual-interface/virtual-interface + +# Generated reports +coverage/ # Backup files *~ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..51cf57b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this repository are documented in this file. + +The project uses date-based versions because it does not publish a package or release artifact. + +## [2026-07-16] + +### Added + +- Make targets for formatting, linting, testing, sanitizer checks, and coverage. +- Aggregate coverage gates of 80% for lines and functions and 70% for branches, with HTML, XML, JSON, and text reports. +- Secure pull request coverage comments and uploaded coverage artifacts. +- Immutable commit pins for third-party GitHub Actions used by CI. +- Contribution, support, security, privacy, pull request, and dependency update policies. + +### Changed + +- CI now performs clean builds for AddressSanitizer, ThreadSanitizer, and UndefinedBehaviorSanitizer. +- UndefinedBehaviorSanitizer findings now terminate the affected test immediately. +- Local WSL2 checks skip the unsupported GCC ThreadSanitizer runtime by default while native Linux CI retains it. +- Example link rules now honor shared linker flags. +- Pi tests validate numeric output without requiring `bc`. +- Sudoku and smart pointer examples print real line breaks instead of escaped text. +- README build instructions, CI documentation, project commands, and badges were expanded and corrected. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..46943ef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing + +Contributions that add focused C++ examples, improve correctness, or strengthen existing tests are welcome. + +## Development setup + +Install GCC 14, GNU Make, clang-format, gcovr, and oneTBB on an Ubuntu-compatible system: + +```bash +sudo apt update +sudo apt install g++-14 make clang-format gcovr jq libtbb-dev +``` + +Build and run the complete repository: + +```bash +make build +make test +``` + +## Adding an example + +1. Create a descriptive directory containing a `Makefile` and source files. +2. Include `../common.mk` and follow a neighboring example's build targets. +3. Add a `tests.sh` script when the behavior has testable output or error handling. +4. Add the example to the appropriate README category. +5. Keep the example self-contained and avoid dependencies unless they demonstrate an essential concept. + +## Before opening a pull request + +Run the same checks used by CI: + +```bash +make lint +make check +make coverage +``` + +`make coverage` requires at least 80% aggregate line and function coverage and 70% branch coverage. Add tests for new +behavior rather than lowering the thresholds. + +Use a focused commit history and explain the motivation, behavior change, and validation in the pull request. Do not include compiled binaries, object files, or generated coverage reports. + +## Reporting problems + +Use the bug report template for reproducible defects. Report security vulnerabilities privately according to [SECURITY.md](./SECURITY.md). diff --git a/Makefile b/Makefile index 71cb95a..7b85b1f 100644 --- a/Makefile +++ b/Makefile @@ -7,14 +7,30 @@ SUBDIRS := $(dir $(wildcard */Makefile)) # Default sanitizer (can be overridden) SANITIZE ?= address +COVERAGE_MIN ?= 80 +COVERAGE_FUNCTION_MIN ?= 80 +COVERAGE_BRANCH_MIN ?= 70 +COVERAGE_DIR ?= coverage +GCOV ?= gcov-14 +IS_WSL := $(shell grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null && echo 1) -.PHONY: all clean run $(SUBDIRS) +ifeq ($(IS_WSL),1) +DEFAULT_SANITIZERS := address undefined +else +DEFAULT_SANITIZERS := address thread undefined +endif + +SANITIZERS ?= $(DEFAULT_SANITIZERS) + +.PHONY: all build clean run test format lint coverage check $(SUBDIRS) # -------------------------- # Build all examples # -------------------------- all: $(SUBDIRS) +build: all + $(SUBDIRS): @echo "=== Building $@ with SANITIZE=$(SANITIZE) ===" $(MAKE) -C $@ SANITIZE=$(SANITIZE) @@ -33,18 +49,28 @@ run: fi; \ done +test: run + # -------------------------- # Clean all examples # -------------------------- clean: - @for dir in $(SUBDIRS); do \ + @set -e; for dir in $(SUBDIRS); do \ $(MAKE) -C $$dir clean; \ done + @rm -rf $(COVERAGE_DIR) + @find . -name '*.gcda' -o -name '*.gcno' | xargs -r rm -f .PHONY: check-format-all check-format +format: + @find . -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.c' -o -name '*.h' \) \ + -not -path './.git/*' -exec clang-format -i {} + + +lint: check-format-all + check-format-all: - @for dir in $(SUBDIRS); do \ + @set -e; for dir in $(SUBDIRS); do \ if [ -d "$$dir" ]; then \ echo "=== clang-format check in $$dir ==="; \ $(MAKE) -f $(MAKEFILE_LIST) check-format DIR=$$dir; \ @@ -64,3 +90,30 @@ check-format: else \ echo "Bash not found; skipping clang-format check"; \ fi + +coverage: + @$(MAKE) clean + @$(MAKE) SANITIZE= COVERAGE=1 build + @$(MAKE) SANITIZE= COVERAGE=1 test + @mkdir -p $(COVERAGE_DIR) + @gcovr --root . \ + --gcov-executable $(GCOV) \ + --exclude-unreachable-branches --exclude-throw-branches \ + --txt --html-details $(COVERAGE_DIR)/index.html \ + --xml $(COVERAGE_DIR)/coverage.xml --xml-pretty \ + --json-summary $(COVERAGE_DIR)/summary.json --json-summary-pretty \ + --fail-under-line $(COVERAGE_MIN) \ + --fail-under-function $(COVERAGE_FUNCTION_MIN) \ + --fail-under-branch $(COVERAGE_BRANCH_MIN) + +check: lint + @if [ "$(IS_WSL)" = "1" ] && [ "$(SANITIZERS)" = "$(DEFAULT_SANITIZERS)" ]; then \ + echo "=== Skipping ThreadSanitizer runtime checks on WSL2 ==="; \ + fi + @set -e; for sanitizer in $(SANITIZERS); do \ + echo "=== Checking with $$sanitizer sanitizer ==="; \ + $(MAKE) clean; \ + $(MAKE) SANITIZE=$$sanitizer build; \ + $(MAKE) SANITIZE=$$sanitizer test; \ + done + @$(MAKE) clean diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..89270a8 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,7 @@ +# Privacy + +This repository contains local command-line C++ examples. The programs do not intentionally collect, transmit, or persist personal data. + +GitHub processes account, contribution, Actions, issue, discussion, and traffic data under GitHub's own privacy terms. Third-party badge providers referenced by the README may receive standard web request metadata, such as an IP address and user agent, when a badge is loaded. + +Do not include credentials, private data, or sensitive logs in issues, discussions, pull requests, test fixtures, or example output. To report a security concern, follow [SECURITY.md](./SECURITY.md). diff --git a/README.md b/README.md index cc5cd40..3f0fc28 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,29 @@ # cpp-coding-exercise [![C++ Build](https://github.com/DoctorLai/cpp-coding-exercise/actions/workflows/ci.yml/badge.svg)](https://github.com/DoctorLai/cpp-coding-exercise/actions/workflows/ci.yml) - -A collection of **modern C++ coding examples** and small exercises, focused on correctness, clarity, and real‑world patterns. +[![Last Commit](https://img.shields.io/github/last-commit/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/commits/main) +[![License](https://img.shields.io/github/license/DoctorLai/cpp-coding-exercise)](./LICENSE) +[![Stars](https://img.shields.io/github/stars/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/stargazers) +[![Code Style](https://img.shields.io/badge/code%20style-clang--format-blue)](./.clang-format) +[![Commit Activity](https://img.shields.io/github/commit-activity/m/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/graphs/commit-activity) +[![Watchers](https://img.shields.io/github/watchers/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/watchers) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) +[![Repo Size](https://img.shields.io/github/repo-size/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise) +[![Top Language](https://img.shields.io/github/languages/top/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/search?l=c%2B%2B) +[![Open PRs](https://img.shields.io/github/issues-pr/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/pulls) +[![Forks](https://img.shields.io/github/forks/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/forks) +[![Open Issues](https://img.shields.io/github/issues/DoctorLai/cpp-coding-exercise)](https://github.com/DoctorLai/cpp-coding-exercise/issues) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/DoctorLai/cpp-coding-exercise) + +A collection of **modern C++ coding examples** and small exercises focused on correctness, clarity, and real-world patterns. This repository is intended as: -* A interview‑prep playground -* A reference for **concurrency, synchronization, low‑level and modern C++ techniques** -* A growing set of **self‑contained, buildable examples** -* A collection of more useful examples (and CLI tools) than leetcode puzzles. +* An interview-preparation playground +* A reference for **concurrency, synchronization, low-level, and modern C++ techniques** +* A growing set of **self-contained, buildable examples** +* A collection of practical examples and CLI tools rather than puzzle-only solutions -Examples include (and will expand to): +Current examples include: * Multithreading * [thread-safe-queue](./thread-safe-queue/) @@ -18,17 +31,13 @@ Examples include (and will expand to): * Smart pointers * [unique-ptr-basics](./unique-ptr-basics/) * [smart-ptr](./smart-ptr/) -* Lock‑free / wait‑free data structures * Views * [views-zip-enumerate](./views-zip-enumerate/) -* Atomics and memory ordering * Folding * [fold-left-fold-right](./fold-left-fold-right/) -* RAII and ownership patterns * Parallelism * [parallel-transform](./parallel-transform/) * [parallel-integral-pi](./parallel-integral-pi/) -* Performance‑oriented C++ idioms * STL and ranges: * [ranges-find-in-vector-string](./ranges-find-in-vector-string/) * Algorithms @@ -38,7 +47,7 @@ Examples include (and will expand to): * [prefix-sum](./prefix-sum/) * [sudoku-solver](./sudoku-solver/) * [pi-monte-carlo](./pi-monte-carlo/) - * [pi](./pi) + * [pi](./pi/) * Data Structures * [map-with-unknown-key](./map-with-unknown-key/) * OOP @@ -53,14 +62,13 @@ Examples include (and will expand to): cpp-coding-exercise/ ├── Makefile # top-level dispatcher ├── common.mk # shared compiler flags -├── thread-safe-queue/ -│ ├── Makefile -│ └── main.cpp -├── / +├── / │ ├── Makefile -│ └── ... +│ ├── main.cpp +│ └── tests.sh # optional behavior tests └── .github/workflows/ - └── ci.yml # GitHub Actions CI + ├── ci.yml # builds, sanitizers, tests, and coverage + └── coverage-comment.yml ``` ### Design principles @@ -72,14 +80,17 @@ cpp-coding-exercise/ --- -## Building +## Prerequisites -### C++ Version ```bash sudo apt update -sudo apt install g++-14 +sudo apt install g++-14 make clang-format gcovr jq libtbb-dev ``` +The examples target C++23 and are tested on Ubuntu with GCC 14. + +## Building + ### Build everything ```bash @@ -105,7 +116,22 @@ Runnable examples expose a `run` target: make run ``` -`make run` on top level will run all built targets. +Top-level `make run` runs all built targets and each available `tests.sh` script. + +## Project commands + +```bash +make build # Build every example with the selected sanitizer +make test # Build as needed, run examples, and execute test scripts +make format # Format all C and C++ source files +make lint # Check formatting without modifying files +make coverage # Generate HTML, XML, JSON, and text coverage reports +make check # Run formatting and supported sanitizer builds/tests +make clean # Remove binaries, objects, and coverage artifacts +``` + +`make coverage` enforces at least 80% line coverage, 80% function coverage, and 70% branch coverage by default. +Override local thresholds with `COVERAGE_MIN`, `COVERAGE_FUNCTION_MIN`, and `COVERAGE_BRANCH_MIN`. --- @@ -131,8 +157,8 @@ make clean Common compiler settings live in [common.mk](./common.mk): ```make -CXX := g++ -CXXFLAGS := -std=c++23 -Wall -Wextra +CXX := g++-14 +CXXFLAGS := -std=c++23 -Wall -Wextra -Werror ``` Individual examples may extend this, e.g.: @@ -143,10 +169,10 @@ CXXFLAGS += -pthread --- -## Address Sanitizer Check -The Address Sanitizer is enabled by default to ensure there is no memory leaks or other memory problems. +## Sanitizers +AddressSanitizer is enabled by default to detect memory safety problems and leaks. -```make +```bash # Builds with AddressSanitizer automatically make @@ -156,46 +182,44 @@ make SANITIZE=thread # UndefinedBehaviorSanitizer make SANITIZE=undefined +# Select the sanitizer matrix used by make check +make check SANITIZERS="address thread undefined" + # No sanitizers make SANITIZE= ``` +GCC ThreadSanitizer cannot reliably reserve its shadow-memory layout under WSL2 and may terminate before an example +starts with `FATAL: ThreadSanitizer: unexpected memory mapping`. On WSL2, `make check` therefore runs ASan and UBSan by +default. Native Linux and GitHub Actions continue to run ASan, TSan, and UBSan. `SANITIZERS` can override either default. + --- ## Clang Format -The `clang-format` is used to ensure the code format. +`clang-format` enforces the repository's C++ style. ```bash -./clang-check.sh *.cpp *.hpp -``` - -At top level, you can do: - -```make -make check-format-all +make format # Apply formatting +make lint # Check all example directories +./clang-check.sh # Equivalent recursive standalone check ``` -At each example directory, you can do: - -```make -make check-format -``` +Each example also exposes `make check-format` from its own directory. --- ## Continuous Integration -GitHub Actions automatically builds all examples on: +GitHub Actions builds all examples on: -* Every push -* Every pull request +* Pushes to `main` +* Pull requests targeting `main` The CI setup requires **no updates** when new example folders are added. -The CI will perform: -1. `./clang-check.sh *.cpp *.hpp` -2. `make SANITIZE=[address, thread, undefined]` -3. `make run` which will run `make run` for each project and `./tests.sh` if it is present. +CI runs `make check SANITIZERS="address thread undefined"`, enforces the documented coverage floors, uploads an HTML +coverage report, and posts a coverage summary on pull requests. Each sanitizer configuration starts from a clean build +so compiler flags cannot be silently reused from a previous configuration. --- @@ -203,7 +227,9 @@ The CI will perform: * C++23 * GNU Make -* GCC / Clang (CI currently uses GCC) +* GCC 14 +* clang-format +* gcovr * Linux (Ubuntu) --- @@ -220,6 +246,14 @@ The CI will perform: --- +## Contributing and support + +See [CONTRIBUTING.md](./CONTRIBUTING.md) before submitting a change. For help, use the channels in +[SUPPORT.md](./SUPPORT.md). Report security concerns privately as described in [SECURITY.md](./SECURITY.md). + +Project changes are recorded in [CHANGELOG.md](./CHANGELOG.md), and the repository's data practices are documented in +[PRIVACY.md](./PRIVACY.md). + ## License [MIT](./LICENSE) (unless otherwise stated in a specific example). @@ -228,6 +262,7 @@ The CI will perform: ## Notes -Many examples intentionally focus on **edge cases** and **failure modes** (data races, lifetime issues, ordering bugs). They are meant to be read, built, and experimented with. +Many examples intentionally focus on **edge cases** and **failure modes** such as data races, lifetime issues, and +ordering bugs. They are meant to be read, built, and experimented with. Contributions and [discussions](https://github.com/DoctorLai/cpp-coding-exercise/discussions) are welcome. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8e29646 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +## Supported versions + +This repository does not publish versioned packages. Security fixes are applied to the latest commit on the `main` branch. + +## Reporting a vulnerability + +Do not open a public issue for a suspected vulnerability. Use GitHub's private vulnerability reporting feature: + +https://github.com/DoctorLai/cpp-coding-exercise/security/advisories/new + +Include the affected example, compiler and platform details, reproduction steps, expected impact, and any sanitizer output. Please allow a reasonable period for investigation before public disclosure. + +## Scope + +Reports involving memory safety, undefined behavior, data races, unsafe ownership, or workflow privilege escalation are particularly useful. General correctness bugs that have no security impact should use the public bug report template. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..acc2388 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,15 @@ +# Support + +## Questions and usage help + +Use [GitHub Discussions](https://github.com/DoctorLai/cpp-coding-exercise/discussions) for questions about an example, C++ concepts, or adapting code for another environment. + +## Bugs and feature requests + +Search existing [issues](https://github.com/DoctorLai/cpp-coding-exercise/issues) before opening a new report. Use the provided issue templates and include the example name, exact command, compiler version, sanitizer configuration, and complete output. + +## Security reports + +Follow [SECURITY.md](./SECURITY.md) for private disclosure. Do not post suspected vulnerabilities in issues or discussions. + +Support is provided on a best-effort basis. This educational repository does not provide commercial support or compatibility guarantees. diff --git a/common.mk b/common.mk index 068230a..6dfc852 100644 --- a/common.mk +++ b/common.mk @@ -10,13 +10,14 @@ OPTFLAGS ?= -O1 -g # Safe for ASAN/TSAN (default) # -------------------------- # Default: AddressSanitizer SANITIZE ?= address +COVERAGE ?= 0 ifeq ($(SANITIZE),address) SAN_FLAGS := -fsanitize=address else ifeq ($(SANITIZE),thread) SAN_FLAGS := -fsanitize=thread else ifeq ($(SANITIZE),undefined) - SAN_FLAGS := -fsanitize=undefined + SAN_FLAGS := -fsanitize=undefined -fno-sanitize-recover=undefined else ifeq ($(SANITIZE),) SAN_FLAGS := else @@ -30,6 +31,11 @@ CXX := g++-14 CXXFLAGS := -std=c++23 -Wall -Wextra -Werror $(OPTFLAGS) $(SAN_FLAGS) LDFLAGS := $(SAN_FLAGS) +ifeq ($(COVERAGE),1) + CXXFLAGS += --coverage + LDFLAGS += --coverage +endif + # -------------------------- # Usage notes # -------------------------- diff --git a/fold-left-fold-right/Makefile b/fold-left-fold-right/Makefile index 6e5fc0f..d0e30be 100644 --- a/fold-left-fold-right/Makefile +++ b/fold-left-fold-right/Makefile @@ -11,7 +11,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/integer-factorization/Makefile b/integer-factorization/Makefile index ab8b3c0..51203a3 100644 --- a/integer-factorization/Makefile +++ b/integer-factorization/Makefile @@ -13,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/map-with-unknown-key/Makefile b/map-with-unknown-key/Makefile index ab8b3c0..51203a3 100644 --- a/map-with-unknown-key/Makefile +++ b/map-with-unknown-key/Makefile @@ -13,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/multithread-sum/Makefile b/multithread-sum/Makefile index 172da2f..9c36064 100644 --- a/multithread-sum/Makefile +++ b/multithread-sum/Makefile @@ -12,7 +12,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/oop-with-exception/Makefile b/oop-with-exception/Makefile index ab8b3c0..51203a3 100644 --- a/oop-with-exception/Makefile +++ b/oop-with-exception/Makefile @@ -13,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/parallel-integral-pi/Makefile b/parallel-integral-pi/Makefile index 172da2f..9c36064 100644 --- a/parallel-integral-pi/Makefile +++ b/parallel-integral-pi/Makefile @@ -12,7 +12,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/pi-monte-carlo/Makefile b/pi-monte-carlo/Makefile index 172da2f..9c36064 100644 --- a/pi-monte-carlo/Makefile +++ b/pi-monte-carlo/Makefile @@ -12,7 +12,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/pi-monte-carlo/tests.sh b/pi-monte-carlo/tests.sh index 24e28b6..e69a60b 100755 --- a/pi-monte-carlo/tests.sh +++ b/pi-monte-carlo/tests.sh @@ -1,15 +1,15 @@ -#!/bin/bash +#!/usr/bin/env bash -set -ex +set -euo pipefail # Run the tests pi=$(./pi-monte-carlo) -# Check that the result is within a reasonable range -if (( $(echo "$pi < 3.0" | bc -l) )) || - (( $(echo "$pi > 3.2" | bc -l) )); then +# Check that the result is numeric and within a reasonable range. +if ! awk -v pi="$pi" 'BEGIN { exit !(pi ~ /^[0-9]+([.][0-9]+)?$/ && pi >= 3.0 && pi <= 3.2) }'; then echo "Test failed: pi is out of range: $pi" exit 1 -else - echo "Test passed: pi is within range: $pi" fi + + +echo "Test passed: pi is within range: $pi" diff --git a/pi/Makefile b/pi/Makefile index 172da2f..9c36064 100644 --- a/pi/Makefile +++ b/pi/Makefile @@ -12,7 +12,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/pi/tests.sh b/pi/tests.sh index c6eab45..fa545d8 100755 --- a/pi/tests.sh +++ b/pi/tests.sh @@ -1,15 +1,14 @@ -#!/bin/bash +#!/usr/bin/env bash -set -ex +set -euo pipefail # Run the tests pi=$(./pi) -# Check that the result is within a reasonable range -if (( $(echo "$pi < 3.0" | bc -l) )) || - (( $(echo "$pi > 3.2" | bc -l) )); then +# Check that the result is numeric and within a reasonable range. +if ! awk -v pi="$pi" 'BEGIN { exit !(pi ~ /^[0-9]+([.][0-9]+)?$/ && pi >= 3.0 && pi <= 3.2) }'; then echo "Test failed: pi is out of range: $pi" exit 1 -else - echo "Test passed: pi is within range: $pi" fi + +echo "Test passed: pi is within range: $pi" diff --git a/prefix-sum/Makefile b/prefix-sum/Makefile index ab8b3c0..51203a3 100644 --- a/prefix-sum/Makefile +++ b/prefix-sum/Makefile @@ -13,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/ranges-find-in-vector-string/Makefile b/ranges-find-in-vector-string/Makefile index ab8b3c0..51203a3 100644 --- a/ranges-find-in-vector-string/Makefile +++ b/ranges-find-in-vector-string/Makefile @@ -13,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/rot47/Makefile b/rot47/Makefile index 172da2f..9c36064 100644 --- a/rot47/Makefile +++ b/rot47/Makefile @@ -12,7 +12,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/smart-ptr/Makefile b/smart-ptr/Makefile index 172da2f..9c36064 100644 --- a/smart-ptr/Makefile +++ b/smart-ptr/Makefile @@ -12,7 +12,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/smart-ptr/main.cpp b/smart-ptr/main.cpp index ab161c1..e3f68c3 100644 --- a/smart-ptr/main.cpp +++ b/smart-ptr/main.cpp @@ -12,7 +12,7 @@ struct Test int main() { - std::cout << "--- Testing SharedPtr ---\\n"; + std::cout << "--- Testing SharedPtr ---\n"; { SharedPtr sp1(new Test(10)); assert(sp1->x == 10); @@ -28,7 +28,7 @@ main() } std::cout << "SharedPtr test passed.\n"; - std::cout << "--- Testing MakeShared ---\\n"; + std::cout << "--- Testing MakeShared ---\n"; { SharedPtr sp = MakeShared(20); assert(sp->x == 20); @@ -36,7 +36,7 @@ main() } std::cout << "MakeShared test passed.\n"; - std::cout << "--- Testing WeakPtr ---\\n"; + std::cout << "--- Testing WeakPtr ---\n"; { SharedPtr sp = MakeShared(30); WeakPtr wp = sp; diff --git a/sudoku-solver/Makefile b/sudoku-solver/Makefile index ab8b3c0..51203a3 100644 --- a/sudoku-solver/Makefile +++ b/sudoku-solver/Makefile @@ -13,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/sudoku-solver/main.cpp b/sudoku-solver/main.cpp index c0c9877..4983acb 100644 --- a/sudoku-solver/main.cpp +++ b/sudoku-solver/main.cpp @@ -224,8 +224,8 @@ parse_positive_limit(const std::string& s) void print_usage(const char* program) { - std::cerr << "Usage: " << program << " <81-char-board> [max_solutions]\\n" - << "Board chars: 1-9 for fixed cells, '.' or '0' for empty cells.\\n"; + std::cerr << "Usage: " << program << " <81-char-board> [max_solutions]\n" + << "Board chars: 1-9 for fixed cells, '.' or '0' for empty cells.\n"; } } // namespace @@ -242,7 +242,7 @@ main(int argc, char* argv[]) if (argc == 3) { const auto parsed_limit = parse_positive_limit(argv[2]); if (!parsed_limit.has_value()) { - std::cerr << "Error: max_solutions must be a positive integer.\\n"; + std::cerr << "Error: max_solutions must be a positive integer.\n"; return 1; } max_solutions = *parsed_limit; @@ -250,7 +250,7 @@ main(int argc, char* argv[]) auto parsed_state = parse_board(argv[1]); if (!parsed_state.has_value()) { - std::cerr << "Error: invalid board input. Expected 81 chars and no row/column/box conflicts.\\n"; + std::cerr << "Error: invalid board input. Expected 81 chars and no row/column/box conflicts.\n"; return 1; } @@ -259,13 +259,13 @@ main(int argc, char* argv[]) solve_dfs(state, solutions, max_solutions); if (solutions.empty()) { - std::cout << "No solutions found.\\n"; + std::cout << "No solutions found.\n"; return 0; } - std::cout << "Found " << solutions.size() << " solution(s).\\n"; + std::cout << "Found " << solutions.size() << " solution(s).\n"; for (const auto& solution : solutions) { - std::cout << solution << "\\n"; + std::cout << solution << "\n"; } return 0; diff --git a/thread-safe-queue/Makefile b/thread-safe-queue/Makefile index 01919ed..f6bfbd8 100644 --- a/thread-safe-queue/Makefile +++ b/thread-safe-queue/Makefile @@ -11,7 +11,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/unique-ptr-basics/Makefile b/unique-ptr-basics/Makefile index e41b505..444a877 100644 --- a/unique-ptr-basics/Makefile +++ b/unique-ptr-basics/Makefile @@ -11,7 +11,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/views-zip-enumerate/Makefile b/views-zip-enumerate/Makefile index 6e5fc0f..d0e30be 100644 --- a/views-zip-enumerate/Makefile +++ b/views-zip-enumerate/Makefile @@ -11,7 +11,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/virtual-interface/Makefile b/virtual-interface/Makefile index 6e5fc0f..d0e30be 100644 --- a/virtual-interface/Makefile +++ b/virtual-interface/Makefile @@ -11,7 +11,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ From 0e3b858e4d7d63740bfb4228eebe717c46061ea1 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Thu, 16 Jul 2026 20:48:34 +0100 Subject: [PATCH 2/3] Fix tests --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++++++++++++++++++-- CHANGELOG.md | 2 ++ README.md | 12 ++++++--- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 465858e..059c7ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: true jobs: - check: + address-and-undefined-sanitizers: runs-on: ubuntu-latest env: ASAN_OPTIONS: detect_leaks=1 @@ -28,7 +28,56 @@ jobs: sudo apt-get install -y g++-14 make clang-format libtbb-dev - name: Run all checks - run: make check SANITIZERS="address thread undefined" + run: make check SANITIZERS="address undefined" + + thread-sanitizer: + runs-on: ubuntu-latest + env: + TBB_SOURCE_COMMIT: 8b829acc65569019edb896c5150d427f288e8aba + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y g++-14 make cmake ninja-build git + + - name: Build ThreadSanitizer-aware oneTBB + run: | + tbb_install_prefix="${RUNNER_TEMP}/oneTBB-tsan" + git init "${RUNNER_TEMP}/oneTBB-source" + git -C "${RUNNER_TEMP}/oneTBB-source" remote add origin https://github.com/uxlfoundation/oneTBB.git + git -C "${RUNNER_TEMP}/oneTBB-source" fetch --depth=1 origin "${TBB_SOURCE_COMMIT}" + git -C "${RUNNER_TEMP}/oneTBB-source" checkout --detach FETCH_HEAD + cmake \ + -S "${RUNNER_TEMP}/oneTBB-source" \ + -B "${RUNNER_TEMP}/oneTBB-build" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_C_COMPILER=gcc-14 \ + -DCMAKE_CXX_COMPILER=g++-14 \ + -DCMAKE_INSTALL_PREFIX="${tbb_install_prefix}" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DTBB_ENABLE_IPO=OFF \ + -DTBBMALLOC_BUILD=OFF \ + -DTBB_SANITIZE=thread \ + -DTBB_STRICT=OFF \ + -DTBB_TEST=OFF + cmake --build "${RUNNER_TEMP}/oneTBB-build" --target install --parallel + echo "TBB_INSTALL_PREFIX=${tbb_install_prefix}" >> "${GITHUB_ENV}" + echo "CPLUS_INCLUDE_PATH=${tbb_install_prefix}/include" >> "${GITHUB_ENV}" + echo "LIBRARY_PATH=${tbb_install_prefix}/lib" >> "${GITHUB_ENV}" + echo "LD_LIBRARY_PATH=${tbb_install_prefix}/lib" >> "${GITHUB_ENV}" + + - name: Build with ThreadSanitizer + run: make SANITIZE=thread build + + - name: Verify instrumented oneTBB linkage + run: ldd parallel-transform/parallel-transform | grep -F "${TBB_INSTALL_PREFIX}/lib/libtbb.so" + + - name: Run with ThreadSanitizer + run: make SANITIZE=thread test coverage: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cf57b..9b2640f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The project uses date-based versions because it does not publish a package or re ### Changed - CI now performs clean builds for AddressSanitizer, ThreadSanitizer, and UndefinedBehaviorSanitizer. +- The CI ThreadSanitizer job now builds a pinned, sanitizer-aware oneTBB to avoid reports caused by an uninstrumented + parallel STL backend. - UndefinedBehaviorSanitizer findings now terminate the affected test immediately. - Local WSL2 checks skip the unsupported GCC ThreadSanitizer runtime by default while native Linux CI retains it. - Example link rules now honor shared linker flags. diff --git a/README.md b/README.md index 3f0fc28..0a3291b 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,11 @@ make SANITIZE= GCC ThreadSanitizer cannot reliably reserve its shadow-memory layout under WSL2 and may terminate before an example starts with `FATAL: ThreadSanitizer: unexpected memory mapping`. On WSL2, `make check` therefore runs ASan and UBSan by -default. Native Linux and GitHub Actions continue to run ASan, TSan, and UBSan. `SANITIZERS` can override either default. +default. Native Linux runs ASan, TSan, and UBSan. `SANITIZERS` can override either default. + +The `parallel-transform` example uses libstdc++'s parallel STL backend, which delegates to oneTBB. TSan requires oneTBB +itself to be sanitizer-aware so that its internal synchronization is visible. The dedicated CI TSan job therefore builds +the pinned oneTBB release with `TBB_SANITIZE=thread` instead of using Ubuntu's ordinary runtime library. --- @@ -217,9 +221,9 @@ GitHub Actions builds all examples on: The CI setup requires **no updates** when new example folders are added. -CI runs `make check SANITIZERS="address thread undefined"`, enforces the documented coverage floors, uploads an HTML -coverage report, and posts a coverage summary on pull requests. Each sanitizer configuration starts from a clean build -so compiler flags cannot be silently reused from a previous configuration. +CI runs ASan and UBSan in one job and TSan with a sanitizer-aware oneTBB build in another. It also enforces the documented +coverage floors, uploads an HTML coverage report, and posts a coverage summary on pull requests. Each sanitizer +configuration starts from a clean build so compiler flags cannot be silently reused from a previous configuration. --- From 34d728f5f8706bb621e7e8d901923209a9e71e18 Mon Sep 17 00:00:00 2001 From: Dr Lai <92519564+doctorlai-msrc@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:55:56 +0100 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/coverage-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml index 3035351..af12688 100644 --- a/.github/workflows/coverage-comment.yml +++ b/.github/workflows/coverage-comment.yml @@ -16,7 +16,7 @@ concurrency: jobs: comment: - if: ${{ github.event.workflow_run.event == 'pull_request' }} + if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Download coverage summary