Every entry point may now be called from any thread. akerr_init() runs exactly once however many threads race into it, the pool hands each slot to exactly one thread, and reservations, registrations and lookups are serialized against each other. One recursive lock covers both tables (src/lock.h, private). Recursive because raising an error re-enters the library -- FAIL needs a pool slot and a status name -- and single because two locks would mean an ordering to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN macros are split into *_locked functions behind wrappers that take and release the lock on one path; consumer callbacks are never called under it. This is an ABI break, hence 2.0.0 and SOVERSION 2: - akerr_next_error() now returns a context that already holds its reference. Finding a free slot and claiming it has to be one operation, or two threads scanning at once are handed the same slot. ENSURE_ERROR_READY no longer increments. - __akerr_last_ignored is thread-local, as is the last-ditch context used to report akerr_release_error(NULL). The threading backend is chosen at configure time by AKERR_THREADS (auto, pthread, none). auto fails the configure when it cannot find POSIX threads rather than quietly building a library that reports itself thread safe and is not. generrno.sh stamps the decision into the generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree with the library about it. Tests: err_threads_init, err_threads_pool and err_threads_registry assert exclusive slot ownership, exactly one winner for a contested range, and every registered name readable back under contention. AKERR_SANITIZE builds the library and the tests with any sanitizer; scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs it. Removing the pool lock makes both the sanitizer and the plain assertions fail, so the tests are not vacuous. Documented in README.md and UPGRADING.md, including what this does not cover: renaming a status while another thread looks it up, and which of two simultaneous unhandled errors sets the exit status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
5.2 KiB
YAML
128 lines
5.2 KiB
YAML
name: libakerror CI Build
|
|
run-name: ${{ gitea.actor }} libakerror 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
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc moreutils
|
|
- name: build and install
|
|
run: |
|
|
mkdir installdir
|
|
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=installdir
|
|
cmake --build build
|
|
cmake --install build
|
|
# --output-junit resolves relative to the test dir, so give an absolute
|
|
# path to land the report in the workspace root for the reporter below.
|
|
- name: test (JUnit)
|
|
run: ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
|
# annotate_only: true skips creating a check run via the Checks API, which
|
|
# Gitea does not support and 404s on (mikepenz/action-junit-report#23).
|
|
# Results surface via the job summary instead.
|
|
- name: publish test results
|
|
if: always()
|
|
uses: mikepenz/action-junit-report@v4
|
|
with:
|
|
report_paths: 'ctest-junit.xml'
|
|
annotate_only: true
|
|
detailed_summary: true
|
|
include_passed: true
|
|
fail_on_failure: 'true'
|
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
|
|
coverage:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v4
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc moreutils python3
|
|
# Run the suite against a gcov-instrumented build and gate on coverage of
|
|
# the library sources. Thresholds keep headroom below the current numbers
|
|
# (src/error.c: ~94% line, ~60% branch) and apply per file as well as to
|
|
# the total, so the generated status-name table cannot mask a regression.
|
|
- name: coverage
|
|
run: |
|
|
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
|
# Publish even when the threshold gate fails, so gaps are visible.
|
|
# Display-only (fail_on_failure: false); the --threshold above is the gate.
|
|
# annotate_only avoids the Checks API 404 on Gitea (see note above).
|
|
- name: publish coverage results
|
|
if: always()
|
|
uses: mikepenz/action-junit-report@v4
|
|
with:
|
|
report_paths: 'coverage-junit.xml'
|
|
annotate_only: true
|
|
detailed_summary: true
|
|
include_passed: true
|
|
fail_on_failure: 'false'
|
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
|
|
thread_sanitizer:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v4
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc moreutils
|
|
# The thread tests assert exclusive ownership of pool slots and reserved
|
|
# ranges, which is checkable without tooling and runs in the job above.
|
|
# This is the run that proves there is no data race underneath them.
|
|
# libtsan arrives with gcc (libgcc-N-dev depends on it); the script
|
|
# disables ASLR because TSan aborts on kernels with vm.mmap_rnd_bits > 28.
|
|
- name: thread sanitizer
|
|
run: |
|
|
scripts/thread_test.sh build/tsan --output-junit "$(pwd)/tsan-junit.xml"
|
|
- name: publish thread sanitizer results
|
|
if: always()
|
|
uses: mikepenz/action-junit-report@v4
|
|
with:
|
|
report_paths: 'tsan-junit.xml'
|
|
annotate_only: true
|
|
detailed_summary: true
|
|
include_passed: true
|
|
fail_on_failure: 'true'
|
|
- 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
|
|
- 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/error.c (fast, deterministic).
|
|
# The threshold keeps headroom below the current score for equivalent
|
|
# mutants; see tests/MUTATION.md. Run the full default target locally for
|
|
# deeper (slower) coverage including the macro header.
|
|
- name: mutation testing
|
|
run: |
|
|
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
|
# Publish even when the threshold gate fails, so survivors are visible.
|
|
# Display-only (fail_on_failure: false); the --threshold above is the gate.
|
|
# annotate_only avoids the Checks API 404 on Gitea (see note above).
|
|
- 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 }}."
|