name: libakstdlib CI Build run-name: ${{ gitea.actor }} libakstdlib test on: [push] jobs: cmake_build: runs-on: ubuntu-latest steps: - run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}." - name: Check out repository code uses: actions/checkout@v4 with: # A top-level build uses deps/libakerror via add_subdirectory, so the # submodule has to be present or the configure step fails outright. submodules: recursive - name: dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake gcc moreutils # This step used to clone and install libakerror@main as well. That was two # different libakerrors depending on where you built: the install went to # /usr/local, while the build below is top-level and so compiles # deps/libakerror at the pinned commit via add_subdirectory -- the # installed one was never actually linked against. TODO.md 2.3. # # The submodule wins. It is the version this repository pins, tests and # ships against, and a CI that tests a different one is testing something # nobody runs. `cmake --install` below still installs both, so the # find_package and pkg-config paths are exercised. - name: build and test run: | # -DAKSL_WERROR=ON here and not locally: a warning that stops the build # mid-thought teaches people to turn warnings off, but a warning that # reaches main is one nobody will ever look at again. cmake -S . -B build -DAKSL_WERROR=ON cmake --build build sudo cmake --install build ctest --test-dir build --output-on-failure - run: echo "🍏 This job's status is ${{ job.status }}." # The sanitizer build is its own job rather than a step on the one above, # because three of the defects fixed in 0.2.0 only ever misbehaved under # instrumentation and the tests that pin them are worth failing on their own. sanitizers: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v4 with: submodules: recursive - name: dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake gcc - name: build and test under ASan + UBSan run: | cmake -S . -B build-asan -DAKSL_SANITIZE=ON -DAKSL_WERROR=ON cmake --build build-asan ctest --test-dir build-asan --output-on-failure - run: echo "🍏 This job's status is ${{ job.status }}." coverage: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v4 with: submodules: recursive - name: dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake gcc python3 # scripts/coverage.py needs nothing but python3 and gcc's own gcov, so # there is no lcov/gcovr to install here. # # The gate is a ratchet, not a target. Across all four sources the suite # covers 99.5% of lines (1643/1651), 46.0% of branches and 100% of # functions (147/147), so 90/40 fails on a real regression -- a test # deleted, or new untested code added -- without tripping over rounding. # The report is printed either way; the uncovered lines it lists are the # missing tests. # # Eight lines are uncovered and every one is deliberate: four # `HANDLE(e, AKERR_ITERATOR_BREAK)` macro artifacts, the size_t overflow # guard in strbuf_reserve (reaching it needs a buffer near SIZE_MAX), and # the short transfer with neither feof nor ferror set, which the standard # permits and Linux never produces. README.md has the detail. # # Branch coverage sits far below line coverage because most branches are # inside libakerror's FAIL_*/ATTEMPT/FINISH expansions -- pool exhaustion, # stack-trace limits, akerr_valid_error_address failures -- which this # library has no way to reach. Chasing them here would be testing # libakerror's macros, which is libakerror's mutation suite's job: macros # expand at the call site, so coverage cannot see them properly from # either side. The gate stays at 40 for that reason and not for want of # tests. - name: coverage run: | cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \ -DAKSL_COVERAGE_THRESHOLD=90 \ -DAKSL_COVERAGE_BRANCH_THRESHOLD=40 cmake --build build-coverage ctest --test-dir build-coverage --output-on-failure cat build-coverage/coverage-summary.txt - run: echo "🍏 This job's status is ${{ job.status }}." mutation_test: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v4 with: # The harness copies the repo and builds the copy top-level, so it # needs deps/libakerror present just like the main job does. submodules: recursive - name: dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake gcc moreutils python3 # Verify the tests actually catch bugs: break the library many ways and # confirm the suite fails. Gated on src/stdlib.c (fast, deterministic); # run the full default target locally for the macro header as well. # # The threshold is a ratchet, not a quality bar. The score is 89.6% # (155/173 killed) now that TODO.md sections 1.2-1.6 have tests; it was # 46.8% when only the list and tree functions were covered. 80 leaves # headroom for the runner while still failing on a real regression (tests # deleted, or new untested code added). The 18 survivors are listed in the # published report -- each one is a missing assertion. - name: mutation testing run: | python3 scripts/mutation_test.py \ --target src/stdlib.c \ --junit mutation-junit.xml \ --threshold 80 # Publish even when the threshold gate fails, so survivors are visible -- # each one is a missing test. Display-only (fail_on_failure: false); the # --threshold above is the gate. annotate_only avoids the Checks API 404 # on Gitea (mikepenz/action-junit-report#23). - name: publish mutation results if: always() uses: mikepenz/action-junit-report@v4 with: report_paths: 'mutation-junit.xml' annotate_only: true detailed_summary: true include_passed: true fail_on_failure: 'false' - run: echo "🍏 This job's status is ${{ job.status }}."