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
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ jobs:

echo "✓ All scripts have proper shebangs"

# tests/lib/common.sh is sourced by test-negative.sh and
# test-performance.sh; this checks it and runs both scripts with no
# stack, where every service test reports itself as skipped.
- name: Test the shared shell test library
run: bash tests/test-common-lib.sh

validate-configs:
name: Validate Configuration Files
runs-on: ubuntu-latest
Expand Down Expand Up @@ -287,6 +293,9 @@ jobs:
echo "ℹ No Python files to validate"
fi

- name: Test scripts/add-profile-labels.py
run: bash tests/test-add-profile-labels.sh

unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
Expand Down
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ dist/
downloads/
eggs/
.eggs/
lib/
!scripts/lib/
lib64/
# Python build output at the top level only; scripts/lib/ and tests/lib/
# hold tracked shell libraries.
/lib/
/lib64/
parts/
sdist/
var/
Expand Down
3 changes: 2 additions & 1 deletion scripts/add-profile-labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""

import re
import sys
from pathlib import Path

# Profile assignments for each service
Expand Down Expand Up @@ -195,4 +196,4 @@ def main():


if __name__ == "__main__":
exit(main())
sys.exit(main())
105 changes: 105 additions & 0 deletions tests/lib/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/bin/bash
################################################################################
# Shared helpers for tests/test-negative.sh and tests/test-performance.sh
################################################################################
# Sourced, not executed. The calling script declares the counters
# TESTS_RUN, TESTS_PASSED, TESTS_FAILED and the array FAILED_TESTS; each test
# function increments TESTS_RUN itself and reports through success or fail.
#
# Functions:
# info, success, warn, fail - print a colored line; success and fail
# also update the counters
# is_container_running NAME - 0 if a container with that exact name runs
# get_password_from_vault SVC - print secret/SVC's password from Vault
# test_suite_setup NAME - print the suite name and record the start
# print_test_results NAME - print the summary; 1 if any test failed
# test_suite_teardown - print the elapsed time
#
# Tests in tests/test-common-lib.sh.
################################################################################

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

info() { echo -e "${BLUE}[TEST]${NC} $1"; }

success() {
echo -e "${GREEN}[PASS]${NC} $1"
TESTS_PASSED=$((TESTS_PASSED + 1))
}

warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }

fail() {
echo -e "${RED}[FAIL]${NC} $1"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$1")
}

# Returns 0 if a running container has exactly this name. Without docker, or
# with the daemon unreachable, nothing is running.
is_container_running() {
local name=$1
docker ps --format '{{.Names}}' 2> /dev/null | grep -qx -- "$name"
}

