diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fa6818c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +# Least privilege: the workflow only needs to read the repository. The repo +# default is `write`, so scope it down explicitly and drop the checkout token +# after fetch, so pull-request-controlled build/test steps never see a +# write-capable token. +permissions: + contents: read + +jobs: + build-and-test: + name: build + unit tests (${{ matrix.cc }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cc: [gcc, clang] + env: + CC: ${{ matrix.cc }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Configure + run: > + cmake -S . -B build + -DCMAKE_BUILD_TYPE=Release + -DBUILD_TESTS=ON -DBUILD_BENCHMARK=OFF -DBUILD_EXAMPLES=OFF + - name: Build + run: cmake --build build -j"$(nproc)" + - name: Unit tests + run: ctest --test-dir build --output-on-failure + + sanitizers: + name: ASan + UBSan + runs-on: ubuntu-latest + env: + CC: clang + CXX: clang++ + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + # Build the whole test suite with sanitizers via CMake and run it through + # CTest, so every registered test target (not just td_test) is covered by + # ASan/UBSan. Build type is Debug on purpose: the project defaults to Release, + # whose top-level "-std=c99" hides POSIX clock_gettime() in minunit.h, so an + # explicit Debug build keeps every test target compilable here. + - name: Configure with ASan + UBSan + run: > + cmake -S . -B build-san + -DCMAKE_BUILD_TYPE=Debug + -DBUILD_TESTS=ON -DBUILD_BENCHMARK=OFF -DBUILD_EXAMPLES=OFF + -DCMAKE_C_FLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined -fno-sanitize-recover=all" + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" + - name: Build + run: cmake --build build-san -j"$(nproc)" + - name: Run all tests under sanitizers + env: + # A fuzzed/huge compression can request an enormous capacity; let the + # library's allocation-failure path run instead of aborting the process. + ASAN_OPTIONS: detect_leaks=1:allocator_may_return_null=1 + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + run: ctest --test-dir build-san --output-on-failure diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0c8a8cb..85da305 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,10 @@ endif() if (BUILD_TESTS) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -std=c99") add_executable(td_test unit/td_test.c unit/minunit.h) + # minunit.h uses clock_gettime(), which the top-level "-std=c99" hides unless + # _POSIX_C_SOURCE is defined on the command line (before any header is included). + # Without this the td_test build fails with "unknown type name 'clockid_t'". + target_compile_definitions(td_test PRIVATE _POSIX_C_SOURCE=200809L) target_link_libraries(td_test tdigest m) enable_testing() add_test(td_test td_test)