Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ lint-docker:
lint-sh:
@echo "Linting shell scripts..." \
&& shellcheck scripts/*.sh scripts/*/*.sh tests/deb/*.sh images/*/bin/* \
&& shellcheck -e SC2154 tests/bats/*/*.bash tests/bats/*/*/*/*.bats \
&& shellcheck -e SC2154 $$(find tests/bats -type f \( -name '*.bash' -o -name '*.bats' \)) \
&& echo "OK"

# Check shell script formatting
Expand Down
131 changes: 131 additions & 0 deletions tests/bats/scripts/generate-checksums.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env bats
# shellcheck shell=bash
#
# Tests for scripts/generate-checksums.sh — release artifact
# checksum generation. The script derives REPO_ROOT from its own
# path and writes to ${REPO_ROOT}/artifacts/release/, so each test
# stages a fake repo (symlinking the script in) to keep output
# confined to BATS_TEST_TMPDIR.

load ../helpers/common

setup() {
common_setup
FAKE_REPO="${BATS_TEST_TMPDIR}/repo"
mkdir -p "${FAKE_REPO}/scripts"
ln -s "${REPO_ROOT}/scripts/generate-checksums.sh" \
"${FAKE_REPO}/scripts/generate-checksums.sh"
SCRIPT="${FAKE_REPO}/scripts/generate-checksums.sh"
OUT_DIR="${FAKE_REPO}/artifacts/release"
export SCRIPT OUT_DIR
}

# Drop empty fixture artifacts into a directory. Each arg is a
# filename; the contents are stable across the suite so checksums
# are deterministic.
_make_artifacts() {
local dir="$1"
shift
mkdir -p "${dir}"
local name
for name in "$@"; do
mkdir -p "$(dirname "${dir}/${name}")"
printf 'fixture-%s' "${name}" > "${dir}/${name}"
done
}

# ── happy path ───────────────────────────────────────────────────────

@test "writes checksums.txt and release-body.md in GNU coreutils format" {
local dist="${FAKE_REPO}/dist"
_make_artifacts "${dist}" "ci-tools_1.0.0_linux-x64.tar.gz" "ci-tools_1.0.0_amd64.deb"
run "${SCRIPT}" "${dist}"
assert_success
assert_file_exist "${OUT_DIR}/checksums.txt"
assert_file_exist "${OUT_DIR}/release-body.md"
# Each line must be "<64 hex> <basename>" — GNU coreutils format
# (matches what `sha256sum -c` expects). grep -E gives per-line
# anchoring, which bash's =~ does not.
run grep -Eq "^[a-f0-9]{64} ci-tools_1\.0\.0_amd64\.deb$" \
"${OUT_DIR}/checksums.txt"
assert_success
run grep -Eq "^[a-f0-9]{64} ci-tools_1\.0\.0_linux-x64\.tar\.gz$" \
"${OUT_DIR}/checksums.txt"
assert_success
}

@test "checksums.txt records every artifact found in the dist dir" {
# Note: the script's final `| sort` sorts lines by the sha256
# prefix (the first column on each line), not by filename, so we
# assert only the *set* of records — pinning the buggy hash-order
# would lock in a quirk that shouldn't be load-bearing.
local dist="${FAKE_REPO}/dist"
_make_artifacts "${dist}" \
"ci-tools_1.0.0_osx-x64.tar.gz" \
"ci-tools_1.0.0_amd64.deb" \
"ci-tools_1.0.0_linux-x64.tar.gz"
run "${SCRIPT}" "${dist}"
assert_success
run awk '{print $2}' "${OUT_DIR}/checksums.txt"
assert_line "ci-tools_1.0.0_osx-x64.tar.gz"
assert_line "ci-tools_1.0.0_amd64.deb"
assert_line "ci-tools_1.0.0_linux-x64.tar.gz"
assert_equal "${#lines[@]}" 3
}

@test "checksums.txt records basenames only, even for nested artifacts" {
local dist="${FAKE_REPO}/dist"
_make_artifacts "${dist}" "deb/ci-tools_1.0.0_amd64.deb"
run "${SCRIPT}" "${dist}"
assert_success
run cat "${OUT_DIR}/checksums.txt"
assert_output --partial " ci-tools_1.0.0_amd64.deb"
refute_output --partial "deb/ci-tools"
}

@test "release-body.md wraps the checksum table in a markdown code block" {
local dist="${FAKE_REPO}/dist"
_make_artifacts "${dist}" "ci-tools_1.0.0_amd64.deb"
run "${SCRIPT}" "${dist}"
assert_success
run cat "${OUT_DIR}/release-body.md"
assert_output --partial "## SHA256 Checksums"
assert_output --partial '```'
assert_output --partial "ci-tools_1.0.0_amd64.deb"
}

@test "defaults to artifacts/release when no dist-dir is given" {
_make_artifacts "${OUT_DIR}" "ci-tools_1.0.0_amd64.deb"
run "${SCRIPT}"
assert_success
assert_file_exist "${OUT_DIR}/checksums.txt"
run cat "${OUT_DIR}/checksums.txt"
assert_output --partial "ci-tools_1.0.0_amd64.deb"
}

# ── argument errors ──────────────────────────────────────────────────

@test "exits 0 and prints usage for --help" {
run "${SCRIPT}" --help
assert_success
assert_output --partial "Usage:"
}

@test "exits 2 (distinct from runtime errors) when called with too many args" {
run "${SCRIPT}" one two
assert_failure 2
assert_output --partial "Usage:"
}

@test "exits 1 when the dist-dir does not exist" {
run "${SCRIPT}" "${FAKE_REPO}/no-such-dir"
assert_failure 1
assert_output --partial "Directory not found"
}

@test "exits nonzero when the dist-dir contains no release artifacts" {
local dist="${FAKE_REPO}/dist"
mkdir -p "${dist}"
run "${SCRIPT}" "${dist}"
assert_failure
}
148 changes: 148 additions & 0 deletions tests/bats/scripts/lib/resolve.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bats
# shellcheck shell=bash
#
# Unit tests for scripts/lib/resolve.sh helpers that do not touch
# the network. The upstream-fetching wrappers (latest_gh_tag,
# fetch_gh_asset, fetch_gh_digests, latest_npm_version,
# latest_luarocks_version) are integration-only and excluded.

load ../../helpers/common

# 64 hex chars, lowercase — the canonical valid SHA256 shape.
VALID_SHA="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

setup() {
common_setup
LIB="${REPO_ROOT}/scripts/lib/resolve.sh"
export LIB
}

# ── validate_sha256 ──────────────────────────────────────────────────

@test "validate_sha256 accepts a 64-char lowercase hex string" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "${VALID_SHA}" "shfmt"
assert_success
assert_output ""
}

@test "validate_sha256 rejects an empty hash and reports (empty)" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "" "shfmt"
assert_failure 1
assert_output --partial "invalid SHA256 for shfmt"
assert_output --partial "(empty)"
}

@test "validate_sha256 rejects uppercase hex" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "shfmt"
assert_failure 1
assert_output --partial "invalid SHA256 for shfmt"
}

@test "validate_sha256 rejects a too-short hash" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "deadbeef" "shfmt"
assert_failure 1
assert_output --partial "invalid SHA256 for shfmt"
}

@test "validate_sha256 rejects a too-long hash" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "${VALID_SHA}a" "shfmt"
assert_failure 1
assert_output --partial "invalid SHA256 for shfmt"
}

@test "validate_sha256 rejects non-hex characters" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" "shfmt"
assert_failure 1
assert_output --partial "invalid SHA256 for shfmt"
}

@test "validate_sha256 includes the tool name in the error message" {
# shellcheck disable=SC1090
source "${LIB}"
run validate_sha256 "nope" "markdownlint-cli2"
assert_failure 1
assert_output --partial "invalid SHA256 for markdownlint-cli2"
}

# ── resolve_local ────────────────────────────────────────────────────

@test "resolve_local returns the pinned override when provided" {
# shellcheck disable=SC1090
source "${LIB}"
run resolve_local "1.0.0" "2.0.0"
assert_success
assert_output "2.0.0"
}

@test "resolve_local returns the current value when no pin is given" {
# shellcheck disable=SC1090
source "${LIB}"
run resolve_local "1.0.0" ""
assert_success
assert_output "1.0.0"
}

@test "resolve_local returns the current value when called with one arg" {
# shellcheck disable=SC1090
source "${LIB}"
run resolve_local "1.0.0"
assert_success
assert_output "1.0.0"
}

@test "resolve_local defaults to 'local' when current is empty and no pin" {
# shellcheck disable=SC1090
source "${LIB}"
run resolve_local "" ""
assert_success
assert_output "local"
}

@test "resolve_local prefers a pinned override even when current is empty" {
# shellcheck disable=SC1090
source "${LIB}"
run resolve_local "" "3.0.0"
assert_success
assert_output "3.0.0"
}

# ── pick_gh_digest ───────────────────────────────────────────────────

@test "pick_gh_digest extracts the matching asset's hex digest" {
# shellcheck disable=SC1090
source "${LIB}"
local digests
digests="shfmt_v3.13.0_linux_amd64=${VALID_SHA}
shfmt_v3.13.0_linux_arm64=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
run pick_gh_digest "${digests}" "shfmt_v3.13.0_linux_amd64"
assert_success
assert_output "${VALID_SHA}"
}

@test "pick_gh_digest errors when the asset is missing from the digest list" {
# shellcheck disable=SC1090
source "${LIB}"
run pick_gh_digest "other_asset=${VALID_SHA}" "shfmt_v3.13.0_linux_amd64"
assert_failure 1
assert_output --partial "no digest found for asset shfmt_v3.13.0_linux_amd64"
}

@test "pick_gh_digest errors when the matched digest is malformed" {
# shellcheck disable=SC1090
source "${LIB}"
run pick_gh_digest "asset=not-a-real-hash" "asset"
assert_failure 1
assert_output --partial "invalid digest for asset"
}
Loading