diff --git a/DELIVERABLES.md b/DELIVERABLES.md new file mode 100644 index 0000000000000000000000000000000000000000..b01d2c9b56478db3fba40656b230a0213943e7b9 --- /dev/null +++ b/DELIVERABLES.md @@ -0,0 +1,131 @@ +# 犀牛鸟项目交付清单 + +> 生成日期: 2026-08-16 +> 项目: OpenCloudOS 软件包 CI 测试套 — Docker 并发测试 +> 用途: 提交 PR / 导师评审前的核对清单 + +--- + +## 一、交付物总览 + +对照《犀牛鸟项目完整指南》要求, 共 4 项交付物, 均已就绪: + +| # | 交付物 | 仓库内路径 | 状态 | +| - | ------ | ---------- | ---- | +| 1 | 主测试脚本 | `testcase/command-execute-docker.sh` | ✅ 完成 | +| 2 | 测试镜像定义 | `testcase/Dockerfile.test` | ✅ 完成 | +| 3 | 说明文档 | `README-docker-test.md` | ✅ 完成 | +| 4 | Pull Request | Gitee 网页操作 | ⏳ 待提交 | + +--- + +## 二、各交付物说明 + +### 1. command-execute-docker.sh(核心) + +- **位置**: `testcase/command-execute-docker.sh` +- **功能**: 每个待测包在独立 Docker 容器内完成 安装 → 命令验证 → 服务检测 → 卸载 +- **亮点**: + - 并发控制:`-j/--jobs N`(默认 4),基于 `xargs -P` + `flock` 原子进度 + - 容器隔离:`--network none` + CPU/内存限制 + `--cap-add` 最小权限 + - 离线测试:`--iso` 挂载本地仓库, 无需外网 + - 结果兼容:6 列 TSV, 与现有 QEMU 方案 `package-command-result-*.txt` 一致 + - 汇总报告: 安装/命令/服务/卸载分类失败 + 数量对账 + - 容错: 容器超时兜底、信号清理孤儿容器 +- **已通过**: `bash -n` 语法检查、`-h` 帮助、无 Docker 环境前置检查冒烟测试 + +### 2. Dockerfile.test + +- **位置**: `testcase/Dockerfile.test` +- **内容**: 基于 `opencloudos/opencloudos:9`, 预装 `file`(识别 ELF)、`procps-ng`(pidof 检测 systemd) +- **构建命令**: + ```bash + docker build -t opencloudos:9-test -f testcase/Dockerfile.test testcase/ + ``` + +### 3. README-docker-test.md + +- **位置**: 仓库根目录 `README-docker-test.md` +- **内容**: 方案概述 / 目录结构 / 环境要求 / 镜像构建 / 运行方法 / 输出说明 / 服务测试方案与局限 / FAQ +- **注意**: 顶部标注了当前验证状态, 完整容器测试需在 Linux 环境落地后再更新 + +### 4. Pull Request(唯一待办) + +见下方"提交 PR 步骤"。 + +--- + +## 三、提交前的自查清单 + +提交 PR 前逐项打勾: + +- [ ] 已确认 `command-execute-docker.sh` 语法正确(`bash -n` 通过) +- [ ] 已在 **Linux 环境**跑通至少一次完整测试(小规模包列表, 如 vim/curl/wget) +- [ ] 测试通过后, 结果文件、汇总报告、对账行均正常 +- [ ] 已确认 `logs/docker/`、`docker-test-results/` 不在 git 跟踪中(已写入 `.gitignore`) +- [ ] 已把"Linux 环境实测结果"更新到 `README-docker-test.md` 顶部验证状态 +- [ ] `git status` 工作区干净, 仅含本方案相关改动 +- [ ] 提交信息描述清晰(含"新增 Docker 并发测试"关键词) + +--- + +## 四、提交 PR 步骤 + +### 4.1 本地提交(已完成) + +当前仓库已有 2 条提交: + +``` +2558b16 docs: README-docker-test 补充验证状态说明 +60594de feat: 新增 Docker 并发软件包功能测试 +``` + +### 4.2 关联远程仓库并推送 + +```bash +cd yum-ci-pkg-test +# 添加你 fork 的远程仓库(替换用户名) +git remote add origin https://gitee.com/<你的用户名>/yum-ci-pkg-test.git +git push -u origin master +``` + +### 4.3 创建 PR + +1. 打开 Gitee 仓库页面 → **Pull Requests** → **+ 新建 Pull Request** +2. 源分支 = 你的 `master`, 目标分支 = `opencloudos-testing/yum-ci-pkg-test` 的 `master` +3. 标题建议: `feat: 新增 Docker 并发软件包功能测试 command-execute-docker.sh` +4. 描述模板(可复制): + +```text +## 方案概述 +基于 Docker 容器替代 QEMU 虚拟机进行软件包 CI 测试。 + +## 主要改动 +- testcase/command-execute-docker.sh: 新增主脚本, 支持并发(-j)与离线 ISO 仓库 +- testcase/Dockerfile.test: 测试镜像构建文件 +- README-docker-test.md: 使用说明与方案局限 + +## 测试流程 +安装 → 命令验证(ELF 可执行文件) → 服务检测 → 卸载 + +## 服务测试说明 +默认容器无 systemd, 服务启停统一 SKIPPED; 已通过 +desktop-white.pkg / service-white.pkg 白名单处理特殊包。 + +## 验证情况 +语法检查通过; Linux 环境实测结果见 README 顶部。 +``` + +--- + +## 五、需要你自行确认的事项 + +| 事项 | 说明 | +| ---- | ---- | +| Linux 环境实测 | 本机为 Windows, 无法跑容器; 需在 WSL2/Linux 执行第 4.2 节命令完成实测 | +| Gitee 账号与 fork | 需要你登录 Gitee fork 上游仓库, 得到 fork 地址 | +| 评审反馈 | PR 提交后按管理员意见修改(大概率集中在服务测试 SKIP 策略上) | + +--- + +— 交付清单完 — diff --git a/README-docker-test.md b/README-docker-test.md new file mode 100644 index 0000000000000000000000000000000000000000..dfee767fe94952994e84d81f099f434298645faa --- /dev/null +++ b/README-docker-test.md @@ -0,0 +1,145 @@ +# Docker 并发软件包功能测试 + +> **验证状态(2026-08-16)**: 脚本已完成并通过 `bash -n` 语法检查、`-h` 帮助 +> 及无 Docker 环境下的前置检查冒烟测试; 因开发机为 Windows(Git Bash 的 +> docker 为 MSYS stub, 且 Docker Desktop 守护进程未启动), **尚未在 Linux +> 环境实际跑通完整容器测试**。首次落地运行时如遇问题, 请参考第 7 节 FAQ, +> 并将结果反馈回本文档。 + +本目录提供基于 **Docker 容器**的软件包并发功能测试方案: + +- 每个待测包在**独立容器**内完成 `安装 → 命令验证 → 服务检测 → 卸载` +- 容器间相互隔离, 通过 `-j` 并发执行, 提升测试效率 +- 支持**本地 ISO 离线仓库**(容器 `--network none`, 杜绝外网干扰) +- 结果格式与现有 QEMU 方案 `package-command-result-*.txt` 完全兼容(6 列 TSV) + +--- + +## 1. 目录结构 + +``` +testcase/ +├── command-execute-docker.sh # 主脚本 (核心交付物) +├── Dockerfile.test # 测试镜像构建文件 +├── desktop-white.pkg # 桌面白名单 (902 个包, 跳过命令测试) +├── service-white.pkg # 服务白名单 (88 个包, 跳过服务启动测试) +└── ... # 其他现有测试用例 +``` + +## 2. 环境要求 + +| 依赖 | 说明 | +| ------ | -------------------------------------------- | +| Docker | 版本 ≥ 20.10, 守护进程正常运行 | +| bash | 主脚本为 Bash 脚本 | +| ISO | OpenCloudOS 9 ISO(或解压后的 ISO 目录),离线测试推荐 | + +> 若不用 ISO, 容器内 `dnf` 将无法联网(脚本使用 `--network none`), +> 仅可测试基础镜像已预装的包, 无实际意义。 + +## 3. 构建测试镜像 + +```bash +cd yum-ci-pkg-test/ +docker build -t opencloudos:9-test -f testcase/Dockerfile.test testcase/ +``` + +镜像内含命令测试所需工具(`file`、`procps-ng` 等)。 +不构建自定义镜像时, 脚本默认使用 `opencloudos/opencloudos:9`, 需先拉取: + +```bash +docker pull opencloudos/opencloudos:9 +``` + +## 4. 运行测试 + +### 4.1 准备待测包列表 + +待测包列表格式为 `pkg_name arch` 两列(兼容现有 `get-pkgs-list` 输出): + +``` +bash x86_64 +vim x86_64 +... +``` + +### 4.2 离线模式(推荐, 基于 ISO) + +```bash +# 挂载 ISO (或直接解压到目录) +mount -o loop OpenCloudOS-9-x86_64.iso /mnt/oc9 + +# 并发测试, 使用本地 ISO 仓库 +./testcase/command-execute-docker.sh \ + pkg.list \ + testcase/desktop-white.pkg \ + testcase/service-white.pkg \ + --image opencloudos:9-test \ + --iso /mnt/oc9 \ + -j 4 +``` + +### 4.3 自定义参数 + +| 参数 | 默认值 | 说明 | +| ---------------- | -------------------------- | ----------------------------- | +| `-j, --jobs N` | `4` | 并发容器数 | +| `--image IMAGE` | `opencloudos/opencloudos:9` | Docker 基础镜像 | +| `--timeout N` | `300` | 单容器超时秒数 | +| `--cpu N` | `1` | 每容器 CPU 核数限制 | +| `--memory N` | `512` | 每容器内存限制(MB) | +| `--iso PATH` | 空 | ISO 挂载路径(本地 repo 源) | +| `-o, --output F` | 自动命名 | 结果文件路径 | +| `--output-dir D` | `./logs/docker` | 结果输出目录 | + +## 5. 输出说明 + +结果文件位于 `logs/docker/`, 6 列 TSV: + +``` +source-package binary_package install command service remove +``` + +| 列 | 取值 | 含义 | +| ------- | --------------- | ---------------------------------------- | +| install | PASS / FAIL | 安装是否成功 | +| command | PASS / FAIL/SKIP | 命令测试(SKIP=桌面白名单包) | +| service | PASS / FAIL/SKIP | 服务检测(SKIP=无 systemd 或服务白名单) | +| remove | PASS / FAIL /SKIP | 卸载是否成功(SKIP=基础镜像预装/受保护包) | + +同时输出: + +- `logs/docker/package-command-result-<时间戳>.txt` — 全部包汇总结果 +- `logs/docker/` 下汇总报告 — 失败原因汇总(安装/命令/服务/卸载分类) +- 每包详细日志 — 失败时的 `dnf`/命令输出(`REASON:` 标记) + +## 6. 服务测试说明 + +默认容器**无 systemd**, 服务启动/停止无法测试, 统一标记为 `SKIPPED` +(原因: 容器无 systemd), 与 `service-white.pkg` 白名单中的包行为一致。 + +若确需测试服务启停, 需满足以下条件(不推荐, 会降低隔离性): + +- 镜像带 systemd init(如 `opencloudos:9-init`), 运行容器时加 `--privileged` 与 `/sbin/init` +- 脚本会自动检测容器内 `pidof systemd`, 存在则执行 + `systemctl start → is-active → stop` 完整测试 + +## 7. 常见问题 + +| 现象 | 解决方法 | +| -------------------------------------- | ---------------------------------------------------- | +| 报错 `docker 未安装` | 安装 Docker 并启动守护进程 | +| 报错 `Docker 守护进程未运行` | `systemctl start docker`(Linux) | +| 报错 `镜像 'xxx' 不存在` | 先 `docker build -t opencloudos:9-test ...` 或 `docker pull` | +| 报错 `ISO 路径 ... 不存在` | 确认 ISO 目录含 `BaseOS/repodata`, 且 `--iso` 指向该目录 | +| 安装全部失败, 日志显示网络不可达 | 未挂载 ISO;容器 `--network none` 无法联网, 请用 `--iso` | +| 结果里 service 列全是 SKIP | 正常现象, 容器无 systemd, 见第 6 节 | + +## 8. 与本仓库其他用例的关系 + +| 方案 | 脚本 | 特点 | +| ----------- | ------------------------------------------- | ----------------------------- | +| QEMU 虚拟机 | `command-execute-whitelist.sh` | 真实系统环境, 可测服务, 慢 | +| Docker 容器 | `command-execute-docker.sh`(本项目) | 并发快、隔离强, 服务 SKIPPED | + +两者结果格式统一, 可互相校验; Docker 方案适合大规模包快速回归。 diff --git a/testcase/Dockerfile.test b/testcase/Dockerfile.test new file mode 100644 index 0000000000000000000000000000000000000000..92633cabbae2da8489979ce90147a763ed70d2c2 --- /dev/null +++ b/testcase/Dockerfile.test @@ -0,0 +1,40 @@ +# ============================================================================ +# Dockerfile.test —— 并发软件包功能测试镜像 +# +# 用途: +# 构建 command-execute-docker.sh 所需的测试基础镜像。 +# 基于 OpenCloudOS 9 官方镜像, 预装命令测试所需工具。 +# +# 构建: +# docker build -t opencloudos:9-test -f testcase/Dockerfile.test testcase/ +# +# 使用 (配合主脚本): +# command-execute-docker.sh pkg.list desktop-white.pkg service-white.pkg \ +# --image opencloudos:9-test --iso /path/to/OpenCloudOS-9-ISO +# +# 说明: +# - 容器运行时使用 --network none 隔离网络, 软件包来源通过 --iso 挂载 +# 的本地仓库提供 (见主脚本 harness 的 DNF_REPOS 逻辑)。 +# - 若确需在容器内访问网络仓库, 可去掉主脚本中的 --network none, +# 但会降低隔离性, 不推荐。 +# ============================================================================ + +FROM opencloudos/opencloudos:9 + +# 镜像元信息 +LABEL maintainer="yum-ci-pkg-test" \ + description="OpenCloudOS 9 软件包并发功能测试镜像" \ + pkg-test="1" + +# 预装测试所需工具: +# file - 命令测试中识别 ELF 可执行文件 +# procps-ng - 提供 pidof, 用于检测容器内是否有 systemd +# 其余 (rpm/dnf/grep/sed/awk/timeout/realpath/mktemp/xargs 等) +# 基础镜像已自带, 无需重复安装 +RUN dnf install -y \ + file \ + procps-ng \ + && dnf clean all + +# 验证关键工具可用 +RUN command -v file && command -v pidof && command -v dnf diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh new file mode 100644 index 0000000000000000000000000000000000000000..d31f43686f29799b6c4e98761b160347e710bacb --- /dev/null +++ b/testcase/command-execute-docker.sh @@ -0,0 +1,677 @@ +#!/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="" + +# -h/--help 优先处理, 不依赖必需参数 +for arg in "$@"; do + [ "$arg" = "-h" ] || [ "$arg" = "--help" ] && usage +done + +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