557 lines
20 KiB
Bash
557 lines
20 KiB
Bash
#!/bin/bash
|
|
|
|
# Docker Hexo Static Blog v0.0.3 - Linux Complete Test Suite Startup Script
|
|
# 完整测试套件启动脚本
|
|
|
|
# set -e # Re-enabled for production use
|
|
|
|
# 颜色定义 (Corrected for echo -e and printf)
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# 配置参数
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR"
|
|
DOCKERFILE_DIR="$(cd "$SCRIPT_DIR/../../../" && pwd)"
|
|
LOG_DIR="$SCRIPT_DIR/logs"
|
|
# SUITE_LOG is defined in main after mkdir -p LOG_DIR
|
|
|
|
# 测试脚本数组
|
|
TESTS=(
|
|
"build_test.sh:构建测试"
|
|
"run_test.sh:运行测试"
|
|
"functional_test.sh:功能测试"
|
|
"log_rotation_test.sh:日志轮转测试"
|
|
"cleanup_test.sh:清理测试"
|
|
)
|
|
|
|
# 函数:显示横幅
|
|
show_banner() {
|
|
echo -e "${CYAN}"
|
|
echo " ╔══════════════════════════════════════════════════════════════╗"
|
|
echo " ║ Docker Hexo Static Blog v0.0.3 ║"
|
|
echo " ║ 完整测试套件 (Linux) ║"
|
|
echo " ║ ║"
|
|
echo " ║ • 自动化构建和部署测试 ║"
|
|
echo " ║ • 功能完整性验证 ║"
|
|
echo " ║ • 日志轮转和权限测试 ║"
|
|
echo " ║ • 安全配置验证 ║"
|
|
echo " ║ • 性能监控 ║"
|
|
echo " ╚══════════════════════════════════════════════════════════════╝"
|
|
echo -e "${NC}"
|
|
}
|
|
|
|
# 函数:记录日志
|
|
log() {
|
|
echo -e "${BLUE}[$(LC_ALL=C date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$SUITE_LOG"
|
|
local tee_status=${PIPESTATUS[1]}
|
|
if [ $tee_status -ne 0 ]; then
|
|
echo -e "${RED}CRITICAL: tee in log failed (status $tee_status) writing to $SUITE_LOG${NC}" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${GREEN}[$(LC_ALL=C date '+%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$SUITE_LOG"
|
|
local tee_status=${PIPESTATUS[1]}
|
|
if [ $tee_status -ne 0 ]; then
|
|
echo -e "${RED}CRITICAL: tee in log_success failed (status $tee_status) writing to $SUITE_LOG${NC}" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[$(LC_ALL=C date '+%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$SUITE_LOG"
|
|
local tee_status=${PIPESTATUS[1]}
|
|
if [ $tee_status -ne 0 ]; then
|
|
# Avoid using log_error here to prevent recursion if SUITE_LOG is the problem
|
|
echo -e "${RED}CRITICAL: tee in log_error failed (status $tee_status) writing to $SUITE_LOG${NC}" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
log_warning() {
|
|
echo -e "${YELLOW}[$(LC_ALL=C date '+%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$SUITE_LOG"
|
|
local tee_status=${PIPESTATUS[1]}
|
|
if [ $tee_status -ne 0 ]; then
|
|
echo -e "${RED}CRITICAL: tee in log_warning failed (status $tee_status) writing to $SUITE_LOG${NC}" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
log_step() {
|
|
echo -e "${CYAN}[$(LC_ALL=C date '+%Y-%m-%d %H:%M:%S')] ▶${NC} $1" | tee -a "$SUITE_LOG"
|
|
local tee_status=${PIPESTATUS[1]}
|
|
if [ $tee_status -ne 0 ]; then
|
|
echo -e "${RED}CRITICAL: tee in log_step failed (status $tee_status) writing to $SUITE_LOG${NC}" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# 函数:显示进度条
|
|
show_progress() {
|
|
local current=$1
|
|
local total=$2
|
|
local width=50
|
|
local percentage=$((current * 100 / total))
|
|
local filled=$((current * width / total))
|
|
local empty=$((width - filled))
|
|
|
|
printf "\r${CYAN}进度: [${NC}" # Corrected format string
|
|
printf "%*s" "$filled" | tr ' ' '=' # Corrected tr command and quoted variable
|
|
printf "%*s" "$empty" | tr ' ' ' ' # Corrected tr command and quoted variable
|
|
printf "${CYAN}] %d%% (%d/%d)${NC}" "$percentage" "$current" "$total" # Quoted variables
|
|
}
|
|
|
|
# 函数:检查前置条件
|
|
check_prerequisites() {
|
|
log_step "检查测试前置条件..."
|
|
|
|
local errors=0
|
|
|
|
# 检查 Docker
|
|
if ! command -v docker &> /dev/null; then
|
|
log_error "Docker 未安装或不在 PATH 中"
|
|
((errors++))
|
|
fi
|
|
|
|
# 检查 Docker 服务
|
|
if ! docker info &> /dev/null; then
|
|
log_error "Docker 服务未运行"
|
|
((errors++))
|
|
fi
|
|
|
|
# 检查 Dockerfile
|
|
if [ ! -f "$DOCKERFILE_DIR/Dockerfile_v0.0.3" ]; then
|
|
log_error "Dockerfile_v0.0.3 不存在于 $DOCKERFILE_DIR"
|
|
((errors++))
|
|
fi
|
|
|
|
# 检查测试脚本
|
|
for test_info in "${TESTS[@]}"; do
|
|
script_name="${test_info%%:*}"
|
|
if [ ! -f "$SCRIPT_DIR/$script_name" ]; then
|
|
log_error "测试脚本不存在: $script_name"
|
|
((errors++))
|
|
elif [ ! -x "$SCRIPT_DIR/$script_name" ]; then
|
|
log_warning "测试脚本没有执行权限: $script_name (将自动修复)"
|
|
chmod +x "$SCRIPT_DIR/$script_name"
|
|
fi
|
|
done
|
|
|
|
if [ $errors -gt 0 ]; then
|
|
log_error "发现 $errors 个前置条件问题,无法继续测试"
|
|
exit 1 # Exit if prerequisites fail
|
|
fi
|
|
|
|
log_success "前置条件检查通过"
|
|
}
|
|
|
|
# 函数:准备测试环境
|
|
prepare_test_environment() {
|
|
log_step "准备测试环境..."
|
|
|
|
# 创建日志目录 (already created in main, but ensure it)
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
# 清理旧的测试资源(可选)
|
|
if [ "${CLEAN_START:-false}" = "true" ]; then
|
|
log "清理旧的测试资源..."
|
|
# Allow these commands to fail without exiting the script immediately
|
|
docker stop hexo-blog-test 2>/dev/null || true
|
|
docker rm hexo-blog-test 2>/dev/null || true
|
|
docker rmi hexo-test:v0.0.3 2>/dev/null || true # Ensure correct image name used elsewhere
|
|
fi
|
|
|
|
# 设置测试脚本权限
|
|
chmod +x "$SCRIPT_DIR"/*.sh
|
|
|
|
log_success "测试环境准备完成"
|
|
}
|
|
|
|
# 函数:运行单个测试
|
|
run_test() {
|
|
local test_info="$1"
|
|
local script_name="${test_info%%:*}"
|
|
local test_description="${test_info##*:}"
|
|
|
|
log_step "执行测试: $test_description ($script_name)"
|
|
|
|
local test_log="$LOG_DIR/${script_name%.sh}.log"
|
|
local start_time
|
|
start_time=$(LC_ALL=C date +%s)
|
|
|
|
# 执行测试脚本
|
|
# The actual script ($script_name) should handle its own set -e behavior.
|
|
# If it fails, its non-zero exit code will be caught here.
|
|
if LC_ALL=C bash "$SCRIPT_DIR/$script_name" > "$test_log" 2>&1; then
|
|
local end_time
|
|
end_time=$(LC_ALL=C date +%s)
|
|
local duration=$((end_time - start_time))
|
|
log_success "$test_description 测试通过 (耗时: ${duration}s)"
|
|
return 0 # Test script succeeded
|
|
else
|
|
local exit_code=$? # Capture exit code of the failing script
|
|
local end_time
|
|
end_time=$(LC_ALL=C date +%s)
|
|
local duration=$((end_time - start_time))
|
|
log_error "$test_description 测试失败 (退出码: $exit_code, 耗时: ${duration}s)"
|
|
log_error "详细日志: $test_log"
|
|
|
|
# 显示失败的最后几行
|
|
echo -e "${RED}错误详情 (最后10行):${NC}"
|
|
tail -10 "$test_log" | sed \'s/^/ /\' # Ensure sed doesn\'t cause issues
|
|
|
|
return 1 # Test script failed
|
|
fi
|
|
}
|
|
|
|
# 函数:运行所有测试
|
|
run_all_tests() {
|
|
log_step "开始执行完整测试套件..."
|
|
|
|
local total_tests=${#TESTS[@]}
|
|
local passed_tests=0
|
|
local failed_tests=0
|
|
local suite_start_time
|
|
suite_start_time=$(LC_ALL=C date +%s)
|
|
|
|
echo "" # Newline before progress
|
|
|
|
for i in "${!TESTS[@]}"; do
|
|
local current=$((i + 1))
|
|
show_progress $current $total_tests
|
|
echo "" # Newline after progress bar, before test execution logs
|
|
|
|
if run_test "${TESTS[$i]}"; then
|
|
((passed_tests++))
|
|
else
|
|
((failed_tests++))
|
|
if [ "${CONTINUE_ON_FAILURE:-true}" = "true" ]; then # Default to true
|
|
log_warning "测试失败,但继续执行剩余测试..."
|
|
else
|
|
log_error "测试失败,停止执行"
|
|
break # Exit loop
|
|
fi
|
|
fi
|
|
echo "" # Newline after test result
|
|
done
|
|
|
|
local suite_end_time
|
|
suite_end_time=$(LC_ALL=C date +%s)
|
|
local total_duration=$((suite_end_time - suite_start_time))
|
|
|
|
# Final progress update to 100% if all tests run
|
|
if [ $failed_tests -eq 0 ] || [ "${CONTINUE_ON_FAILURE:-true}" = "true" ]; then
|
|
show_progress $total_tests $total_tests
|
|
echo "" # Newline after final progress
|
|
fi
|
|
|
|
echo ""
|
|
log_step "测试套件执行完成"
|
|
log "总耗时: ${total_duration}s"
|
|
log "通过测试: $passed_tests/$total_tests"
|
|
log "失败测试: $failed_tests/$total_tests"
|
|
|
|
# 生成测试报告
|
|
generate_test_report $passed_tests $failed_tests $total_duration
|
|
|
|
if [ $failed_tests -eq 0 ]; then
|
|
log_success "所有测试都通过了!🎉"
|
|
return 0 # Overall success
|
|
else
|
|
log_error "有 $failed_tests 个测试失败"
|
|
return 1 # Overall failure
|
|
fi
|
|
}
|
|
|
|
# 函数:生成测试报告
|
|
generate_test_report() {
|
|
local passed=$1
|
|
local failed=$2
|
|
local duration=$3
|
|
local total=$((passed + failed))
|
|
local success_rate=0
|
|
if [ $total -gt 0 ]; then
|
|
success_rate=$(( passed * 100 / total ))
|
|
fi
|
|
|
|
local report_file="$LOG_DIR/test_suite_report.txt"
|
|
|
|
log_step "生成测试报告..."
|
|
|
|
# Use cat with explicit EOF marker
|
|
cat > "$report_file" <<-EOF
|
|
Docker Hexo Static Blog v0.0.3 - 完整测试套件报告 (Linux)
|
|
========================================================
|
|
|
|
测试时间: $(LC_ALL=C date)
|
|
测试环境: Linux ($(uname -r))
|
|
Docker 版本: $(docker --version)
|
|
测试位置: $SCRIPT_DIR
|
|
|
|
测试结果概要:
|
|
- 总测试数: $total
|
|
- 通过测试: $passed
|
|
- 失败测试: $failed
|
|
- 成功率: $success_rate%
|
|
- 总耗时: ${duration}s
|
|
|
|
详细测试结果:
|
|
EOF
|
|
|
|
for test_info in "${TESTS[@]}"; do
|
|
local script_name="${test_info%%:*}"
|
|
local test_description="${test_info##*:}"
|
|
local test_log_file="$LOG_DIR/${script_name%.sh}.log" # Renamed to avoid conflict
|
|
|
|
echo "- $test_description:" >> "$report_file"
|
|
if [ -f "$test_log_file" ]; then
|
|
# Check for success markers more robustly
|
|
# Assuming test scripts output specific success strings or exit 0
|
|
# For now, rely on the run_test logic that populates passed/failed counts
|
|
# This report section can be enhanced if tests output specific markers
|
|
if grep -q -E "SUCCESS|成功|✓|测试通过" "$test_log_file" 2>/dev/null || \
|
|
( [ -s "$test_log_file" ] && ! grep -q -E "FAIL|失败|✗|测试失败" "$test_log_file" 2>/dev/null && \
|
|
grep -q "构建成功" "$test_log_file" 2>/dev/null ) ; then # Example for build_test
|
|
echo " 状态: ✓ 通过" >> "$report_file"
|
|
elif grep -q -E "FAIL|失败|✗|测试失败" "$test_log_file" 2>/dev/null; then
|
|
echo " 状态: ✗ 失败" >> "$report_file"
|
|
else
|
|
# If log exists but no clear pass/fail, mark as indeterminate or check exit code if stored
|
|
echo " 状态: ? 结果未知 (检查日志)" >> "$report_file"
|
|
fi
|
|
echo " 日志: $test_log_file" >> "$report_file"
|
|
else
|
|
echo " 状态: ? 未执行或日志丢失" >> "$report_file"
|
|
fi
|
|
echo "" >> "$report_file"
|
|
done
|
|
|
|
# 添加系统信息
|
|
echo "系统信息:" >> "$report_file"
|
|
echo "- 操作系统: $(uname -s)" >> "$report_file"
|
|
echo "- 内核版本: $(uname -r)" >> "$report_file"
|
|
echo "- 架构: $(uname -m)" >> "$report_file"
|
|
# Ensure free and df commands don't fail due to locale
|
|
echo "- 可用内存: $(LC_ALL=C free -h | grep '^Mem:' | awk '{print $7}')" >> "$report_file"
|
|
echo "- 磁盘空间: $(LC_ALL=C df -h . | tail -1 | awk '{print $4}')" >> "$report_file"
|
|
|
|
log_success "测试报告已生成: $report_file"
|
|
}
|
|
|
|
# 函数:显示帮助信息
|
|
show_help() {
|
|
cat <<-EOF
|
|
Docker Hexo Static Blog v0.0.3 - Linux 完整测试套件
|
|
|
|
用法: $0 [选项]
|
|
|
|
选项:
|
|
--help, -h 显示此帮助信息
|
|
--clean-start 清理旧资源后开始测试 (设置 CLEAN_START=true)
|
|
--stop-on-failure 第一个测试失败时停止 (设置 CONTINUE_ON_FAILURE=false)
|
|
--list 列出所有可用的测试
|
|
--test <script_name> 只运行指定的测试脚本 (例如: build_test.sh)
|
|
--report-only 只生成报告,不运行测试 (需要现有日志)
|
|
|
|
环境变量:
|
|
CLEAN_START=true 等同于 --clean-start
|
|
CONTINUE_ON_FAILURE=false 等同于 --stop-on-failure
|
|
|
|
示例:
|
|
$0 # 运行完整测试套件
|
|
$0 --clean-start # 清理后运行测试
|
|
$0 --test build_test.sh # 只运行构建测试
|
|
|
|
测试脚本:
|
|
EOF
|
|
|
|
for test_info in "${TESTS[@]}"; do
|
|
local script_name="${test_info%%:*}"
|
|
local test_description="${test_info##*:}"
|
|
printf " %-25s %s\\n" "$script_name" "$test_description"
|
|
done
|
|
}
|
|
|
|
# 函数:列出测试
|
|
list_tests() {
|
|
echo "可用的测试脚本:"
|
|
echo ""
|
|
for i in "${!TESTS[@]}"; do
|
|
local test_info="${TESTS[$i]}"
|
|
local script_name="${test_info%%:*}"
|
|
local test_description="${test_info##*:}"
|
|
printf "%d. %-25s - %s\\n" $((i + 1)) "$script_name" "$test_description"
|
|
done
|
|
}
|
|
|
|
# 函数:运行单个指定测试
|
|
run_single_test() {
|
|
local target_script="$1"
|
|
|
|
# Find the test_info for the target_script
|
|
local found_test_info=""
|
|
for test_info_item in "${TESTS[@]}"; do
|
|
local script_name_item="${test_info_item%%:*}"
|
|
if [ "$script_name_item" = "$target_script" ]; then
|
|
found_test_info="$test_info_item"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$found_test_info" ]; then
|
|
log_error "未找到测试脚本: $target_script"
|
|
echo "可用的测试脚本:"
|
|
list_tests # Call list_tests function
|
|
return 1
|
|
fi
|
|
|
|
# Prepare environment for single test run
|
|
# Note: SUITE_LOG might not be set if main() isn't fully run.
|
|
# For simplicity, single test runs will log to their own file and console.
|
|
# More robust single test logging would require initializing SUITE_LOG.
|
|
mkdir -p "$LOG_DIR" # Ensure log dir exists
|
|
export SUITE_LOG="$LOG_DIR/single_test_run_$(date +%Y%m%d_%H%M%S).log" # Temporary suite log for this run
|
|
echo "Running single test: $target_script. Main log: $SUITE_LOG" > "$SUITE_LOG"
|
|
|
|
|
|
log_step "运行单个测试: $target_script"
|
|
# check_prerequisites # Optional: run for single test
|
|
prepare_test_environment # Run prepare, it handles CLEAN_START
|
|
|
|
run_test "$found_test_info"
|
|
local test_exit_code=$?
|
|
|
|
if [ $test_exit_code -eq 0 ]; then
|
|
log_success "$target_script 测试通过"
|
|
else
|
|
log_error "$target_script 测试失败"
|
|
fi
|
|
return $test_exit_code
|
|
}
|
|
|
|
# 主函数
|
|
main() {
|
|
# Ensure LOG_DIR exists before SUITE_LOG is defined
|
|
mkdir -p "$LOG_DIR"
|
|
# Define SUITE_LOG here so all log functions can use it
|
|
# This was previously in the parameter parsing block for main call
|
|
export SUITE_LOG="$LOG_DIR/test_suite_$(LC_ALL=C date +%Y%m%d_%H%M%S).log"
|
|
|
|
show_banner
|
|
|
|
log "Docker Hexo Static Blog v0.0.3 完整测试套件启动"
|
|
log "================================================"
|
|
log "脚本位置: $SCRIPT_DIR"
|
|
log "Dockerfile 位置: $DOCKERFILE_DIR"
|
|
log "日志位置: $LOG_DIR (主套件日志: $SUITE_LOG)"
|
|
|
|
check_prerequisites
|
|
prepare_test_environment # Handles CLEAN_START logic
|
|
run_all_tests
|
|
# run_all_tests returns 0 for all pass, 1 for any failure
|
|
return $? # Propagate the exit status of run_all_tests
|
|
}
|
|
|
|
# --- Main script execution starts here ---
|
|
|
|
# Default behavior: continue on failure
|
|
export CONTINUE_ON_FAILURE="${CONTINUE_ON_FAILURE:-true}"
|
|
# Default behavior: don't clean start unless specified
|
|
export CLEAN_START="${CLEAN_START:-false}"
|
|
|
|
|
|
# Argument parsing
|
|
if [ $# -eq 0 ]; then
|
|
main
|
|
exit $? # Exit with main's status
|
|
fi
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--help|-h)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
--list)
|
|
list_tests
|
|
exit 0
|
|
;;
|
|
--clean-start)
|
|
export CLEAN_START=true
|
|
# If it's the only arg, run main. If others follow, they'll be processed.
|
|
shift
|
|
if [ $# -eq 0 ]; then main; exit $?; fi
|
|
;;
|
|
--stop-on-failure)
|
|
export CONTINUE_ON_FAILURE=false
|
|
shift
|
|
if [ $# -eq 0 ]; then main; exit $?; fi
|
|
;;
|
|
--test)
|
|
if [ -z "$2" ]; then
|
|
# Temporarily set SUITE_LOG for this error message if not already set
|
|
export SUITE_LOG="${SUITE_LOG:-$LOG_DIR/error_$(date +%Y%m%d_%H%M%S).log}"
|
|
mkdir -p "$(dirname "$SUITE_LOG")"
|
|
log_error "请指定要运行的测试脚本名称 (例如: build_test.sh)"
|
|
show_help
|
|
exit 1
|
|
fi
|
|
run_single_test "$2"
|
|
exit $? # Exit with single test's status
|
|
;;
|
|
--report-only)
|
|
# Ensure SUITE_LOG is defined for logging within generate_test_report
|
|
export SUITE_LOG="${SUITE_LOG:-$LOG_DIR/report_only_$(date +%Y%m%d_%H%M%S).log}"
|
|
mkdir -p "$(dirname "$SUITE_LOG")"
|
|
if [ -d "$LOG_DIR" ] && [ "$(find "$LOG_DIR" -name \'*.log\' -print -quit)" ]; then
|
|
# Dummy values for passed/failed/duration as we are only generating report from existing logs
|
|
generate_test_report 0 0 0
|
|
exit $?
|
|
else
|
|
log_error "没有找到测试日志目录 $LOG_DIR 或日志文件以生成报告。"
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
# If main hasn't run yet due to options, run it now.
|
|
# This handles the case where options like --clean-start are given,
|
|
# and then the script is expected to run the full suite.
|
|
if ! ps -p $$ -o comm= | grep -q "start.sh"; then # Basic check if main was already invoked
|
|
main
|
|
exit $?
|
|
else
|
|
# If options were processed and we still have an unknown one, it's an error.
|
|
export SUITE_LOG="${SUITE_LOG:-$LOG_DIR/error_$(date +%Y%m%d_%H%M%S).log}"
|
|
mkdir -p "$(dirname "$SUITE_LOG")"
|
|
log_error "未知参数: $1"
|
|
show_help
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
# shift # Shift only if argument was consumed and not an exit/main call
|
|
done
|
|
|
|
# If loop finishes and main hasn't been called (e.g. only options like --clean-start were set)
|
|
# This logic might be complex depending on desired option interactions.
|
|
# A common pattern is to set flags and have one call to main() at the end.
|
|
# The current structure calls main or exits within the case statement for most paths.
|
|
# If we reach here, it implies options were processed that set flags, and now main should run.
|
|
if [ "${#BASH_SOURCE[@]}" -eq 1 ] && [ -z "${_MAIN_CALLED_VIA_OPTIONS:-}" ]; then
|
|
# Check if main was called by an option that then shifted out all args
|
|
# This is a fallback if options like --clean-start are given,
|
|
# and the parameter parsing was modified to call main if --clean-start is the only arg.
|
|
# So this path might not be strictly needed anymore.
|
|
: # Do nothing, main should have been called or script exited.
|
|
fi
|
|
|
|
# Final exit status should be from main or specific option handlers.
|
|
# Bash scripts exit with the status of the last command if not specified.
|
|
# The explicit `exit $?` after main calls ensures this.
|