a1a17e81a1
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.
533 lines
20 KiB
Bash
533 lines
20 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
############################################################################################
|
|
#### logrotate-smoke-tests.sh — Verify logrotate configs, rotation, and log hygiene ####
|
|
#### Zero external dependencies. Read-only — never modifies configs or log files. ####
|
|
#### Requires: bash 4+, coreutils ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.01 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### sudo ./logrotate-smoke-tests.sh ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
############################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
LOG_DIR="${LOG_DIR:-/var/log}"
|
|
MAX_LOG_SIZE_MB="${MAX_LOG_SIZE_MB:-500}"
|
|
MAX_DISK_USAGE_PCT="${MAX_DISK_USAGE_PCT:-80}"
|
|
MAX_ARCHIVE_AGE_DAYS="${MAX_ARCHIVE_AGE_DAYS:-90}"
|
|
SKIP_SYNTAX="${SKIP_SYNTAX:-false}"
|
|
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}" # text, tap, junit
|
|
JUNIT_FILE="${JUNIT_FILE:-smoke-results.xml}"
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
PASS=0
|
|
FAIL=0
|
|
SKIP=0
|
|
TOTAL=0
|
|
RESULTS=()
|
|
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"
|
|
local 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"
|
|
local 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"
|
|
local 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
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# TEST SUITES
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
# ── 1. Binary & Config ───────────────────────────────────────────────
|
|
test_binary_and_config() {
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Binary & Config${RESET}"
|
|
fi
|
|
|
|
# 1. logrotate binary exists and is executable
|
|
local lr_path
|
|
lr_path=$(command -v logrotate 2>/dev/null) || lr_path=""
|
|
if [[ -n "$lr_path" && -x "$lr_path" ]]; then
|
|
record_pass "logrotate binary exists" "$lr_path"
|
|
else
|
|
record_fail "logrotate binary exists" "logrotate not found in PATH"
|
|
fi
|
|
|
|
# 2. /etc/logrotate.conf exists and is readable
|
|
if [[ -r /etc/logrotate.conf ]]; then
|
|
record_pass "/etc/logrotate.conf exists and is readable"
|
|
else
|
|
record_fail "/etc/logrotate.conf exists and is readable" "file missing or not readable"
|
|
fi
|
|
|
|
# 4. /etc/logrotate.d/ directory exists with configs
|
|
if [[ -d /etc/logrotate.d ]]; then
|
|
local conf_count
|
|
conf_count=$(find /etc/logrotate.d -maxdepth 1 -type f 2>/dev/null | wc -l)
|
|
if [[ "$conf_count" -gt 0 ]]; then
|
|
record_pass "/etc/logrotate.d/ directory exists" "${conf_count} config files"
|
|
else
|
|
record_fail "/etc/logrotate.d/ directory exists" "directory exists but contains no config files"
|
|
fi
|
|
else
|
|
record_fail "/etc/logrotate.d/ directory exists" "directory missing"
|
|
fi
|
|
}
|
|
|
|
# ── 2. Config Syntax ─────────────────────────────────────────────────
|
|
test_config_syntax() {
|
|
if [[ "$SKIP_SYNTAX" == "true" ]]; then
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Config Syntax${RESET}"
|
|
fi
|
|
record_skip "logrotate.conf syntax valid" "SKIP_SYNTAX=true"
|
|
return
|
|
fi
|
|
|
|
local lr_path
|
|
lr_path=$(command -v logrotate 2>/dev/null) || lr_path=""
|
|
if [[ -z "$lr_path" ]]; then
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Config Syntax${RESET}"
|
|
fi
|
|
record_skip "logrotate.conf syntax valid" "logrotate binary not found"
|
|
return
|
|
fi
|
|
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Config Syntax${RESET}"
|
|
fi
|
|
|
|
# 3. logrotate config syntax valid
|
|
local syntax_output
|
|
if syntax_output=$(logrotate -d /etc/logrotate.conf 2>&1); then
|
|
record_pass "logrotate.conf syntax valid"
|
|
verbose "logrotate -d output: ${syntax_output:0:200}"
|
|
else
|
|
record_fail "logrotate.conf syntax valid" "logrotate -d returned errors"
|
|
verbose "logrotate -d output: ${syntax_output:0:500}"
|
|
fi
|
|
|
|
# 5. Individual config syntax check
|
|
if [[ -d /etc/logrotate.d ]]; then
|
|
local cfg
|
|
for cfg in /etc/logrotate.d/*; do
|
|
[[ -f "$cfg" ]] || continue
|
|
local cfg_name
|
|
cfg_name=$(basename "$cfg")
|
|
|
|
local cfg_output
|
|
if cfg_output=$(logrotate -d "$cfg" 2>&1); then
|
|
record_pass "logrotate.d/${cfg_name}" "syntax valid"
|
|
else
|
|
record_fail "logrotate.d/${cfg_name}" "syntax error"
|
|
verbose "logrotate -d ${cfg} output: ${cfg_output:0:500}"
|
|
fi
|
|
done
|
|
fi
|
|
}
|
|
|
|
# ── 3. Log Hygiene ───────────────────────────────────────────────────
|
|
test_log_hygiene() {
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Log Hygiene${RESET}"
|
|
fi
|
|
|
|
# 6. Compressed log archives exist where expected
|
|
local archive_count
|
|
archive_count=$(find "$LOG_DIR" -maxdepth 2 -type f \( -name '*.gz' -o -name '*.xz' \) 2>/dev/null | wc -l)
|
|
if [[ "$archive_count" -gt 0 ]]; then
|
|
record_pass "Compressed log archives exist" "${archive_count} archives found"
|
|
else
|
|
record_fail "Compressed log archives exist" "no .gz or .xz files found in ${LOG_DIR}"
|
|
fi
|
|
|
|
# 7. No oversized unrotated logs
|
|
local oversized
|
|
oversized=$(find "$LOG_DIR" -maxdepth 2 -type f -not -name '*.gz' -not -name '*.xz' \
|
|
-size +"${MAX_LOG_SIZE_MB}M" 2>/dev/null) || oversized=""
|
|
if [[ -z "$oversized" ]]; then
|
|
record_pass "No oversized unrotated logs" "all under ${MAX_LOG_SIZE_MB} MB"
|
|
else
|
|
local oversized_count
|
|
oversized_count=$(echo "$oversized" | wc -l)
|
|
local first_file
|
|
first_file=$(echo "$oversized" | head -1)
|
|
record_fail "No oversized unrotated logs" "${oversized_count} file(s) exceed ${MAX_LOG_SIZE_MB} MB (e.g. ${first_file})"
|
|
fi
|
|
|
|
# 8. Log directory permissions
|
|
if [[ -d "$LOG_DIR" ]]; then
|
|
local dir_owner
|
|
dir_owner=$(stat -c '%U' "$LOG_DIR" 2>/dev/null) || dir_owner="unknown"
|
|
local dir_perms
|
|
dir_perms=$(stat -c '%a' "$LOG_DIR" 2>/dev/null) || dir_perms="000"
|
|
|
|
local world_writable=false
|
|
if [[ "${dir_perms: -1}" =~ [2367] ]]; then
|
|
world_writable=true
|
|
fi
|
|
|
|
if [[ "$dir_owner" == "root" && "$world_writable" == "false" ]]; then
|
|
record_pass "${LOG_DIR} permissions" "owner ${dir_owner}, mode ${dir_perms}"
|
|
elif [[ "$dir_owner" != "root" ]]; then
|
|
record_fail "${LOG_DIR} permissions" "owner is ${dir_owner}, expected root"
|
|
else
|
|
record_fail "${LOG_DIR} permissions" "world-writable (mode ${dir_perms})"
|
|
fi
|
|
else
|
|
record_fail "${LOG_DIR} permissions" "directory does not exist"
|
|
fi
|
|
}
|
|
|
|
# ── 4. Disk & Retention ──────────────────────────────────────────────
|
|
test_disk_and_retention() {
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Disk & Retention${RESET}"
|
|
fi
|
|
|
|
# 9. /var/log disk usage under threshold
|
|
local usage_pct
|
|
usage_pct=$(df "$LOG_DIR" 2>/dev/null | awk 'NR==2 {gsub(/%/,""); print $5}') || usage_pct=""
|
|
if [[ -n "$usage_pct" ]]; then
|
|
if [[ "$usage_pct" -lt "$MAX_DISK_USAGE_PCT" ]]; then
|
|
record_pass "${LOG_DIR} disk usage" "${usage_pct}% (under ${MAX_DISK_USAGE_PCT}% threshold)"
|
|
else
|
|
record_fail "${LOG_DIR} disk usage" "${usage_pct}% (exceeds ${MAX_DISK_USAGE_PCT}% threshold)"
|
|
fi
|
|
else
|
|
record_skip "${LOG_DIR} disk usage" "could not determine disk usage"
|
|
fi
|
|
|
|
# 10. Stale .gz/.xz files older than retention threshold
|
|
local stale_count
|
|
stale_count=$(find "$LOG_DIR" -maxdepth 2 -type f \( -name '*.gz' -o -name '*.xz' \) \
|
|
-mtime +"${MAX_ARCHIVE_AGE_DAYS}" 2>/dev/null | wc -l)
|
|
if [[ "$stale_count" -eq 0 ]]; then
|
|
record_pass "No stale archives" "none older than ${MAX_ARCHIVE_AGE_DAYS} days"
|
|
else
|
|
record_fail "No stale archives" "${stale_count} archive(s) older than ${MAX_ARCHIVE_AGE_DAYS} days"
|
|
fi
|
|
|
|
# 11. logrotate status file exists and is recent
|
|
local status_file="/var/lib/logrotate/logrotate.status"
|
|
if [[ ! -f "$status_file" ]]; then
|
|
# Some distros use /var/lib/logrotate.status
|
|
status_file="/var/lib/logrotate.status"
|
|
fi
|
|
|
|
if [[ -f "$status_file" ]]; then
|
|
local status_mtime
|
|
status_mtime=$(stat -c '%Y' "$status_file" 2>/dev/null) || status_mtime=0
|
|
local now
|
|
now=$(date +%s)
|
|
local age_days=$(( (now - status_mtime) / 86400 ))
|
|
|
|
if [[ "$age_days" -le 2 ]]; then
|
|
local status_date
|
|
status_date=$(date -d "@${status_mtime}" +%Y-%m-%d 2>/dev/null) || status_date="unknown"
|
|
record_pass "logrotate status file" "updated ${status_date}"
|
|
else
|
|
record_fail "logrotate status file" "last updated ${age_days} days ago"
|
|
fi
|
|
else
|
|
record_fail "logrotate status file" "not found at /var/lib/logrotate/logrotate.status or /var/lib/logrotate.status"
|
|
fi
|
|
}
|
|
|
|
# ── 5. Common Misconfigs ─────────────────────────────────────────────
|
|
test_misconfigs() {
|
|
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Common Misconfigs${RESET}"
|
|
fi
|
|
|
|
if [[ ! -d /etc/logrotate.d ]]; then
|
|
record_skip "Config 'rotate' directive check" "/etc/logrotate.d not found"
|
|
record_skip "Config 'compress' directive check" "/etc/logrotate.d not found"
|
|
return
|
|
fi
|
|
|
|
# 12a. Check for missing 'rotate' directive
|
|
local missing_rotate=()
|
|
local cfg
|
|
for cfg in /etc/logrotate.d/*; do
|
|
[[ -f "$cfg" ]] || continue
|
|
if ! grep -qE '^\s*rotate\s+' "$cfg" 2>/dev/null; then
|
|
# Check if the main config has a global rotate
|
|
if ! grep -qE '^\s*rotate\s+' /etc/logrotate.conf 2>/dev/null; then
|
|
missing_rotate+=("$(basename "$cfg")")
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [[ ${#missing_rotate[@]} -eq 0 ]]; then
|
|
record_pass "All configs have 'rotate' directive"
|
|
else
|
|
record_fail "All configs have 'rotate' directive" "missing in: ${missing_rotate[*]}"
|
|
fi
|
|
|
|
# 12b. Check for missing 'compress' directive
|
|
local missing_compress=()
|
|
for cfg in /etc/logrotate.d/*; do
|
|
[[ -f "$cfg" ]] || continue
|
|
if ! grep -qE '^\s*(compress|nocompress)' "$cfg" 2>/dev/null; then
|
|
if ! grep -qE '^\s*compress' /etc/logrotate.conf 2>/dev/null; then
|
|
missing_compress+=("$(basename "$cfg")")
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [[ ${#missing_compress[@]} -eq 0 ]]; then
|
|
record_pass "All configs have 'compress' directive"
|
|
else
|
|
record_fail "All configs have 'compress' directive" "missing in: ${missing_compress[*]}"
|
|
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} ${LOG_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="logrotate-smoke-tests" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
|
|
JUNIT_EOF
|
|
|
|
for result in "${RESULTS[@]}"; do
|
|
local status name detail
|
|
status=$(echo "$result" | cut -d'|' -f1)
|
|
name=$(echo "$result" | cut -d'|' -f2)
|
|
detail=$(echo "$result" | cut -d'|' -f3)
|
|
|
|
# XML-escape the values
|
|
name=$(echo "$name" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g')
|
|
detail=$(echo "$detail" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g')
|
|
|
|
case "$status" in
|
|
PASS)
|
|
echo " <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
|
|
[[ -n "$detail" ]] && echo " <system-out>${detail}</system-out>" >> "$JUNIT_FILE"
|
|
echo " </testcase>" >> "$JUNIT_FILE"
|
|
;;
|
|
FAIL)
|
|
echo " <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
|
|
echo " <failure message=\"${detail}\">FAILED: ${name} — ${detail}</failure>" >> "$JUNIT_FILE"
|
|
echo " </testcase>" >> "$JUNIT_FILE"
|
|
;;
|
|
SKIP)
|
|
echo " <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
|
|
echo " <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE"
|
|
echo " </testcase>" >> "$JUNIT_FILE"
|
|
;;
|
|
esac
|
|
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 logrotate configuration, rotation health, and log hygiene.
|
|
Read-only — never modifies configs or log files.
|
|
|
|
Options:
|
|
--format FORMAT Output: text (default), tap, junit
|
|
--junit-file FILE JUnit output path (default: smoke-results.xml)
|
|
--verbose Show debug output
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
Environment variables:
|
|
LOG_DIR Log directory (default: /var/log)
|
|
MAX_LOG_SIZE_MB Oversized log threshold in MB (default: 500)
|
|
MAX_DISK_USAGE_PCT Disk usage threshold in % (default: 80)
|
|
MAX_ARCHIVE_AGE_DAYS Stale archive threshold in days (default: 90)
|
|
SKIP_SYNTAX Skip logrotate -d syntax checks (default: false)
|
|
|
|
Examples:
|
|
# Basic run
|
|
sudo ./$(basename "$0")
|
|
|
|
# Custom thresholds
|
|
MAX_LOG_SIZE_MB=200 MAX_DISK_USAGE_PCT=70 sudo ./$(basename "$0")
|
|
|
|
# Skip syntax checks
|
|
SKIP_SYNTAX=true sudo ./$(basename "$0")
|
|
|
|
# JUnit output for CI
|
|
sudo ./$(basename "$0") --format junit --junit-file results.xml
|
|
|
|
# TAP output
|
|
sudo ./$(basename "$0") --format tap
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--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
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
|
print_tap_header
|
|
else
|
|
echo ""
|
|
echo -e "${BOLD}Log Rotation Smoke Tests${RESET}"
|
|
echo -e "Target: ${LOG_DIR}"
|
|
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
fi
|
|
|
|
# Run test suites
|
|
test_binary_and_config
|
|
test_config_syntax
|
|
test_log_hygiene
|
|
test_disk_and_retention
|
|
test_misconfigs
|
|
|
|
# Output
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
|
print_tap_footer
|
|
elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
|
|
print_summary
|
|
write_junit
|
|
else
|
|
print_summary
|
|
fi
|
|
|
|
# Exit code
|
|
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
|
}
|
|
|
|
main "$@"
|