Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m24s
akbasic CI Build / sanitizers (push) Successful in 4m59s
akbasic CI Build / coverage (push) Failing after 4m55s
akbasic CI Build / akgl_build (push) Failing after 4m54s
akbasic CI Build / mutation_test (push) Has been cancelled
Every job in ci.yaml has failed on every run since #2, on main and on every branch, for two reasons that have nothing to do with any of the code under test. moreutils, for errno(1). deps/libakerror/scripts/generrno.sh shells out to it to generate its errno name table and to stamp AKERR_LAST_ERRNO_VALUE into the generated akerror.h. The runner does not have it, and its absence does not fail the build -- which is what made this expensive to find. The script prints "errno: command not found", emits an empty akerr_init_errno() so no errno ever gets a name, and substitutes an empty AKERR_LAST_ERRNO_VALUE, so every code in libakerror's reserved band collapses from (134 + n) to ( + n), i.e. to n. AKERR_OUTOFBOUNDS is (AKERR_LAST_ERRNO_VALUE + 2) and therefore becomes literally 2, so ERR(2) answers "Out Of Bounds Error" where ENOENT's "No such file or directory" belongs. That is the docs_examples failure at docs/15-error-codes.md:105, and it took cmake_build, sanitizers and coverage down with it; mutation_test then aborted on "baseline is not green" because the same suite is its baseline. Reproduced locally by shimming errno(1) out of PATH: identical failure, same test #47, same two lines of diff. With errno present the suite is 112/112. The nine X11 dev packages, for akgl_build, which has never once been green. SDL_X11 and its sub-options default ON on Linux, and CheckX11() calls SDL_missing_dependency() -- a hard CMake error, not a downgrade -- for any whose header is absent. The runner has libx11-dev but none of the extension packages, so configuration stopped at the first of them: "Couldn't find dependency package for XCURSOR". The list is SDL's own, from deps/SDL/docs/README-linux.md, reduced to the X11 entries; the audio, Wayland, KMSDRM and Vulkan backends all degrade to "not found" and skip themselves rather than erroring, so they cost nothing to leave out. Turning the missing options OFF would also configure and is the wrong answer -- the point of that job is that an AKGL build is a real SDL program. The tests still run headless under SDL_VIDEODRIVER=dummy. release.yaml's full_mutation job builds the same tree and gets moreutils for the same reason. No source file changes. The port itself was never the problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
357 lines
18 KiB
YAML
357 lines
18 KiB
YAML
name: akbasic CI Build
|
|
run-name: ${{ gitea.actor }} akbasic test
|
|
on: [push]
|
|
|
|
# Push-triggered checks only. The doxygen gate and the whole-tree mutation run
|
|
# live in release.yaml, which is manual (workflow_dispatch) because between them
|
|
# they cost hours of runner time and are release gates rather than per-commit
|
|
# ones.
|
|
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:
|
|
# Not recursive, deliberately. The top-level build needs
|
|
# deps/libakerror and deps/libakstdlib, via add_subdirectory, and
|
|
# nothing else: the golden corpus and the Commodore font now live in
|
|
# this repository (tests/reference/ and assets/fonts/), so
|
|
# deps/basicinterpret is no longer a build dependency at all.
|
|
# It does *not* need deps/libakgl, which is guarded behind
|
|
# AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would
|
|
# clone SDL, SDL_image, SDL_mixer, SDL_ttf and jansson for a target
|
|
# that is never configured. libakstdlib's own nested deps/libakerror is
|
|
# skipped for the same reason: our CMakeLists declares akerror::akerror
|
|
# first and libakstdlib guards on if(NOT TARGET ...).
|
|
submodules: true
|
|
# moreutils is load-bearing, not incidental: it supplies errno(1), which
|
|
# deps/libakerror/scripts/generrno.sh shells out to at build time to
|
|
# generate its errno name table and to stamp AKERR_LAST_ERRNO_VALUE into
|
|
# the generated akerror.h.
|
|
#
|
|
# Its absence does not fail the build, which is what made this expensive to
|
|
# find. The script prints "errno: command not found" twice, emits an empty
|
|
# akerr_init_errno() so no errno ever gets a name, and substitutes an empty
|
|
# AKERR_LAST_ERRNO_VALUE -- so every code in libakerror's reserved band
|
|
# collapses from (134 + n) to ( + n), i.e. to n. AKERR_OUTOFBOUNDS is
|
|
# (AKERR_LAST_ERRNO_VALUE + 2), so it becomes literally 2, and ERR(2)
|
|
# answers "Out Of Bounds Error" where ENOENT's "No such file or directory"
|
|
# belongs. That is the docs_examples failure at docs/15-error-codes.md:105
|
|
# that this job carried on every run since #2.
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc moreutils
|
|
- name: build
|
|
run: |
|
|
cmake -S . -B build
|
|
cmake --build build --parallel
|
|
# The suite is 78 cases: 41 golden files byte-compared against the Go
|
|
# reference's own corpus (checked in at tests/reference/, see its README),
|
|
# 9 local golden cases for verbs the reference never implemented, 25 unit
|
|
# tests, 2 embedding examples, and 1 known-failing test that asserts the
|
|
# *correct* contract for defects carried over from the reference (TODO.md
|
|
# section 6). A green run therefore does not mean defect-free -- see
|
|
# AKBASIC_KNOWN_FAILING_TESTS.
|
|
#
|
|
# AKBASIC_WITH_AKGL is off here, which is the point rather than an
|
|
# omission: this job is what proves the interpreter builds and passes on a
|
|
# machine with no SDL. The akgl_build job below covers the other half.
|
|
#
|
|
# --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 }}."
|
|
|
|
sanitizers:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v4
|
|
with:
|
|
submodules: true
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc moreutils
|
|
# The whole suite under ASan and UBSan, golden files included. This is the
|
|
# gate libakstdlib's TODO.md section 1 calls its highest-value missing item
|
|
# -- worth having here from the start, because this library is all fixed
|
|
# pools and manual buffer arithmetic, which is exactly what it catches.
|
|
- name: build and test under ASan + UBSan
|
|
run: |
|
|
cmake -S . -B build-asan \
|
|
-DAKBASIC_SANITIZE=ON \
|
|
-DCMAKE_BUILD_TYPE=Debug
|
|
cmake --build build-asan --parallel
|
|
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: true
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc gcovr moreutils
|
|
# The gate is a ratchet, not a target: src/ sits at 94.6% of lines and
|
|
# 98.6% of functions, so 90 fails on a real regression (a test deleted, or
|
|
# new untested code added) without tripping over rounding.
|
|
#
|
|
# The akbasic_akgl and akbasic_frontend targets are not in this figure.
|
|
# Neither is built in a default configuration, and instrumenting them would
|
|
# drag SDL into the coverage job for four thin adaptors and a host. What
|
|
# covers them instead is the akgl_build job below.
|
|
#
|
|
# There is deliberately no branch gate. Branch coverage reads about 18%
|
|
# and is not a meaningful number here, for the reason libakgl's and
|
|
# libakstdlib's TODO.md both record: the akerror control-flow macros expand
|
|
# into large branch trees at every call site, most of them unreachable in
|
|
# normal operation. Gating on it would mean writing tests for libakerror's
|
|
# macros, which is libakerror's mutation suite's job.
|
|
#
|
|
# --root . with a src/ filter keeps the dependency trees out of the report.
|
|
# Note gcovr searches for .gcda under --root, so a stale instrumented build
|
|
# left in the source directory would be folded in -- that is libakgl defect
|
|
# #13, and the reason build*/ is gitignored rather than kept around.
|
|
- name: coverage
|
|
run: |
|
|
cmake -S . -B build-coverage \
|
|
-DAKBASIC_COVERAGE=ON \
|
|
-DCMAKE_BUILD_TYPE=Debug
|
|
cmake --build build-coverage --parallel
|
|
ctest --test-dir build-coverage --output-on-failure
|
|
mkdir -p build-coverage/coverage
|
|
gcovr --root . --filter 'src/.*' \
|
|
--print-summary \
|
|
--html-details build-coverage/coverage/index.html \
|
|
--xml build-coverage/coverage/coverage.xml \
|
|
--fail-under-line 90
|
|
# Publish even when the threshold gate fails, so the uncovered lines are
|
|
# visible -- each one is a missing test.
|
|
- name: upload coverage reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: code-coverage
|
|
path: build-coverage/coverage/
|
|
if-no-files-found: warn
|
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
|
|
akgl_build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# submodules: true, *not* recursive, and then the libakgl dependencies
|
|
# by hand in the next step. Recursive would work and costs an extra
|
|
# 461 MB: it descends into SDL_image/external, SDL_mixer/external and
|
|
# SDL_ttf/external, which are aom, dav1d, libjxl, libtiff, mpg123,
|
|
# opus, flac, freetype, harfbuzz and a dozen more. None of them is
|
|
# used -- every one configures as "Could NOT find" or falls back to
|
|
# the system copy -- so cloning them is pure checkout time.
|
|
submodules: true
|
|
# libakgl builds its vendored SDL only when it is the top-level project;
|
|
# embedded it takes a find_package path instead, so our CMakeLists declares
|
|
# those targets first and needs the submodules present. Six of them, none
|
|
# recursive. Filed upstream as libakgl API-gap item 5.
|
|
- name: libakgl dependencies
|
|
run: |
|
|
git -C deps/libakgl submodule update --init \
|
|
deps/SDL deps/SDL_image deps/SDL_mixer deps/SDL_ttf \
|
|
deps/jansson deps/semver
|
|
# libfreetype-dev and libharfbuzz-dev are load-bearing, not incidental.
|
|
# SDL_ttf prefers the system copies -- it reports "Using system freetype
|
|
# library" and links libfreetype.so.6 -- and without them it would reach
|
|
# for deps/SDL_ttf/external/freetype, which the checkout above
|
|
# deliberately does not clone. Installing two dev packages is much cheaper
|
|
# than cloning freetype and harfbuzz.
|
|
#
|
|
# The X11 dev packages are the other half, and they are what this job has
|
|
# been dying on since run #2 -- it has never once been green. SDL_X11 and
|
|
# its nine sub-options default ON on Linux, and CheckX11() in
|
|
# deps/SDL/cmake/sdlchecks.cmake calls SDL_missing_dependency() -- a hard
|
|
# CMake error, not a downgrade -- for any one of them whose header is
|
|
# absent. The runner image carries libx11-dev (X11/XKBlib.h resolved) but
|
|
# none of the extension packages, so configuration stopped at the first of
|
|
# the nine: "Couldn't find dependency package for XCURSOR".
|
|
#
|
|
# Turning the missing ones OFF would also configure, and is the wrong
|
|
# answer: the point of this job is that an AKGL build is a real SDL
|
|
# program, and a real SDL program on Linux builds the X11 backend. The
|
|
# tests still run headless under SDL_VIDEODRIVER=dummy -- see the env block
|
|
# below -- so this buys a faithful build, not a display.
|
|
#
|
|
# The list is SDL's own, from deps/SDL/docs/README-linux.md, reduced to the
|
|
# X11 entries: the audio, Wayland, KMSDRM and Vulkan backends all degrade
|
|
# to "not found" and skip themselves rather than erroring, so they cost
|
|
# nothing to leave out. Nine packages, all in noble.
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc g++ pkg-config moreutils \
|
|
libfreetype-dev libharfbuzz-dev \
|
|
libx11-dev libxext-dev libxcursor-dev libxi-dev libxfixes-dev \
|
|
libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev
|
|
# The akgl-backed half: the text sink and the graphics, audio and input
|
|
# backends, the standalone SDL frontend, and the two suites that drive
|
|
# them against a real software renderer and read the pixels back.
|
|
#
|
|
# It is a separate job rather than a flag on cmake_build because
|
|
# AKBASIC_WITH_AKGL is off by default and that default is the point: the
|
|
# interpreter and its whole 70-case suite build and pass on a machine with
|
|
# no SDL. Keeping the two apart is what proves that claim on every push
|
|
# rather than asserting it.
|
|
#
|
|
# Cost, measured rather than guessed: about 25 s to clone the six
|
|
# submodules, 25 s to configure and 330 object files to compile -- roughly
|
|
# a minute of CPU. Cheaper than it looks, because SDL3 compiles out almost
|
|
# every backend it does not need here.
|
|
- name: build with libakgl
|
|
run: |
|
|
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
|
|
cmake --build build-akgl --parallel
|
|
# Dummy video and audio drivers, set per test by CMakeLists rather than in
|
|
# this job's environment, because an AKGL build of `basic` is now an SDL
|
|
# program and the golden cases run *it*: forty-one real windows is not what
|
|
# anybody running the suite wanted. The suites create software renderers
|
|
# and read them back with SDL_RenderReadPixels, so none of this needs a
|
|
# display, a sound card or an offscreen harness -- the same approach
|
|
# deps/libakgl/tests/draw.c takes.
|
|
#
|
|
# The env block stays anyway. It is redundant with the per-test property
|
|
# and costs nothing, and it is what keeps a hand-run `ctest` in this
|
|
# directory behaving the same way.
|
|
#
|
|
# 72 cases: the default build's 72, minus the three no_device ones whose
|
|
# premise is a driver with no devices attached -- which is exactly what
|
|
# this build contradicts -- plus akgl_backends and akgl_frontend. The
|
|
# golden cases run here too on purpose, and they are now doing double duty:
|
|
# they are the ones that must keep passing when SDL is present, *and* they
|
|
# are what proves the SDL frontend changes no output anywhere in the
|
|
# corpus.
|
|
# docs_screenshots runs here and nowhere else, because rendering a figure
|
|
# needs the SDL half. It re-renders every picture in docs/ and compares it
|
|
# byte for byte with the checked-in copy, so a chapter whose listing was
|
|
# edited without regenerating fails on this push rather than shipping a
|
|
# picture of code that no longer exists.
|
|
# akgl_typing reports Skipped here and that is correct: it needs a real X
|
|
# server, a window manager and xdotool to type at a focused window, and a
|
|
# CI runner has none of the three. It is a developer-machine gate, and the
|
|
# one that covers the path upstream of SDL -- see its script for why that
|
|
# distinction cost a bug once.
|
|
- name: test with libakgl
|
|
env:
|
|
SDL_VIDEODRIVER: dummy
|
|
SDL_AUDIODRIVER: dummy
|
|
SDL_RENDER_DRIVER: software
|
|
run: ctest --test-dir build-akgl --output-on-failure --output-junit "$(pwd)/ctest-akgl-junit.xml"
|
|
- name: publish test results
|
|
if: always()
|
|
uses: mikepenz/action-junit-report@v4
|
|
with:
|
|
report_paths: 'ctest-akgl-junit.xml'
|
|
annotate_only: true
|
|
detailed_summary: true
|
|
include_passed: true
|
|
fail_on_failure: 'true'
|
|
check_name: 'akgl test results'
|
|
- 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 configures a build inside the copy,
|
|
# so it needs the same submodules the main build does. The golden
|
|
# corpus is part of what kills mutants and it is checked in now, so
|
|
# that is libakerror and libakstdlib and nothing else.
|
|
submodules: true
|
|
- name: dependencies
|
|
run: |
|
|
sudo apt-get update -y
|
|
sudo apt-get install -y cmake gcc python3 moreutils
|
|
# Verify the tests actually catch bugs: break the library many ways and
|
|
# confirm the suite fails. This matters more here than in an ordinary C
|
|
# library, because the akerror control-flow macros expand at their call
|
|
# sites -- gcov attributes ATTEMPT/CATCH/PASS to the caller, so coverage
|
|
# cannot see them and mutation testing is the only thing that checks them.
|
|
#
|
|
# Bounded to one small, fast, deterministic file. It used to be two --
|
|
# src/convert.c was the other -- but that file has been deleted: it existed
|
|
# only because libakstdlib's aksl_ato* family could not report a conversion
|
|
# failure, and 0.2.0 fixed that. src/value.c is the file most worth
|
|
# mutating and is deliberately *not* here: 368 mutants at ~11s each is
|
|
# about 70 minutes, because almost everything links against it. Run the
|
|
# default target locally for the whole tree:
|
|
# cmake --build build --target mutation
|
|
#
|
|
# The threshold is a ratchet, not a quality bar. The measured score for
|
|
# src/symtab.c is 74.1% (46 killed by test, 14 by compile, 21 survived, 81
|
|
# total); 65 leaves headroom for runner variance while still failing on a
|
|
# real regression -- a test deleted, or new untested code added.
|
|
#
|
|
# src/audio_tables.c was measured as a replacement second file and scored
|
|
# 64.7%, below the gate. That is a real test gap rather than a reason to
|
|
# avoid the file: almost every survivor is in akbasic_audio_state_init,
|
|
# where nothing asserts that a freshly initialised audio state is actually
|
|
# zeroed and defaulted -- the same gap this job's own history records
|
|
# closing for src/symtab.c. That is issue #25; add the file back when it
|
|
# is closed.
|
|
#
|
|
# It was 73.1% before writing this job. The run's own findings closed the
|
|
# gap: nothing exercised a maximum-length symbol-table key, so every
|
|
# `MAX_KEY - 1` off-by-one in a strncpy survived, and nothing asserted a
|
|
# freshly initialised table was actually zeroed. Adding those two
|
|
# assertions to tests/symtab.c killed five mutants. The same run found that
|
|
# errno was never asserted clear before a strtoll, which is what stops a
|
|
# stale ERANGE from failing a valid conversion -- that assertion outlived
|
|
# the wrapper it was written for and is now in tests/numeric_contract.c,
|
|
# asserted against libakstdlib.
|
|
#
|
|
# The 21 remaining survivors are listed in the published report. Most are
|
|
# ICR mutants on loop and accumulator initialisers that a stronger
|
|
# placement assertion would catch. Issue #25.
|
|
- name: mutation testing
|
|
run: |
|
|
python3 scripts/mutation_test.py \
|
|
--target src/symtab.c \
|
|
--junit mutation-junit.xml \
|
|
--threshold 65
|
|
# 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 }}."
|