Files
linux-scripts/backup-smoke-tests.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

516 lines
25 KiB
Bash

#!/usr/bin/env bash
#####################################################################################
#### backup-smoke-tests.sh — Verify backups are actually working ####
#### Checks existence, recency, size, integrity, snapshot count, locks, restore. ####
#### Supports: restic, borg, directory, rsnapshot ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version: 1.01 ####
#### ####
#### Usage: ####
#### export BACKUP_TYPE="restic" ####
#### export BACKUP_REPO="s3:s3.example.com/bucket" ####
#### ./backup-smoke-tests.sh ####
#### ####
#### See --help for all options. ####
#####################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
BACKUP_TYPE="${BACKUP_TYPE:-}"
BACKUP_REPO="${BACKUP_REPO:-}"
BACKUP_DIR="${BACKUP_DIR:-}"
MAX_AGE_HOURS="${MAX_AGE_HOURS:-26}"
MIN_SNAPSHOTS="${MIN_SNAPSHOTS:-1}"
MIN_SIZE_MB="${MIN_SIZE_MB:-1}"
RESTORE_TEST_FILE="${RESTORE_TEST_FILE:-}"
SKIP_RESTORE="${SKIP_RESTORE:-false}"
SKIP_INTEGRITY="${SKIP_INTEGRITY:-false}"
MOUNT_CHECK="${MOUNT_CHECK:-}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}" # text, tap, junit
JUNIT_FILE="${JUNIT_FILE:-backup-results.xml}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── State ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
TOTAL=0
RESULTS=()
RESTORE_TMP=""
START_TIME=""
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
# ── Test Result Recording ─────────────────────────────────────────────
record_pass() {
local name="$1" detail="${2:-}"
((PASS++)) || true; ((TOTAL++)) || true
RESULTS+=("PASS|${name}|${detail}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then echo "ok ${TOTAL} - ${name}"
else echo -e " ${GREEN}${RESET} ${name}${detail:+ — ${detail}}"; fi
}
record_fail() {
local name="$1" detail="${2:-}"
((FAIL++)) || true; ((TOTAL++)) || true
RESULTS+=("FAIL|${name}|${detail}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
echo "not ok ${TOTAL} - ${name}"
[[ -n "$detail" ]] && echo " # ${detail}"
else echo -e " ${RED}${RESET} ${name}${detail:+ — ${detail}}"; fi
}
record_skip() {
local name="$1" reason="${2:-}"
((SKIP++)) || true; ((TOTAL++)) || true
RESULTS+=("SKIP|${name}|${reason}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
else echo -e " ${YELLOW}${RESET} ${name}${reason:+ — ${reason}}"; fi
}
# ── Helpers ───────────────────────────────────────────────────────────
has_cmd() { command -v "$1" >/dev/null 2>&1; }
restore_ok() { find "${RESTORE_TMP}" -type f | grep -q .; }
require_tool() { if ! has_cmd "$1"; then record_skip "$2" "$1 not installed"; return 1; fi; }
# ── Cleanup ───────────────────────────────────────────────────────────
# shellcheck disable=SC2317
cleanup() { [[ -n "${RESTORE_TMP}" && -d "${RESTORE_TMP}" ]] && rm -rf "${RESTORE_TMP}"; }
trap cleanup EXIT
# ══════════════════════════════════════════════════════════════════════
# TEST SUITES
# ══════════════════════════════════════════════════════════════════════
# ── 1. Repository Health ─────────────────────────────────────────────
test_repo_health() {
echo ""
echo -e "${BOLD}Repository Health${RESET}"
# 1a. Mount check (if configured)
if [[ -n "${MOUNT_CHECK}" ]]; then
if mountpoint -q "${MOUNT_CHECK}" 2>/dev/null; then
record_pass "Mount check" "${MOUNT_CHECK} is mounted"
else
record_fail "Mount check" "${MOUNT_CHECK} is not mounted"
warn "Skipping remaining tests — mount not available"
return
fi
fi
# 1b. Backup exists
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Repository exists" || return
if restic cat config >/dev/null 2>&1; then record_pass "Repository exists"
else record_fail "Repository exists" "not accessible"; fi ;;
borg)
require_tool borg "Repository exists" || return
if borg info 2>/dev/null | grep -q "Repository ID"; then record_pass "Repository exists"
else record_fail "Repository exists" "not accessible"; fi ;;
directory|rsnapshot)
if [[ -d "${BACKUP_DIR}" ]]; then record_pass "Backup directory exists"
else record_fail "Backup directory exists" "${BACKUP_DIR} not found"; fi ;;
esac
# 1c. Repository reachable
case "${BACKUP_TYPE}" in
restic)
if [[ "${BACKUP_REPO}" =~ ^(s3|sftp|rest): ]]; then
require_tool restic "Repository reachable" || return
if restic cat config >/dev/null 2>&1; then record_pass "Repository reachable"
else record_fail "Repository reachable" "remote repository unreachable"; fi
else record_pass "Repository reachable" "local"; fi ;;
borg)
if [[ "${BACKUP_REPO}" =~ ^ssh:// || "${BACKUP_REPO}" =~ .*@.*:.* ]]; then
require_tool borg "Repository reachable" || return
if borg info >/dev/null 2>&1; then record_pass "Repository reachable"
else record_fail "Repository reachable" "remote repository unreachable"; fi
else record_pass "Repository reachable" "local"; fi ;;
directory|rsnapshot)
if [[ -r "${BACKUP_DIR}" ]]; then record_pass "Backup directory reachable"
else record_fail "Backup directory reachable" "${BACKUP_DIR} not readable"; fi ;;
esac
}
# ── 2. Backup Status ─────────────────────────────────────────────────
test_backup_status() {
echo ""
echo -e "${BOLD}Backup Status${RESET}"
# 2a. Recent backup
local last_ts="" max_age_s=$((MAX_AGE_HOURS * 3600))
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Recent backup" || { test_size; test_snapshot_count; return; }
local latest
latest=$(restic snapshots --json --latest 1 2>/dev/null) || true
if [[ -z "${latest}" || "${latest}" == "[]" || "${latest}" == "null" ]]; then
record_fail "Recent backup" "no snapshots found"
else
local time_str
time_str=$(echo "${latest}" | grep -oP '"time"\s*:\s*"\K[^"]+' | head -1)
if [[ -z "${time_str}" ]]; then record_fail "Recent backup" "could not parse snapshot time"
else last_ts=$(date -d "${time_str}" +%s 2>/dev/null) || true; fi
fi ;;
borg)
require_tool borg "Recent backup" || { test_size; test_snapshot_count; return; }
local borg_time
borg_time=$(borg list --format '{time}{NL}' 2>/dev/null | tail -1) || true
if [[ -z "${borg_time}" ]]; then record_fail "Recent backup" "no archives found"
else last_ts=$(date -d "${borg_time}" +%s 2>/dev/null) || true; fi ;;
directory|rsnapshot)
local newest
newest=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -type d -printf '%T@\n' 2>/dev/null | sort -rn | head -1)
[[ -z "${newest}" ]] && newest=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -printf '%T@\n' 2>/dev/null | sort -rn | head -1)
if [[ -z "${newest}" ]]; then record_fail "Recent backup" "no backups found in ${BACKUP_DIR}"
else last_ts="${newest%%.*}"; fi ;;
esac
if [[ -n "${last_ts}" ]]; then
local now_ts age_s age_h
now_ts=$(date +%s); age_s=$((now_ts - last_ts)); age_h=$((age_s / 3600))
if [[ ${age_s} -le ${max_age_s} ]]; then record_pass "Recent backup" "${age_h}h ago (max ${MAX_AGE_HOURS}h)"
else record_fail "Recent backup" "${age_h}h ago (max ${MAX_AGE_HOURS}h)"; fi
fi
test_size
test_snapshot_count
}
test_size() {
local size_mb=0
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Backup size" || return
local stats total_bytes
stats=$(restic stats --json --mode raw-data 2>/dev/null) || true
total_bytes=$(echo "${stats}" | grep -oP '"total_size"\s*:\s*\K[0-9]+' | head -1) || true
[[ -n "${total_bytes}" ]] && size_mb=$((total_bytes / 1048576)) ;;
borg)
require_tool borg "Backup size" || return
local size_str num unit
size_str=$(borg info 2>/dev/null | grep -i "all archives" | grep -oP '[0-9.]+\s*(TB|GB|MB|kB)' | head -1) || true
if [[ -n "${size_str}" ]]; then
num=$(echo "${size_str}" | grep -oP '[0-9.]+'); unit=$(echo "${size_str}" | grep -oP '[A-Za-z]+')
case "${unit}" in
TB) size_mb=$(echo "${num} * 1048576" | bc 2>/dev/null | cut -d. -f1) || size_mb=999999 ;;
GB) size_mb=$(echo "${num} * 1024" | bc 2>/dev/null | cut -d. -f1) || size_mb=999999 ;;
MB) size_mb=$(echo "${num}" | cut -d. -f1) ;;
kB) size_mb=0 ;;
esac
fi ;;
directory|rsnapshot)
size_mb=$(du -sm "${BACKUP_DIR}" 2>/dev/null | awk '{print $1}') || size_mb=0 ;;
esac
if [[ ${size_mb} -ge ${MIN_SIZE_MB} ]]; then record_pass "Backup size" "${size_mb} MB (min ${MIN_SIZE_MB} MB)"
else record_fail "Backup size" "${size_mb} MB < ${MIN_SIZE_MB} MB"; fi
}
test_snapshot_count() {
local count=0
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Snapshot count" || return
count=$(restic snapshots --json 2>/dev/null | grep -c '"time"') || count=0 ;;
borg)
require_tool borg "Snapshot count" || return
count=$(borg list 2>/dev/null | wc -l) || count=0 ;;
directory)
count=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 | wc -l) || count=0 ;;
rsnapshot)
count=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -type d | wc -l) || count=0 ;;
esac
if [[ ${count} -ge ${MIN_SNAPSHOTS} ]]; then record_pass "Snapshot count" "${count} (min ${MIN_SNAPSHOTS})"
else record_fail "Snapshot count" "${count} < ${MIN_SNAPSHOTS}"; fi
}
# ── 3. Integrity ─────────────────────────────────────────────────────
test_integrity_suite() {
echo ""
echo -e "${BOLD}Integrity${RESET}"
# 3a. Integrity check
if [[ "${SKIP_INTEGRITY}" == "true" ]]; then
record_skip "Integrity check" "SKIP_INTEGRITY=true"
else
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Integrity check" || return
if restic check 2>/dev/null; then record_pass "Integrity check"
else record_fail "Integrity check" "restic check failed"; fi ;;
borg)
require_tool borg "Integrity check" || return
if borg check 2>/dev/null; then record_pass "Integrity check"
else record_fail "Integrity check" "borg check failed"; fi ;;
directory|rsnapshot)
record_skip "Integrity check" "not applicable for ${BACKUP_TYPE}" ;;
esac
fi
# 3b. Lock check
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Lock check" || return
local lock_output
lock_output=$(restic list locks 2>/dev/null) || true
if [[ -z "${lock_output}" ]]; then record_pass "Lock check" "no stale locks"
else record_fail "Lock check" "$(echo "${lock_output}" | wc -l) lock(s) found"; fi ;;
borg)
require_tool borg "Lock check" || return
if borg info 2>&1 | grep -qi "lock"; then record_fail "Lock check" "repository appears locked"
else record_pass "Lock check" "no stale locks"; fi ;;
directory|rsnapshot)
local lc
lc=$(find "${BACKUP_DIR}" -maxdepth 1 \( -name "*.lock" -o -name ".lock" \) 2>/dev/null | wc -l) || lc=0
if [[ ${lc} -eq 0 ]]; then record_pass "Lock check" "no stale locks"
else record_fail "Lock check" "${lc} lock file(s) in ${BACKUP_DIR}"; fi ;;
esac
}
# ── 4. Recovery ──────────────────────────────────────────────────────
test_recovery() {
echo ""
echo -e "${BOLD}Recovery${RESET}"
if [[ "${SKIP_RESTORE}" == "true" ]]; then record_skip "Test restore" "SKIP_RESTORE=true"; return; fi
RESTORE_TMP=$(mktemp -d /tmp/backup-smoke-test-XXXXXX)
case "${BACKUP_TYPE}" in
restic)
require_tool restic "Test restore" || return
restic_restore "${RESTORE_TEST_FILE}" ;;
borg)
require_tool borg "Test restore" || return
borg_restore "${RESTORE_TEST_FILE}" ;;
directory)
dir_restore "${RESTORE_TEST_FILE:+${BACKUP_DIR}/${RESTORE_TEST_FILE}}" ;;
rsnapshot)
dir_restore "${RESTORE_TEST_FILE:+${BACKUP_DIR}/${RESTORE_TEST_FILE}}" ;;
esac
}
restic_restore() {
local target="${1:-}"
if [[ -z "${target}" ]]; then
target=$(restic ls latest 2>/dev/null | head -1) || true
[[ -z "${target}" ]] && { record_skip "Test restore" "no files in latest snapshot"; return; }
fi
if restic restore latest --target "${RESTORE_TMP}" --include "${target}" 2>/dev/null && restore_ok; then
record_pass "Test restore" "file restored successfully"
else record_fail "Test restore" "restic restore failed"; fi
}
borg_restore() {
local archive target
archive=$(borg list --format '{archive}{NL}' 2>/dev/null | tail -1) || true
[[ -z "${archive}" ]] && { record_skip "Test restore" "no archives found"; return; }
target="${1:-}"
if [[ -z "${target}" ]]; then
target=$(borg list "::${archive}" --format '{path}{NL}' 2>/dev/null | grep -v '/$' | head -1) || true
[[ -z "${target}" ]] && { record_skip "Test restore" "no files in latest archive"; return; }
fi
if (cd "${RESTORE_TMP}" && borg extract "::${archive}" "${target}" 2>/dev/null) && restore_ok; then
record_pass "Test restore" "file restored successfully"
else record_fail "Test restore" "borg extract failed"; fi
}
dir_restore() {
local src_file="${1:-}"
[[ -z "${src_file}" ]] && src_file=$(find "${BACKUP_DIR}" -type f 2>/dev/null | head -1)
[[ -z "${src_file}" || ! -f "${src_file}" ]] && { record_skip "Test restore" "no files in backup directory"; return; }
local dest_file
dest_file="${RESTORE_TMP}/$(basename "${src_file}")"
if cp "${src_file}" "${dest_file}" 2>/dev/null && [[ -f "${dest_file}" ]]; then
record_pass "Test restore" "file copied successfully"
else record_fail "Test restore" "copy failed"; fi
}
# ══════════════════════════════════════════════════════════════════════
# OUTPUT
# ══════════════════════════════════════════════════════════════════════
print_summary() {
local end_time; end_time=$(date +%s)
local duration=$(( end_time - START_TIME ))
echo ""
echo -e "${BOLD}────────────────────────────────────────${RESET}"
echo -e "${BOLD}Summary${RESET} ${BACKUP_TYPE} ${BACKUP_REPO:-${BACKUP_DIR}}"
echo -e " ${GREEN}${PASS} passed${RESET} ${RED}${FAIL} failed${RESET} ${YELLOW}${SKIP} skipped${RESET} (${duration}s)"
echo -e "${BOLD}────────────────────────────────────────${RESET}"
if [[ $FAIL -eq 0 ]]; then echo -e "${GREEN}${BOLD}All tests passed.${RESET}"
else echo -e "${RED}${BOLD}${FAIL} test(s) failed.${RESET}"; fi
}
print_tap_header() {
echo "TAP version 13"
}
print_tap_footer() {
echo "1..${TOTAL}"
echo "# pass ${PASS}"
echo "# fail ${FAIL}"
echo "# skip ${SKIP}"
}
write_junit() {
local end_time; end_time=$(date +%s)
local duration=$(( end_time - START_TIME ))
cat > "$JUNIT_FILE" <<JUNIT_EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
<testsuite name="backup-smoke-tests" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
JUNIT_EOF
for result in "${RESULTS[@]}"; do
local status name detail
IFS='|' read -r status name detail <<< "$result"
name=$(echo "$name" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
detail=$(echo "$detail" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
echo " <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
case "$status" in
PASS) [[ -n "$detail" ]] && echo " <system-out>${detail}</system-out>" >> "$JUNIT_FILE" ;;
FAIL) echo " <failure message=\"${detail}\">FAILED: ${name}${detail}</failure>" >> "$JUNIT_FILE" ;;
SKIP) echo " <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE" ;;
esac
echo " </testcase>" >> "$JUNIT_FILE"
done
echo " </testsuite>" >> "$JUNIT_FILE"
echo "</testsuites>" >> "$JUNIT_FILE"
log "JUnit report written to ${JUNIT_FILE}"
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Smoke-test backup repositories. Supports restic, borg, directory, and rsnapshot.
Required environment variables:
BACKUP_TYPE Backend type: restic, borg, directory, rsnapshot
BACKUP_REPO Repository path/URL (restic, borg)
BACKUP_DIR Directory path (directory, rsnapshot)
Optional environment variables:
MAX_AGE_HOURS Max age of latest backup in hours (default: 26)
MIN_SNAPSHOTS Minimum snapshot count (default: 1)
MIN_SIZE_MB Minimum backup size in MB (default: 1)
RESTORE_TEST_FILE Specific file to restore (default: auto-detect)
MOUNT_CHECK Mountpoint to verify before testing (e.g. /mnt/backup)
RESTIC_PASSWORD Restic repository password
BORG_PASSPHRASE Borg repository passphrase
Options:
--skip-restore Skip the test-restore check
--skip-integrity Skip the integrity check (can be slow)
--format FORMAT Output: text (default), tap, junit
--junit-file FILE JUnit output path (default: backup-results.xml)
--verbose Show debug output
--no-color Disable colored output
--help Show this help
Examples:
BACKUP_TYPE=restic BACKUP_REPO=s3:s3.example.com/bucket ./$(basename "$0")
BACKUP_TYPE=borg BACKUP_REPO=/mnt/backup ./$(basename "$0") --format junit
BACKUP_TYPE=directory BACKUP_DIR=/backup/daily ./$(basename "$0") --skip-restore
BACKUP_TYPE=directory BACKUP_DIR=/mnt/s3/backups MOUNT_CHECK=/mnt/s3 ./$(basename "$0")
BACKUP_TYPE=restic BACKUP_REPO=/srv/restic ./$(basename "$0") --format tap
EOF
}
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-restore) SKIP_RESTORE=true ;;
--skip-integrity) SKIP_INTEGRITY=true ;;
--format) OUTPUT_FORMAT="$2"; shift ;;
--junit-file) JUNIT_FILE="$2"; shift ;;
--verbose) VERBOSE=true ;;
--no-color) COLOR=never ;;
--help|-h) usage; exit 0 ;;
*) err "Unknown option: $1"; usage; exit 1 ;;
esac
shift
done
setup_colors
# Validate required vars
if [[ -z "${BACKUP_TYPE}" ]]; then
err "BACKUP_TYPE is required (restic, borg, directory, rsnapshot)"; echo ""; usage; exit 1
fi
case "${BACKUP_TYPE}" in
restic|borg)
[[ -z "${BACKUP_REPO}" ]] && { err "BACKUP_REPO is required for ${BACKUP_TYPE}"; echo ""; usage; exit 1; } ;;
directory|rsnapshot)
[[ -z "${BACKUP_DIR}" ]] && { err "BACKUP_DIR is required for ${BACKUP_TYPE}"; echo ""; usage; exit 1; } ;;
*) err "Unsupported BACKUP_TYPE '${BACKUP_TYPE}'"; echo ""; usage; exit 1 ;;
esac
# Export backend variables
if [[ "${BACKUP_TYPE}" == "restic" ]]; then
export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-${BACKUP_REPO}}"
[[ -n "${RESTIC_PASSWORD:-}" ]] && export RESTIC_PASSWORD
[[ -n "${RESTIC_PASSWORD_FILE:-}" ]] && export RESTIC_PASSWORD_FILE
[[ -n "${AWS_ACCESS_KEY_ID:-}" ]] && export AWS_ACCESS_KEY_ID
[[ -n "${AWS_SECRET_ACCESS_KEY:-}" ]] && export AWS_SECRET_ACCESS_KEY
fi
if [[ "${BACKUP_TYPE}" == "borg" ]]; then
export BORG_REPO="${BACKUP_REPO}"
[[ -n "${BORG_PASSPHRASE:-}" ]] && export BORG_PASSPHRASE
[[ -n "${BORG_PASSCOMMAND:-}" ]] && export BORG_PASSCOMMAND
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
fi
START_TIME=$(date +%s)
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
print_tap_header
else
echo ""
echo -e "${BOLD}Backup Smoke Tests${RESET}"
echo -e "Backend: ${BACKUP_TYPE} Target: ${BACKUP_REPO:-${BACKUP_DIR}}"
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
fi
test_repo_health
test_backup_status
test_integrity_suite
test_recovery
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
print_tap_footer
elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
print_summary
write_junit
else
print_summary
fi
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
}
main "$@"