# Prints the password stored at secret/<service> in Vault (KV v2). Uses
# VAULT_ADDR (default http://localhost:8200) and VAULT_TOKEN, or the token in
# ~/.config/vault/root-token. Returns 1 and prints nothing when there is no
# token, Vault does not answer, or the secret has no password.
get_password_from_vault() {
local service=$1
local addr=${VAULT_ADDR:-http://localhost:8200}
local token=${VAULT_TOKEN:-}
if [ -z "$token" ]; then
token=$(cat "$HOME/.config/vault/root-token" 2> /dev/null) || token=""
fi
[ -n "$token" ] || return 1

local password
password=$(curl -sf --max-time 5 -H "X-Vault-Token: $token" \
"$addr/v1/secret/data/$service" 2> /dev/null |
jq -r '.data.data.password // empty' 2> /dev/null) || password=""
[ -n "$password" ] || return 1
printf '%s\n' "$password"
}

test_suite_setup() {
TEST_SUITE_NAME=${1:-Test}
TEST_SUITE_START=$(date +%s)
info "Starting $TEST_SUITE_NAME test suite"
echo
}

# Prints the counters and the names of failed tests. Returns 1 if any test
# failed, so the calling script exits non-zero under set -e.
print_test_results() {
local name=${1:-Test}
echo
echo "========================================="
echo " $name Test Results"
echo "========================================="
echo "Tests run: $TESTS_RUN"
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
if [ "$TESTS_FAILED" -gt 0 ]; then
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
local t
for t in "${FAILED_TESTS[@]}"; do
echo -e " ${RED}✗${NC} $t"
done
echo
return 1
fi
echo
echo -e "${GREEN}All $name tests passed${NC}"
echo
return 0
}

test_suite_teardown() {
local start=${TEST_SUITE_START:-$(date +%s)}
info "${TEST_SUITE_NAME:-Test} test suite finished in $(($(date +%s) - start))s"
}
31 changes: 31 additions & 0 deletions tests/test-add-profile-labels.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
################################################################################
# Tests for scripts/add-profile-labels.py
################################################################################
# Runs a copy of the script in a temporary tree that has no
# docker-compose.yml, so main() returns 1 without touching any file. The run
# uses `python3 -S`, which skips the site module: the exit status must still
# come from main(), not from a NameError on the site-provided exit().
#
# Usage: ./tests/test-add-profile-labels.sh
################################################################################

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/scripts"
cp "$SCRIPT_DIR/../scripts/add-profile-labels.py" "$WORK/scripts/"

rc=0
out=$(python3 -S "$WORK/scripts/add-profile-labels.py" 2>&1) || rc=$?

if [ "$rc" -eq 1 ] && printf '%s\n' "$out" | grep -q "not found" &&
! printf '%s\n' "$out" | grep -q "NameError"; then
echo "PASS: exits 1 from main() when docker-compose.yml is missing, without the site module"
else
echo "FAIL: exits 1 from main() when docker-compose.yml is missing, without the site module (exit $rc)"
printf '%s\n' "$out" | tail -3
exit 1
fi
148 changes: 148 additions & 0 deletions tests/test-common-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/bin/bash
################################################################################
# Tests for tests/lib/common.sh
################################################################################
# Checks that the shared test library is tracked by git, that it defines the
# helpers tests/test-negative.sh and tests/test-performance.sh call, and that
# both scripts run to completion when no DevStack container is running (every
# service test then reports itself as skipped). Needs no services: a stub
# `docker` on PATH lists no containers, and HOME points at an empty directory
# so no Vault token is found.
#
# Usage: ./tests/test-common-lib.sh
#
# Exit Codes:
# 0 - All tests passed
# 1 - One or more tests failed
################################################################################

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
LIB="$SCRIPT_DIR/lib/common.sh"

RUN=0
FAILURES=0

pass() {
RUN=$((RUN + 1))
echo "PASS: $1"
}

failed() {
RUN=$((RUN + 1))
FAILURES=$((FAILURES + 1))
echo "FAIL: $1"
}

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/bin" "$WORK/home"
printf '#!/bin/sh\nexit 0\n' > "$WORK/bin/docker"
chmod +x "$WORK/bin/docker"

# 1. The library is not excluded by .gitignore.
if ! git -C "$REPO_ROOT" check-ignore -q tests/lib/common.sh; then
pass ".gitignore does not exclude tests/lib/common.sh"
else
failed ".gitignore does not exclude tests/lib/common.sh"
fi

# 2. The library exists and defines every helper the two scripts call.
if [ -f "$LIB" ]; then
pass "tests/lib/common.sh exists"
else
failed "tests/lib/common.sh exists"
fi

missing=$(
# shellcheck source=/dev/null
source "$LIB" > /dev/null 2>&1 || exit 0
for fn in info success warn fail is_container_running get_password_from_vault \
print_test_results test_suite_setup test_suite_teardown; do
declare -F "$fn" > /dev/null || printf '%s ' "$fn"
done
)
if [ -f "$LIB" ] && [ -z "$missing" ]; then
pass "defines all nine helpers"
else
failed "defines all nine helpers (missing: ${missing:-library not found})"
fi

# 3. success and fail update the counters the scripts declare.
counts=$(
TESTS_PASSED=0
TESTS_FAILED=0
FAILED_TESTS=()
# shellcheck source=/dev/null
source "$LIB" > /dev/null 2>&1 || exit 0
success "a" > /dev/null
success "b" > /dev/null
fail "c went wrong" > /dev/null
echo "$TESTS_PASSED $TESTS_FAILED ${FAILED_TESTS[*]}"
)
if [ "$counts" = "2 1 c went wrong" ]; then
pass "success and fail update TESTS_PASSED, TESTS_FAILED and FAILED_TESTS"
else
failed "success and fail update TESTS_PASSED, TESTS_FAILED and FAILED_TESTS (got '$counts')"
fi

# 4. print_test_results exits 0 without failures and 1 with them, and names
# the failed tests.
results=$(
# shellcheck source=/dev/null
source "$LIB" > /dev/null 2>&1 || exit 0
TESTS_RUN=2 TESTS_PASSED=2 TESTS_FAILED=0 FAILED_TESTS=()
print_test_results "Demo" > /dev/null
clean=$?
# shellcheck disable=SC2034 # read by print_test_results
TESTS_RUN=2 TESTS_PASSED=1 TESTS_FAILED=1 FAILED_TESTS=("broken thing")
out=$(print_test_results "Demo")
dirty=$?
printf '%s %s %s' "$clean" "$dirty" "$(printf '%s' "$out" | grep -c "broken thing")"
)
if [ "$results" = "0 1 1" ]; then
pass "print_test_results returns 0 or 1 and lists failed tests"
else
failed "print_test_results returns 0 or 1 and lists failed tests (got '$results')"
fi

# 5. Container and Vault helpers fail cleanly when nothing is running.
helpers=$(
export PATH="$WORK/bin:$PATH" HOME="$WORK/home"
unset VAULT_TOKEN
# shellcheck source=/dev/null
source "$LIB" > /dev/null 2>&1 || exit 0
is_container_running dev-postgres && echo "running" || echo "not-running"
pw=$(get_password_from_vault postgres 2> /dev/null) && echo "got:$pw" || echo "no-password:${pw}"
)
if [ "$helpers" = "not-running
no-password:" ]; then
pass "is_container_running and get_password_from_vault fail with no stack"
else
failed "is_container_running and get_password_from_vault fail with no stack (got '$helpers')"
fi

# 6. Both scripts parse and run every test to completion with no stack.
for entry in test-negative.sh:12 test-performance.sh:10; do
script=${entry%%:*}
count=${entry##*:}
if ! bash -n "$SCRIPT_DIR/$script"; then
failed "$script passes bash -n"
continue
fi
rc=0
out=$(PATH="$WORK/bin:$PATH" HOME="$WORK/home" VAULT_TOKEN='' \
bash "$SCRIPT_DIR/$script" 2>&1) || rc=$?
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -q "Tests run: $count\$"; then
pass "$script runs all $count tests and exits 0 with no stack"
else
failed "$script runs all $count tests and exits 0 with no stack (exit $rc)"
printf '%s\n' "$out" | tail -5
fi
done

echo
echo "Tests run: $RUN, failed: $FAILURES"
[ "$FAILURES" -eq 0 ]
8 changes: 4 additions & 4 deletions tests/test-negative.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # listed in the header; nothing reads it yet
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"

# Load common test library
Expand All @@ -120,11 +120,11 @@ run_python() {

# Test counters
TESTS_RUN=0
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # read by tests/lib/common.sh, sourced above
TESTS_PASSED=0
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # read by tests/lib/common.sh, sourced above
TESTS_FAILED=0
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # read by tests/lib/common.sh, sourced above
declare -a FAILED_TESTS=()

################################################################################
Expand Down
10 changes: 5 additions & 5 deletions tests/test-performance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # listed in the header; nothing reads it yet
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"

# Load common test library
Expand All @@ -109,18 +109,18 @@ run_python() {

# Test counters
TESTS_RUN=0
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # read by tests/lib/common.sh, sourced above
TESTS_PASSED=0
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # read by tests/lib/common.sh, sourced above
TESTS_FAILED=0
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # read by tests/lib/common.sh, sourced above
declare -a FAILED_TESTS=()

# Performance thresholds (milliseconds)
VAULT_THRESHOLD=200
API_THRESHOLD=500
DB_QUERY_THRESHOLD=1000
# shellcheck disable=SC2034 # for tests/lib/common.sh, sourced above; .gitignore excludes it from the repository
# shellcheck disable=SC2034 # listed in the header; nothing reads it yet
DB_CONNECTION_THRESHOLD=2000

################################################################################
Expand Down
Loading