Files
akbasic/.gitea/workflows/release.yaml
Andrew Kesterson ecdc6b60ef
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 2m52s
akbasic CI Build / sanitizers (push) Successful in 3m9s
akbasic CI Build / coverage (push) Failing after 3m12s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Successful in 7m27s
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.

Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.

The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.

tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.

Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.

One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.

72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00

137 lines
6.0 KiB
YAML

name: akbasic Release Build
run-name: ${{ gitea.actor }} akbasic release checks
# Manual only. Nothing here runs on push: the full mutation set is thousands of
# mutants and hours of runner time, which is a release-gate cost, not a
# per-commit one. Trigger it from the Actions tab.
on:
workflow_dispatch:
inputs:
mutation_threshold:
description: "Fail if the mutation score falls below this percentage"
required: false
default: "65"
mutation_targets:
description: "Space-separated files to mutate; empty means the whole src/ tree"
required: false
default: ""
jobs:
# Moved here from ci.yaml. The docs are wanted for a release, not on every
# push, and building them beside the release mutation run keeps the two
# artefacts a release needs in one place.
docs:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
# No submodules at all. Doxyfile's INPUT is include/akbasic, src and
# examples, none of which live under deps/, and doxygen does not need to
# resolve <akerror.h> to parse a declaration. Verified by running it
# against a tree with no deps/ present.
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y doxygen graphviz
# This is a gate, not a convenience. The Doxyfile sets
# WARN_AS_ERROR = FAIL_ON_WARNINGS, so a doc block that documents some of a
# function's parameters but not all of them fails the build. All 114 public
# declarations under include/akbasic carry a @brief, a @param per
# parameter, a @return and their @throws; keep it that way.
#
# OUTPUT_DIRECTORY is build/docs and doxygen will not create a missing
# parent, so make it first -- there is no cmake step in this job to do it.
- name: build API documentation
run: |
mkdir -p build
doxygen Doxyfile
- name: upload API documentation
if: always()
uses: actions/upload-artifact@v4
with:
name: api-documentation
path: build/docs/html/
if-no-files-found: error
- run: echo "🍏 This job's status is ${{ job.status }}."
full_mutation:
runs-on: ubuntu-latest
# 3675 mutants across the whole src/ tree. Cost per mutant is one incremental
# rebuild plus one full ctest run, which measures at roughly 2s for a leaf
# file and 11s for src/value.c, where almost everything links against it.
# Budget most of a day and expect it to finish well inside that; the ceiling
# exists so a mutant that wedges the runner cannot hold it forever.
timeout-minutes: 720
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 -- including
# deps/basicinterpret, because the golden corpus is part of what kills
# mutants.
submodules: true
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc python3
# The whole akbasic-owned src/ tree. ci.yaml runs a two-file subset on every
# push; this is the one that actually covers the interpreter.
#
# Mutation testing is the only gate that sees the error-handling control
# flow at all: libakerror's ATTEMPT/CATCH/PASS macros expand at their call
# sites, so gcov attributes them to the caller and line coverage cannot
# measure them.
#
# The default threshold matches ci.yaml's rather than being stricter.
# Whole-tree coverage is uneven -- src/symtab.c is the only file measured
# on the push path, at 74.1%, and the rest is unmeasured, so a tighter
# number here would be a guess. Raise it with the workflow input once a
# full run has established a real baseline.
#
# The two inputs arrive through the environment rather than being
# interpolated straight into the script. ${{ }} substitution happens before
# the shell sees the line, so a value containing shell metacharacters would
# otherwise run as code. This workflow is manual and owner-triggered, but
# the safe form costs nothing.
- name: mutation testing (full tree)
env:
MUTATION_TARGETS: ${{ gitea.event.inputs.mutation_targets }}
MUTATION_THRESHOLD: ${{ gitea.event.inputs.mutation_threshold }}
run: |
set -eu
# Word-splitting is the point here: the input is a space-separated
# list. An empty input leaves $targets empty and the harness falls
# through to its own default, which is the whole src/ tree.
targets=""
for f in ${MUTATION_TARGETS:-}; do
targets="$targets --target $f"
done
# shellcheck disable=SC2086
python3 scripts/mutation_test.py \
$targets \
--junit mutation-junit.xml \
--threshold "${MUTATION_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'
# Keep the raw report as well as the annotations: a release wants the
# survivor list on file, and the job summary is not durable.
- name: upload mutation report
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: mutation-junit.xml
if-no-files-found: warn
- run: echo "🍏 This job's status is ${{ job.status }}."