Files
libakstdlib/.gitea/workflows/ci.yaml
Andrew Kesterson 95e5002512
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m49s
libakstdlib CI Build / coverage (push) Successful in 2m37s
libakstdlib CI Build / mutation_test (push) Successful in 10m13s
Version the library at 0.1.0
project() now carries VERSION 0.1.0, and is the single place a version number
is spelled. It flows into generated version macros, the shared library's
VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new
akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to
nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried
no soname at all.

0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
fixes change documented behaviour, so the API is not being promised yet. While
the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are
different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt
and the #if in tests/test_version.c encode that rule and are tested against
each other.

include/akstdlib_version.h.in is configured into the build tree as
akstdlib_version.h and installed beside akstdlib.h. It defines
AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
AKSL_VERSION_NUMBER is computed rather than written as a literal, because a
literal 000100 is octal in C and would make 0.1.0 compare as 64;
test_version.c asserts it against the runtime components, so a rewrite to a
literal fails.

Those macros record what a caller was compiled against. aksl_version(),
aksl_version_string() and aksl_version_soname() report what actually loaded,
and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both.
It is a macro so that it expands at the caller's site and captures the
caller's numbers; the function compares them against the ones baked into the
library. Compatibility is "same soname", so patch is ignored -- a caller built
against 0.1.0 keeps working against 0.1.7.

Normally the soname catches a mismatch at load time and the check never fires.
It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under
the 0.1 filename loads happily, and only the check notices.

write_basic_package_version_file() uses SameMinorVersion to mirror the soname,
falling back to ExactVersion below CMake 3.11 where that mode does not exist.
The fallback is stricter than the soname rule -- it pins the patch level too --
but never laxer, and wrongly refusing a good pairing beats wrongly accepting a
bad one.

Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches
and 25/25 functions. That puts branch coverage back over the old 45 gate, but
the gate stays at 40: 0.1 points of headroom is not a ratchet.

ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of
tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config
--modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and
1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0
dropped in under the 0.1 filename caught by it, and an embedded
add_subdirectory build keeping its own version rather than the parent's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:40:49 -04:00

123 lines
5.3 KiB
YAML

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
# Depends on libakerror@main
git clone https://source.starfort.tech/andrew/libakerror.git
cd libakerror
cmake -S . -B build
cmake --build build
cmake --install build
- name: build and test
run: |
cmake -S . -B build
cmake --build build
sudo cmake --install build
ctest --test-dir build --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: src/stdlib.c is at 99.1% of lines
# and 45.1% of branches, 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.
#
# Branch coverage is back over 45 (the aksl_version_* functions are fully
# covered, which lifted it from 44.3%), but the gate stays at 40: 0.1
# points of headroom is not a ratchet, it is a coin flip on the next
# rounding change.
#
# The branch gate was 45 against 51.0% until the libakerror 1.0.0 bump.
# Nothing about this library's tests changed: line coverage held at
# 99.0% (200/202) and function coverage at 100% (21/21), but the branch
# denominator went from 661 to 1087 because the 1.0.0 PREPARE_ERROR /
# FAIL_* macros expand to more branches at every call site in
# src/stdlib.c, and most of the added branches are not reachable from the
# way this library calls them. 337/661 became 481/1087 -- 144 more
# branches covered, 426 more branches counted. 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). Re-ratcheted rather than papered over.
- 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 }}."