aboutsummaryrefslogtreecommitdiff
path: root/scripts/check-consistency.sh
blob: 600db25cbe872619a3764bf41551603131d1e02b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/bin/sh
# Check that the daemon source and docs/PROTOCOL.md have not drifted apart:
#
#   1. every command in the `g_cmd_<domain>[]` tables (src/commands.c and
#      src/cmd_*.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  primary command table source, default $REPO_ROOT/src/commands.c
#   CMDS_GLOB   shell glob of command table sources,
#               default "$COMMANDS_C $REPO_ROOT/src/cmd_*.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}
CMDS_GLOB=${CMDS_GLOB:-$COMMANDS_C $REPO_ROOT/src/cmd_*.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 --------------------------------------

for f in $CMDS_GLOB; do
    [ -f "$f" ] || continue
    awk '
        /const struct command g_cmd_[a-z_]+\[\][[: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
        }
    ' "$f"
done | sort -u > "$tmp/impl_commands"

if [ ! -s "$tmp/impl_commands" ]; then
    echo "check-consistency: no commands parsed from $CMDS_GLOB" >&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 $CMDS_GLOB 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"