Local CI Debugging Guide
This repo depends on no extra local wrapper scripts: every check executed in .github/workflows/*.yml can be reproduced locally with the repo-root cmake + run_tests.py. You can close the loop entirely locally; only pushing images to GHCR / publishing a Release needs GitHub.
Modes supported by run_tests.py: all / utest / example / integration / fuzz / benchmark / perf / cc.
Note:
run_tests.pyhard-codes thebuild/path. If, like CI, you use multiple build directories (build-asan,build-cov,build-fuzz, …), symlinkbuildto the target directory as the workflows do:rm -rf build && ln -s build-asan build.
Quick Start
Section titled “Quick Start”cd /data/workspace/quicX
# Build and run unit + integration tests (matches the build-and-test job of ci.yml)cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTING=ONcmake --build build --parallel 4python3 run_tests.py utestpython3 run_tests.py integration
# Other common modespython3 run_tests.py example # examplespython3 run_tests.py benchmark # benchmarkspython3 run_tests.py perf # test/perf performance testspython3 run_tests.py cc # congestion-control simulator testsWorkflow ↔ Local Command Equivalents
Section titled “Workflow ↔ Local Command Equivalents”| Workflow File | Trigger | Local Equivalent | GitHub Required? |
|---|---|---|---|
ci.yml | push / PR | cmake -S . -B build ... && cmake --build build + run_tests.py utest / integration | ❌ |
sanitizer.yml | push / PR / daily | clang + -fsanitize={address,undefined,thread} builds into build-{asan,ubsan,tsan}, then run_tests.py utest | ❌ |
coverage.yml | push / PR | gcc + --coverage + lcov (see below) | ❌ |
lint.yml | push / PR | clang-format --dry-run --Werror <files> / clang-tidy -p build-tidy <files> | ❌ |
fuzz-smoke.yml | PR / nightly | -DENABLE_FUZZING=ON build into build-fuzz, short run of each target | ❌ |
Three Local Debugging Postures
Section titled “Three Local Debugging Postures”Posture 1: cmake + run_tests.py directly (recommended)
Section titled “Posture 1: cmake + run_tests.py directly (recommended)”The closest to the development loop — build + test straight on the host, fast, no Docker.
# edit code → rebuild → run only the affected testsvim src/quic/stream/send_stream.cppcmake --build build --parallel 4python3 run_tests.py utestCI’s build-and-test job matrix is {gcc, clang} × {Debug, Release} — 4 configurations; locally, covering at least gcc Debug and clang Release is recommended. CI config parameters: -DENABLE_TESTING=ON -DBUILD_EXAMPLES=ON -DENABLE_INTEROP=OFF -DENABLE_FUZZING=OFF.
Posture 2: Run workflows for real in local Docker with act
Section titled “Posture 2: Run workflows for real in local Docker with act”act can emulate the ubuntu-latest runner in local Docker, nearly equivalent to real CI.
# 1. Install act (one time)curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash -s -- -b /usr/local/bin
# 2. Common commandsact -l # list all workflows and jobsact -n # dry-run, syntax check only (no execution)act -W .github/workflows/ci.yml # run ci.ymlact -j build-and-test # run a specific jobThe first run pulls catthehacker/ubuntu:full-22.04 (~1-2 GB); be patient. It’s cached afterwards.
Caveats when using act:
scheduletriggers,secrets, GHCR pushes etc. don’t truly take effect, but the execution path is observable.windows-latest/macos-latestare unsupported; those parts need real CI.
Posture 3: Pure command line (for AI agents / scripts)
Section titled “Posture 3: Pure command line (for AI agents / scripts)”No script dependency at all:
# build + test (matches CI's build-and-test job)cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTING=ONcmake --build build --parallel 4python3 run_tests.py utestpython3 run_tests.py integrationPer-Job Details
Section titled “Per-Job Details”build (ci.yml)
Section titled “build (ci.yml)”- Compiles all static libs + unit tests + integration tests + examples
- Matrix: {gcc, clang} × {Debug, Release} = 4 configurations
- Locally, running the
gcc Debugandclang Releasepair is recommended coverage
sanitize (sanitizer.yml)
Section titled “sanitize (sanitizer.yml)”- clang only, building into
build-asan/build-ubsan/build-tsan - asan:
-fsanitize=address -fno-omit-frame-pointer -O1 -g, checking heap overflow / use-after-free / leaks - ubsan:
-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer -O1 -g, checking undefined behavior (integer overflow, null-pointer dereference, etc.) - tsan:
-fsanitize=thread -fno-omit-frame-pointer -O1 -g, checking data races - After building, symlink
buildto the target directory, then runrun_tests.py utest - TSAN is especially important: quicX has many multi-threaded workers; run tsan at least once locally
cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_C_FLAGS="-fsanitize=thread -fno-omit-frame-pointer -O1 -g" \ -DCMAKE_CXX_FLAGS="-fsanitize=thread -fno-omit-frame-pointer -O1 -g" \ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" \ -DENABLE_TESTING=ONcmake --build build-tsan --parallel 4rm -rf build && ln -s build-tsan buildTSAN_OPTIONS=halt_on_error=1 python3 run_tests.py utestcoverage (coverage.yml)
Section titled “coverage (coverage.yml)”- gcc +
--coverage -O0 -g -fprofile-update=atomic, building intobuild-cov - lcov excludes
/usr/*,third/,build*/,test/ - Generates the HTML report:
coverage-html/index.html - Threshold is 60% (currently below-threshold only warns, non-blocking)
interop (moved out of CI)
Section titled “interop (moved out of CI)”The in-repo interop test environment (
interop_runner.pyand the related compose files) has been removed, together with theinterop.ymlCI workflow. Interop testing now runs directly with the official quic-interop-runner (build thequicx-interop:latestimage locally, then let the official runner schedule it). Seeguide/interop_runbook.mdfor the full workflow.
lint (lint.yml)
Section titled “lint (lint.yml)”- On PRs, checks only changed files (git diff)
- clang-format:
clang-format --dry-run --Werror <file> - clang-tidy: first generate
build-tidy/compile_commands.jsonwith-DCMAKE_EXPORT_COMPILE_COMMANDS=ON, thenclang-tidy -p build-tidy <file> - Currently non-blocking (warning)
fuzz-smoke (fuzz-smoke.yml)
Section titled “fuzz-smoke (fuzz-smoke.yml)”- Configured
-DENABLE_FUZZING=ON -DENABLE_TESTING=OFF -DBUILD_EXAMPLES=OFF, buildingbuild-fuzz - libFuzzer runs each target for 60s
- Crashes found are uploaded to artifacts
- Corpora kept in
fuzz-corpus/(when run locally)
Q: Local build passes, why does CI fail?
Section titled “Q: Local build passes, why does CI fail?”- submodules: CI uses
submodules: recursive; locally rungit submodule update --init --recursivefirst - dependencies: CI uses ubuntu-22.04; an older local system may lack
ninja-build/lcov/clang-14/clang-format-14/clang-tidy-14 - concurrency: local
run_tests.pyruns integration concurrently; multiple connections binding the same port can conflict — adjust env vars or run serially
Q: act is too slow / eats too much disk?
Section titled “Q: act is too slow / eats too much disk?”- Use
act -nto dry-run and validate yaml syntax first - Just run cmake +
run_tests.pyon the host — much faster - Use act only for first-time / pre-merge final verification
Q: How do I run just one unit test?
Section titled “Q: How do I run just one unit test?”cmake --build build --parallel 4./build/bin/quicx_utest --gtest_filter='*YourTest*' --gtest_color=yesQ: Some files show 100% uncovered in the coverage report?
Section titled “Q: Some files show 100% uncovered in the coverage report?”Usually CMake’s collect_sources gathered source files referenced by no test. Running genhtml --ignore-errors source,unmapped already ignores such errors; to raise coverage, write unit tests.
Q: What can only be verified by pushing?
Section titled “Q: What can only be verified by pushing?”Only two things:
- Pushing the
ghcr.io/quicx/quicx-interopimage (needsGITHUB_TOKEN) - Filing a PR to quic-interop/quic-interop-runner to get on the leaderboard
Everything else can close the loop locally.
