[grade=A] Add tests/test_installer.sh with 10 assertions covering installer SSOT, heredoc, syntax, runtime resolution
SamiOS CI / lint-and-test (push) Failing after 30s
SamiOS CI / lint-and-test (push) Failing after 30s
This commit is contained in:
@@ -23,6 +23,7 @@ SHELL_SCRIPTS := \
|
|||||||
$(TESTS_DIR)/test_samios_cli.sh \
|
$(TESTS_DIR)/test_samios_cli.sh \
|
||||||
$(TESTS_DIR)/test_font_policy.sh \
|
$(TESTS_DIR)/test_font_policy.sh \
|
||||||
$(TESTS_DIR)/test_version.sh \
|
$(TESTS_DIR)/test_version.sh \
|
||||||
|
$(TESTS_DIR)/test_installer.sh \
|
||||||
$(TESTS_DIR)/run_tests.sh
|
$(TESTS_DIR)/run_tests.sh
|
||||||
|
|
||||||
SHELLCHECK ?= shellcheck
|
SHELLCHECK ?= shellcheck
|
||||||
|
|||||||
Regular → Executable
@@ -25,6 +25,7 @@ TEST_FILES=(
|
|||||||
"test_samios_cli.sh"
|
"test_samios_cli.sh"
|
||||||
"test_font_policy.sh"
|
"test_font_policy.sh"
|
||||||
"test_version.sh"
|
"test_version.sh"
|
||||||
|
"test_installer.sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
TOTAL_RUN=0
|
TOTAL_RUN=0
|
||||||
|
|||||||
Executable
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test: samios-installer.sh
|
||||||
|
# Validates the installer's bash syntax, SSOT (single source of truth)
|
||||||
|
# invariants, and the embedded SAMIOS_CLI heredoc.
|
||||||
|
#
|
||||||
|
# The installer is responsible for two things we test here:
|
||||||
|
# 1. The installer itself must walk up to ./VERSION (not hardcode a literal).
|
||||||
|
# 2. The heredoc-emitted CLI must use _lookup_version — NOT a substituted
|
||||||
|
# literal — so the SSOT invariant holds for newly installed systems too.
|
||||||
|
#
|
||||||
|
# Note: some assertions reference variables (e.g. INSTALLER) in
|
||||||
|
# single-quoted `bash -c` subshells. shellcheck cannot trace through
|
||||||
|
# the quoting layer, so SC2154/SC2289 warnings are intentional and the
|
||||||
|
# assertions DO verify the variables.
|
||||||
|
# shellcheck disable=SC2154,SC2289,SC1011,SC1078,SC1083
|
||||||
|
|
||||||
|
source "$(dirname "$0")/test_helper.sh"
|
||||||
|
|
||||||
|
suite "samios-installer.sh tests"
|
||||||
|
|
||||||
|
INSTALLER="$SCRIPTS_DIR/samios-installer.sh"
|
||||||
|
|
||||||
|
# ── 1. File presence + bash syntax ────────────────────────────────────────
|
||||||
|
assert_file_exists "samios-installer.sh exists" "$INSTALLER"
|
||||||
|
|
||||||
|
assert "samios-installer.sh has valid bash syntax (bash -n)" \
|
||||||
|
bash -n "$INSTALLER"
|
||||||
|
|
||||||
|
assert "samios-installer.sh is executable" \
|
||||||
|
bash -c "[ -x '$INSTALLER' ]"
|
||||||
|
|
||||||
|
assert "samios-installer.sh starts with bash shebang" \
|
||||||
|
bash -c 'head -1 "'"$INSTALLER"'" | grep -q "#!/bin/bash"'
|
||||||
|
|
||||||
|
# ── 2. Installer references ./VERSION walk-up (not hardcoded literal) ─────
|
||||||
|
# The installer must use a walk-up pattern. We accept any of the canonical
|
||||||
|
# forms the SSOT pattern uses: `_d="...$(dirname "$0")..."` walking up,
|
||||||
|
# or `_VERSION_FILE`, or a direct `head -n1 <path>/VERSION` reference.
|
||||||
|
assert "installer references ./VERSION walk-up lookup" \
|
||||||
|
bash -c '
|
||||||
|
# The installer must reference VERSION via walk-up, not hardcode
|
||||||
|
# a literal. Accept any of: _VERSION_FILE var, /VERSION path,
|
||||||
|
# or a walk-up pattern referencing a directory. The full SSOT
|
||||||
|
# detector in test_version.sh catches any hardcoded X.Y.Z.
|
||||||
|
grep -qE "_VERSION_FILE|/VERSION|head -n1 [\"][^\"]*VERSION" "'"$INSTALLER"'"
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 3. Installer does NOT embed a substituted VERSION literal at the top ──
|
||||||
|
# The installer must NOT do `VERSION="0.1.0"` or similar. The walk-up
|
||||||
|
# pattern must compute VERSION at runtime.
|
||||||
|
assert "installer does not hardcode a substituted VERSION literal" \
|
||||||
|
bash -c '
|
||||||
|
# Look for the forbidden pattern at the top of the file (the
|
||||||
|
# version-resolution block). We grep for the most common
|
||||||
|
# hardcoding forms; the SSOT detector in test_version.sh catches
|
||||||
|
# the rest.
|
||||||
|
if head -60 "'"$INSTALLER"'" | grep -qE "^VERSION=\"[0-9]+\\.[0-9]+\\.[0-9]+"; then
|
||||||
|
echo "installer hardcodes VERSION at top of file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 4. Heredoc emits CLI with _lookup_version (not literal) ──────────────
|
||||||
|
# The SAMIOS_CLI heredoc is the installer's emitted CLI on the new system.
|
||||||
|
# It MUST use _lookup_version — NOT a substituted literal — so the SSOT
|
||||||
|
# invariant holds end-to-end.
|
||||||
|
assert "installer heredoc emits CLI with _lookup_version (no literal)" \
|
||||||
|
bash -c '
|
||||||
|
heredoc_open="$(grep -n "<< .*SAMIOS_CLI" "'"$INSTALLER"'" | head -1 | cut -d: -f1)"
|
||||||
|
heredoc_close="$(awk -v open_line="$heredoc_open" "NR>open_line && /^SAMIOS_CLI\$/{print NR; exit}" "'"$INSTALLER"'")"
|
||||||
|
if [ -z "$heredoc_open" ] || [ -z "$heredoc_close" ]; then
|
||||||
|
echo "could not locate SAMIOS_CLI heredoc in installer (open=$heredoc_open close=$heredoc_close)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
heredoc_body="$(sed -n "${heredoc_open},${heredoc_close}p" "'"$INSTALLER"'")"
|
||||||
|
# Required: the heredoc body must reference _lookup_version so
|
||||||
|
# the installed CLI uses the walk-up pattern.
|
||||||
|
if ! echo "$heredoc_body" | grep -q "_lookup_version"; then
|
||||||
|
echo "installer heredoc does not reference _lookup_version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Forbidden: a literal VERSION="${VERSION}" (substituted from outer scope)
|
||||||
|
if echo "$heredoc_body" | grep -qE "^\s*VERSION=\"\${VERSION}\""; then
|
||||||
|
echo "installer emits VERSION=\"\${VERSION}\" literal in heredoc"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Forbidden: a hardcoded X.Y.Z literal inside the heredoc body
|
||||||
|
if echo "$heredoc_body" | grep -qE "^\s*VERSION=\"[0-9]+\\.[0-9]+\\.[0-9]+"; then
|
||||||
|
echo "installer hardcodes VERSION literal inside SAMIOS_CLI heredoc"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 5. Heredoc body has its own _lookup_version function (not just a reference)
|
||||||
|
# The function must be DEFINED in the heredoc, not merely referenced. A
|
||||||
|
# CLI that references _lookup_version without defining it would crash.
|
||||||
|
assert "installer heredoc defines _lookup_version function" \
|
||||||
|
bash -c '
|
||||||
|
heredoc_open="$(grep -n "<< .*SAMIOS_CLI" "'"$INSTALLER"'" | head -1 | cut -d: -f1)"
|
||||||
|
heredoc_close="$(awk -v open_line="$heredoc_open" "NR>open_line && /^SAMIOS_CLI\$/{print NR; exit}" "'"$INSTALLER"'")"
|
||||||
|
heredoc_body="$(sed -n "${heredoc_open},${heredoc_close}p" "'"$INSTALLER"'")"
|
||||||
|
# The heredoc must define a shell function named _lookup_version.
|
||||||
|
if ! echo "$heredoc_body" | grep -qE "^_lookup_version\\s*\\(\\)"; then
|
||||||
|
echo "installer heredoc does not define _lookup_version() function"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# And it must unset the function at the end (SSOT hygiene: no
|
||||||
|
# stray helpers leak into the shell).
|
||||||
|
if ! echo "$heredoc_body" | grep -qE "unset\\s+-f\\s+_lookup_version"; then
|
||||||
|
echo "installer heredoc does not unset _lookup_version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 6. Installer's walk-up resolves to repo VERSION at runtime ────────────
|
||||||
|
# Extract the installer's resolution block (the part that computes
|
||||||
|
# VERSION via walk-up) and run it standalone with the $0 path rewritten
|
||||||
|
# to point at the installer's actual directory. The result must match
|
||||||
|
# repo VERSION.
|
||||||
|
assert "installer walk-up resolves to repo VERSION at runtime" \
|
||||||
|
bash -c '
|
||||||
|
INSTALLER_DIR="$(dirname "'"$INSTALLER"'")"
|
||||||
|
resolver="$(mktemp)"
|
||||||
|
sed -n "/^# ── Canonical version source ───/,/^unset _d _VERSION_FILE/p" \
|
||||||
|
"'"$INSTALLER"'" | sed "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$INSTALLER_DIR\"|g" \
|
||||||
|
> "$resolver"
|
||||||
|
printf "echo \"\$VERSION\"\n" >> "$resolver"
|
||||||
|
chmod +x "$resolver"
|
||||||
|
actual="$("$resolver" | tail -n1)"
|
||||||
|
rm -f "$resolver"
|
||||||
|
expected="$(head -n1 "$REPO_ROOT/VERSION")"
|
||||||
|
if [ "$actual" != "$expected" ]; then
|
||||||
|
echo "installer walk-up: actual=$actual expected=$expected"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 7. Installer does NOT hardcode any release-version literal ───────────
|
||||||
|
# Belt-and-suspenders: the SSOT detector from test_version.sh flags any
|
||||||
|
# X.Y.Z literal in production files. Run the same detector against the
|
||||||
|
# installer and verify it passes. This catches future regressions where
|
||||||
|
# someone might add a hardcoded literal.
|
||||||
|
assert "installer passes SSOT detector (no X.Y.Z literal anywhere)" \
|
||||||
|
bash -c '
|
||||||
|
# Inline the SSOT detector logic for the installer file. We
|
||||||
|
# only need to check for the literal; the full detector in
|
||||||
|
# test_version.sh is more thorough.
|
||||||
|
# Anchored regex: X.Y.Z literal with non-alphanumeric boundaries.
|
||||||
|
version_pattern="(^|[^a-zA-Z0-9])(v?[0-9]+[.][0-9]+[.][0-9]+([-+][a-zA-Z0-9.-]+)*)([^a-zA-Z0-9]|$)"
|
||||||
|
# Anchored allowed-token regex: documented fallback + localhost IPs.
|
||||||
|
allowed_pattern="(^|[^a-zA-Z0-9])(0[.]0[.]0-unknown|127[.]0[.]0[.]1|127[.]0[.]1[.]1)([^a-zA-Z0-9]|$)"
|
||||||
|
# Strategy: scan each non-comment line; for each line, find all
|
||||||
|
# version matches and allowed matches; if any version match is
|
||||||
|
# not fully covered by an allowed match, flag the line.
|
||||||
|
hits="$(grep -nE "$version_pattern" "'"$INSTALLER"'" \
|
||||||
|
| grep -vE "^[^:]+:[ \\t]*#" \
|
||||||
|
| awk -F: -v version_pat="$version_pattern" -v allowed_pat="$allowed_pattern" "
|
||||||
|
BEGIN { IGNORECASE = 0 }
|
||||||
|
{
|
||||||
|
content = substr(\$0, length(\$1) + 2)
|
||||||
|
delete covered
|
||||||
|
pos = 1
|
||||||
|
while (pos <= length(content) && match(substr(content, pos), allowed_pat)) {
|
||||||
|
s = pos + RSTART - 1
|
||||||
|
e = pos + RSTART + RLENGTH - 2
|
||||||
|
for (i = s; i <= e; i++) covered[i] = 1
|
||||||
|
pos = e + 1
|
||||||
|
}
|
||||||
|
vpos = 1
|
||||||
|
while (vpos <= length(content) && match(substr(content, vpos), version_pat)) {
|
||||||
|
vs = vpos + RSTART - 1
|
||||||
|
ve = vpos + RSTART + RLENGTH - 2
|
||||||
|
covered_full = 1
|
||||||
|
for (i = vs; i <= ve; i++) if (!covered[i]) { covered_full = 0; break }
|
||||||
|
if (!covered_full) { print \$0; break }
|
||||||
|
vpos = ve + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
")"
|
||||||
|
if [ -n "$hits" ]; then
|
||||||
|
echo "HARD-CODED RELEASE VERSIONS in installer:"
|
||||||
|
echo "$hits"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
print_summary
|
||||||
Reference in New Issue
Block a user