tinymux/testcases/tools/FnCoverage
Stephen Dennis c3973f1642 test(tools): FnCoverage counted builtins this build does not have (#1160)
With ctime() covered, FnCoverage still reported 10 uncovered builtins --
the SQL/recordset family.  They are not untested.  They do not exist:

  SQL, MAPSQL                       #if defined(INLINESQL)
  RSERROR RSNEXT RSPREV RSREC       #if defined(STUB_SLAVE)
  RSRECNEXT RSRECPREV RSRELEASE
  RSROWS

Neither macro is defined in the configuration the corpus runs in --
CLAUDE.md has --enable-stubslave as a release-build flag, omitted for smoke
-- and calling one answers "#-1 FUNCTION (SQL) NOT FOUND".  Measured, not
inferred: all ten return that under muxscript.

The tool greps the table with a flat regex and has no notion of #if, so it
was reporting work no test could do.  That is the wrong direction for a
coverage number to be wrong in: it invents a backlog, and #1380 has been
carrying these as "needs live session / modules / side effects" when the
first problem is that they are compiled out.

Guards are now resolved against what this build actually defines, from the
two places the macros come from -- -D flags in the engine Makefile
(REALITY_LVLS, WOD_REALMS, TINYMUX_JIT) and autoconf's header.  Generic
rather than a STUB_SLAVE special case, because there were already two
different guards and hardcoding one would only move the error.

  before: 503 builtins, 493 referenced (98%), 10 with no reference
  after:  493 builtins, 493 referenced (100%), 0 with no reference,
          10 excluded as absent from this build

Excluded rather than dropped, and listed with their guard under --list:
"cannot be covered in this build" is a different fact from "nobody wrote a
test", and a reader chasing #1160 needs to tell them apart.

The single-macro, non-nested assumption is verified against the table as it
stands and written into the comment, because a compound guard would be read
as its first macro and fail silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:26:46 -06:00

162 lines
6.1 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# FnCoverage — which registered builtins have no reference in the corpus.
#
# #1160 measured 196 uncovered builtins. A day later it was 144, because
# several test files landed in between. A number that moves that fast is
# only useful if it is cheap to re-measure, so this does the measuring
# rather than leaving it in an issue body to go stale.
#
# "Covered" here means the name appears in call position — NAME( — in some
# testcases/*.mux. That is a weak notion deliberately: it answers "does
# anything reference this at all", which is the question #1160 asks. It
# says nothing about whether the reference asserts anything, so treat it as
# a floor, not a measure of test quality.
#
# Usage:
# ./tools/FnCoverage summary only
# ./tools/FnCoverage --list summary plus the uncovered names
# ./tools/FnCoverage --tier1 exit 1 if any Tier 1 name is uncovered
#
# Exit: 0 normally; 1 if --tier1 is given and a Tier 1 name is uncovered.
set -u
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
TESTCASES=$(CDPATH= cd -- "$HERE/.." && pwd)
REPO=$(CDPATH= cd -- "$TESTCASES/.." && pwd)
FUNCTIONS="$REPO/mux/modules/engine/functions.cpp"
MODE=${1:-}
if [ ! -f "$FUNCTIONS" ]; then
echo "FnCoverage: cannot find $FUNCTIONS" >&2
exit 2
fi
WORK=$(mktemp -d "${TMPDIR:-/tmp}/fncov.XXXXXX") || exit 2
trap 'rm -rf "$WORK"' EXIT
# Registered builtins: the { T("NAME"), fun_... } table entries.
#
# The table is not flat. Some entries sit inside #if blocks and are absent
# from builds that do not define the guard, and counting those as gaps makes
# the number wrong in the direction that matters: it reports work no test
# could do, because in this configuration the function does not exist and
# answers "#-1 FUNCTION (NAME) NOT FOUND".
#
# So resolve each guard against what this build actually defines rather than
# assuming. Two sources, because the macros come from two places: -D flags
# in the engine Makefile (REALITY_LVLS, WOD_REALMS, TINYMUX_JIT) and
# autoconf's header (HAVE_*, and STUB_SLAVE when --enable-stubslave is on).
#
# Today this splits out ten names -- the SQL/recordset family behind
# STUB_SLAVE and INLINESQL, neither of which the smoke build defines. They
# are reported separately rather than dropped: "cannot be covered in this
# build" is a different fact from "nobody wrote a test", and a reader chasing
# #1160 needs to tell them apart.
#
# Guards are assumed non-nested and single-macro. Verified against the
# table as it stands: every #if inside it is one of #if defined(INLINESQL),
# #if defined(STUB_SLAVE), #if defined(TINYMUX_JIT), #ifdef REALITY_LVLS,
# #ifdef TINYMUX_JIT. A compound guard such as #if defined(A) || defined(B)
# would be read as A alone, so if one is ever added here this needs
# revisiting -- the failure would be silent, which is why it is written down
# rather than left to be rediscovered.
{
grep -ohE '[-]D[A-Za-z_][A-Za-z0-9_]*' \
"$REPO/mux/modules/engine/Makefile" 2>/dev/null | sed 's/^-D//'
sed -n 's/^#define[[:space:]]\{1,\}\([A-Za-z_][A-Za-z0-9_]*\).*/\1/p' \
"$REPO/mux/src/autoconf.h" 2>/dev/null
} | sort -u > "$WORK/defined"
awk '
/^#if[[:space:]]+defined\(/ {
g = $0; sub(/.*defined\(/, "", g); sub(/\).*/, "", g); guard = g; next
}
/^#ifdef[[:space:]]/ { guard = $2; next }
/^#if[[:space:]]/ { guard = "?"; next }
/^#endif/ { guard = ""; next }
match($0, /\{[[:space:]]*T\("[A-Z0-9_]+"\)[[:space:]]*,[[:space:]]*fun_/) {
n = $0; sub(/.*T\("/, "", n); sub(/"\).*/, "", n)
print (guard == "" ? "-" : guard) " " n
}
' "$FUNCTIONS" | sort -u > "$WORK/tagged"
: > "$WORK/all"; : > "$WORK/guarded"
while read -r g n; do
if [ "$g" = "-" ] || grep -qx "$g" "$WORK/defined"; then
printf '%s\n' "$n" >> "$WORK/all"
else
printf '%s (%s)\n' "$n" "$g" >> "$WORK/guarded"
fi
done < "$WORK/tagged"
sort -u -o "$WORK/all" "$WORK/all"
sort -u -o "$WORK/guarded" "$WORK/guarded"
# Names beginning with _ are internal helpers reached only from generated
# softcode, never written by a user, so they are excluded from the ratio
# rather than counted as gaps.
grep '^_' "$WORK/all" > "$WORK/internal" || true
grep -v '^_' "$WORK/all" > "$WORK/user"
cat "$TESTCASES"/*.mux 2>/dev/null | tr 'A-Z' 'a-z' > "$WORK/corpus"
: > "$WORK/uncovered"
while read -r fn; do
low=$(printf '%s' "$fn" | tr 'A-Z' 'a-z')
if ! grep -qE "(^|[^a-z0-9_])${low}[[:space:]]*\(" "$WORK/corpus"; then
printf '%s\n' "$fn" >> "$WORK/uncovered"
fi
done < "$WORK/user"
n_all=$(wc -l < "$WORK/user")
n_int=$(wc -l < "$WORK/internal")
n_grd=$(wc -l < "$WORK/guarded")
n_unc=$(wc -l < "$WORK/uncovered")
n_cov=$((n_all - n_unc))
pct=$(( n_cov * 100 / (n_all > 0 ? n_all : 1) ))
echo "=== builtin corpus coverage (#1160) ==="
printf ' user-callable builtins : %d\n' "$n_all"
printf ' referenced in a test : %d (%d%%)\n' "$n_cov" "$pct"
printf ' no reference at all : %d\n' "$n_unc"
printf ' internal helpers (excluded): %d\n' "$n_int"
printf ' absent from this build (excluded): %d\n' "$n_grd"
# Tier 1 — the eval/composition family. #1160 singled these out because
# the JIT does its most aggressive work here (nested compilation, carg
# marshalling, result-type handoff) and #1159 lived in exactly this set.
TIER1="ULOCAL UDEFAULT DEFAULT EDEFAULT ZFUN SUBEVAL FOREACH MUNGE LMATH FILTERBOOL"
t1_missing=""
for f in $TIER1; do
if grep -qx "$f" "$WORK/uncovered"; then
t1_missing="$t1_missing $f"
fi
done
echo
if [ -n "$t1_missing" ]; then
echo " TIER 1 UNCOVERED:$t1_missing"
else
echo " Tier 1 (eval/composition): all covered"
fi
if [ "$MODE" = "--list" ]; then
echo
echo "Uncovered:"
fold -s -w 72 < "$WORK/uncovered" | sed 's/^/ /'
tr '\n' ' ' < "$WORK/uncovered" | fold -s -w 72 | sed 's/^/ /'
echo
if [ "$n_grd" -gt 0 ]; then
echo
echo "Excluded -- guarded by a macro this build does not define, so"
echo "no test in the corpus can reference them:"
sed 's/^/ /' < "$WORK/guarded"
fi
fi
if [ "$MODE" = "--tier1" ] && [ -n "$t1_missing" ]; then
exit 1
fi
exit 0