From b6aeaa8995ae81562a4bf11937959b09027116ff Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 09:32:29 +0200 Subject: scripts: check command and error-code drift against the docs --- scripts/check-consistency.sh | 196 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100755 scripts/check-consistency.sh (limited to 'scripts') diff --git a/scripts/check-consistency.sh b/scripts/check-consistency.sh new file mode 100755 index 0000000..f67695f --- /dev/null +++ b/scripts/check-consistency.sh @@ -0,0 +1,196 @@ +#!/bin/sh +# Check that the daemon source and docs/PROTOCOL.md have not drifted apart: +# +# 1. every command in g_commands[] (src/commands.c) is listed in a +# PROTOCOL.md table whose first column is "Command", and vice versa; +# 2. every error code passed to fail()/failf()/add_error() in src/*.c is +# documented in PROTOCOL.md section 5.3, and vice versa (documented but +# unused codes are warnings only, some are reserved). +# +# Run from the repo root: sh scripts/check-consistency.sh +# Exits 0 when there is no drift, 1 when drift is found, 2 on parse errors. +# +# Environment overrides (used by the self-tests): +# REPO_ROOT repository root, default "." (run from the repo root) +# COMMANDS_C command table source, default $REPO_ROOT/src/commands.c +# PROTOCOL_MD protocol document, default $REPO_ROOT/docs/PROTOCOL.md +# SRC_GLOB shell glob of sources scanned for error codes, +# default $REPO_ROOT/src/*.c +# +# Commands deliberately kept out of the PROTOCOL.md tables. Prefer documenting +# the command in a table over adding it here. Keep sorted, space separated. +ALLOW_UNDOCUMENTED_COMMANDS="" + +set -eu +export LC_ALL=C + +REPO_ROOT=${REPO_ROOT:-.} +COMMANDS_C=${COMMANDS_C:-$REPO_ROOT/src/commands.c} +PROTOCOL_MD=${PROTOCOL_MD:-$REPO_ROOT/docs/PROTOCOL.md} +SRC_GLOB=${SRC_GLOB:-$REPO_ROOT/src/*.c} + +if [ ! -f "$COMMANDS_C" ]; then + echo "check-consistency: command source not found: $COMMANDS_C" >&2 + exit 2 +fi +if [ ! -f "$PROTOCOL_MD" ]; then + echo "check-consistency: protocol document not found: $PROTOCOL_MD" >&2 + exit 2 +fi + +tmp=$(mktemp -d "${TMPDIR:-/tmp}/bokf-consistency-XXXXXX") +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +# --- extract what the code implements -------------------------------------- + +awk ' + /g_commands[[:space:]]*\[[[:space:]]*\][[:space:]]*=/ { in_table = 1; next } + in_table && /^[[:space:]]*\};/ { in_table = 0 } + in_table && /^[[:space:]]*\{[[:space:]]*"/ { + line = $0 + sub(/^[^{]*\{[[:space:]]*"/, "", line) + sub(/".*/, "", line) + if (line != "") + print line + } +' "$COMMANDS_C" | sort -u > "$tmp/impl_commands" + +if [ ! -s "$tmp/impl_commands" ]; then + echo "check-consistency: no commands parsed from $COMMANDS_C" >&2 + exit 2 +fi + +# --- extract what the docs promise ----------------------------------------- + +# Only rows of tables whose header first cell is "Command" count; prose, +# argument names and other tables must not produce command names. +awk ' + /^\|/ { + n = split($0, cells, "|") + cell = cells[2] + gsub(/^[[:space:]]+/, "", cell) + gsub(/[[:space:]]+$/, "", cell) + if (cell == "Command") { in_table = 1; next } + if (in_table) { + rest = cell + while (match(rest, /`[a-z][a-z0-9_.]*`/)) { + print substr(rest, RSTART + 1, RLENGTH - 2) + rest = substr(rest, RSTART + RLENGTH) + } + } + next + } + { in_table = 0 } +' "$PROTOCOL_MD" | sort -u > "$tmp/doc_commands" + +if [ ! -s "$tmp/doc_commands" ]; then + echo "check-consistency: no command tables parsed from $PROTOCOL_MD" >&2 + exit 2 +fi + +# Error codes are the backticked ALL_CAPS words inside section 5.3. +awk ' + /^###[[:space:]]+5\.3/ { in_section = 1; next } + in_section && /^###/ { in_section = 0 } + in_section { + rest = $0 + while (match(rest, /`[A-Z][A-Z0-9_]+`/)) { + print substr(rest, RSTART + 1, RLENGTH - 2) + rest = substr(rest, RSTART + RLENGTH) + } + } +' "$PROTOCOL_MD" | sort -u > "$tmp/doc_codes" + +if [ ! -s "$tmp/doc_codes" ]; then + echo "check-consistency: no error codes parsed from $PROTOCOL_MD 5.3" >&2 + exit 2 +fi + +# --- compare ---------------------------------------------------------------- + +: > "$tmp/allow_commands" +for name in $ALLOW_UNDOCUMENTED_COMMANDS; do + printf '%s\n' "$name" >> "$tmp/allow_commands" +done +sort -u -o "$tmp/allow_commands" "$tmp/allow_commands" +cat "$tmp/doc_commands" "$tmp/allow_commands" | sort -u > "$tmp/known_commands" + +comm -23 "$tmp/impl_commands" "$tmp/known_commands" > "$tmp/cmd_undocumented" +comm -13 "$tmp/impl_commands" "$tmp/doc_commands" > "$tmp/cmd_unimplemented" + +for f in $SRC_GLOB; do + [ -f "$f" ] || continue + grep -E '(^|[^A-Za-z0-9_])(fail|failf|add_error)[[:space:]]*\(' "$f" | + grep -oE '"[A-Z][A-Z0-9_]{1,}"' || true +done | tr -d '"' | sort -u > "$tmp/used_codes" + +if [ ! -s "$tmp/used_codes" ]; then + echo "check-consistency: no error-code call sites parsed from $SRC_GLOB" >&2 + exit 2 +fi + +comm -23 "$tmp/used_codes" "$tmp/doc_codes" > "$tmp/code_undocumented" +comm -13 "$tmp/used_codes" "$tmp/doc_codes" > "$tmp/code_unused" + +# --- report ----------------------------------------------------------------- + +status=0 +problems=0 +warnings=0 + +section() { + echo + echo "== $1 ==" +} + +section "commands: implemented in $COMMANDS_C but not documented in $PROTOCOL_MD" +if [ -s "$tmp/cmd_undocumented" ]; then + while IFS= read -r name; do + echo "command: undocumented: $name" + problems=$((problems + 1)) + done < "$tmp/cmd_undocumented" + status=1 +else + echo "(none)" +fi + +section "commands: documented in $PROTOCOL_MD but not implemented" +if [ -s "$tmp/cmd_unimplemented" ]; then + while IFS= read -r name; do + echo "command: documented but unimplemented: $name" + problems=$((problems + 1)) + done < "$tmp/cmd_unimplemented" + status=1 +else + echo "(none)" +fi + +section "error codes: used in src but not documented in $PROTOCOL_MD 5.3" +if [ -s "$tmp/code_undocumented" ]; then + while IFS= read -r code; do + echo "error-code: undocumented: $code" + problems=$((problems + 1)) + done < "$tmp/code_undocumented" + status=1 +else + echo "(none)" +fi + +section "warnings (not failures)" +if [ -s "$tmp/code_unused" ]; then + while IFS= read -r code; do + echo "warning: documented but unused in src: $code" + warnings=$((warnings + 1)) + done < "$tmp/code_unused" + echo "(documented codes may be reserved for future use)" +else + echo "(none)" +fi + +echo +if [ "$status" -eq 0 ]; then + echo "consistency: ok ($problems problems, $warnings warnings)" +else + echo "consistency: FAILED ($problems problems, $warnings warnings)" +fi +exit "$status" -- cgit v1.3