diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..cb2204786f38afef74a8a91d33c1ef95515259bc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell scripts must use LF line endings +*.sh text eol=lf diff --git a/.trae/rules/project.md b/.trae/rules/project.md new file mode 100644 index 0000000000000000000000000000000000000000..9397269f4ceb875bd9d271c07cf4da29f000fe8d --- /dev/null +++ b/.trae/rules/project.md @@ -0,0 +1,82 @@ +# 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/ # 娴嬭瘯鏃ュ織鍜岀粨鏋?``` + +## 浠g爜瑙勮寖 + +### 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 鍘熷瓙閫掑 + +### 姝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` diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh new file mode 100755 index 0000000000000000000000000000000000000000..d608801080550c474884730c998445503321d944 --- /dev/null +++ b/testcase/command-execute-docker.sh @@ -0,0 +1,698 @@ +#!/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:9-local" +CONTAINER_TIMEOUT=300 +CPU_LIMIT=1 +MEMORY_LIMIT=512 +OUTPUT_DIR="./logs/docker" +ISO_PATH="" +NO_NETWORK=0 +USE_HOST_NET=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 离线模式自动启用) + --host-net 使用宿主网络模式(容器共享宿主网络栈) + -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 ;; + --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 ;; + --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 + # 过滤:禁用非 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="" +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" + 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" + 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 USE_HOST_NET + +# ============================================================================ +# 执行测试 +# ============================================================================ +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