From 182985def21d6fca5b938a4e6bf61378b770a188 Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Tue, 14 Jul 2026 15:05:10 +0800 Subject: [PATCH 1/7] feat: add Docker-based concurrent package test script - Container isolation per package for parallel testing - Local ISO repo support via --iso and dnf --repofrompath - --cap-add for RPM unpacking in containers - 6-column TSV output (source-pkg, binary-pkg, install, command, service, remove) - xargs -P concurrency with memory/CPU limits - Whitelist support for desktop/service packages --- testcase/command-execute-docker.sh | 672 +++++++++++++++++++++++++++++ 1 file changed, 672 insertions(+) create mode 100755 testcase/command-execute-docker.sh diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh new file mode 100755 index 0000000..d743109 --- /dev/null +++ b/testcase/command-execute-docker.sh @@ -0,0 +1,672 @@ +#!/bin/bash +############################################################################### +# @用例名称: command-execute-docker +# @用例级别: 3 +# @用例类型: 功能测试 +# @自动化: 1 +# +# Docker 并发软件包功能测试脚本 +# 用法: command-execute-docker.sh [选项] +# +# 功能: +# - 基于 Docker 容器实现软件包测试的并发执行与隔离 +# - 每个待测包在独立容器内完成: 安装 → 命令验证 → 服务检测 → 卸载 +# - 结果格式与现有 package-command-result-*.txt 兼容(6 列 TSV) +# - 支持本地 ISO 挂载作为 repo 源(--iso),无需网络 +# +# 服务测试方案与局限: +# 默认容器无 systemd,服务测试统一标记为 SKIPPED(原因: 容器无 systemd)。 +# 若需测试服务启停,需使用带 systemd init 的镜像(如 --privileged + /sbin/init), +# 此时脚本会自动检测 systemd 并执行 start/status(is-active)/stop 测试。 +# 局限: --privileged 降低了隔离性,init 镜像体积更大、启动更慢。 +# +# 输出格式 (Tab 分隔, 6 列): +# source-package binary_package install command service remove +############################################################################### + +set -uo pipefail + +# ============================================================================ +# 默认配置 +# ============================================================================ +JOBS=4 +BASE_IMAGE="opencloudos/opencloudos:9" +CONTAINER_TIMEOUT=300 +CPU_LIMIT=1 +MEMORY_LIMIT=512 +OUTPUT_DIR="./logs/docker" +ISO_PATH="" + +# ============================================================================ +# 颜色输出 +# ============================================================================ +if [ -t 1 ]; then + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' +else + RED='' GREEN='' YELLOW='' CYAN='' NC='' +fi + +log_info() { echo -e "${CYAN}[INFO]${NC} $(date '+%H:%M:%S') $*" >&2; } +log_ok() { echo -e "${GREEN}[OK]${NC} $(date '+%H:%M:%S') $*" >&2; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*" >&2; } +log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; } + +# ============================================================================ +# 用法说明 +# ============================================================================ +usage() { + cat >&2 <<'EOF' +用法: command-execute-docker.sh [选项] + +必需参数: + 待测软件包列表文件 (格式: pkg_name arch) + 桌面白名单文件(跳过命令测试) + 服务白名单文件(需特殊配置,跳过服务启动测试) + +选项: + --iso PATH ISO 挂载路径,用于本地 repo(推荐离线测试) + -j, --jobs N 并发度(默认: 4) + --image IMAGE Docker 基础镜像(默认: opencloudos/opencloudos:9) + --timeout N 单容器超时秒数(默认: 300) + --cpu N 每容器 CPU 核数限制(默认: 1) + --memory N 每容器内存限制/MB(默认: 512) + -o, --output FILE 结果文件路径(默认: 自动命名于 OUTPUT_DIR) + --output-dir DIR 输出目录(默认: ./logs/docker) + -h, --help 显示帮助信息 + +输出格式 (Tab 分隔, 6 列): + source-package binary_package install command service remove + +示例: + command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ + --iso /run/media/user/OC-9-6-x86_64 --image opencloudos:9-local -j 8 + command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ + --image opencloudos:9-local -j 4 --timeout 600 +EOF + exit 0 +} + +# ============================================================================ +# 参数解析 +# ============================================================================ +PKG_LIST="" +DESKTOP_WHITELIST="" +SERVICE_WHITELIST="" +OUTPUT_FILE="" + +if [ $# -lt 3 ]; then + log_error "需要 3 个必需参数: pkg_list desktop_whitelist service_whitelist" + usage +fi + +PKG_LIST="$(realpath "$1")" +DESKTOP_WHITELIST="$(realpath "$2")" +SERVICE_WHITELIST="$(realpath "$3")" +shift 3 + +while [ $# -gt 0 ]; do + case "$1" in + --iso) ISO_PATH="$(realpath "$2")"; shift 2 ;; + -j|--jobs) JOBS="$2"; shift 2 ;; + --image) BASE_IMAGE="$2"; shift 2 ;; + --timeout) CONTAINER_TIMEOUT="$2"; shift 2 ;; + --cpu) CPU_LIMIT="$2"; shift 2 ;; + --memory) MEMORY_LIMIT="$2"; shift 2 ;; + -o|--output) OUTPUT_FILE="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + -h|--help) usage ;; + *) log_error "未知选项: $1"; usage ;; + esac +done + +# ============================================================================ +# 前置检查 +# ============================================================================ +for f in "$PKG_LIST" "$DESKTOP_WHITELIST" "$SERVICE_WHITELIST"; do + [ -f "$f" ] || { log_error "文件不存在: $f"; exit 1; } +done + +command -v docker >/dev/null 2>&1 || { log_error "docker 未安装"; exit 1; } +docker info >/dev/null 2>&1 || { log_error "Docker 守护进程未运行"; exit 1; } + +if ! docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then + log_error "镜像 '$BASE_IMAGE' 不存在,请先拉取或构建" + exit 1 +fi + +if [ -n "$ISO_PATH" ]; then + [ -d "$ISO_PATH/BaseOS/repodata" ] || { + log_error "ISO 路径 '$ISO_PATH/BaseOS/repodata' 不存在" + exit 1 + } +fi + +# ============================================================================ +# 工作目录 +# ============================================================================ +WORKDIR="$(mktemp -d "/tmp/docker-pkg-test.XXXXXX")" +TOTAL_FAIL=0 + +# [改进 #5] 信号处理: Ctrl+C 时清理孤儿容器 +CLEANUP_CONTAINERS=0 +cleanup_containers() { + docker ps -q --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker stop --time 2 >/dev/null 2>&1 + docker ps -aq --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker rm -f >/dev/null 2>&1 +} +trap 'CLEANUP_CONTAINERS=1; cleanup_containers; log_warn "收到中断信号,已清理测试容器"; exit 130' INT TERM + +trap 'if [ "${TOTAL_FAIL:-0}" -gt 0 ] || [ "$CLEANUP_CONTAINERS" -eq 1 ]; then log_warn "工作目录保留: $WORKDIR"; else rm -rf "$WORKDIR"; fi' EXIT + +FRAGMENTS_DIR="$WORKDIR/fragments" +mkdir -p "$FRAGMENTS_DIR" +mkdir -p "$OUTPUT_DIR" + +if [ -z "$OUTPUT_FILE" ]; then + OUTPUT_FILE="${OUTPUT_DIR}/package-command-result-$(date '+%Y%m%d-%H%M%S').txt" +fi + +echo -e "# source-package\tbinary_package\tinstall\tcommand\tservice\tremove" > "$OUTPUT_FILE" + +# ============================================================================ +# 过滤软件包列表 +# ============================================================================ +FILTERED_LIST="$WORKDIR/pkg.filtered.list" +grep -v 'livepatch-kernel-\|-debuginfo-\|-debugsource-' "$PKG_LIST" \ + | awk '{print $1}' | sort -u > "$FILTERED_LIST" + +TOTAL_PKGS="$(wc -l < "$FILTERED_LIST")" +if [ "$TOTAL_PKGS" -eq 0 ]; then + log_warn "过滤后无待测包" + exit 0 +fi + +HAS_APPSTREAM=0 +[ -n "$ISO_PATH" ] && [ -d "$ISO_PATH/AppStream/repodata" ] && HAS_APPSTREAM=1 + +log_info "待测包: $TOTAL_PKGS | 并发: $JOBS | 镜像: $BASE_IMAGE" +[ -n "$ISO_PATH" ] && log_info "ISO repo: $ISO_PATH" + +# ============================================================================ +# 生成容器内测试脚本 (harness) +# +# 输出: +# stdout: 单行 TSV (src_pkg bin_pkg install cmd svc remove) +# stderr: 调试日志 + 结构化 REASON 标记,供汇总报告解析 +# REASON:INSTALL:FAIL:<原因> +# REASON:INSTALL:SKIP:<原因> +# REASON:CMD:FAIL:<命令路径> <原因> +# REASON:CMD:SKIP:<原因> +# REASON:SVC:FAIL:<服务名> <原因> +# REASON:SVC:SKIP:<原因> +# REASON:REMOVE:FAIL:<原因> +# REASON:REMOVE:SKIP:<原因> +# ============================================================================ + +# [改进 #9] 服务单元过滤正则提取为变量,提高可读性 +SVC_UNIT_EXCLUDE_PATTERN='@|systemd-.*\.(service|socket)|initrd.*\.(service|target)|anaconda.*\.(service|target)|coreos.*\.(service|target)|run-[a-z0-9]\{33\}\.(service|target)|man-db-cache-update\.service|rc-local\.service|nvidia-persistenced\.service|NetworkManager-wait-online\.service|systemd-fsck.*\.service|selinux-autorelabel\.service' + +CONTAINER_SCRIPT="$WORKDIR/container-test.sh" +cat > "$CONTAINER_SCRIPT" <<'HARNESS_EOF' +#!/bin/bash +set -uo pipefail + +PKG="$TEST_PKG_NAME" +DESKTOP_WL="$TEST_DESKTOP_WL" +SVC_WL="$TEST_SERVICE_WL" +ISO_MOUNT="$TEST_ISO_MOUNT" + +# ---------- 配置 ISO repo ---------- +if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then + rm -f /etc/yum.repos.d/*.repo + DNF_REPOS="--repofrompath=baseos,file://${ISO_MOUNT}/BaseOS --setopt=baseos.gpgcheck=0" + if [ -d "$ISO_MOUNT/AppStream/repodata" ]; then + DNF_REPOS="$DNF_REPOS --repofrompath=appstream,file://${ISO_MOUNT}/AppStream --setopt=appstream.gpgcheck=0" + fi +else + DNF_REPOS="" +fi + +# ---------- 获取源码包名 ---------- +get_src() { + local bin="$1" src + src="$(rpm -qi "$bin" 2>/dev/null | grep '^Source RPM' | sed 's/Source RPM: //' | sed 's/-[0-9].*//')" + if [ -n "$src" ]; then echo "$src"; return; fi + src="$(dnf $DNF_REPOS repoquery --queryformat '%{sourcerpm}' "$bin" 2>/dev/null | head -1 | sed 's/-[0-9].*//')" + echo "${src:-$bin}" +} + +# ---------- 安装 ---------- +INSTALL="FAIL" +INST_LOG="$(mktemp)" +WAS_ALREADY_INSTALLED=0 + +if dnf $DNF_REPOS install -y "$PKG" > "$INST_LOG" 2>&1; then + if grep -q "Nothing to do\|Already installed\|is already installed" "$INST_LOG" 2>/dev/null; then + WAS_ALREADY_INSTALLED=1 + fi + if ! grep -q "scriptlet failed" "$INST_LOG" && ! grep -q "No such file or directory" "$INST_LOG"; then + INSTALL="PASS" + fi +else + if grep -q "conflicts with" "$INST_LOG"; then + INSTALL="PASS" + fi +fi + +SRC_PKG="$(get_src "$PKG")" + +if [ "$INSTALL" = "FAIL" ]; then + # 提取失败原因 + reason="$(tail -5 "$INST_LOG" | tr '\n' ' ' | sed 's/ */ /g')" + echo "REASON:INSTALL:FAIL:$reason" >&2 + cat "$INST_LOG" >&2 + rm -f "$INST_LOG" + printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP\n' "$SRC_PKG" "$PKG" + exit 1 +fi + +if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then + echo "REASON:INSTALL:SKIP:已预装于基础镜像 (dnf: Already installed)" >&2 +fi +rm -f "$INST_LOG" + +# ---------- 命令测试 ---------- +CMD="SKIP" +REAL_PKG="$(rpm -q --queryformat='%{name}' "$PKG" 2>/dev/null)" + +if [ -n "$REAL_PKG" ] && [ -f "$DESKTOP_WL" ] && grep -qx "$REAL_PKG" "$DESKTOP_WL"; then + # 桌面白名单包,跳过命令测试 + echo "REASON:CMD:SKIP:桌面白名单, 跳过命令测试" >&2 +elif [ -n "$REAL_PKG" ]; then + FLIST="$(mktemp)" + CLIST="$(mktemp)" + + rpm -ql "$PKG" 2>/dev/null \ + | grep -E '^/usr/(bin|sbin)/' | grep -v '\.so' | grep -v '/usr/bin/nde-' > "$FLIST" + + if [ -s "$FLIST" ]; then + xargs file < "$FLIST" 2>/dev/null | grep 'ELF ' | sed 's/:.*//' | while read -r f; do + [ -x "$f" ] && echo "$f" + done > "$CLIST" + fi + + if [ -s "$CLIST" ]; then + NR="$(wc -l < "$CLIST")" + IDX=0 + CFAIL=0 + while [ $IDX -lt $NR ]; do + IDX=$((IDX + 1)) + TCMD="$(sed -n "${IDX}p" "$CLIST")" + [ -z "$TCMD" ] && continue + CLOG="$(mktemp)" + CPASS=0 + CFAIL_REASON="" + if timeout -s SIGKILL 3 "$TCMD" --help > "$CLOG" 2>&1; then + CPASS=1 + elif timeout -s SIGKILL 3 "$TCMD" --version >> "$CLOG" 2>&1; then + CPASS=1 + elif grep -q "error while loading shared libraries" "$CLOG"; then + CFAIL_REASON="缺少共享库 $(grep 'error while loading' "$CLOG" | head -1 | sed 's/.*: //')" + CPASS=0 + elif ! realpath "$TCMD" >/dev/null 2>&1; then + CFAIL_REASON="realpath 失败 (符号链接目标不存在)" + CPASS=0 + elif timeout -s SIGKILL 3 "$TCMD" >> "$CLOG" 2>&1; then + CPASS=1 + elif grep -iqw 'invalid\|argument\|Unknown\|option\|Usage' "$CLOG"; then + CPASS=1 + else + CFAIL_REASON="退出码非0" + CPASS=0 + fi + if [ $CPASS -eq 0 ]; then + CFAIL=$((CFAIL + 1)) + echo "REASON:CMD:FAIL:$TCMD $CFAIL_REASON" >&2 + fi + rm -f "$CLOG" + done + [ $CFAIL -eq 0 ] && CMD="PASS" || CMD="FAIL" + fi + rm -f "$FLIST" "$CLIST" +fi + +# ---------- 服务测试 ---------- +SVC="SKIP" +HAS_SYSTEMD=0 +if pidof systemd >/dev/null 2>&1; then + HAS_SYSTEMD=1 +fi + +if [ -n "$REAL_PKG" ]; then + SLIST="$(mktemp)" + rpm -ql "$PKG" 2>/dev/null \ + | grep -E '^/usr/lib/systemd/system/.*\.(service|socket|target)$' \ + | grep -vE "$SVC_UNIT_EXCLUDE_PATTERN" \ + | sed 's|.*/||' > "$SLIST" + + if [ -s "$SLIST" ]; then + # 检查是否在服务白名单中 + IS_SVC_WHITELIST=0 + if [ -f "$SVC_WL" ] && grep -q "^${REAL_PKG}" "$SVC_WL"; then + IS_SVC_WHITELIST=1 + fi + + if [ "$IS_SVC_WHITELIST" -eq 1 ]; then + SVC="SKIP" + echo "REASON:SVC:SKIP:含服务包, 需特殊配置, 跳过服务启动" >&2 + elif [ "$HAS_SYSTEMD" -eq 0 ]; then + SVC="SKIP" + echo "REASON:SVC:SKIP:容器环境不支持 systemd, 无法测试服务启停" >&2 + else + # 有 systemd,执行 start/status/stop 测试 + SFAIL=0 + while read -r svc; do + [ -z "$svc" ] && continue + # start + if ! timeout -s SIGKILL 30 systemctl --no-pager start "$svc" >/dev/null 2>&1; then + SFAIL=$((SFAIL + 1)) + echo "REASON:SVC:FAIL:$svc start 失败 (systemctl start 返回非0)" >&2 + continue + fi + # status / is-active + if ! systemctl --no-pager is-active "$svc" >/dev/null 2>&1; then + SFAIL=$((SFAIL + 1)) + echo "REASON:SVC:FAIL:$svc status 未 active (systemctl is-active 检查失败)" >&2 + systemctl --no-pager stop "$svc" >/dev/null 2>&1 + continue + fi + # stop + if ! timeout -s SIGKILL 30 systemctl --no-pager stop "$svc" >/dev/null 2>&1; then + if systemctl --no-pager stop "$svc" 2>&1 | grep -iqw "configured to refuse manual start/stop"; then + # 拒绝手动 stop 不算失败 + : + else + SFAIL=$((SFAIL + 1)) + echo "REASON:SVC:FAIL:$svc stop 失败 (systemctl stop 返回非0)" >&2 + fi + fi + done < "$SLIST" + [ $SFAIL -eq 0 ] && SVC="PASS" || SVC="FAIL" + fi + else + # 无 systemd unit 文件 + SVC="PASS" + fi + rm -f "$SLIST" +fi + +# ---------- 卸载 ---------- +REMOVE="FAIL" +RM_LOG="$(mktemp)" + +if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then + REMOVE="PASS" + echo "REASON:REMOVE:SKIP:已预装于基础镜像, 跳过卸载" >&2 +else + if dnf $DNF_REPOS remove -y --setopt=clean_requirements_on_remove=False "$PKG" > "$RM_LOG" 2>&1; then + if grep -q "scriptlet failed" "$RM_LOG"; then + reason="$(grep 'scriptlet failed' "$RM_LOG" | head -1 | tr '\n' ' ')" + echo "REASON:REMOVE:FAIL:$reason" >&2 + cat "$RM_LOG" >&2 + else + REMOVE="PASS" + fi + else + if grep -iq "protected packages" "$RM_LOG"; then + REMOVE="PASS" + echo "REASON:REMOVE:SKIP:受保护包, 跳过卸载" >&2 + else + reason="$(tail -5 "$RM_LOG" | tr '\n' ' ' | sed 's/ */ /g')" + echo "REASON:REMOVE:FAIL:$reason" >&2 + cat "$RM_LOG" >&2 + fi + fi +fi +rm -f "$RM_LOG" + +printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$SRC_PKG" "$PKG" "$INSTALL" "$CMD" "$SVC" "$REMOVE" +HARNESS_EOF +chmod +x "$CONTAINER_SCRIPT" + +# ============================================================================ +# 进度计数器(改进 #7) +# ============================================================================ +PROGRESS_FILE="$WORKDIR/progress" +echo "0" > "$PROGRESS_FILE" + +run_one_package() { + local pkg="$1" + local result rc=0 + + local docker_volumes="-v ${DESKTOP_WHITELIST}:/tmp/desktop-wl:ro" + docker_volumes="$docker_volumes -v ${SERVICE_WHITELIST}:/tmp/svc-wl:ro" + docker_volumes="$docker_volumes -v ${CONTAINER_SCRIPT}:/tmp/test.sh:ro" + + if [ -n "$ISO_PATH" ]; then + docker_volumes="$docker_volumes -v ${ISO_PATH}:/mnt/iso:ro" + fi + + # [改进 #4] 添加 --network none,隔离网络,防止容器意外访问外网 + result="$(timeout "$CONTAINER_TIMEOUT" docker run --rm \ + $docker_volumes \ + -e TEST_PKG_NAME="$pkg" \ + -e TEST_DESKTOP_WL="/tmp/desktop-wl" \ + -e TEST_SERVICE_WL="/tmp/svc-wl" \ + -e TEST_ISO_MOUNT="/mnt/iso" \ + --cap-add=CHOWN --cap-add=FSETID --cap-add=FOWNER --cap-add=DAC_OVERRIDE \ + --memory="${MEMORY_LIMIT}m" \ + --cpus="$CPU_LIMIT" \ + --network none \ + -l pkg-test=1 \ + "$BASE_IMAGE" \ + bash /tmp/test.sh \ + 2>"$FRAGMENTS_DIR/${pkg}.log")" || rc=$? + + # [Bug #1 修复] 超时/异常退出时使用 \t 分隔,保持 TSV 格式一致 + if [ $rc -eq 124 ]; then + echo "REASON:INSTALL:FAIL:安装超时(>${CONTAINER_TIMEOUT}s)" >> "$FRAGMENTS_DIR/${pkg}.log" + result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" + log_error "[$pkg] 超时 (${CONTAINER_TIMEOUT}s)" + elif [ $rc -ne 0 ] && [ -z "$result" ]; then + echo "REASON:INSTALL:FAIL:容器异常退出(退出码:$rc)" >> "$FRAGMENTS_DIR/${pkg}.log" + result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" + fi + + echo "$result" > "$FRAGMENTS_DIR/${pkg}.result" + + local inst cmd svc rm + inst="$(echo "$result" | awk -F'\t' '{print $3}')" + cmd="$(echo "$result" | awk -F'\t' '{print $4}')" + svc="$(echo "$result" | awk -F'\t' '{print $5}')" + rm="$(echo "$result" | awk -F'\t' '{print $6}')" + + # [改进 #7] 原子递增进度 + local done_count + done_count="$(flock "$PROGRESS_FILE" bash -c "echo \$(( \$(cat '$PROGRESS_FILE') + 1 )) | tee '$PROGRESS_FILE'")" + + if [ "$inst" = "PASS" ] && [ "$cmd" != "FAIL" ] && [ "$svc" != "FAIL" ] && [ "$rm" = "PASS" ]; then + log_ok "[${done_count}/${TOTAL_PKGS}] $pkg install=$inst cmd=$cmd svc=$svc remove=$rm" + else + log_error "[${done_count}/${TOTAL_PKGS}] $pkg install=$inst cmd=$cmd svc=$svc remove=$rm" + fi +} + +export -f run_one_package +export DESKTOP_WHITELIST SERVICE_WHITELIST BASE_IMAGE CONTAINER_SCRIPT FRAGMENTS_DIR ISO_PATH +export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS +export SVC_UNIT_EXCLUDE_PATTERN + +# ============================================================================ +# 执行测试 +# ============================================================================ +log_info "开始: $TOTAL_PKGS 个包, $JOBS 并发" +echo "---------------------------------------------------------------" >&2 + +START_TIME="$(date +%s)" +xargs -P "$JOBS" -I{} bash -c 'run_one_package "$@"' _ {} < "$FILTERED_LIST" +END_TIME="$(date +%s)" +ELAPSED=$(( END_TIME - START_TIME )) + +echo "---------------------------------------------------------------" >&2 +log_info "完成,耗时 ${ELAPSED}s" + +# ============================================================================ +# 合并结果 +# ============================================================================ +find "$FRAGMENTS_DIR" -name '*.result' -print0 \ + | sort -z | xargs -0 cat \ + | sort -t$'\t' -k2,2 \ + >> "$OUTPUT_FILE" + +# ============================================================================ +# 生成汇总报告(匹配验收标准格式) +# ============================================================================ +SUMMARY_FILE="${OUTPUT_DIR}/test-summary-$(date '+%Y%m%d-%H%M%S').txt" +TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" + +TOTAL=0 +PASSED=0 +FAILED=0 +SKIPPED=0 + +# 分类收集 +INSTALL_FAIL_LIST=() +REMOVE_FAIL_LIST=() +CMD_FAIL_LIST=() +SVC_FAIL_LIST=() +SKIP_LIST=() + +while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do + [[ "$src_pkg" == \#* ]] && continue + [[ -z "$src_pkg" ]] && continue + TOTAL=$((TOTAL + 1)) + + pkg_failed=0 + pkg_all_skip=1 # [改进 #3] 跟踪是否所有阶段都是 SKIP + pkg_has_skip=0 + + # ---- 安装 ---- + if [ "$install" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:INSTALL:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:INSTALL:FAIL://' | tr '\t' ' ')" + INSTALL_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") + elif [ "$install" = "PASS" ]; then + pkg_all_skip=0 + if grep -q 'REASON:INSTALL:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then + pkg_has_skip=1 + fi + fi + + # ---- 命令 ---- + if [ "$cmd" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:CMD:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:CMD:FAIL://' | tr '\t' ' ')" + CMD_FAIL_LIST+=("${bin_pkg} ${reason:-命令测试失败}") + elif [ "$cmd" = "PASS" ]; then + pkg_all_skip=0 + elif [ "$cmd" = "SKIP" ]; then + pkg_has_skip=1 + fi + + # ---- 服务 ---- + if [ "$svc" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:SVC:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:SVC:FAIL://' | tr '\t' ' ')" + SVC_FAIL_LIST+=("${bin_pkg} ${reason:-服务测试失败}") + elif [ "$svc" = "PASS" ]; then + pkg_all_skip=0 + elif [ "$svc" = "SKIP" ]; then + pkg_has_skip=1 + fi + + # ---- 卸载 ---- + if [ "$remove" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:REMOVE:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:REMOVE:FAIL://' | tr '\t' ' ')" + REMOVE_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") + elif [ "$remove" = "PASS" ]; then + pkg_all_skip=0 + if grep -q 'REASON:REMOVE:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then + pkg_has_skip=1 + fi + fi + + # ---- 分类(改进 #3: PASS+合理SKIP → PASSED,仅全部SKIP → SKIPPED)---- + if [ $pkg_failed -eq 1 ]; then + FAILED=$((FAILED + 1)) + elif [ $pkg_all_skip -eq 1 ] && [ $pkg_has_skip -eq 1 ]; then + # 所有阶段都是 SKIP(如安装SKIP+卸载SKIP的预装保护包) + SKIPPED=$((SKIPPED + 1)) + all_skips="$(grep 'REASON:.*:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | sed 's/REASON:[^:]*:SKIP://' | tr '\n' ', ' | sed 's/, $//')" + SKIP_LIST+=("${bin_pkg} (${all_skips:-全部跳过})") + else + # 有 PASS 且无 FAIL → 通过(即使部分阶段是合理的 SKIP) + PASSED=$((PASSED + 1)) + fi +done < "$OUTPUT_FILE" + +# ---- 写入汇总文件 ---- +{ + echo "========== 测试失败汇总 (${TIMESTAMP}) ==========" + echo "环境: docker | 并发度: ${JOBS} | 待测包总数: ${TOTAL} | 通过: ${PASSED} | 失败: ${FAILED} | 跳过: ${SKIPPED}" + echo "" + + echo "【安装失败】(${#INSTALL_FAIL_LIST[@]})" + if [ ${#INSTALL_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${INSTALL_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【卸载失败】(${#REMOVE_FAIL_LIST[@]})" + if [ ${#REMOVE_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${REMOVE_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【命令测试失败】(${#CMD_FAIL_LIST[@]})" + if [ ${#CMD_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${CMD_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【服务测试失败】(${#SVC_FAIL_LIST[@]})" + if [ ${#SVC_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${SVC_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【跳过项】(${#SKIP_LIST[@]})" + if [ ${#SKIP_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${SKIP_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "=======================================================" + echo "对账: 通过(${PASSED}) + 失败(${FAILED}) + 跳过(${SKIPPED}) = $((PASSED + FAILED + SKIPPED)) / 待测总数(${TOTAL})" + if [ $((PASSED + FAILED + SKIPPED)) -ne "$TOTAL" ]; then + echo " *** 对账不匹配! ***" + else + echo " 对账匹配" + fi +} | tee "$SUMMARY_FILE" + +echo "" +echo "===============================================================" +echo " Result file: $OUTPUT_FILE" +echo " Summary file: $SUMMARY_FILE" +echo " Per-pkg logs: $FRAGMENTS_DIR/*.log" +echo "===============================================================" + +TOTAL_FAIL="$FAILED" +[ $TOTAL_FAIL -gt 0 ] && exit 1 +exit 0 \ No newline at end of file -- Gitee From d57d6d010ba352831501539e478025a942bf48f8 Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Fri, 31 Jul 2026 01:38:04 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DDocker=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=B5=8B=E8=AF=95=E5=9C=A8=E7=BA=BF=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=89=E4=B8=AA=E5=85=B3=E9=94=AE=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 --network none 硬编码导致在线模式容器无法联网 - ISO模式自动启用 --network none - 在线模式默认允许网络,新增 --no-network 选项手动控制 2. 修复在线模式下容器repo配置与宿主不一致 - 挂载宿主 /etc/yum.repos.d 到容器 /tmp/host-repos - harness内复制repo配置使容器与被测系统一致 3. 修复 SVC_UNIT_EXCLUDE_PATTERN ERE正则错误 - BRE模式 {33} 在ERE(grep -E)下应写为 {33} - 正则硬编码入harness heredoc避免shell变量glob风险 4. 清理死代码、添加repo空目录边界检查、写入项目规则 --- .trae/rules/project.md | 90 ++++++++++++++++++++++++++++++ testcase/command-execute-docker.sh | 45 +++++++++------ 2 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 .trae/rules/project.md diff --git a/.trae/rules/project.md b/.trae/rules/project.md new file mode 100644 index 0000000..f951d0a --- /dev/null +++ b/.trae/rules/project.md @@ -0,0 +1,90 @@ +# yum-ci-pkg-test 项目规则 + +## 项目概述 +OpenCloudOS 软件包 CI 测试套(yum-ci-pkg-test),用于验证软件包的功能,包括安装/卸载、二进制命令可执行性和服务测试。 + +## 技术栈 +- Shell (Bash) 脚本为主 +- 测试框架: tsuite (C语言编写的自定义测试框架) +- Docker (并发测试方案) +- OpenCloudOS 9.x (目标运行环境) + +## 目录结构 +``` +yum-ci-pkg-test/ +├── testcase/ # 测试用例脚本 +│ ├── command/ # 命令列表 (按发行版/架构) +│ ├── config/ # 配置文件 (ts.conf, oc.conf) +│ ├── kabi/ # KABI symvers (按架构) +│ ├── kconfig/ # 内核配置 (按发行版/架构) +│ ├── service/ # 服务列表 +│ ├── yum/ # yum repo 配置 +│ ├── command-execute-whitelist.sh # 原有QEMU串行测试 +│ └── command-execute-docker.sh # Docker并发测试 (新增) +├── tst_lib/ # 测试库 +│ ├── ts_common.sh # 核心公共函数 (install/remove/cmd/svc/test_package_command) +│ ├── ts_common.c # C语言测试库 +│ └── other_common.sh # 其他公共函数 +├── tst_common/ # 通用测试组件 +└── logs/ # 测试日志和结果 +``` + +## 代码规范 + +### Shell 脚本规范 +- 变量: snake_case, 全局变量加 `g_` 前缀, 局部变量加 `local` 修饰 +- 函数: snake_case +- 环境变量: 全大写 +- 以下划线 `_` 开头的函数和变量不允许用例直接调用 +- 使用 `set -uo pipefail` 严格模式 +- 使用 `[[ ]]` 而非 `[ ]` 进行条件测试(除非兼容性需要) +- 使用 `$(...)` 而非反引号进行命令替换 + +### 结果格式 +- TSV (Tab分隔), 6列: `source-package\tbinary_package\tinstall\tcommand\tservice\tremove` +- 每列取值: PASS / FAIL / SKIP + +### REASON 标记格式 (用于失败原因追溯) +``` +REASON:INSTALL:FAIL:<原因> +REASON:CMD:FAIL:<命令路径> <原因> +REASON:SVC:FAIL:<服务名> <原因> +REASON:REMOVE:FAIL:<原因> +REASON:*:SKIP:<原因> +``` + +## Docker 并发测试规则 + +### 运行环境 +- 基础镜像: `opencloudos/opencloudos:9` +- 运行环境: OpenCloudOS 9.6 VM, Docker 已安装 +- SSH: root@192.168.42.2 (密钥或密码认证) + +### 核心原则 +1. 不破坏现有用例 (command-execute-whitelist.sh 保持不变) +2. 容器内 repo 与被测系统一致 (在线模式挂载宿主 /etc/yum.repos.d) +3. 服务测试: 默认容器无 systemd, 服务标记 SKIPPED; 仅带 --privileged + /sbin/init 时才测试 start/status/stop +4. 每个包独立容器执行 install → command → service → remove +5. 测试完成后自动销毁容器 (--rm) +6. 结果格式与现有 package-command-result-*.txt 兼容 + +### 关键实现细节 +- 并发通过 `xargs -P` 实现 +- 容器测试脚本 (harness) 动态生成到 `/tmp/docker-pkg-test.XXXXXX/container-test.sh` +- ISO 模式: `--network none`, 通过 `--repofrompath` 使用本地文件 repo +- 在线模式: 允许网络, 挂载宿主 `/etc/yum.repos.d` 到容器 `/tmp/host-repos`, harness 内复制到 `/etc/yum.repos.d` +- `--no-network` 标志可在线模式下也禁用网络 +- 信号处理: Ctrl+C 时自动清理所有 pkg-test 标签容器 +- 进度计数器: 使用 flock 原子递增 + +### 正则注意事项 +- 容器 harness 使用 `grep -E` (扩展正则/ERE) +- ERE 模式下 `{n}` 无需反斜杠转义 (BRE 模式需 `\{n\}`) +- 原始 ts_common.sh 使用 BRE (grep 无 -E), 复制正则到 Docker harness 时需适配 + +## VM 验证流程 +1. 确保 Docker 守护进程运行 +2. 确保基础镜像存在 (`docker images | grep opencloudos`) +3. 准备测试数据: pkg-to-test.list, desktop-white.pkg, service-white.pkg +4. 运行: `bash testcase/command-execute-docker.sh -j 4` +5. 检查结果: `logs/docker/package-command-result-*.txt` 和 `logs/docker/test-summary-*.txt` diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh index d743109..0710bff 100755 --- a/testcase/command-execute-docker.sh +++ b/testcase/command-execute-docker.sh @@ -36,6 +36,7 @@ CPU_LIMIT=1 MEMORY_LIMIT=512 OUTPUT_DIR="./logs/docker" ISO_PATH="" +NO_NETWORK=0 # ============================================================================ # 颜色输出 @@ -65,6 +66,7 @@ usage() { 选项: --iso PATH ISO 挂载路径,用于本地 repo(推荐离线测试) + --no-network 禁用容器网络(ISO 离线模式自动启用) -j, --jobs N 并发度(默认: 4) --image IMAGE Docker 基础镜像(默认: opencloudos/opencloudos:9) --timeout N 单容器超时秒数(默认: 300) @@ -107,6 +109,7 @@ shift 3 while [ $# -gt 0 ]; do case "$1" in --iso) ISO_PATH="$(realpath "$2")"; shift 2 ;; + --no-network) NO_NETWORK=1; shift ;; -j|--jobs) JOBS="$2"; shift 2 ;; --image) BASE_IMAGE="$2"; shift 2 ;; --timeout) CONTAINER_TIMEOUT="$2"; shift 2 ;; @@ -180,9 +183,6 @@ if [ "$TOTAL_PKGS" -eq 0 ]; then exit 0 fi -HAS_APPSTREAM=0 -[ -n "$ISO_PATH" ] && [ -d "$ISO_PATH/AppStream/repodata" ] && HAS_APPSTREAM=1 - log_info "待测包: $TOTAL_PKGS | 并发: $JOBS | 镜像: $BASE_IMAGE" [ -n "$ISO_PATH" ] && log_info "ISO repo: $ISO_PATH" @@ -202,9 +202,6 @@ log_info "待测包: $TOTAL_PKGS | 并发: $JOBS | 镜像: $BASE_IMAGE" # REASON:REMOVE:SKIP:<原因> # ============================================================================ -# [改进 #9] 服务单元过滤正则提取为变量,提高可读性 -SVC_UNIT_EXCLUDE_PATTERN='@|systemd-.*\.(service|socket)|initrd.*\.(service|target)|anaconda.*\.(service|target)|coreos.*\.(service|target)|run-[a-z0-9]\{33\}\.(service|target)|man-db-cache-update\.service|rc-local\.service|nvidia-persistenced\.service|NetworkManager-wait-online\.service|systemd-fsck.*\.service|selinux-autorelabel\.service' - CONTAINER_SCRIPT="$WORKDIR/container-test.sh" cat > "$CONTAINER_SCRIPT" <<'HARNESS_EOF' #!/bin/bash @@ -214,14 +211,20 @@ PKG="$TEST_PKG_NAME" DESKTOP_WL="$TEST_DESKTOP_WL" SVC_WL="$TEST_SERVICE_WL" ISO_MOUNT="$TEST_ISO_MOUNT" +HAS_HOST_REPOS="$TEST_HAS_HOST_REPOS" -# ---------- 配置 ISO repo ---------- +# ---------- 配置 repo ---------- if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then rm -f /etc/yum.repos.d/*.repo DNF_REPOS="--repofrompath=baseos,file://${ISO_MOUNT}/BaseOS --setopt=baseos.gpgcheck=0" if [ -d "$ISO_MOUNT/AppStream/repodata" ]; then DNF_REPOS="$DNF_REPOS --repofrompath=appstream,file://${ISO_MOUNT}/AppStream --setopt=appstream.gpgcheck=0" fi +elif [ "$HAS_HOST_REPOS" = "1" ] && [ -d "/tmp/host-repos" ] && ls /tmp/host-repos/*.repo >/dev/null 2>&1; then + # 在线模式:复制宿主 repo 配置到容器,使容器 repo 与被测系统一致 + rm -f /etc/yum.repos.d/*.repo + cp /tmp/host-repos/*.repo /etc/yum.repos.d/ 2>/dev/null + DNF_REPOS="" else DNF_REPOS="" fi @@ -341,7 +344,7 @@ if [ -n "$REAL_PKG" ]; then SLIST="$(mktemp)" rpm -ql "$PKG" 2>/dev/null \ | grep -E '^/usr/lib/systemd/system/.*\.(service|socket|target)$' \ - | grep -vE "$SVC_UNIT_EXCLUDE_PATTERN" \ + | grep -vE '@|systemd-.*\.(service|socket)|initrd.*\.(service|target)|anaconda.*\.(service|target)|coreos.*\.(service|target)|run-[a-z0-9]{33}\.(service|target)|man-db-cache-update\.service|rc-local\.service|nvidia-persistenced\.service|NetworkManager-wait-online\.service|systemd-fsck.*\.service|selinux-autorelabel\.service' \ | sed 's|.*/||' > "$SLIST" if [ -s "$SLIST" ]; then @@ -441,22 +444,33 @@ run_one_package() { local docker_volumes="-v ${DESKTOP_WHITELIST}:/tmp/desktop-wl:ro" docker_volumes="$docker_volumes -v ${SERVICE_WHITELIST}:/tmp/svc-wl:ro" docker_volumes="$docker_volumes -v ${CONTAINER_SCRIPT}:/tmp/test.sh:ro" + local docker_envs="-e TEST_PKG_NAME=$pkg" + docker_envs="$docker_envs -e TEST_DESKTOP_WL=/tmp/desktop-wl" + docker_envs="$docker_envs -e TEST_SERVICE_WL=/tmp/svc-wl" + local docker_network="" if [ -n "$ISO_PATH" ]; then docker_volumes="$docker_volumes -v ${ISO_PATH}:/mnt/iso:ro" + docker_envs="$docker_envs -e TEST_ISO_MOUNT=/mnt/iso" + # ISO 离线模式:无需网络,无需宿主 repo + docker_network="--network none" + else + # 在线模式:挂载宿主 repo 配置,允许容器联网同步 repo + docker_envs="$docker_envs -e TEST_ISO_MOUNT=" + docker_volumes="$docker_volumes -v /etc/yum.repos.d:/tmp/host-repos:ro" + docker_envs="$docker_envs -e TEST_HAS_HOST_REPOS=1" + if [ "$NO_NETWORK" -eq 1 ]; then + docker_network="--network none" + fi fi - # [改进 #4] 添加 --network none,隔离网络,防止容器意外访问外网 result="$(timeout "$CONTAINER_TIMEOUT" docker run --rm \ $docker_volumes \ - -e TEST_PKG_NAME="$pkg" \ - -e TEST_DESKTOP_WL="/tmp/desktop-wl" \ - -e TEST_SERVICE_WL="/tmp/svc-wl" \ - -e TEST_ISO_MOUNT="/mnt/iso" \ + $docker_envs \ --cap-add=CHOWN --cap-add=FSETID --cap-add=FOWNER --cap-add=DAC_OVERRIDE \ --memory="${MEMORY_LIMIT}m" \ --cpus="$CPU_LIMIT" \ - --network none \ + $docker_network \ -l pkg-test=1 \ "$BASE_IMAGE" \ bash /tmp/test.sh \ @@ -493,8 +507,7 @@ run_one_package() { export -f run_one_package export DESKTOP_WHITELIST SERVICE_WHITELIST BASE_IMAGE CONTAINER_SCRIPT FRAGMENTS_DIR ISO_PATH -export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS -export SVC_UNIT_EXCLUDE_PATTERN +export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS NO_NETWORK # ============================================================================ # 执行测试 -- Gitee From c9e510bbf68b11a8558efa47fb9d165c7b3c37d1 Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Tue, 14 Jul 2026 15:05:10 +0800 Subject: [PATCH 3/7] feat: add Docker-based concurrent package test script - Container isolation per package for parallel testing - Local ISO repo support via --iso and dnf --repofrompath - --cap-add for RPM unpacking in containers - 6-column TSV output (source-pkg, binary-pkg, install, command, service, remove) - xargs -P concurrency with memory/CPU limits - Whitelist support for desktop/service packages --- testcase/command-execute-docker.sh | 685 +++++++++++++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100755 testcase/command-execute-docker.sh diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh new file mode 100755 index 0000000..9c4d21e --- /dev/null +++ b/testcase/command-execute-docker.sh @@ -0,0 +1,685 @@ +#!/bin/bash +############################################################################### +# @用例名称: command-execute-docker +# @用例级别: 3 +# @用例类型: 功能测试 +# @自动化: 1 +# +# Docker 并发软件包功能测试脚本 +# 用法: command-execute-docker.sh [选项] +# +# 功能: +# - 基于 Docker 容器实现软件包测试的并发执行与隔离 +# - 每个待测包在独立容器内完成: 安装 → 命令验证 → 服务检测 → 卸载 +# - 结果格式与现有 package-command-result-*.txt 兼容(6 列 TSV) +# - 支持本地 ISO 挂载作为 repo 源(--iso),无需网络 +# +# 服务测试方案与局限: +# 默认容器无 systemd,服务测试统一标记为 SKIPPED(原因: 容器无 systemd)。 +# 若需测试服务启停,需使用带 systemd init 的镜像(如 --privileged + /sbin/init), +# 此时脚本会自动检测 systemd 并执行 start/status(is-active)/stop 测试。 +# 局限: --privileged 降低了隔离性,init 镜像体积更大、启动更慢。 +# +# 输出格式 (Tab 分隔, 6 列): +# source-package binary_package install command service remove +############################################################################### + +set -uo pipefail + +# ============================================================================ +# 默认配置 +# ============================================================================ +JOBS=4 +BASE_IMAGE="opencloudos/opencloudos:9" +CONTAINER_TIMEOUT=300 +CPU_LIMIT=1 +MEMORY_LIMIT=512 +OUTPUT_DIR="./logs/docker" +ISO_PATH="" +NO_NETWORK=0 + +# ============================================================================ +# 颜色输出 +# ============================================================================ +if [ -t 1 ]; then + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' +else + RED='' GREEN='' YELLOW='' CYAN='' NC='' +fi + +log_info() { echo -e "${CYAN}[INFO]${NC} $(date '+%H:%M:%S') $*" >&2; } +log_ok() { echo -e "${GREEN}[OK]${NC} $(date '+%H:%M:%S') $*" >&2; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*" >&2; } +log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; } + +# ============================================================================ +# 用法说明 +# ============================================================================ +usage() { + cat >&2 <<'EOF' +用法: command-execute-docker.sh [选项] + +必需参数: + 待测软件包列表文件 (格式: pkg_name arch) + 桌面白名单文件(跳过命令测试) + 服务白名单文件(需特殊配置,跳过服务启动测试) + +选项: + --iso PATH ISO 挂载路径,用于本地 repo(推荐离线测试) + --no-network 禁用容器网络(ISO 离线模式自动启用) + -j, --jobs N 并发度(默认: 4) + --image IMAGE Docker 基础镜像(默认: opencloudos/opencloudos:9) + --timeout N 单容器超时秒数(默认: 300) + --cpu N 每容器 CPU 核数限制(默认: 1) + --memory N 每容器内存限制/MB(默认: 512) + -o, --output FILE 结果文件路径(默认: 自动命名于 OUTPUT_DIR) + --output-dir DIR 输出目录(默认: ./logs/docker) + -h, --help 显示帮助信息 + +输出格式 (Tab 分隔, 6 列): + source-package binary_package install command service remove + +示例: + command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ + --iso /run/media/user/OC-9-6-x86_64 --image opencloudos:9-local -j 8 + command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ + --image opencloudos:9-local -j 4 --timeout 600 +EOF + exit 0 +} + +# ============================================================================ +# 参数解析 +# ============================================================================ +PKG_LIST="" +DESKTOP_WHITELIST="" +SERVICE_WHITELIST="" +OUTPUT_FILE="" + +if [ $# -lt 3 ]; then + log_error "需要 3 个必需参数: pkg_list desktop_whitelist service_whitelist" + usage +fi + +PKG_LIST="$(realpath "$1")" +DESKTOP_WHITELIST="$(realpath "$2")" +SERVICE_WHITELIST="$(realpath "$3")" +shift 3 + +while [ $# -gt 0 ]; do + case "$1" in + --iso) ISO_PATH="$(realpath "$2")"; shift 2 ;; + --no-network) NO_NETWORK=1; shift ;; + -j|--jobs) JOBS="$2"; shift 2 ;; + --image) BASE_IMAGE="$2"; shift 2 ;; + --timeout) CONTAINER_TIMEOUT="$2"; shift 2 ;; + --cpu) CPU_LIMIT="$2"; shift 2 ;; + --memory) MEMORY_LIMIT="$2"; shift 2 ;; + -o|--output) OUTPUT_FILE="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + -h|--help) usage ;; + *) log_error "未知选项: $1"; usage ;; + esac +done + +# ============================================================================ +# 前置检查 +# ============================================================================ +for f in "$PKG_LIST" "$DESKTOP_WHITELIST" "$SERVICE_WHITELIST"; do + [ -f "$f" ] || { log_error "文件不存在: $f"; exit 1; } +done + +command -v docker >/dev/null 2>&1 || { log_error "docker 未安装"; exit 1; } +docker info >/dev/null 2>&1 || { log_error "Docker 守护进程未运行"; exit 1; } + +if ! docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then + log_error "镜像 '$BASE_IMAGE' 不存在,请先拉取或构建" + exit 1 +fi + +if [ -n "$ISO_PATH" ]; then + [ -d "$ISO_PATH/BaseOS/repodata" ] || { + log_error "ISO 路径 '$ISO_PATH/BaseOS/repodata' 不存在" + exit 1 + } +fi + +# ============================================================================ +# 工作目录 +# ============================================================================ +WORKDIR="$(mktemp -d "/tmp/docker-pkg-test.XXXXXX")" +TOTAL_FAIL=0 + +# [改进 #5] 信号处理: Ctrl+C 时清理孤儿容器 +CLEANUP_CONTAINERS=0 +cleanup_containers() { + docker ps -q --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker stop --time 2 >/dev/null 2>&1 + docker ps -aq --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker rm -f >/dev/null 2>&1 +} +trap 'CLEANUP_CONTAINERS=1; cleanup_containers; log_warn "收到中断信号,已清理测试容器"; exit 130' INT TERM + +trap 'if [ "${TOTAL_FAIL:-0}" -gt 0 ] || [ "$CLEANUP_CONTAINERS" -eq 1 ]; then log_warn "工作目录保留: $WORKDIR"; else rm -rf "$WORKDIR"; fi' EXIT + +FRAGMENTS_DIR="$WORKDIR/fragments" +mkdir -p "$FRAGMENTS_DIR" +mkdir -p "$OUTPUT_DIR" + +if [ -z "$OUTPUT_FILE" ]; then + OUTPUT_FILE="${OUTPUT_DIR}/package-command-result-$(date '+%Y%m%d-%H%M%S').txt" +fi + +echo -e "# source-package\tbinary_package\tinstall\tcommand\tservice\tremove" > "$OUTPUT_FILE" + +# ============================================================================ +# 过滤软件包列表 +# ============================================================================ +FILTERED_LIST="$WORKDIR/pkg.filtered.list" +grep -v 'livepatch-kernel-\|-debuginfo-\|-debugsource-' "$PKG_LIST" \ + | awk '{print $1}' | sort -u > "$FILTERED_LIST" + +TOTAL_PKGS="$(wc -l < "$FILTERED_LIST")" +if [ "$TOTAL_PKGS" -eq 0 ]; then + log_warn "过滤后无待测包" + exit 0 +fi + +log_info "待测包: $TOTAL_PKGS | 并发: $JOBS | 镜像: $BASE_IMAGE" +[ -n "$ISO_PATH" ] && log_info "ISO repo: $ISO_PATH" + +# ============================================================================ +# 生成容器内测试脚本 (harness) +# +# 输出: +# stdout: 单行 TSV (src_pkg bin_pkg install cmd svc remove) +# stderr: 调试日志 + 结构化 REASON 标记,供汇总报告解析 +# REASON:INSTALL:FAIL:<原因> +# REASON:INSTALL:SKIP:<原因> +# REASON:CMD:FAIL:<命令路径> <原因> +# REASON:CMD:SKIP:<原因> +# REASON:SVC:FAIL:<服务名> <原因> +# REASON:SVC:SKIP:<原因> +# REASON:REMOVE:FAIL:<原因> +# REASON:REMOVE:SKIP:<原因> +# ============================================================================ + +CONTAINER_SCRIPT="$WORKDIR/container-test.sh" +cat > "$CONTAINER_SCRIPT" <<'HARNESS_EOF' +#!/bin/bash +set -uo pipefail + +PKG="$TEST_PKG_NAME" +DESKTOP_WL="$TEST_DESKTOP_WL" +SVC_WL="$TEST_SERVICE_WL" +ISO_MOUNT="$TEST_ISO_MOUNT" +HAS_HOST_REPOS="$TEST_HAS_HOST_REPOS" + +# ---------- 配置 repo ---------- +if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then + rm -f /etc/yum.repos.d/*.repo + DNF_REPOS="--repofrompath=baseos,file://${ISO_MOUNT}/BaseOS --setopt=baseos.gpgcheck=0" + if [ -d "$ISO_MOUNT/AppStream/repodata" ]; then + DNF_REPOS="$DNF_REPOS --repofrompath=appstream,file://${ISO_MOUNT}/AppStream --setopt=appstream.gpgcheck=0" + fi +elif [ "$HAS_HOST_REPOS" = "1" ] && [ -d "/tmp/host-repos" ] && ls /tmp/host-repos/*.repo >/dev/null 2>&1; then + # 在线模式:复制宿主 repo 配置到容器,使容器 repo 与被测系统一致 + rm -f /etc/yum.repos.d/*.repo + cp /tmp/host-repos/*.repo /etc/yum.repos.d/ 2>/dev/null + DNF_REPOS="" +else + DNF_REPOS="" +fi + +# ---------- 获取源码包名 ---------- +get_src() { + local bin="$1" src + src="$(rpm -qi "$bin" 2>/dev/null | grep '^Source RPM' | sed 's/Source RPM: //' | sed 's/-[0-9].*//')" + if [ -n "$src" ]; then echo "$src"; return; fi + src="$(dnf $DNF_REPOS repoquery --queryformat '%{sourcerpm}' "$bin" 2>/dev/null | head -1 | sed 's/-[0-9].*//')" + echo "${src:-$bin}" +} + +# ---------- 安装 ---------- +INSTALL="FAIL" +INST_LOG="$(mktemp)" +WAS_ALREADY_INSTALLED=0 + +if dnf $DNF_REPOS install -y "$PKG" > "$INST_LOG" 2>&1; then + if grep -q "Nothing to do\|Already installed\|is already installed" "$INST_LOG" 2>/dev/null; then + WAS_ALREADY_INSTALLED=1 + fi + if ! grep -q "scriptlet failed" "$INST_LOG" && ! grep -q "No such file or directory" "$INST_LOG"; then + INSTALL="PASS" + fi +else + if grep -q "conflicts with" "$INST_LOG"; then + INSTALL="PASS" + fi +fi + +SRC_PKG="$(get_src "$PKG")" + +if [ "$INSTALL" = "FAIL" ]; then + # 提取失败原因 + reason="$(tail -5 "$INST_LOG" | tr '\n' ' ' | sed 's/ */ /g')" + echo "REASON:INSTALL:FAIL:$reason" >&2 + cat "$INST_LOG" >&2 + rm -f "$INST_LOG" + printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP\n' "$SRC_PKG" "$PKG" + exit 1 +fi + +if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then + echo "REASON:INSTALL:SKIP:已预装于基础镜像 (dnf: Already installed)" >&2 +fi +rm -f "$INST_LOG" + +# ---------- 命令测试 ---------- +CMD="SKIP" +REAL_PKG="$(rpm -q --queryformat='%{name}' "$PKG" 2>/dev/null)" + +if [ -n "$REAL_PKG" ] && [ -f "$DESKTOP_WL" ] && grep -qx "$REAL_PKG" "$DESKTOP_WL"; then + # 桌面白名单包,跳过命令测试 + echo "REASON:CMD:SKIP:桌面白名单, 跳过命令测试" >&2 +elif [ -n "$REAL_PKG" ]; then + FLIST="$(mktemp)" + CLIST="$(mktemp)" + + rpm -ql "$PKG" 2>/dev/null \ + | grep -E '^/usr/(bin|sbin)/' | grep -v '\.so' | grep -v '/usr/bin/nde-' > "$FLIST" + + if [ -s "$FLIST" ]; then + xargs file < "$FLIST" 2>/dev/null | grep 'ELF ' | sed 's/:.*//' | while read -r f; do + [ -x "$f" ] && echo "$f" + done > "$CLIST" + fi + + if [ -s "$CLIST" ]; then + NR="$(wc -l < "$CLIST")" + IDX=0 + CFAIL=0 + while [ $IDX -lt $NR ]; do + IDX=$((IDX + 1)) + TCMD="$(sed -n "${IDX}p" "$CLIST")" + [ -z "$TCMD" ] && continue + CLOG="$(mktemp)" + CPASS=0 + CFAIL_REASON="" + if timeout -s SIGKILL 3 "$TCMD" --help > "$CLOG" 2>&1; then + CPASS=1 + elif timeout -s SIGKILL 3 "$TCMD" --version >> "$CLOG" 2>&1; then + CPASS=1 + elif grep -q "error while loading shared libraries" "$CLOG"; then + CFAIL_REASON="缺少共享库 $(grep 'error while loading' "$CLOG" | head -1 | sed 's/.*: //')" + CPASS=0 + elif ! realpath "$TCMD" >/dev/null 2>&1; then + CFAIL_REASON="realpath 失败 (符号链接目标不存在)" + CPASS=0 + elif timeout -s SIGKILL 3 "$TCMD" >> "$CLOG" 2>&1; then + CPASS=1 + elif grep -iqw 'invalid\|argument\|Unknown\|option\|Usage' "$CLOG"; then + CPASS=1 + else + CFAIL_REASON="退出码非0" + CPASS=0 + fi + if [ $CPASS -eq 0 ]; then + CFAIL=$((CFAIL + 1)) + echo "REASON:CMD:FAIL:$TCMD $CFAIL_REASON" >&2 + fi + rm -f "$CLOG" + done + [ $CFAIL -eq 0 ] && CMD="PASS" || CMD="FAIL" + fi + rm -f "$FLIST" "$CLIST" +fi + +# ---------- 服务测试 ---------- +SVC="SKIP" +HAS_SYSTEMD=0 +if pidof systemd >/dev/null 2>&1; then + HAS_SYSTEMD=1 +fi + +if [ -n "$REAL_PKG" ]; then + SLIST="$(mktemp)" + rpm -ql "$PKG" 2>/dev/null \ + | grep -E '^/usr/lib/systemd/system/.*\.(service|socket|target)$' \ + | grep -vE '@|systemd-.*\.(service|socket)|initrd.*\.(service|target)|anaconda.*\.(service|target)|coreos.*\.(service|target)|run-[a-z0-9]{33}\.(service|target)|man-db-cache-update\.service|rc-local\.service|nvidia-persistenced\.service|NetworkManager-wait-online\.service|systemd-fsck.*\.service|selinux-autorelabel\.service' \ + | sed 's|.*/||' > "$SLIST" + + if [ -s "$SLIST" ]; then + # 检查是否在服务白名单中 + IS_SVC_WHITELIST=0 + if [ -f "$SVC_WL" ] && grep -q "^${REAL_PKG}" "$SVC_WL"; then + IS_SVC_WHITELIST=1 + fi + + if [ "$IS_SVC_WHITELIST" -eq 1 ]; then + SVC="SKIP" + echo "REASON:SVC:SKIP:含服务包, 需特殊配置, 跳过服务启动" >&2 + elif [ "$HAS_SYSTEMD" -eq 0 ]; then + SVC="SKIP" + echo "REASON:SVC:SKIP:容器环境不支持 systemd, 无法测试服务启停" >&2 + else + # 有 systemd,执行 start/status/stop 测试 + SFAIL=0 + while read -r svc; do + [ -z "$svc" ] && continue + # start + if ! timeout -s SIGKILL 30 systemctl --no-pager start "$svc" >/dev/null 2>&1; then + SFAIL=$((SFAIL + 1)) + echo "REASON:SVC:FAIL:$svc start 失败 (systemctl start 返回非0)" >&2 + continue + fi + # status / is-active + if ! systemctl --no-pager is-active "$svc" >/dev/null 2>&1; then + SFAIL=$((SFAIL + 1)) + echo "REASON:SVC:FAIL:$svc status 未 active (systemctl is-active 检查失败)" >&2 + systemctl --no-pager stop "$svc" >/dev/null 2>&1 + continue + fi + # stop + if ! timeout -s SIGKILL 30 systemctl --no-pager stop "$svc" >/dev/null 2>&1; then + if systemctl --no-pager stop "$svc" 2>&1 | grep -iqw "configured to refuse manual start/stop"; then + # 拒绝手动 stop 不算失败 + : + else + SFAIL=$((SFAIL + 1)) + echo "REASON:SVC:FAIL:$svc stop 失败 (systemctl stop 返回非0)" >&2 + fi + fi + done < "$SLIST" + [ $SFAIL -eq 0 ] && SVC="PASS" || SVC="FAIL" + fi + else + # 无 systemd unit 文件 + SVC="PASS" + fi + rm -f "$SLIST" +fi + +# ---------- 卸载 ---------- +REMOVE="FAIL" +RM_LOG="$(mktemp)" + +if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then + REMOVE="PASS" + echo "REASON:REMOVE:SKIP:已预装于基础镜像, 跳过卸载" >&2 +else + if dnf $DNF_REPOS remove -y --setopt=clean_requirements_on_remove=False "$PKG" > "$RM_LOG" 2>&1; then + if grep -q "scriptlet failed" "$RM_LOG"; then + reason="$(grep 'scriptlet failed' "$RM_LOG" | head -1 | tr '\n' ' ')" + echo "REASON:REMOVE:FAIL:$reason" >&2 + cat "$RM_LOG" >&2 + else + REMOVE="PASS" + fi + else + if grep -iq "protected packages" "$RM_LOG"; then + REMOVE="PASS" + echo "REASON:REMOVE:SKIP:受保护包, 跳过卸载" >&2 + else + reason="$(tail -5 "$RM_LOG" | tr '\n' ' ' | sed 's/ */ /g')" + echo "REASON:REMOVE:FAIL:$reason" >&2 + cat "$RM_LOG" >&2 + fi + fi +fi +rm -f "$RM_LOG" + +printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$SRC_PKG" "$PKG" "$INSTALL" "$CMD" "$SVC" "$REMOVE" +HARNESS_EOF +chmod +x "$CONTAINER_SCRIPT" + +# ============================================================================ +# 进度计数器(改进 #7) +# ============================================================================ +PROGRESS_FILE="$WORKDIR/progress" +echo "0" > "$PROGRESS_FILE" + +run_one_package() { + local pkg="$1" + local result rc=0 + + local docker_volumes="-v ${DESKTOP_WHITELIST}:/tmp/desktop-wl:ro" + docker_volumes="$docker_volumes -v ${SERVICE_WHITELIST}:/tmp/svc-wl:ro" + docker_volumes="$docker_volumes -v ${CONTAINER_SCRIPT}:/tmp/test.sh:ro" + local docker_envs="-e TEST_PKG_NAME=$pkg" + docker_envs="$docker_envs -e TEST_DESKTOP_WL=/tmp/desktop-wl" + docker_envs="$docker_envs -e TEST_SERVICE_WL=/tmp/svc-wl" + local docker_network="" + + if [ -n "$ISO_PATH" ]; then + docker_volumes="$docker_volumes -v ${ISO_PATH}:/mnt/iso:ro" + docker_envs="$docker_envs -e TEST_ISO_MOUNT=/mnt/iso" + # ISO 离线模式:无需网络,无需宿主 repo + docker_network="--network none" + else + # 在线模式:挂载宿主 repo 配置,允许容器联网同步 repo + docker_envs="$docker_envs -e TEST_ISO_MOUNT=" + docker_volumes="$docker_volumes -v /etc/yum.repos.d:/tmp/host-repos:ro" + docker_envs="$docker_envs -e TEST_HAS_HOST_REPOS=1" + if [ "$NO_NETWORK" -eq 1 ]; then + docker_network="--network none" + fi + fi + + result="$(timeout "$CONTAINER_TIMEOUT" docker run --rm \ + $docker_volumes \ + $docker_envs \ + --cap-add=CHOWN --cap-add=FSETID --cap-add=FOWNER --cap-add=DAC_OVERRIDE \ + --memory="${MEMORY_LIMIT}m" \ + --cpus="$CPU_LIMIT" \ + $docker_network \ + -l pkg-test=1 \ + "$BASE_IMAGE" \ + bash /tmp/test.sh \ + 2>"$FRAGMENTS_DIR/${pkg}.log")" || rc=$? + + # [Bug #1 修复] 超时/异常退出时使用 \t 分隔,保持 TSV 格式一致 + if [ $rc -eq 124 ]; then + echo "REASON:INSTALL:FAIL:安装超时(>${CONTAINER_TIMEOUT}s)" >> "$FRAGMENTS_DIR/${pkg}.log" + result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" + log_error "[$pkg] 超时 (${CONTAINER_TIMEOUT}s)" + elif [ $rc -ne 0 ] && [ -z "$result" ]; then + echo "REASON:INSTALL:FAIL:容器异常退出(退出码:$rc)" >> "$FRAGMENTS_DIR/${pkg}.log" + result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" + fi + + echo "$result" > "$FRAGMENTS_DIR/${pkg}.result" + + local inst cmd svc rm + inst="$(echo "$result" | awk -F'\t' '{print $3}')" + cmd="$(echo "$result" | awk -F'\t' '{print $4}')" + svc="$(echo "$result" | awk -F'\t' '{print $5}')" + rm="$(echo "$result" | awk -F'\t' '{print $6}')" + + # [改进 #7] 原子递增进度 + local done_count + done_count="$(flock "$PROGRESS_FILE" bash -c "echo \$(( \$(cat '$PROGRESS_FILE') + 1 )) | tee '$PROGRESS_FILE'")" + + if [ "$inst" = "PASS" ] && [ "$cmd" != "FAIL" ] && [ "$svc" != "FAIL" ] && [ "$rm" = "PASS" ]; then + log_ok "[${done_count}/${TOTAL_PKGS}] $pkg install=$inst cmd=$cmd svc=$svc remove=$rm" + else + log_error "[${done_count}/${TOTAL_PKGS}] $pkg install=$inst cmd=$cmd svc=$svc remove=$rm" + fi +} + +export -f run_one_package +export DESKTOP_WHITELIST SERVICE_WHITELIST BASE_IMAGE CONTAINER_SCRIPT FRAGMENTS_DIR ISO_PATH +export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS NO_NETWORK + +# ============================================================================ +# 执行测试 +# ============================================================================ +log_info "开始: $TOTAL_PKGS 个包, $JOBS 并发" +echo "---------------------------------------------------------------" >&2 + +START_TIME="$(date +%s)" +xargs -P "$JOBS" -I{} bash -c 'run_one_package "$@"' _ {} < "$FILTERED_LIST" +END_TIME="$(date +%s)" +ELAPSED=$(( END_TIME - START_TIME )) + +echo "---------------------------------------------------------------" >&2 +log_info "完成,耗时 ${ELAPSED}s" + +# ============================================================================ +# 合并结果 +# ============================================================================ +find "$FRAGMENTS_DIR" -name '*.result' -print0 \ + | sort -z | xargs -0 cat \ + | sort -t$'\t' -k2,2 \ + >> "$OUTPUT_FILE" + +# ============================================================================ +# 生成汇总报告(匹配验收标准格式) +# ============================================================================ +SUMMARY_FILE="${OUTPUT_DIR}/test-summary-$(date '+%Y%m%d-%H%M%S').txt" +TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" + +TOTAL=0 +PASSED=0 +FAILED=0 +SKIPPED=0 + +# 分类收集 +INSTALL_FAIL_LIST=() +REMOVE_FAIL_LIST=() +CMD_FAIL_LIST=() +SVC_FAIL_LIST=() +SKIP_LIST=() + +while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do + [[ "$src_pkg" == \#* ]] && continue + [[ -z "$src_pkg" ]] && continue + TOTAL=$((TOTAL + 1)) + + pkg_failed=0 + pkg_all_skip=1 # [改进 #3] 跟踪是否所有阶段都是 SKIP + pkg_has_skip=0 + + # ---- 安装 ---- + if [ "$install" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:INSTALL:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:INSTALL:FAIL://' | tr '\t' ' ')" + INSTALL_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") + elif [ "$install" = "PASS" ]; then + pkg_all_skip=0 + if grep -q 'REASON:INSTALL:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then + pkg_has_skip=1 + fi + fi + + # ---- 命令 ---- + if [ "$cmd" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:CMD:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:CMD:FAIL://' | tr '\t' ' ')" + CMD_FAIL_LIST+=("${bin_pkg} ${reason:-命令测试失败}") + elif [ "$cmd" = "PASS" ]; then + pkg_all_skip=0 + elif [ "$cmd" = "SKIP" ]; then + pkg_has_skip=1 + fi + + # ---- 服务 ---- + if [ "$svc" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:SVC:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:SVC:FAIL://' | tr '\t' ' ')" + SVC_FAIL_LIST+=("${bin_pkg} ${reason:-服务测试失败}") + elif [ "$svc" = "PASS" ]; then + pkg_all_skip=0 + elif [ "$svc" = "SKIP" ]; then + pkg_has_skip=1 + fi + + # ---- 卸载 ---- + if [ "$remove" = "FAIL" ]; then + pkg_failed=1 + reason="$(grep 'REASON:REMOVE:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:REMOVE:FAIL://' | tr '\t' ' ')" + REMOVE_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") + elif [ "$remove" = "PASS" ]; then + pkg_all_skip=0 + if grep -q 'REASON:REMOVE:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then + pkg_has_skip=1 + fi + fi + + # ---- 分类(改进 #3: PASS+合理SKIP → PASSED,仅全部SKIP → SKIPPED)---- + if [ $pkg_failed -eq 1 ]; then + FAILED=$((FAILED + 1)) + elif [ $pkg_all_skip -eq 1 ] && [ $pkg_has_skip -eq 1 ]; then + # 所有阶段都是 SKIP(如安装SKIP+卸载SKIP的预装保护包) + SKIPPED=$((SKIPPED + 1)) + all_skips="$(grep 'REASON:.*:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | sed 's/REASON:[^:]*:SKIP://' | tr '\n' ', ' | sed 's/, $//')" + SKIP_LIST+=("${bin_pkg} (${all_skips:-全部跳过})") + else + # 有 PASS 且无 FAIL → 通过(即使部分阶段是合理的 SKIP) + PASSED=$((PASSED + 1)) + fi +done < "$OUTPUT_FILE" + +# ---- 写入汇总文件 ---- +{ + echo "========== 测试失败汇总 (${TIMESTAMP}) ==========" + echo "环境: docker | 并发度: ${JOBS} | 待测包总数: ${TOTAL} | 通过: ${PASSED} | 失败: ${FAILED} | 跳过: ${SKIPPED}" + echo "" + + echo "【安装失败】(${#INSTALL_FAIL_LIST[@]})" + if [ ${#INSTALL_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${INSTALL_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【卸载失败】(${#REMOVE_FAIL_LIST[@]})" + if [ ${#REMOVE_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${REMOVE_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【命令测试失败】(${#CMD_FAIL_LIST[@]})" + if [ ${#CMD_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${CMD_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【服务测试失败】(${#SVC_FAIL_LIST[@]})" + if [ ${#SVC_FAIL_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${SVC_FAIL_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "【跳过项】(${#SKIP_LIST[@]})" + if [ ${#SKIP_LIST[@]} -eq 0 ]; then + echo "(无)" + else + for item in "${SKIP_LIST[@]}"; do echo " $item"; done + fi + echo "" + + echo "=======================================================" + echo "对账: 通过(${PASSED}) + 失败(${FAILED}) + 跳过(${SKIPPED}) = $((PASSED + FAILED + SKIPPED)) / 待测总数(${TOTAL})" + if [ $((PASSED + FAILED + SKIPPED)) -ne "$TOTAL" ]; then + echo " *** 对账不匹配! ***" + else + echo " 对账匹配" + fi +} | tee "$SUMMARY_FILE" + +echo "" +echo "===============================================================" +echo " Result file: $OUTPUT_FILE" +echo " Summary file: $SUMMARY_FILE" +echo " Per-pkg logs: $FRAGMENTS_DIR/*.log" +echo "===============================================================" + +TOTAL_FAIL="$FAILED" +[ $TOTAL_FAIL -gt 0 ] && exit 1 +exit 0 \ No newline at end of file -- Gitee From f5037644f32aabdfdb6c141cd8f8e1cfed7702b8 Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Fri, 31 Jul 2026 01:49:08 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20CRLF=20?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C=E7=AC=A6=E4=B8=BA=20LF=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E9=BB=98=E8=AE=A4=E9=95=9C=E5=83=8F=E5=90=8D=E4=B8=BA?= =?UTF-8?q?=20opencloudos:9-local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/rules/project.md | 142 +++++++-------- testcase/command-execute-docker.sh | 270 +++++++++++++---------------- 2 files changed, 189 insertions(+), 223 deletions(-) diff --git a/.trae/rules/project.md b/.trae/rules/project.md index f951d0a..9397269 100644 --- a/.trae/rules/project.md +++ b/.trae/rules/project.md @@ -1,90 +1,82 @@ -# yum-ci-pkg-test 项目规则 +# yum-ci-pkg-test 椤圭洰瑙勫垯 -## 项目概述 -OpenCloudOS 软件包 CI 测试套(yum-ci-pkg-test),用于验证软件包的功能,包括安装/卸载、二进制命令可执行性和服务测试。 +## 椤圭洰姒傝堪 +OpenCloudOS 杞欢鍖?CI 娴嬭瘯濂楋紙yum-ci-pkg-test锛夛紝鐢ㄤ簬楠岃瘉杞欢鍖呯殑鍔熻兘锛屽寘鎷畨瑁?鍗歌浇銆佷簩杩涘埗鍛戒护鍙墽琛屾€у拰鏈嶅姟娴嬭瘯銆? +## 鎶€鏈爤 +- Shell (Bash) 鑴氭湰涓轰富 +- 娴嬭瘯妗嗘灦: tsuite (C璇█缂栧啓鐨勮嚜瀹氫箟娴嬭瘯妗嗘灦) +- Docker (骞跺彂娴嬭瘯鏂规) +- OpenCloudOS 9.x (鐩爣杩愯鐜) -## 技术栈 -- Shell (Bash) 脚本为主 -- 测试框架: tsuite (C语言编写的自定义测试框架) -- Docker (并发测试方案) -- OpenCloudOS 9.x (目标运行环境) - -## 目录结构 +## 鐩綍缁撴瀯 ``` yum-ci-pkg-test/ -├── testcase/ # 测试用例脚本 -│ ├── command/ # 命令列表 (按发行版/架构) -│ ├── config/ # 配置文件 (ts.conf, oc.conf) -│ ├── kabi/ # KABI symvers (按架构) -│ ├── kconfig/ # 内核配置 (按发行版/架构) -│ ├── service/ # 服务列表 -│ ├── yum/ # yum repo 配置 -│ ├── command-execute-whitelist.sh # 原有QEMU串行测试 -│ └── command-execute-docker.sh # Docker并发测试 (新增) -├── tst_lib/ # 测试库 -│ ├── ts_common.sh # 核心公共函数 (install/remove/cmd/svc/test_package_command) -│ ├── ts_common.c # C语言测试库 -│ └── other_common.sh # 其他公共函数 -├── tst_common/ # 通用测试组件 -└── logs/ # 测试日志和结果 -``` - -## 代码规范 +鈹溾攢鈹€ testcase/ # 娴嬭瘯鐢ㄤ緥鑴氭湰 +鈹? 鈹溾攢鈹€ command/ # 鍛戒护鍒楄〃 (鎸夊彂琛岀増/鏋舵瀯) +鈹? 鈹溾攢鈹€ config/ # 閰嶇疆鏂囦欢 (ts.conf, oc.conf) +鈹? 鈹溾攢鈹€ kabi/ # KABI symvers (鎸夋灦鏋? +鈹? 鈹溾攢鈹€ kconfig/ # 鍐呮牳閰嶇疆 (鎸夊彂琛岀増/鏋舵瀯) +鈹? 鈹溾攢鈹€ service/ # 鏈嶅姟鍒楄〃 +鈹? 鈹溾攢鈹€ yum/ # yum repo 閰嶇疆 +鈹? 鈹溾攢鈹€ command-execute-whitelist.sh # 鍘熸湁QEMU涓茶娴嬭瘯 +鈹? 鈹斺攢鈹€ command-execute-docker.sh # Docker骞跺彂娴嬭瘯 (鏂板) +鈹溾攢鈹€ tst_lib/ # 娴嬭瘯搴?鈹? 鈹溾攢鈹€ ts_common.sh # 鏍稿績鍏叡鍑芥暟 (install/remove/cmd/svc/test_package_command) +鈹? 鈹溾攢鈹€ ts_common.c # C璇█娴嬭瘯搴?鈹? 鈹斺攢鈹€ other_common.sh # 鍏朵粬鍏叡鍑芥暟 +鈹溾攢鈹€ tst_common/ # 閫氱敤娴嬭瘯缁勪欢 +鈹斺攢鈹€ logs/ # 娴嬭瘯鏃ュ織鍜岀粨鏋?``` -### Shell 脚本规范 -- 变量: snake_case, 全局变量加 `g_` 前缀, 局部变量加 `local` 修饰 -- 函数: snake_case -- 环境变量: 全大写 -- 以下划线 `_` 开头的函数和变量不允许用例直接调用 -- 使用 `set -uo pipefail` 严格模式 -- 使用 `[[ ]]` 而非 `[ ]` 进行条件测试(除非兼容性需要) -- 使用 `$(...)` 而非反引号进行命令替换 +## 浠g爜瑙勮寖 -### 结果格式 -- TSV (Tab分隔), 6列: `source-package\tbinary_package\tinstall\tcommand\tservice\tremove` -- 每列取值: PASS / FAIL / SKIP +### Shell 鑴氭湰瑙勮寖 +- 鍙橀噺: snake_case, 鍏ㄥ眬鍙橀噺鍔?`g_` 鍓嶇紑, 灞€閮ㄥ彉閲忓姞 `local` 淇グ +- 鍑芥暟: snake_case +- 鐜鍙橀噺: 鍏ㄥぇ鍐?- 浠ヤ笅鍒掔嚎 `_` 寮€澶寸殑鍑芥暟鍜屽彉閲忎笉鍏佽鐢ㄤ緥鐩存帴璋冪敤 +- 浣跨敤 `set -uo pipefail` 涓ユ牸妯″紡 +- 浣跨敤 `[[ ]]` 鑰岄潪 `[ ]` 杩涜鏉′欢娴嬭瘯锛堥櫎闈炲吋瀹规€ч渶瑕侊級 +- 浣跨敤 `$(...)` 鑰岄潪鍙嶅紩鍙疯繘琛屽懡浠ゆ浛鎹? +### 缁撴灉鏍煎紡 +- TSV (Tab鍒嗛殧), 6鍒? `source-package\tbinary_package\tinstall\tcommand\tservice\tremove` +- 姣忓垪鍙栧€? PASS / FAIL / SKIP -### REASON 标记格式 (用于失败原因追溯) +### REASON 鏍囪鏍煎紡 (鐢ㄤ簬澶辫触鍘熷洜杩芥函) ``` -REASON:INSTALL:FAIL:<原因> -REASON:CMD:FAIL:<命令路径> <原因> -REASON:SVC:FAIL:<服务名> <原因> -REASON:REMOVE:FAIL:<原因> -REASON:*:SKIP:<原因> +REASON:INSTALL:FAIL:<鍘熷洜> +REASON:CMD:FAIL:<鍛戒护璺緞> <鍘熷洜> +REASON:SVC:FAIL:<鏈嶅姟鍚? <鍘熷洜> +REASON:REMOVE:FAIL:<鍘熷洜> +REASON:*:SKIP:<鍘熷洜> ``` -## Docker 并发测试规则 +## Docker 骞跺彂娴嬭瘯瑙勫垯 -### 运行环境 -- 基础镜像: `opencloudos/opencloudos:9` -- 运行环境: OpenCloudOS 9.6 VM, Docker 已安装 -- SSH: root@192.168.42.2 (密钥或密码认证) +### 杩愯鐜 +- 鍩虹闀滃儚: `opencloudos/opencloudos:9` +- 杩愯鐜: OpenCloudOS 9.6 VM, Docker 宸插畨瑁?- SSH: root@192.168.42.2 (瀵嗛挜鎴栧瘑鐮佽璇? -### 核心原则 -1. 不破坏现有用例 (command-execute-whitelist.sh 保持不变) -2. 容器内 repo 与被测系统一致 (在线模式挂载宿主 /etc/yum.repos.d) -3. 服务测试: 默认容器无 systemd, 服务标记 SKIPPED; 仅带 --privileged + /sbin/init 时才测试 start/status/stop -4. 每个包独立容器执行 install → command → service → remove -5. 测试完成后自动销毁容器 (--rm) -6. 结果格式与现有 package-command-result-*.txt 兼容 +### 鏍稿績鍘熷垯 +1. 涓嶇牬鍧忕幇鏈夌敤渚?(command-execute-whitelist.sh 淇濇寔涓嶅彉) +2. 瀹瑰櫒鍐?repo 涓庤娴嬬郴缁熶竴鑷?(鍦ㄧ嚎妯″紡鎸傝浇瀹夸富 /etc/yum.repos.d) +3. 鏈嶅姟娴嬭瘯: 榛樿瀹瑰櫒鏃?systemd, 鏈嶅姟鏍囪 SKIPPED; 浠呭甫 --privileged + /sbin/init 鏃舵墠娴嬭瘯 start/status/stop +4. 姣忎釜鍖呯嫭绔嬪鍣ㄦ墽琛?install 鈫?command 鈫?service 鈫?remove +5. 娴嬭瘯瀹屾垚鍚庤嚜鍔ㄩ攢姣佸鍣?(--rm) +6. 缁撴灉鏍煎紡涓庣幇鏈?package-command-result-*.txt 鍏煎 -### 关键实现细节 -- 并发通过 `xargs -P` 实现 -- 容器测试脚本 (harness) 动态生成到 `/tmp/docker-pkg-test.XXXXXX/container-test.sh` -- ISO 模式: `--network none`, 通过 `--repofrompath` 使用本地文件 repo -- 在线模式: 允许网络, 挂载宿主 `/etc/yum.repos.d` 到容器 `/tmp/host-repos`, harness 内复制到 `/etc/yum.repos.d` -- `--no-network` 标志可在线模式下也禁用网络 -- 信号处理: Ctrl+C 时自动清理所有 pkg-test 标签容器 -- 进度计数器: 使用 flock 原子递增 +### 鍏抽敭瀹炵幇缁嗚妭 +- 骞跺彂閫氳繃 `xargs -P` 瀹炵幇 +- 瀹瑰櫒娴嬭瘯鑴氭湰 (harness) 鍔ㄦ€佺敓鎴愬埌 `/tmp/docker-pkg-test.XXXXXX/container-test.sh` +- ISO 妯″紡: `--network none`, 閫氳繃 `--repofrompath` 浣跨敤鏈湴鏂囦欢 repo +- 鍦ㄧ嚎妯″紡: 鍏佽缃戠粶, 鎸傝浇瀹夸富 `/etc/yum.repos.d` 鍒板鍣?`/tmp/host-repos`, harness 鍐呭鍒跺埌 `/etc/yum.repos.d` +- `--no-network` 鏍囧織鍙湪绾挎ā寮忎笅涔熺鐢ㄧ綉缁?- 淇″彿澶勭悊: Ctrl+C 鏃惰嚜鍔ㄦ竻鐞嗘墍鏈?pkg-test 鏍囩瀹瑰櫒 +- 杩涘害璁℃暟鍣? 浣跨敤 flock 鍘熷瓙閫掑 -### 正则注意事项 -- 容器 harness 使用 `grep -E` (扩展正则/ERE) -- ERE 模式下 `{n}` 无需反斜杠转义 (BRE 模式需 `\{n\}`) -- 原始 ts_common.sh 使用 BRE (grep 无 -E), 复制正则到 Docker harness 时需适配 +### 姝e垯娉ㄦ剰浜嬮」 +- 瀹瑰櫒 harness 浣跨敤 `grep -E` (鎵╁睍姝e垯/ERE) +- ERE 妯″紡涓?`{n}` 鏃犻渶鍙嶆枩鏉犺浆涔?(BRE 妯″紡闇€ `\{n\}`) +- 鍘熷 ts_common.sh 浣跨敤 BRE (grep 鏃?-E), 澶嶅埗姝e垯鍒?Docker harness 鏃堕渶閫傞厤 -## VM 验证流程 -1. 确保 Docker 守护进程运行 -2. 确保基础镜像存在 (`docker images | grep opencloudos`) -3. 准备测试数据: pkg-to-test.list, desktop-white.pkg, service-white.pkg -4. 运行: `bash testcase/command-execute-docker.sh -j 4` -5. 检查结果: `logs/docker/package-command-result-*.txt` 和 `logs/docker/test-summary-*.txt` +## VM 楠岃瘉娴佺▼ +1. 纭繚 Docker 瀹堟姢杩涚▼杩愯 +2. 纭繚鍩虹闀滃儚瀛樺湪 (`docker images | grep opencloudos`) +3. 鍑嗗娴嬭瘯鏁版嵁: pkg-to-test.list, desktop-white.pkg, service-white.pkg +4. 杩愯: `bash testcase/command-execute-docker.sh -j 4` +5. 妫€鏌ョ粨鏋? `logs/docker/package-command-result-*.txt` 鍜?`logs/docker/test-summary-*.txt` diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh index 0710bff..912350f 100755 --- a/testcase/command-execute-docker.sh +++ b/testcase/command-execute-docker.sh @@ -1,36 +1,30 @@ #!/bin/bash ############################################################################### -# @用例名称: command-execute-docker -# @用例级别: 3 -# @用例类型: 功能测试 -# @自动化: 1 +# @鐢ㄤ緥鍚嶇О: command-execute-docker +# @鐢ㄤ緥绾у埆: 3 +# @鐢ㄤ緥绫诲瀷: 鍔熻兘娴嬭瘯 +# @鑷姩鍖? 1 # -# Docker 并发软件包功能测试脚本 -# 用法: command-execute-docker.sh [选项] +# Docker 骞跺彂杞欢鍖呭姛鑳芥祴璇曡剼鏈?# 鐢ㄦ硶: command-execute-docker.sh [閫夐」] # -# 功能: -# - 基于 Docker 容器实现软件包测试的并发执行与隔离 -# - 每个待测包在独立容器内完成: 安装 → 命令验证 → 服务检测 → 卸载 -# - 结果格式与现有 package-command-result-*.txt 兼容(6 列 TSV) -# - 支持本地 ISO 挂载作为 repo 源(--iso),无需网络 +# 鍔熻兘: +# - 鍩轰簬 Docker 瀹瑰櫒瀹炵幇杞欢鍖呮祴璇曠殑骞跺彂鎵ц涓庨殧绂?# - 姣忎釜寰呮祴鍖呭湪鐙珛瀹瑰櫒鍐呭畬鎴? 瀹夎 鈫?鍛戒护楠岃瘉 鈫?鏈嶅姟妫€娴?鈫?鍗歌浇 +# - 缁撴灉鏍煎紡涓庣幇鏈?package-command-result-*.txt 鍏煎锛? 鍒?TSV锛?# - 鏀寔鏈湴 ISO 鎸傝浇浣滀负 repo 婧愶紙--iso锛夛紝鏃犻渶缃戠粶 # -# 服务测试方案与局限: -# 默认容器无 systemd,服务测试统一标记为 SKIPPED(原因: 容器无 systemd)。 -# 若需测试服务启停,需使用带 systemd init 的镜像(如 --privileged + /sbin/init), -# 此时脚本会自动检测 systemd 并执行 start/status(is-active)/stop 测试。 -# 局限: --privileged 降低了隔离性,init 镜像体积更大、启动更慢。 -# -# 输出格式 (Tab 分隔, 6 列): +# 鏈嶅姟娴嬭瘯鏂规涓庡眬闄? +# 榛樿瀹瑰櫒鏃?systemd锛屾湇鍔℃祴璇曠粺涓€鏍囪涓?SKIPPED锛堝師鍥? 瀹瑰櫒鏃?systemd锛夈€?# 鑻ラ渶娴嬭瘯鏈嶅姟鍚仠锛岄渶浣跨敤甯?systemd init 鐨勯暅鍍忥紙濡?--privileged + /sbin/init锛夛紝 +# 姝ゆ椂鑴氭湰浼氳嚜鍔ㄦ娴?systemd 骞舵墽琛?start/status(is-active)/stop 娴嬭瘯銆?# 灞€闄? --privileged 闄嶄綆浜嗛殧绂绘€э紝init 闀滃儚浣撶Н鏇村ぇ銆佸惎鍔ㄦ洿鎱€?# +# 杈撳嚭鏍煎紡 (Tab 鍒嗛殧, 6 鍒?: # source-package binary_package install command service remove ############################################################################### set -uo pipefail # ============================================================================ -# 默认配置 +# 榛樿閰嶇疆 # ============================================================================ JOBS=4 -BASE_IMAGE="opencloudos/opencloudos:9" +BASE_IMAGE="opencloudos:9-local" CONTAINER_TIMEOUT=300 CPU_LIMIT=1 MEMORY_LIMIT=512 @@ -39,7 +33,7 @@ ISO_PATH="" NO_NETWORK=0 # ============================================================================ -# 颜色输出 +# 棰滆壊杈撳嚭 # ============================================================================ if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' @@ -53,33 +47,24 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*" >&2; } log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; } # ============================================================================ -# 用法说明 +# 鐢ㄦ硶璇存槑 # ============================================================================ usage() { cat >&2 <<'EOF' -用法: command-execute-docker.sh [选项] - -必需参数: - 待测软件包列表文件 (格式: pkg_name arch) - 桌面白名单文件(跳过命令测试) - 服务白名单文件(需特殊配置,跳过服务启动测试) - -选项: - --iso PATH ISO 挂载路径,用于本地 repo(推荐离线测试) - --no-network 禁用容器网络(ISO 离线模式自动启用) - -j, --jobs N 并发度(默认: 4) - --image IMAGE Docker 基础镜像(默认: opencloudos/opencloudos:9) - --timeout N 单容器超时秒数(默认: 300) - --cpu N 每容器 CPU 核数限制(默认: 1) - --memory N 每容器内存限制/MB(默认: 512) - -o, --output FILE 结果文件路径(默认: 自动命名于 OUTPUT_DIR) - --output-dir DIR 输出目录(默认: ./logs/docker) - -h, --help 显示帮助信息 - -输出格式 (Tab 分隔, 6 列): +鐢ㄦ硶: command-execute-docker.sh [閫夐」] + +蹇呴渶鍙傛暟: + 寰呮祴杞欢鍖呭垪琛ㄦ枃浠?(鏍煎紡: pkg_name arch) + 妗岄潰鐧藉悕鍗曟枃浠讹紙璺宠繃鍛戒护娴嬭瘯锛? 鏈嶅姟鐧藉悕鍗曟枃浠讹紙闇€鐗规畩閰嶇疆锛岃烦杩囨湇鍔″惎鍔ㄦ祴璇曪級 + +閫夐」: + --iso PATH ISO 鎸傝浇璺緞锛岀敤浜庢湰鍦?repo锛堟帹鑽愮绾挎祴璇曪級 + --no-network 绂佺敤瀹瑰櫒缃戠粶锛圛SO 绂荤嚎妯″紡鑷姩鍚敤锛? -j, --jobs N 骞跺彂搴︼紙榛樿: 4锛? --image IMAGE Docker 鍩虹闀滃儚锛堥粯璁? opencloudos/opencloudos:9锛? --timeout N 鍗曞鍣ㄨ秴鏃剁鏁帮紙榛樿: 300锛? --cpu N 姣忓鍣?CPU 鏍告暟闄愬埗锛堥粯璁? 1锛? --memory N 姣忓鍣ㄥ唴瀛橀檺鍒?MB锛堥粯璁? 512锛? -o, --output FILE 缁撴灉鏂囦欢璺緞锛堥粯璁? 鑷姩鍛藉悕浜?OUTPUT_DIR锛? --output-dir DIR 杈撳嚭鐩綍锛堥粯璁? ./logs/docker锛? -h, --help 鏄剧ず甯姪淇℃伅 + +杈撳嚭鏍煎紡 (Tab 鍒嗛殧, 6 鍒?: source-package binary_package install command service remove -示例: +绀轰緥: command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ --iso /run/media/user/OC-9-6-x86_64 --image opencloudos:9-local -j 8 command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ @@ -89,7 +74,7 @@ EOF } # ============================================================================ -# 参数解析 +# 鍙傛暟瑙f瀽 # ============================================================================ PKG_LIST="" DESKTOP_WHITELIST="" @@ -97,7 +82,7 @@ SERVICE_WHITELIST="" OUTPUT_FILE="" if [ $# -lt 3 ]; then - log_error "需要 3 个必需参数: pkg_list desktop_whitelist service_whitelist" + log_error "闇€瑕?3 涓繀闇€鍙傛暟: pkg_list desktop_whitelist service_whitelist" usage fi @@ -118,47 +103,45 @@ while [ $# -gt 0 ]; do -o|--output) OUTPUT_FILE="$2"; shift 2 ;; --output-dir) OUTPUT_DIR="$2"; shift 2 ;; -h|--help) usage ;; - *) log_error "未知选项: $1"; usage ;; + *) log_error "鏈煡閫夐」: $1"; usage ;; esac done # ============================================================================ -# 前置检查 -# ============================================================================ +# 鍓嶇疆妫€鏌?# ============================================================================ for f in "$PKG_LIST" "$DESKTOP_WHITELIST" "$SERVICE_WHITELIST"; do - [ -f "$f" ] || { log_error "文件不存在: $f"; exit 1; } + [ -f "$f" ] || { log_error "鏂囦欢涓嶅瓨鍦? $f"; exit 1; } done -command -v docker >/dev/null 2>&1 || { log_error "docker 未安装"; exit 1; } -docker info >/dev/null 2>&1 || { log_error "Docker 守护进程未运行"; exit 1; } +command -v docker >/dev/null 2>&1 || { log_error "docker 鏈畨瑁?; exit 1; } +docker info >/dev/null 2>&1 || { log_error "Docker 瀹堟姢杩涚▼鏈繍琛?; exit 1; } if ! docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then - log_error "镜像 '$BASE_IMAGE' 不存在,请先拉取或构建" + log_error "闀滃儚 '$BASE_IMAGE' 涓嶅瓨鍦紝璇峰厛鎷夊彇鎴栨瀯寤? exit 1 fi if [ -n "$ISO_PATH" ]; then [ -d "$ISO_PATH/BaseOS/repodata" ] || { - log_error "ISO 路径 '$ISO_PATH/BaseOS/repodata' 不存在" + log_error "ISO 璺緞 '$ISO_PATH/BaseOS/repodata' 涓嶅瓨鍦? exit 1 } fi # ============================================================================ -# 工作目录 +# 宸ヤ綔鐩綍 # ============================================================================ WORKDIR="$(mktemp -d "/tmp/docker-pkg-test.XXXXXX")" TOTAL_FAIL=0 -# [改进 #5] 信号处理: Ctrl+C 时清理孤儿容器 -CLEANUP_CONTAINERS=0 +# [鏀硅繘 #5] 淇″彿澶勭悊: Ctrl+C 鏃舵竻鐞嗗鍎垮鍣?CLEANUP_CONTAINERS=0 cleanup_containers() { docker ps -q --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker stop --time 2 >/dev/null 2>&1 docker ps -aq --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker rm -f >/dev/null 2>&1 } -trap 'CLEANUP_CONTAINERS=1; cleanup_containers; log_warn "收到中断信号,已清理测试容器"; exit 130' INT TERM +trap 'CLEANUP_CONTAINERS=1; cleanup_containers; log_warn "鏀跺埌涓柇淇″彿锛屽凡娓呯悊娴嬭瘯瀹瑰櫒"; exit 130' INT TERM -trap 'if [ "${TOTAL_FAIL:-0}" -gt 0 ] || [ "$CLEANUP_CONTAINERS" -eq 1 ]; then log_warn "工作目录保留: $WORKDIR"; else rm -rf "$WORKDIR"; fi' EXIT +trap 'if [ "${TOTAL_FAIL:-0}" -gt 0 ] || [ "$CLEANUP_CONTAINERS" -eq 1 ]; then log_warn "宸ヤ綔鐩綍淇濈暀: $WORKDIR"; else rm -rf "$WORKDIR"; fi' EXIT FRAGMENTS_DIR="$WORKDIR/fragments" mkdir -p "$FRAGMENTS_DIR" @@ -171,35 +154,33 @@ fi echo -e "# source-package\tbinary_package\tinstall\tcommand\tservice\tremove" > "$OUTPUT_FILE" # ============================================================================ -# 过滤软件包列表 -# ============================================================================ +# 杩囨护杞欢鍖呭垪琛?# ============================================================================ FILTERED_LIST="$WORKDIR/pkg.filtered.list" grep -v 'livepatch-kernel-\|-debuginfo-\|-debugsource-' "$PKG_LIST" \ | awk '{print $1}' | sort -u > "$FILTERED_LIST" TOTAL_PKGS="$(wc -l < "$FILTERED_LIST")" if [ "$TOTAL_PKGS" -eq 0 ]; then - log_warn "过滤后无待测包" + log_warn "杩囨护鍚庢棤寰呮祴鍖? exit 0 fi -log_info "待测包: $TOTAL_PKGS | 并发: $JOBS | 镜像: $BASE_IMAGE" +log_info "寰呮祴鍖? $TOTAL_PKGS | 骞跺彂: $JOBS | 闀滃儚: $BASE_IMAGE" [ -n "$ISO_PATH" ] && log_info "ISO repo: $ISO_PATH" # ============================================================================ -# 生成容器内测试脚本 (harness) +# 鐢熸垚瀹瑰櫒鍐呮祴璇曡剼鏈?(harness) # -# 输出: -# stdout: 单行 TSV (src_pkg bin_pkg install cmd svc remove) -# stderr: 调试日志 + 结构化 REASON 标记,供汇总报告解析 -# REASON:INSTALL:FAIL:<原因> -# REASON:INSTALL:SKIP:<原因> -# REASON:CMD:FAIL:<命令路径> <原因> -# REASON:CMD:SKIP:<原因> -# REASON:SVC:FAIL:<服务名> <原因> -# REASON:SVC:SKIP:<原因> -# REASON:REMOVE:FAIL:<原因> -# REASON:REMOVE:SKIP:<原因> +# 杈撳嚭: +# stdout: 鍗曡 TSV (src_pkg bin_pkg install cmd svc remove) +# stderr: 璋冭瘯鏃ュ織 + 缁撴瀯鍖?REASON 鏍囪锛屼緵姹囨€绘姤鍛婅В鏋?# REASON:INSTALL:FAIL:<鍘熷洜> +# REASON:INSTALL:SKIP:<鍘熷洜> +# REASON:CMD:FAIL:<鍛戒护璺緞> <鍘熷洜> +# REASON:CMD:SKIP:<鍘熷洜> +# REASON:SVC:FAIL:<鏈嶅姟鍚? <鍘熷洜> +# REASON:SVC:SKIP:<鍘熷洜> +# REASON:REMOVE:FAIL:<鍘熷洜> +# REASON:REMOVE:SKIP:<鍘熷洜> # ============================================================================ CONTAINER_SCRIPT="$WORKDIR/container-test.sh" @@ -213,7 +194,7 @@ SVC_WL="$TEST_SERVICE_WL" ISO_MOUNT="$TEST_ISO_MOUNT" HAS_HOST_REPOS="$TEST_HAS_HOST_REPOS" -# ---------- 配置 repo ---------- +# ---------- 閰嶇疆 repo ---------- if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then rm -f /etc/yum.repos.d/*.repo DNF_REPOS="--repofrompath=baseos,file://${ISO_MOUNT}/BaseOS --setopt=baseos.gpgcheck=0" @@ -221,15 +202,14 @@ if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then DNF_REPOS="$DNF_REPOS --repofrompath=appstream,file://${ISO_MOUNT}/AppStream --setopt=appstream.gpgcheck=0" fi elif [ "$HAS_HOST_REPOS" = "1" ] && [ -d "/tmp/host-repos" ] && ls /tmp/host-repos/*.repo >/dev/null 2>&1; then - # 在线模式:复制宿主 repo 配置到容器,使容器 repo 与被测系统一致 - rm -f /etc/yum.repos.d/*.repo + # 鍦ㄧ嚎妯″紡锛氬鍒跺涓?repo 閰嶇疆鍒板鍣紝浣垮鍣?repo 涓庤娴嬬郴缁熶竴鑷? rm -f /etc/yum.repos.d/*.repo cp /tmp/host-repos/*.repo /etc/yum.repos.d/ 2>/dev/null DNF_REPOS="" else DNF_REPOS="" fi -# ---------- 获取源码包名 ---------- +# ---------- 鑾峰彇婧愮爜鍖呭悕 ---------- get_src() { local bin="$1" src src="$(rpm -qi "$bin" 2>/dev/null | grep '^Source RPM' | sed 's/Source RPM: //' | sed 's/-[0-9].*//')" @@ -238,7 +218,7 @@ get_src() { echo "${src:-$bin}" } -# ---------- 安装 ---------- +# ---------- 瀹夎 ---------- INSTALL="FAIL" INST_LOG="$(mktemp)" WAS_ALREADY_INSTALLED=0 @@ -259,7 +239,7 @@ fi SRC_PKG="$(get_src "$PKG")" if [ "$INSTALL" = "FAIL" ]; then - # 提取失败原因 + # 鎻愬彇澶辫触鍘熷洜 reason="$(tail -5 "$INST_LOG" | tr '\n' ' ' | sed 's/ */ /g')" echo "REASON:INSTALL:FAIL:$reason" >&2 cat "$INST_LOG" >&2 @@ -269,17 +249,16 @@ if [ "$INSTALL" = "FAIL" ]; then fi if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then - echo "REASON:INSTALL:SKIP:已预装于基础镜像 (dnf: Already installed)" >&2 + echo "REASON:INSTALL:SKIP:宸查瑁呬簬鍩虹闀滃儚 (dnf: Already installed)" >&2 fi rm -f "$INST_LOG" -# ---------- 命令测试 ---------- +# ---------- 鍛戒护娴嬭瘯 ---------- CMD="SKIP" REAL_PKG="$(rpm -q --queryformat='%{name}' "$PKG" 2>/dev/null)" if [ -n "$REAL_PKG" ] && [ -f "$DESKTOP_WL" ] && grep -qx "$REAL_PKG" "$DESKTOP_WL"; then - # 桌面白名单包,跳过命令测试 - echo "REASON:CMD:SKIP:桌面白名单, 跳过命令测试" >&2 + # 妗岄潰鐧藉悕鍗曞寘锛岃烦杩囧懡浠ゆ祴璇? echo "REASON:CMD:SKIP:妗岄潰鐧藉悕鍗? 璺宠繃鍛戒护娴嬭瘯" >&2 elif [ -n "$REAL_PKG" ]; then FLIST="$(mktemp)" CLIST="$(mktemp)" @@ -309,17 +288,17 @@ elif [ -n "$REAL_PKG" ]; then elif timeout -s SIGKILL 3 "$TCMD" --version >> "$CLOG" 2>&1; then CPASS=1 elif grep -q "error while loading shared libraries" "$CLOG"; then - CFAIL_REASON="缺少共享库 $(grep 'error while loading' "$CLOG" | head -1 | sed 's/.*: //')" + CFAIL_REASON="缂哄皯鍏变韩搴?$(grep 'error while loading' "$CLOG" | head -1 | sed 's/.*: //')" CPASS=0 elif ! realpath "$TCMD" >/dev/null 2>&1; then - CFAIL_REASON="realpath 失败 (符号链接目标不存在)" + CFAIL_REASON="realpath 澶辫触 (绗﹀彿閾炬帴鐩爣涓嶅瓨鍦?" CPASS=0 elif timeout -s SIGKILL 3 "$TCMD" >> "$CLOG" 2>&1; then CPASS=1 elif grep -iqw 'invalid\|argument\|Unknown\|option\|Usage' "$CLOG"; then CPASS=1 else - CFAIL_REASON="退出码非0" + CFAIL_REASON="閫€鍑虹爜闈?" CPASS=0 fi if [ $CPASS -eq 0 ]; then @@ -333,7 +312,7 @@ elif [ -n "$REAL_PKG" ]; then rm -f "$FLIST" "$CLIST" fi -# ---------- 服务测试 ---------- +# ---------- 鏈嶅姟娴嬭瘯 ---------- SVC="SKIP" HAS_SYSTEMD=0 if pidof systemd >/dev/null 2>&1; then @@ -348,7 +327,7 @@ if [ -n "$REAL_PKG" ]; then | sed 's|.*/||' > "$SLIST" if [ -s "$SLIST" ]; then - # 检查是否在服务白名单中 + # 妫€鏌ユ槸鍚﹀湪鏈嶅姟鐧藉悕鍗曚腑 IS_SVC_WHITELIST=0 if [ -f "$SVC_WL" ] && grep -q "^${REAL_PKG}" "$SVC_WL"; then IS_SVC_WHITELIST=1 @@ -356,55 +335,55 @@ if [ -n "$REAL_PKG" ]; then if [ "$IS_SVC_WHITELIST" -eq 1 ]; then SVC="SKIP" - echo "REASON:SVC:SKIP:含服务包, 需特殊配置, 跳过服务启动" >&2 + echo "REASON:SVC:SKIP:鍚湇鍔″寘, 闇€鐗规畩閰嶇疆, 璺宠繃鏈嶅姟鍚姩" >&2 elif [ "$HAS_SYSTEMD" -eq 0 ]; then SVC="SKIP" - echo "REASON:SVC:SKIP:容器环境不支持 systemd, 无法测试服务启停" >&2 + echo "REASON:SVC:SKIP:瀹瑰櫒鐜涓嶆敮鎸?systemd, 鏃犳硶娴嬭瘯鏈嶅姟鍚仠" >&2 else - # 有 systemd,执行 start/status/stop 测试 + # 鏈?systemd锛屾墽琛?start/status/stop 娴嬭瘯 SFAIL=0 while read -r svc; do [ -z "$svc" ] && continue # start if ! timeout -s SIGKILL 30 systemctl --no-pager start "$svc" >/dev/null 2>&1; then SFAIL=$((SFAIL + 1)) - echo "REASON:SVC:FAIL:$svc start 失败 (systemctl start 返回非0)" >&2 + echo "REASON:SVC:FAIL:$svc start 澶辫触 (systemctl start 杩斿洖闈?)" >&2 continue fi # status / is-active if ! systemctl --no-pager is-active "$svc" >/dev/null 2>&1; then SFAIL=$((SFAIL + 1)) - echo "REASON:SVC:FAIL:$svc status 未 active (systemctl is-active 检查失败)" >&2 + echo "REASON:SVC:FAIL:$svc status 鏈?active (systemctl is-active 妫€鏌ュけ璐?" >&2 systemctl --no-pager stop "$svc" >/dev/null 2>&1 continue fi # stop if ! timeout -s SIGKILL 30 systemctl --no-pager stop "$svc" >/dev/null 2>&1; then if systemctl --no-pager stop "$svc" 2>&1 | grep -iqw "configured to refuse manual start/stop"; then - # 拒绝手动 stop 不算失败 + # 鎷掔粷鎵嬪姩 stop 涓嶇畻澶辫触 : else SFAIL=$((SFAIL + 1)) - echo "REASON:SVC:FAIL:$svc stop 失败 (systemctl stop 返回非0)" >&2 + echo "REASON:SVC:FAIL:$svc stop 澶辫触 (systemctl stop 杩斿洖闈?)" >&2 fi fi done < "$SLIST" [ $SFAIL -eq 0 ] && SVC="PASS" || SVC="FAIL" fi else - # 无 systemd unit 文件 + # 鏃?systemd unit 鏂囦欢 SVC="PASS" fi rm -f "$SLIST" fi -# ---------- 卸载 ---------- +# ---------- 鍗歌浇 ---------- REMOVE="FAIL" RM_LOG="$(mktemp)" if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then REMOVE="PASS" - echo "REASON:REMOVE:SKIP:已预装于基础镜像, 跳过卸载" >&2 + echo "REASON:REMOVE:SKIP:宸查瑁呬簬鍩虹闀滃儚, 璺宠繃鍗歌浇" >&2 else if dnf $DNF_REPOS remove -y --setopt=clean_requirements_on_remove=False "$PKG" > "$RM_LOG" 2>&1; then if grep -q "scriptlet failed" "$RM_LOG"; then @@ -417,7 +396,7 @@ else else if grep -iq "protected packages" "$RM_LOG"; then REMOVE="PASS" - echo "REASON:REMOVE:SKIP:受保护包, 跳过卸载" >&2 + echo "REASON:REMOVE:SKIP:鍙椾繚鎶ゅ寘, 璺宠繃鍗歌浇" >&2 else reason="$(tail -5 "$RM_LOG" | tr '\n' ' ' | sed 's/ */ /g')" echo "REASON:REMOVE:FAIL:$reason" >&2 @@ -432,8 +411,7 @@ HARNESS_EOF chmod +x "$CONTAINER_SCRIPT" # ============================================================================ -# 进度计数器(改进 #7) -# ============================================================================ +# 杩涘害璁℃暟鍣紙鏀硅繘 #7锛?# ============================================================================ PROGRESS_FILE="$WORKDIR/progress" echo "0" > "$PROGRESS_FILE" @@ -452,10 +430,10 @@ run_one_package() { if [ -n "$ISO_PATH" ]; then docker_volumes="$docker_volumes -v ${ISO_PATH}:/mnt/iso:ro" docker_envs="$docker_envs -e TEST_ISO_MOUNT=/mnt/iso" - # ISO 离线模式:无需网络,无需宿主 repo + # ISO 绂荤嚎妯″紡锛氭棤闇€缃戠粶锛屾棤闇€瀹夸富 repo docker_network="--network none" else - # 在线模式:挂载宿主 repo 配置,允许容器联网同步 repo + # 鍦ㄧ嚎妯″紡锛氭寕杞藉涓?repo 閰嶇疆锛屽厑璁稿鍣ㄨ仈缃戝悓姝?repo docker_envs="$docker_envs -e TEST_ISO_MOUNT=" docker_volumes="$docker_volumes -v /etc/yum.repos.d:/tmp/host-repos:ro" docker_envs="$docker_envs -e TEST_HAS_HOST_REPOS=1" @@ -476,13 +454,12 @@ run_one_package() { bash /tmp/test.sh \ 2>"$FRAGMENTS_DIR/${pkg}.log")" || rc=$? - # [Bug #1 修复] 超时/异常退出时使用 \t 分隔,保持 TSV 格式一致 - if [ $rc -eq 124 ]; then - echo "REASON:INSTALL:FAIL:安装超时(>${CONTAINER_TIMEOUT}s)" >> "$FRAGMENTS_DIR/${pkg}.log" + # [Bug #1 淇] 瓒呮椂/寮傚父閫€鍑烘椂浣跨敤 \t 鍒嗛殧锛屼繚鎸?TSV 鏍煎紡涓€鑷? if [ $rc -eq 124 ]; then + echo "REASON:INSTALL:FAIL:瀹夎瓒呮椂(>${CONTAINER_TIMEOUT}s)" >> "$FRAGMENTS_DIR/${pkg}.log" result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" - log_error "[$pkg] 超时 (${CONTAINER_TIMEOUT}s)" + log_error "[$pkg] 瓒呮椂 (${CONTAINER_TIMEOUT}s)" elif [ $rc -ne 0 ] && [ -z "$result" ]; then - echo "REASON:INSTALL:FAIL:容器异常退出(退出码:$rc)" >> "$FRAGMENTS_DIR/${pkg}.log" + echo "REASON:INSTALL:FAIL:瀹瑰櫒寮傚父閫€鍑?閫€鍑虹爜:$rc)" >> "$FRAGMENTS_DIR/${pkg}.log" result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" fi @@ -494,7 +471,7 @@ run_one_package() { svc="$(echo "$result" | awk -F'\t' '{print $5}')" rm="$(echo "$result" | awk -F'\t' '{print $6}')" - # [改进 #7] 原子递增进度 + # [鏀硅繘 #7] 鍘熷瓙閫掑杩涘害 local done_count done_count="$(flock "$PROGRESS_FILE" bash -c "echo \$(( \$(cat '$PROGRESS_FILE') + 1 )) | tee '$PROGRESS_FILE'")" @@ -510,9 +487,9 @@ export DESKTOP_WHITELIST SERVICE_WHITELIST BASE_IMAGE CONTAINER_SCRIPT FRAGMENTS export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS NO_NETWORK # ============================================================================ -# 执行测试 +# 鎵ц娴嬭瘯 # ============================================================================ -log_info "开始: $TOTAL_PKGS 个包, $JOBS 并发" +log_info "寮€濮? $TOTAL_PKGS 涓寘, $JOBS 骞跺彂" echo "---------------------------------------------------------------" >&2 START_TIME="$(date +%s)" @@ -521,10 +498,10 @@ END_TIME="$(date +%s)" ELAPSED=$(( END_TIME - START_TIME )) echo "---------------------------------------------------------------" >&2 -log_info "完成,耗时 ${ELAPSED}s" +log_info "瀹屾垚锛岃€楁椂 ${ELAPSED}s" # ============================================================================ -# 合并结果 +# 鍚堝苟缁撴灉 # ============================================================================ find "$FRAGMENTS_DIR" -name '*.result' -print0 \ | sort -z | xargs -0 cat \ @@ -532,8 +509,7 @@ find "$FRAGMENTS_DIR" -name '*.result' -print0 \ >> "$OUTPUT_FILE" # ============================================================================ -# 生成汇总报告(匹配验收标准格式) -# ============================================================================ +# 鐢熸垚姹囨€绘姤鍛婏紙鍖归厤楠屾敹鏍囧噯鏍煎紡锛?# ============================================================================ SUMMARY_FILE="${OUTPUT_DIR}/test-summary-$(date '+%Y%m%d-%H%M%S').txt" TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" @@ -542,7 +518,7 @@ PASSED=0 FAILED=0 SKIPPED=0 -# 分类收集 +# 鍒嗙被鏀堕泦 INSTALL_FAIL_LIST=() REMOVE_FAIL_LIST=() CMD_FAIL_LIST=() @@ -555,14 +531,14 @@ while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do TOTAL=$((TOTAL + 1)) pkg_failed=0 - pkg_all_skip=1 # [改进 #3] 跟踪是否所有阶段都是 SKIP + pkg_all_skip=1 # [鏀硅繘 #3] 璺熻釜鏄惁鎵€鏈夐樁娈甸兘鏄?SKIP pkg_has_skip=0 - # ---- 安装 ---- + # ---- 瀹夎 ---- if [ "$install" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:INSTALL:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:INSTALL:FAIL://' | tr '\t' ' ')" - INSTALL_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") + INSTALL_FAIL_LIST+=("${bin_pkg} 鍘熷洜: ${reason:-鏈煡}") elif [ "$install" = "PASS" ]; then pkg_all_skip=0 if grep -q 'REASON:INSTALL:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then @@ -570,33 +546,33 @@ while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do fi fi - # ---- 命令 ---- + # ---- 鍛戒护 ---- if [ "$cmd" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:CMD:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:CMD:FAIL://' | tr '\t' ' ')" - CMD_FAIL_LIST+=("${bin_pkg} ${reason:-命令测试失败}") + CMD_FAIL_LIST+=("${bin_pkg} ${reason:-鍛戒护娴嬭瘯澶辫触}") elif [ "$cmd" = "PASS" ]; then pkg_all_skip=0 elif [ "$cmd" = "SKIP" ]; then pkg_has_skip=1 fi - # ---- 服务 ---- + # ---- 鏈嶅姟 ---- if [ "$svc" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:SVC:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:SVC:FAIL://' | tr '\t' ' ')" - SVC_FAIL_LIST+=("${bin_pkg} ${reason:-服务测试失败}") + SVC_FAIL_LIST+=("${bin_pkg} ${reason:-鏈嶅姟娴嬭瘯澶辫触}") elif [ "$svc" = "PASS" ]; then pkg_all_skip=0 elif [ "$svc" = "SKIP" ]; then pkg_has_skip=1 fi - # ---- 卸载 ---- + # ---- 鍗歌浇 ---- if [ "$remove" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:REMOVE:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:REMOVE:FAIL://' | tr '\t' ' ')" - REMOVE_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") + REMOVE_FAIL_LIST+=("${bin_pkg} 鍘熷洜: ${reason:-鏈煡}") elif [ "$remove" = "PASS" ]; then pkg_all_skip=0 if grep -q 'REASON:REMOVE:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then @@ -604,72 +580,70 @@ while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do fi fi - # ---- 分类(改进 #3: PASS+合理SKIP → PASSED,仅全部SKIP → SKIPPED)---- + # ---- 鍒嗙被锛堟敼杩?#3: PASS+鍚堢悊SKIP 鈫?PASSED锛屼粎鍏ㄩ儴SKIP 鈫?SKIPPED锛?--- if [ $pkg_failed -eq 1 ]; then FAILED=$((FAILED + 1)) elif [ $pkg_all_skip -eq 1 ] && [ $pkg_has_skip -eq 1 ]; then - # 所有阶段都是 SKIP(如安装SKIP+卸载SKIP的预装保护包) - SKIPPED=$((SKIPPED + 1)) + # 鎵€鏈夐樁娈甸兘鏄?SKIP锛堝瀹夎SKIP+鍗歌浇SKIP鐨勯瑁呬繚鎶ゅ寘锛? SKIPPED=$((SKIPPED + 1)) all_skips="$(grep 'REASON:.*:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | sed 's/REASON:[^:]*:SKIP://' | tr '\n' ', ' | sed 's/, $//')" - SKIP_LIST+=("${bin_pkg} (${all_skips:-全部跳过})") + SKIP_LIST+=("${bin_pkg} (${all_skips:-鍏ㄩ儴璺宠繃})") else - # 有 PASS 且无 FAIL → 通过(即使部分阶段是合理的 SKIP) - PASSED=$((PASSED + 1)) + # 鏈?PASS 涓旀棤 FAIL 鈫?閫氳繃锛堝嵆浣块儴鍒嗛樁娈垫槸鍚堢悊鐨?SKIP锛? PASSED=$((PASSED + 1)) fi done < "$OUTPUT_FILE" -# ---- 写入汇总文件 ---- +# ---- 鍐欏叆姹囨€绘枃浠?---- { - echo "========== 测试失败汇总 (${TIMESTAMP}) ==========" - echo "环境: docker | 并发度: ${JOBS} | 待测包总数: ${TOTAL} | 通过: ${PASSED} | 失败: ${FAILED} | 跳过: ${SKIPPED}" + echo "========== 娴嬭瘯澶辫触姹囨€?(${TIMESTAMP}) ==========" + echo "鐜: docker | 骞跺彂搴? ${JOBS} | 寰呮祴鍖呮€绘暟: ${TOTAL} | 閫氳繃: ${PASSED} | 澶辫触: ${FAILED} | 璺宠繃: ${SKIPPED}" echo "" - echo "【安装失败】(${#INSTALL_FAIL_LIST[@]})" + echo "銆愬畨瑁呭け璐ャ€?${#INSTALL_FAIL_LIST[@]})" if [ ${#INSTALL_FAIL_LIST[@]} -eq 0 ]; then - echo "(无)" + echo "(鏃?" else for item in "${INSTALL_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "【卸载失败】(${#REMOVE_FAIL_LIST[@]})" + echo "銆愬嵏杞藉け璐ャ€?${#REMOVE_FAIL_LIST[@]})" if [ ${#REMOVE_FAIL_LIST[@]} -eq 0 ]; then - echo "(无)" + echo "(鏃?" else for item in "${REMOVE_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "【命令测试失败】(${#CMD_FAIL_LIST[@]})" + echo "銆愬懡浠ゆ祴璇曞け璐ャ€?${#CMD_FAIL_LIST[@]})" if [ ${#CMD_FAIL_LIST[@]} -eq 0 ]; then - echo "(无)" + echo "(鏃?" else for item in "${CMD_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "【服务测试失败】(${#SVC_FAIL_LIST[@]})" + echo "銆愭湇鍔℃祴璇曞け璐ャ€?${#SVC_FAIL_LIST[@]})" if [ ${#SVC_FAIL_LIST[@]} -eq 0 ]; then - echo "(无)" + echo "(鏃?" else for item in "${SVC_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "【跳过项】(${#SKIP_LIST[@]})" + echo "銆愯烦杩囬」銆?${#SKIP_LIST[@]})" if [ ${#SKIP_LIST[@]} -eq 0 ]; then - echo "(无)" + echo "(鏃?" else for item in "${SKIP_LIST[@]}"; do echo " $item"; done fi echo "" echo "=======================================================" - echo "对账: 通过(${PASSED}) + 失败(${FAILED}) + 跳过(${SKIPPED}) = $((PASSED + FAILED + SKIPPED)) / 待测总数(${TOTAL})" + echo "瀵硅处: 閫氳繃(${PASSED}) + 澶辫触(${FAILED}) + 璺宠繃(${SKIPPED}) = $((PASSED + FAILED + SKIPPED)) / 寰呮祴鎬绘暟(${TOTAL})" if [ $((PASSED + FAILED + SKIPPED)) -ne "$TOTAL" ]; then - echo " *** 对账不匹配! ***" + echo " *** 瀵硅处涓嶅尮閰? ***" else - echo " 对账匹配" + echo " 瀵硅处鍖归厤" fi } | tee "$SUMMARY_FILE" -- Gitee From 3f44e881ecf4f80c49ea13911b590bc7e17702d9 Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Fri, 31 Jul 2026 01:56:15 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DDocker=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=B5=8B=E8=AF=95=E5=85=B3=E9=94=AE=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=AD=A3CRLF=E4=B8=BALF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 --network none 硬编码:ISO模式自动启用,在线模式默认允许网络,新增 --no-network 2. 修复在线模式repo同步:挂载宿主 /etc/yum.repos.d 到容器 3. 修复 SVC_UNIT_EXCLUDE_PATTERN ERE正则 (BRE {33} -> ERE {33}),硬编码入harness 4. 修正默认镜像为 opencloudos:9-local 5. 清理死代码、添加 host-repos 空目录边界检查 6. 添加 .gitattributes 强制 *.sh 使用 LF,添加项目规则 --- testcase/command-execute-docker.sh | 266 ++++++++++++++++------------- 1 file changed, 145 insertions(+), 121 deletions(-) diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh index 912350f..76b3d8a 100755 --- a/testcase/command-execute-docker.sh +++ b/testcase/command-execute-docker.sh @@ -1,27 +1,33 @@ #!/bin/bash ############################################################################### -# @鐢ㄤ緥鍚嶇О: command-execute-docker -# @鐢ㄤ緥绾у埆: 3 -# @鐢ㄤ緥绫诲瀷: 鍔熻兘娴嬭瘯 -# @鑷姩鍖? 1 +# @用例名称: command-execute-docker +# @用例级别: 3 +# @用例类型: 功能测试 +# @自动化: 1 # -# Docker 骞跺彂杞欢鍖呭姛鑳芥祴璇曡剼鏈?# 鐢ㄦ硶: command-execute-docker.sh [閫夐」] +# Docker 并发软件包功能测试脚本 +# 用法: command-execute-docker.sh [选项] # -# 鍔熻兘: -# - 鍩轰簬 Docker 瀹瑰櫒瀹炵幇杞欢鍖呮祴璇曠殑骞跺彂鎵ц涓庨殧绂?# - 姣忎釜寰呮祴鍖呭湪鐙珛瀹瑰櫒鍐呭畬鎴? 瀹夎 鈫?鍛戒护楠岃瘉 鈫?鏈嶅姟妫€娴?鈫?鍗歌浇 -# - 缁撴灉鏍煎紡涓庣幇鏈?package-command-result-*.txt 鍏煎锛? 鍒?TSV锛?# - 鏀寔鏈湴 ISO 鎸傝浇浣滀负 repo 婧愶紙--iso锛夛紝鏃犻渶缃戠粶 +# 功能: +# - 基于 Docker 容器实现软件包测试的并发执行与隔离 +# - 每个待测包在独立容器内完成: 安装 → 命令验证 → 服务检测 → 卸载 +# - 结果格式与现有 package-command-result-*.txt 兼容(6 列 TSV) +# - 支持本地 ISO 挂载作为 repo 源(--iso),无需网络 # -# 鏈嶅姟娴嬭瘯鏂规涓庡眬闄? -# 榛樿瀹瑰櫒鏃?systemd锛屾湇鍔℃祴璇曠粺涓€鏍囪涓?SKIPPED锛堝師鍥? 瀹瑰櫒鏃?systemd锛夈€?# 鑻ラ渶娴嬭瘯鏈嶅姟鍚仠锛岄渶浣跨敤甯?systemd init 鐨勯暅鍍忥紙濡?--privileged + /sbin/init锛夛紝 -# 姝ゆ椂鑴氭湰浼氳嚜鍔ㄦ娴?systemd 骞舵墽琛?start/status(is-active)/stop 娴嬭瘯銆?# 灞€闄? --privileged 闄嶄綆浜嗛殧绂绘€э紝init 闀滃儚浣撶Н鏇村ぇ銆佸惎鍔ㄦ洿鎱€?# -# 杈撳嚭鏍煎紡 (Tab 鍒嗛殧, 6 鍒?: +# 服务测试方案与局限: +# 默认容器无 systemd,服务测试统一标记为 SKIPPED(原因: 容器无 systemd)。 +# 若需测试服务启停,需使用带 systemd init 的镜像(如 --privileged + /sbin/init), +# 此时脚本会自动检测 systemd 并执行 start/status(is-active)/stop 测试。 +# 局限: --privileged 降低了隔离性,init 镜像体积更大、启动更慢。 +# +# 输出格式 (Tab 分隔, 6 列): # source-package binary_package install command service remove ############################################################################### set -uo pipefail # ============================================================================ -# 榛樿閰嶇疆 +# 默认配置 # ============================================================================ JOBS=4 BASE_IMAGE="opencloudos:9-local" @@ -33,7 +39,7 @@ ISO_PATH="" NO_NETWORK=0 # ============================================================================ -# 棰滆壊杈撳嚭 +# 颜色输出 # ============================================================================ if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' @@ -47,24 +53,33 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*" >&2; } log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; } # ============================================================================ -# 鐢ㄦ硶璇存槑 +# 用法说明 # ============================================================================ usage() { cat >&2 <<'EOF' -鐢ㄦ硶: command-execute-docker.sh [閫夐」] - -蹇呴渶鍙傛暟: - 寰呮祴杞欢鍖呭垪琛ㄦ枃浠?(鏍煎紡: pkg_name arch) - 妗岄潰鐧藉悕鍗曟枃浠讹紙璺宠繃鍛戒护娴嬭瘯锛? 鏈嶅姟鐧藉悕鍗曟枃浠讹紙闇€鐗规畩閰嶇疆锛岃烦杩囨湇鍔″惎鍔ㄦ祴璇曪級 - -閫夐」: - --iso PATH ISO 鎸傝浇璺緞锛岀敤浜庢湰鍦?repo锛堟帹鑽愮绾挎祴璇曪級 - --no-network 绂佺敤瀹瑰櫒缃戠粶锛圛SO 绂荤嚎妯″紡鑷姩鍚敤锛? -j, --jobs N 骞跺彂搴︼紙榛樿: 4锛? --image IMAGE Docker 鍩虹闀滃儚锛堥粯璁? opencloudos/opencloudos:9锛? --timeout N 鍗曞鍣ㄨ秴鏃剁鏁帮紙榛樿: 300锛? --cpu N 姣忓鍣?CPU 鏍告暟闄愬埗锛堥粯璁? 1锛? --memory N 姣忓鍣ㄥ唴瀛橀檺鍒?MB锛堥粯璁? 512锛? -o, --output FILE 缁撴灉鏂囦欢璺緞锛堥粯璁? 鑷姩鍛藉悕浜?OUTPUT_DIR锛? --output-dir DIR 杈撳嚭鐩綍锛堥粯璁? ./logs/docker锛? -h, --help 鏄剧ず甯姪淇℃伅 - -杈撳嚭鏍煎紡 (Tab 鍒嗛殧, 6 鍒?: +用法: command-execute-docker.sh [选项] + +必需参数: + 待测软件包列表文件 (格式: pkg_name arch) + 桌面白名单文件(跳过命令测试) + 服务白名单文件(需特殊配置,跳过服务启动测试) + +选项: + --iso PATH ISO 挂载路径,用于本地 repo(推荐离线测试) + --no-network 禁用容器网络(ISO 离线模式自动启用) + -j, --jobs N 并发度(默认: 4) + --image IMAGE Docker 基础镜像(默认: opencloudos/opencloudos:9) + --timeout N 单容器超时秒数(默认: 300) + --cpu N 每容器 CPU 核数限制(默认: 1) + --memory N 每容器内存限制/MB(默认: 512) + -o, --output FILE 结果文件路径(默认: 自动命名于 OUTPUT_DIR) + --output-dir DIR 输出目录(默认: ./logs/docker) + -h, --help 显示帮助信息 + +输出格式 (Tab 分隔, 6 列): source-package binary_package install command service remove -绀轰緥: +示例: command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ --iso /run/media/user/OC-9-6-x86_64 --image opencloudos:9-local -j 8 command-execute-docker.sh pkg.list desktop-wl.pkg svc-wl.pkg \ @@ -74,7 +89,7 @@ EOF } # ============================================================================ -# 鍙傛暟瑙f瀽 +# 参数解析 # ============================================================================ PKG_LIST="" DESKTOP_WHITELIST="" @@ -82,7 +97,7 @@ SERVICE_WHITELIST="" OUTPUT_FILE="" if [ $# -lt 3 ]; then - log_error "闇€瑕?3 涓繀闇€鍙傛暟: pkg_list desktop_whitelist service_whitelist" + log_error "需要 3 个必需参数: pkg_list desktop_whitelist service_whitelist" usage fi @@ -103,45 +118,47 @@ while [ $# -gt 0 ]; do -o|--output) OUTPUT_FILE="$2"; shift 2 ;; --output-dir) OUTPUT_DIR="$2"; shift 2 ;; -h|--help) usage ;; - *) log_error "鏈煡閫夐」: $1"; usage ;; + *) log_error "未知选项: $1"; usage ;; esac done # ============================================================================ -# 鍓嶇疆妫€鏌?# ============================================================================ +# 前置检查 +# ============================================================================ for f in "$PKG_LIST" "$DESKTOP_WHITELIST" "$SERVICE_WHITELIST"; do - [ -f "$f" ] || { log_error "鏂囦欢涓嶅瓨鍦? $f"; exit 1; } + [ -f "$f" ] || { log_error "文件不存在: $f"; exit 1; } done -command -v docker >/dev/null 2>&1 || { log_error "docker 鏈畨瑁?; exit 1; } -docker info >/dev/null 2>&1 || { log_error "Docker 瀹堟姢杩涚▼鏈繍琛?; exit 1; } +command -v docker >/dev/null 2>&1 || { log_error "docker 未安装"; exit 1; } +docker info >/dev/null 2>&1 || { log_error "Docker 守护进程未运行"; exit 1; } if ! docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then - log_error "闀滃儚 '$BASE_IMAGE' 涓嶅瓨鍦紝璇峰厛鎷夊彇鎴栨瀯寤? + log_error "镜像 '$BASE_IMAGE' 不存在,请先拉取或构建" exit 1 fi if [ -n "$ISO_PATH" ]; then [ -d "$ISO_PATH/BaseOS/repodata" ] || { - log_error "ISO 璺緞 '$ISO_PATH/BaseOS/repodata' 涓嶅瓨鍦? + log_error "ISO 路径 '$ISO_PATH/BaseOS/repodata' 不存在" exit 1 } fi # ============================================================================ -# 宸ヤ綔鐩綍 +# 工作目录 # ============================================================================ WORKDIR="$(mktemp -d "/tmp/docker-pkg-test.XXXXXX")" TOTAL_FAIL=0 -# [鏀硅繘 #5] 淇″彿澶勭悊: Ctrl+C 鏃舵竻鐞嗗鍎垮鍣?CLEANUP_CONTAINERS=0 +# [改进 #5] 信号处理: Ctrl+C 时清理孤儿容器 +CLEANUP_CONTAINERS=0 cleanup_containers() { docker ps -q --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker stop --time 2 >/dev/null 2>&1 docker ps -aq --filter "label=pkg-test=1" 2>/dev/null | xargs -r docker rm -f >/dev/null 2>&1 } -trap 'CLEANUP_CONTAINERS=1; cleanup_containers; log_warn "鏀跺埌涓柇淇″彿锛屽凡娓呯悊娴嬭瘯瀹瑰櫒"; exit 130' INT TERM +trap 'CLEANUP_CONTAINERS=1; cleanup_containers; log_warn "收到中断信号,已清理测试容器"; exit 130' INT TERM -trap 'if [ "${TOTAL_FAIL:-0}" -gt 0 ] || [ "$CLEANUP_CONTAINERS" -eq 1 ]; then log_warn "宸ヤ綔鐩綍淇濈暀: $WORKDIR"; else rm -rf "$WORKDIR"; fi' EXIT +trap 'if [ "${TOTAL_FAIL:-0}" -gt 0 ] || [ "$CLEANUP_CONTAINERS" -eq 1 ]; then log_warn "工作目录保留: $WORKDIR"; else rm -rf "$WORKDIR"; fi' EXIT FRAGMENTS_DIR="$WORKDIR/fragments" mkdir -p "$FRAGMENTS_DIR" @@ -154,33 +171,35 @@ fi echo -e "# source-package\tbinary_package\tinstall\tcommand\tservice\tremove" > "$OUTPUT_FILE" # ============================================================================ -# 杩囨护杞欢鍖呭垪琛?# ============================================================================ +# 过滤软件包列表 +# ============================================================================ FILTERED_LIST="$WORKDIR/pkg.filtered.list" grep -v 'livepatch-kernel-\|-debuginfo-\|-debugsource-' "$PKG_LIST" \ | awk '{print $1}' | sort -u > "$FILTERED_LIST" TOTAL_PKGS="$(wc -l < "$FILTERED_LIST")" if [ "$TOTAL_PKGS" -eq 0 ]; then - log_warn "杩囨护鍚庢棤寰呮祴鍖? + log_warn "过滤后无待测包" exit 0 fi -log_info "寰呮祴鍖? $TOTAL_PKGS | 骞跺彂: $JOBS | 闀滃儚: $BASE_IMAGE" +log_info "待测包: $TOTAL_PKGS | 并发: $JOBS | 镜像: $BASE_IMAGE" [ -n "$ISO_PATH" ] && log_info "ISO repo: $ISO_PATH" # ============================================================================ -# 鐢熸垚瀹瑰櫒鍐呮祴璇曡剼鏈?(harness) +# 生成容器内测试脚本 (harness) # -# 杈撳嚭: -# stdout: 鍗曡 TSV (src_pkg bin_pkg install cmd svc remove) -# stderr: 璋冭瘯鏃ュ織 + 缁撴瀯鍖?REASON 鏍囪锛屼緵姹囨€绘姤鍛婅В鏋?# REASON:INSTALL:FAIL:<鍘熷洜> -# REASON:INSTALL:SKIP:<鍘熷洜> -# REASON:CMD:FAIL:<鍛戒护璺緞> <鍘熷洜> -# REASON:CMD:SKIP:<鍘熷洜> -# REASON:SVC:FAIL:<鏈嶅姟鍚? <鍘熷洜> -# REASON:SVC:SKIP:<鍘熷洜> -# REASON:REMOVE:FAIL:<鍘熷洜> -# REASON:REMOVE:SKIP:<鍘熷洜> +# 输出: +# stdout: 单行 TSV (src_pkg bin_pkg install cmd svc remove) +# stderr: 调试日志 + 结构化 REASON 标记,供汇总报告解析 +# REASON:INSTALL:FAIL:<原因> +# REASON:INSTALL:SKIP:<原因> +# REASON:CMD:FAIL:<命令路径> <原因> +# REASON:CMD:SKIP:<原因> +# REASON:SVC:FAIL:<服务名> <原因> +# REASON:SVC:SKIP:<原因> +# REASON:REMOVE:FAIL:<原因> +# REASON:REMOVE:SKIP:<原因> # ============================================================================ CONTAINER_SCRIPT="$WORKDIR/container-test.sh" @@ -194,7 +213,7 @@ SVC_WL="$TEST_SERVICE_WL" ISO_MOUNT="$TEST_ISO_MOUNT" HAS_HOST_REPOS="$TEST_HAS_HOST_REPOS" -# ---------- 閰嶇疆 repo ---------- +# ---------- 配置 repo ---------- if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then rm -f /etc/yum.repos.d/*.repo DNF_REPOS="--repofrompath=baseos,file://${ISO_MOUNT}/BaseOS --setopt=baseos.gpgcheck=0" @@ -202,14 +221,15 @@ if [ -n "$ISO_MOUNT" ] && [ -d "$ISO_MOUNT/BaseOS/repodata" ]; then DNF_REPOS="$DNF_REPOS --repofrompath=appstream,file://${ISO_MOUNT}/AppStream --setopt=appstream.gpgcheck=0" fi elif [ "$HAS_HOST_REPOS" = "1" ] && [ -d "/tmp/host-repos" ] && ls /tmp/host-repos/*.repo >/dev/null 2>&1; then - # 鍦ㄧ嚎妯″紡锛氬鍒跺涓?repo 閰嶇疆鍒板鍣紝浣垮鍣?repo 涓庤娴嬬郴缁熶竴鑷? rm -f /etc/yum.repos.d/*.repo + # 在线模式:复制宿主 repo 配置到容器,使容器 repo 与被测系统一致 + rm -f /etc/yum.repos.d/*.repo cp /tmp/host-repos/*.repo /etc/yum.repos.d/ 2>/dev/null DNF_REPOS="" else DNF_REPOS="" fi -# ---------- 鑾峰彇婧愮爜鍖呭悕 ---------- +# ---------- 获取源码包名 ---------- get_src() { local bin="$1" src src="$(rpm -qi "$bin" 2>/dev/null | grep '^Source RPM' | sed 's/Source RPM: //' | sed 's/-[0-9].*//')" @@ -218,7 +238,7 @@ get_src() { echo "${src:-$bin}" } -# ---------- 瀹夎 ---------- +# ---------- 安装 ---------- INSTALL="FAIL" INST_LOG="$(mktemp)" WAS_ALREADY_INSTALLED=0 @@ -239,7 +259,7 @@ fi SRC_PKG="$(get_src "$PKG")" if [ "$INSTALL" = "FAIL" ]; then - # 鎻愬彇澶辫触鍘熷洜 + # 提取失败原因 reason="$(tail -5 "$INST_LOG" | tr '\n' ' ' | sed 's/ */ /g')" echo "REASON:INSTALL:FAIL:$reason" >&2 cat "$INST_LOG" >&2 @@ -249,16 +269,17 @@ if [ "$INSTALL" = "FAIL" ]; then fi if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then - echo "REASON:INSTALL:SKIP:宸查瑁呬簬鍩虹闀滃儚 (dnf: Already installed)" >&2 + echo "REASON:INSTALL:SKIP:已预装于基础镜像 (dnf: Already installed)" >&2 fi rm -f "$INST_LOG" -# ---------- 鍛戒护娴嬭瘯 ---------- +# ---------- 命令测试 ---------- CMD="SKIP" REAL_PKG="$(rpm -q --queryformat='%{name}' "$PKG" 2>/dev/null)" if [ -n "$REAL_PKG" ] && [ -f "$DESKTOP_WL" ] && grep -qx "$REAL_PKG" "$DESKTOP_WL"; then - # 妗岄潰鐧藉悕鍗曞寘锛岃烦杩囧懡浠ゆ祴璇? echo "REASON:CMD:SKIP:妗岄潰鐧藉悕鍗? 璺宠繃鍛戒护娴嬭瘯" >&2 + # 桌面白名单包,跳过命令测试 + echo "REASON:CMD:SKIP:桌面白名单, 跳过命令测试" >&2 elif [ -n "$REAL_PKG" ]; then FLIST="$(mktemp)" CLIST="$(mktemp)" @@ -288,17 +309,17 @@ elif [ -n "$REAL_PKG" ]; then elif timeout -s SIGKILL 3 "$TCMD" --version >> "$CLOG" 2>&1; then CPASS=1 elif grep -q "error while loading shared libraries" "$CLOG"; then - CFAIL_REASON="缂哄皯鍏变韩搴?$(grep 'error while loading' "$CLOG" | head -1 | sed 's/.*: //')" + CFAIL_REASON="缺少共享库 $(grep 'error while loading' "$CLOG" | head -1 | sed 's/.*: //')" CPASS=0 elif ! realpath "$TCMD" >/dev/null 2>&1; then - CFAIL_REASON="realpath 澶辫触 (绗﹀彿閾炬帴鐩爣涓嶅瓨鍦?" + CFAIL_REASON="realpath 失败 (符号链接目标不存在)" CPASS=0 elif timeout -s SIGKILL 3 "$TCMD" >> "$CLOG" 2>&1; then CPASS=1 elif grep -iqw 'invalid\|argument\|Unknown\|option\|Usage' "$CLOG"; then CPASS=1 else - CFAIL_REASON="閫€鍑虹爜闈?" + CFAIL_REASON="退出码非0" CPASS=0 fi if [ $CPASS -eq 0 ]; then @@ -312,7 +333,7 @@ elif [ -n "$REAL_PKG" ]; then rm -f "$FLIST" "$CLIST" fi -# ---------- 鏈嶅姟娴嬭瘯 ---------- +# ---------- 服务测试 ---------- SVC="SKIP" HAS_SYSTEMD=0 if pidof systemd >/dev/null 2>&1; then @@ -327,7 +348,7 @@ if [ -n "$REAL_PKG" ]; then | sed 's|.*/||' > "$SLIST" if [ -s "$SLIST" ]; then - # 妫€鏌ユ槸鍚﹀湪鏈嶅姟鐧藉悕鍗曚腑 + # 检查是否在服务白名单中 IS_SVC_WHITELIST=0 if [ -f "$SVC_WL" ] && grep -q "^${REAL_PKG}" "$SVC_WL"; then IS_SVC_WHITELIST=1 @@ -335,55 +356,55 @@ if [ -n "$REAL_PKG" ]; then if [ "$IS_SVC_WHITELIST" -eq 1 ]; then SVC="SKIP" - echo "REASON:SVC:SKIP:鍚湇鍔″寘, 闇€鐗规畩閰嶇疆, 璺宠繃鏈嶅姟鍚姩" >&2 + echo "REASON:SVC:SKIP:含服务包, 需特殊配置, 跳过服务启动" >&2 elif [ "$HAS_SYSTEMD" -eq 0 ]; then SVC="SKIP" - echo "REASON:SVC:SKIP:瀹瑰櫒鐜涓嶆敮鎸?systemd, 鏃犳硶娴嬭瘯鏈嶅姟鍚仠" >&2 + echo "REASON:SVC:SKIP:容器环境不支持 systemd, 无法测试服务启停" >&2 else - # 鏈?systemd锛屾墽琛?start/status/stop 娴嬭瘯 + # 有 systemd,执行 start/status/stop 测试 SFAIL=0 while read -r svc; do [ -z "$svc" ] && continue # start if ! timeout -s SIGKILL 30 systemctl --no-pager start "$svc" >/dev/null 2>&1; then SFAIL=$((SFAIL + 1)) - echo "REASON:SVC:FAIL:$svc start 澶辫触 (systemctl start 杩斿洖闈?)" >&2 + echo "REASON:SVC:FAIL:$svc start 失败 (systemctl start 返回非0)" >&2 continue fi # status / is-active if ! systemctl --no-pager is-active "$svc" >/dev/null 2>&1; then SFAIL=$((SFAIL + 1)) - echo "REASON:SVC:FAIL:$svc status 鏈?active (systemctl is-active 妫€鏌ュけ璐?" >&2 + echo "REASON:SVC:FAIL:$svc status 未 active (systemctl is-active 检查失败)" >&2 systemctl --no-pager stop "$svc" >/dev/null 2>&1 continue fi # stop if ! timeout -s SIGKILL 30 systemctl --no-pager stop "$svc" >/dev/null 2>&1; then if systemctl --no-pager stop "$svc" 2>&1 | grep -iqw "configured to refuse manual start/stop"; then - # 鎷掔粷鎵嬪姩 stop 涓嶇畻澶辫触 + # 拒绝手动 stop 不算失败 : else SFAIL=$((SFAIL + 1)) - echo "REASON:SVC:FAIL:$svc stop 澶辫触 (systemctl stop 杩斿洖闈?)" >&2 + echo "REASON:SVC:FAIL:$svc stop 失败 (systemctl stop 返回非0)" >&2 fi fi done < "$SLIST" [ $SFAIL -eq 0 ] && SVC="PASS" || SVC="FAIL" fi else - # 鏃?systemd unit 鏂囦欢 + # 无 systemd unit 文件 SVC="PASS" fi rm -f "$SLIST" fi -# ---------- 鍗歌浇 ---------- +# ---------- 卸载 ---------- REMOVE="FAIL" RM_LOG="$(mktemp)" if [ "$WAS_ALREADY_INSTALLED" -eq 1 ]; then REMOVE="PASS" - echo "REASON:REMOVE:SKIP:宸查瑁呬簬鍩虹闀滃儚, 璺宠繃鍗歌浇" >&2 + echo "REASON:REMOVE:SKIP:已预装于基础镜像, 跳过卸载" >&2 else if dnf $DNF_REPOS remove -y --setopt=clean_requirements_on_remove=False "$PKG" > "$RM_LOG" 2>&1; then if grep -q "scriptlet failed" "$RM_LOG"; then @@ -396,7 +417,7 @@ else else if grep -iq "protected packages" "$RM_LOG"; then REMOVE="PASS" - echo "REASON:REMOVE:SKIP:鍙椾繚鎶ゅ寘, 璺宠繃鍗歌浇" >&2 + echo "REASON:REMOVE:SKIP:受保护包, 跳过卸载" >&2 else reason="$(tail -5 "$RM_LOG" | tr '\n' ' ' | sed 's/ */ /g')" echo "REASON:REMOVE:FAIL:$reason" >&2 @@ -411,7 +432,8 @@ HARNESS_EOF chmod +x "$CONTAINER_SCRIPT" # ============================================================================ -# 杩涘害璁℃暟鍣紙鏀硅繘 #7锛?# ============================================================================ +# 进度计数器(改进 #7) +# ============================================================================ PROGRESS_FILE="$WORKDIR/progress" echo "0" > "$PROGRESS_FILE" @@ -430,10 +452,8 @@ run_one_package() { if [ -n "$ISO_PATH" ]; then docker_volumes="$docker_volumes -v ${ISO_PATH}:/mnt/iso:ro" docker_envs="$docker_envs -e TEST_ISO_MOUNT=/mnt/iso" - # ISO 绂荤嚎妯″紡锛氭棤闇€缃戠粶锛屾棤闇€瀹夸富 repo docker_network="--network none" else - # 鍦ㄧ嚎妯″紡锛氭寕杞藉涓?repo 閰嶇疆锛屽厑璁稿鍣ㄨ仈缃戝悓姝?repo docker_envs="$docker_envs -e TEST_ISO_MOUNT=" docker_volumes="$docker_volumes -v /etc/yum.repos.d:/tmp/host-repos:ro" docker_envs="$docker_envs -e TEST_HAS_HOST_REPOS=1" @@ -454,12 +474,13 @@ run_one_package() { bash /tmp/test.sh \ 2>"$FRAGMENTS_DIR/${pkg}.log")" || rc=$? - # [Bug #1 淇] 瓒呮椂/寮傚父閫€鍑烘椂浣跨敤 \t 鍒嗛殧锛屼繚鎸?TSV 鏍煎紡涓€鑷? if [ $rc -eq 124 ]; then - echo "REASON:INSTALL:FAIL:瀹夎瓒呮椂(>${CONTAINER_TIMEOUT}s)" >> "$FRAGMENTS_DIR/${pkg}.log" + # [Bug #1 修复] 超时/异常退出时使用 \t 分隔,保持 TSV 格式一致 + if [ $rc -eq 124 ]; then + echo "REASON:INSTALL:FAIL:安装超时(>${CONTAINER_TIMEOUT}s)" >> "$FRAGMENTS_DIR/${pkg}.log" result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" - log_error "[$pkg] 瓒呮椂 (${CONTAINER_TIMEOUT}s)" + log_error "[$pkg] 超时 (${CONTAINER_TIMEOUT}s)" elif [ $rc -ne 0 ] && [ -z "$result" ]; then - echo "REASON:INSTALL:FAIL:瀹瑰櫒寮傚父閫€鍑?閫€鍑虹爜:$rc)" >> "$FRAGMENTS_DIR/${pkg}.log" + echo "REASON:INSTALL:FAIL:容器异常退出(退出码:$rc)" >> "$FRAGMENTS_DIR/${pkg}.log" result="$(printf '%s\t%s\tFAIL\tSKIP\tSKIP\tSKIP' "$pkg" "$pkg")" fi @@ -471,7 +492,7 @@ run_one_package() { svc="$(echo "$result" | awk -F'\t' '{print $5}')" rm="$(echo "$result" | awk -F'\t' '{print $6}')" - # [鏀硅繘 #7] 鍘熷瓙閫掑杩涘害 + # [改进 #7] 原子递增进度 local done_count done_count="$(flock "$PROGRESS_FILE" bash -c "echo \$(( \$(cat '$PROGRESS_FILE') + 1 )) | tee '$PROGRESS_FILE'")" @@ -487,9 +508,9 @@ export DESKTOP_WHITELIST SERVICE_WHITELIST BASE_IMAGE CONTAINER_SCRIPT FRAGMENTS export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS NO_NETWORK # ============================================================================ -# 鎵ц娴嬭瘯 +# 执行测试 # ============================================================================ -log_info "寮€濮? $TOTAL_PKGS 涓寘, $JOBS 骞跺彂" +log_info "开始: $TOTAL_PKGS 个包, $JOBS 并发" echo "---------------------------------------------------------------" >&2 START_TIME="$(date +%s)" @@ -498,10 +519,10 @@ END_TIME="$(date +%s)" ELAPSED=$(( END_TIME - START_TIME )) echo "---------------------------------------------------------------" >&2 -log_info "瀹屾垚锛岃€楁椂 ${ELAPSED}s" +log_info "完成,耗时 ${ELAPSED}s" # ============================================================================ -# 鍚堝苟缁撴灉 +# 合并结果 # ============================================================================ find "$FRAGMENTS_DIR" -name '*.result' -print0 \ | sort -z | xargs -0 cat \ @@ -509,7 +530,8 @@ find "$FRAGMENTS_DIR" -name '*.result' -print0 \ >> "$OUTPUT_FILE" # ============================================================================ -# 鐢熸垚姹囨€绘姤鍛婏紙鍖归厤楠屾敹鏍囧噯鏍煎紡锛?# ============================================================================ +# 生成汇总报告(匹配验收标准格式) +# ============================================================================ SUMMARY_FILE="${OUTPUT_DIR}/test-summary-$(date '+%Y%m%d-%H%M%S').txt" TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" @@ -518,7 +540,7 @@ PASSED=0 FAILED=0 SKIPPED=0 -# 鍒嗙被鏀堕泦 +# 分类收集 INSTALL_FAIL_LIST=() REMOVE_FAIL_LIST=() CMD_FAIL_LIST=() @@ -531,14 +553,14 @@ while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do TOTAL=$((TOTAL + 1)) pkg_failed=0 - pkg_all_skip=1 # [鏀硅繘 #3] 璺熻釜鏄惁鎵€鏈夐樁娈甸兘鏄?SKIP + pkg_all_skip=1 # [改进 #3] 跟踪是否所有阶段都是 SKIP pkg_has_skip=0 - # ---- 瀹夎 ---- + # ---- 安装 ---- if [ "$install" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:INSTALL:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:INSTALL:FAIL://' | tr '\t' ' ')" - INSTALL_FAIL_LIST+=("${bin_pkg} 鍘熷洜: ${reason:-鏈煡}") + INSTALL_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") elif [ "$install" = "PASS" ]; then pkg_all_skip=0 if grep -q 'REASON:INSTALL:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then @@ -546,33 +568,33 @@ while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do fi fi - # ---- 鍛戒护 ---- + # ---- 命令 ---- if [ "$cmd" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:CMD:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:CMD:FAIL://' | tr '\t' ' ')" - CMD_FAIL_LIST+=("${bin_pkg} ${reason:-鍛戒护娴嬭瘯澶辫触}") + CMD_FAIL_LIST+=("${bin_pkg} ${reason:-命令测试失败}") elif [ "$cmd" = "PASS" ]; then pkg_all_skip=0 elif [ "$cmd" = "SKIP" ]; then pkg_has_skip=1 fi - # ---- 鏈嶅姟 ---- + # ---- 服务 ---- if [ "$svc" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:SVC:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:SVC:FAIL://' | tr '\t' ' ')" - SVC_FAIL_LIST+=("${bin_pkg} ${reason:-鏈嶅姟娴嬭瘯澶辫触}") + SVC_FAIL_LIST+=("${bin_pkg} ${reason:-服务测试失败}") elif [ "$svc" = "PASS" ]; then pkg_all_skip=0 elif [ "$svc" = "SKIP" ]; then pkg_has_skip=1 fi - # ---- 鍗歌浇 ---- + # ---- 卸载 ---- if [ "$remove" = "FAIL" ]; then pkg_failed=1 reason="$(grep 'REASON:REMOVE:FAIL:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | head -1 | sed 's/REASON:REMOVE:FAIL://' | tr '\t' ' ')" - REMOVE_FAIL_LIST+=("${bin_pkg} 鍘熷洜: ${reason:-鏈煡}") + REMOVE_FAIL_LIST+=("${bin_pkg} 原因: ${reason:-未知}") elif [ "$remove" = "PASS" ]; then pkg_all_skip=0 if grep -q 'REASON:REMOVE:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null; then @@ -580,70 +602,72 @@ while IFS=$'\t' read -r src_pkg bin_pkg install cmd svc remove; do fi fi - # ---- 鍒嗙被锛堟敼杩?#3: PASS+鍚堢悊SKIP 鈫?PASSED锛屼粎鍏ㄩ儴SKIP 鈫?SKIPPED锛?--- + # ---- 分类(改进 #3: PASS+合理SKIP → PASSED,仅全部SKIP → SKIPPED)---- if [ $pkg_failed -eq 1 ]; then FAILED=$((FAILED + 1)) elif [ $pkg_all_skip -eq 1 ] && [ $pkg_has_skip -eq 1 ]; then - # 鎵€鏈夐樁娈甸兘鏄?SKIP锛堝瀹夎SKIP+鍗歌浇SKIP鐨勯瑁呬繚鎶ゅ寘锛? SKIPPED=$((SKIPPED + 1)) + # 所有阶段都是 SKIP(如安装SKIP+卸载SKIP的预装保护包) + SKIPPED=$((SKIPPED + 1)) all_skips="$(grep 'REASON:.*:SKIP:' "$FRAGMENTS_DIR/${bin_pkg}.log" 2>/dev/null | sed 's/REASON:[^:]*:SKIP://' | tr '\n' ', ' | sed 's/, $//')" - SKIP_LIST+=("${bin_pkg} (${all_skips:-鍏ㄩ儴璺宠繃})") + SKIP_LIST+=("${bin_pkg} (${all_skips:-全部跳过})") else - # 鏈?PASS 涓旀棤 FAIL 鈫?閫氳繃锛堝嵆浣块儴鍒嗛樁娈垫槸鍚堢悊鐨?SKIP锛? PASSED=$((PASSED + 1)) + # 有 PASS 且无 FAIL → 通过(即使部分阶段是合理的 SKIP) + PASSED=$((PASSED + 1)) fi done < "$OUTPUT_FILE" -# ---- 鍐欏叆姹囨€绘枃浠?---- +# ---- 写入汇总文件 ---- { - echo "========== 娴嬭瘯澶辫触姹囨€?(${TIMESTAMP}) ==========" - echo "鐜: docker | 骞跺彂搴? ${JOBS} | 寰呮祴鍖呮€绘暟: ${TOTAL} | 閫氳繃: ${PASSED} | 澶辫触: ${FAILED} | 璺宠繃: ${SKIPPED}" + echo "========== 测试失败汇总 (${TIMESTAMP}) ==========" + echo "环境: docker | 并发度: ${JOBS} | 待测包总数: ${TOTAL} | 通过: ${PASSED} | 失败: ${FAILED} | 跳过: ${SKIPPED}" echo "" - echo "銆愬畨瑁呭け璐ャ€?${#INSTALL_FAIL_LIST[@]})" + echo "【安装失败】(${#INSTALL_FAIL_LIST[@]})" if [ ${#INSTALL_FAIL_LIST[@]} -eq 0 ]; then - echo "(鏃?" + echo "(无)" else for item in "${INSTALL_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "銆愬嵏杞藉け璐ャ€?${#REMOVE_FAIL_LIST[@]})" + echo "【卸载失败】(${#REMOVE_FAIL_LIST[@]})" if [ ${#REMOVE_FAIL_LIST[@]} -eq 0 ]; then - echo "(鏃?" + echo "(无)" else for item in "${REMOVE_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "銆愬懡浠ゆ祴璇曞け璐ャ€?${#CMD_FAIL_LIST[@]})" + echo "【命令测试失败】(${#CMD_FAIL_LIST[@]})" if [ ${#CMD_FAIL_LIST[@]} -eq 0 ]; then - echo "(鏃?" + echo "(无)" else for item in "${CMD_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "銆愭湇鍔℃祴璇曞け璐ャ€?${#SVC_FAIL_LIST[@]})" + echo "【服务测试失败】(${#SVC_FAIL_LIST[@]})" if [ ${#SVC_FAIL_LIST[@]} -eq 0 ]; then - echo "(鏃?" + echo "(无)" else for item in "${SVC_FAIL_LIST[@]}"; do echo " $item"; done fi echo "" - echo "銆愯烦杩囬」銆?${#SKIP_LIST[@]})" + echo "【跳过项】(${#SKIP_LIST[@]})" if [ ${#SKIP_LIST[@]} -eq 0 ]; then - echo "(鏃?" + echo "(无)" else for item in "${SKIP_LIST[@]}"; do echo " $item"; done fi echo "" echo "=======================================================" - echo "瀵硅处: 閫氳繃(${PASSED}) + 澶辫触(${FAILED}) + 璺宠繃(${SKIPPED}) = $((PASSED + FAILED + SKIPPED)) / 寰呮祴鎬绘暟(${TOTAL})" + echo "对账: 通过(${PASSED}) + 失败(${FAILED}) + 跳过(${SKIPPED}) = $((PASSED + FAILED + SKIPPED)) / 待测总数(${TOTAL})" if [ $((PASSED + FAILED + SKIPPED)) -ne "$TOTAL" ]; then - echo " *** 瀵硅处涓嶅尮閰? ***" + echo " *** 对账不匹配! ***" else - echo " 瀵硅处鍖归厤" + echo " 对账匹配" fi } | tee "$SUMMARY_FILE" -- Gitee From 8bc1f2a4435eb97d096888e94558c4359436c62f Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Fri, 31 Jul 2026 02:12:34 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20--host-net=20?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E6=94=AF=E6=8C=81=E5=AE=BF=E4=B8=BB=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E6=A8=A1=E5=BC=8F=EF=BC=88=E5=AE=B9=E5=99=A8NAT?= =?UTF-8?q?=E4=B8=8D=E9=80=9A=E6=97=B6=E4=BD=BF=E7=94=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- testcase/command-execute-docker.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh index 76b3d8a..655d925 100755 --- a/testcase/command-execute-docker.sh +++ b/testcase/command-execute-docker.sh @@ -37,6 +37,7 @@ MEMORY_LIMIT=512 OUTPUT_DIR="./logs/docker" ISO_PATH="" NO_NETWORK=0 +USE_HOST_NET=0 # ============================================================================ # 颜色输出 @@ -67,6 +68,7 @@ usage() { 选项: --iso PATH ISO 挂载路径,用于本地 repo(推荐离线测试) --no-network 禁用容器网络(ISO 离线模式自动启用) + --host-net 使用宿主网络模式(容器共享宿主网络栈) -j, --jobs N 并发度(默认: 4) --image IMAGE Docker 基础镜像(默认: opencloudos/opencloudos:9) --timeout N 单容器超时秒数(默认: 300) @@ -110,6 +112,7 @@ while [ $# -gt 0 ]; do case "$1" in --iso) ISO_PATH="$(realpath "$2")"; shift 2 ;; --no-network) NO_NETWORK=1; shift ;; + --host-net) USE_HOST_NET=1; shift ;; -j|--jobs) JOBS="$2"; shift 2 ;; --image) BASE_IMAGE="$2"; shift 2 ;; --timeout) CONTAINER_TIMEOUT="$2"; shift 2 ;; @@ -453,6 +456,11 @@ run_one_package() { docker_volumes="$docker_volumes -v ${ISO_PATH}:/mnt/iso:ro" docker_envs="$docker_envs -e TEST_ISO_MOUNT=/mnt/iso" docker_network="--network none" + elif [ "$USE_HOST_NET" -eq 1 ]; then + docker_envs="$docker_envs -e TEST_ISO_MOUNT=" + docker_volumes="$docker_volumes -v /etc/yum.repos.d:/tmp/host-repos:ro" + docker_envs="$docker_envs -e TEST_HAS_HOST_REPOS=1" + docker_network="--network host" else docker_envs="$docker_envs -e TEST_ISO_MOUNT=" docker_volumes="$docker_volumes -v /etc/yum.repos.d:/tmp/host-repos:ro" @@ -505,7 +513,7 @@ run_one_package() { export -f run_one_package export DESKTOP_WHITELIST SERVICE_WHITELIST BASE_IMAGE CONTAINER_SCRIPT FRAGMENTS_DIR ISO_PATH -export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS NO_NETWORK +export CONTAINER_TIMEOUT CPU_LIMIT MEMORY_LIMIT PROGRESS_FILE TOTAL_PKGS NO_NETWORK USE_HOST_NET # ============================================================================ # 执行测试 -- Gitee From 75e6cf37882d68a40fbb8ddd014949d763e695c8 Mon Sep 17 00:00:00 2001 From: FrostSnow35 <15104465+frostsnow35@user.noreply.gitee.com> Date: Fri, 31 Jul 2026 02:16:06 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=E5=AE=BF=E4=B8=BBrepo=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E5=90=8E=E8=BF=87=E6=BB=A4=E9=9D=9EOpenCloudOS?= =?UTF-8?q?=E6=BA=90=EF=BC=8C=E9=81=BF=E5=85=8D=E7=AC=AC=E4=B8=89=E6=96=B9?= =?UTF-8?q?repo=20SSL=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- testcase/command-execute-docker.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh index 655d925..d608801 100755 --- a/testcase/command-execute-docker.sh +++ b/testcase/command-execute-docker.sh @@ -227,6 +227,13 @@ elif [ "$HAS_HOST_REPOS" = "1" ] && [ -d "/tmp/host-repos" ] && ls /tmp/host-rep # 在线模式:复制宿主 repo 配置到容器,使容器 repo 与被测系统一致 rm -f /etc/yum.repos.d/*.repo cp /tmp/host-repos/*.repo /etc/yum.repos.d/ 2>/dev/null + # 过滤:禁用非 OpenCloudOS 的 repo(如 docker-ce 等第三方源会因SSL/网络问题导致 dnf 失败) + for f in /etc/yum.repos.d/*.repo; do + [ -f "$f" ] || continue + if ! grep -qi 'opencloudos' "$f" 2>/dev/null; then + sed -i 's/^enabled=1/enabled=0/g' "$f" + fi + done DNF_REPOS="" else DNF_REPOS="" -- Gitee