From bedd0a85497c41fd4b7e8d89992abe03707f41b7 Mon Sep 17 00:00:00 2001 From: ruirui6946 <2733936092@qq.com> Date: Sat, 11 Jul 2026 03:42:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20IJV5MW=20chroot=20?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E8=BD=AF=E4=BB=B6=E5=8C=85=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +- testcase/Makefile | 4 + testcase/command-execute-chroot.md | 89 +++ testcase/command-execute-chroot.sh | 746 ++++++++++++++++++ testcase/tests/test-command-execute-chroot.sh | 325 ++++++++ 5 files changed, 1172 insertions(+), 4 deletions(-) create mode 100644 testcase/command-execute-chroot.md create mode 100755 testcase/command-execute-chroot.sh create mode 100755 testcase/tests/test-command-execute-chroot.sh diff --git a/README.md b/README.md index 8597cde..465b794 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # TencentOS发行版检查 检查项目包括 -- KABI -- 软件包安装 -- 内核模块加载 -- systemd服务启动 \ No newline at end of file +- KABI +- 软件包安装 +- 内核模块加载 +- systemd服务启动 + +## 软件包命令并发隔离测试 + +[`testcase/command-execute-chroot.sh`](testcase/command-execute-chroot.sh) 提供基于独立 chroot 的并发软件包功能测试,覆盖安装、命令探测、服务检查、卸载、逐包超时与分类汇总。参数、服务跳过策略及环境限制见 [`testcase/command-execute-chroot.md`](testcase/command-execute-chroot.md)。 diff --git a/testcase/Makefile b/testcase/Makefile index 1b4db5b..a270336 100644 --- a/testcase/Makefile +++ b/testcase/Makefile @@ -19,3 +19,7 @@ clean: rm -rfv $(ALL_OBJ) $(ALL_BIN) cleanall: clean + +.PHONY: check-command-execute-chroot +check-command-execute-chroot: + bash tests/test-command-execute-chroot.sh diff --git a/testcase/command-execute-chroot.md b/testcase/command-execute-chroot.md new file mode 100644 index 0000000..279f360 --- /dev/null +++ b/testcase/command-execute-chroot.md @@ -0,0 +1,89 @@ +# command-execute-chroot 使用说明 + +`command-execute-chroot.sh` 是 `command-execute-whitelist.sh` 的并发隔离版本。旧用例保持不变;新脚本为每个待测包复制独立基础 rootfs,并在其中完成安装、命令探测、服务检查和卸载。 + +## 运行环境 + +- 必须在专用 QEMU/CI 虚拟机中以 root 运行; +- 主机需提供 Bash 4.3+、dnf/yum、rpm、chroot、mount、findmnt、strace、file、timeout; +- 软件源必须能够为 `dnf --installroot` 提供基础包与待测包; +- 磁盘或文件系统最好支持 reflink,脚本会优先执行 `cp -a --reflink=auto`;不支持时自动回退到普通复制。 + +> chroot 只提供文件系统隔离,不是安全沙箱。脚本按需求 bind 挂载 `/dev`、`/proc`、`/sys`,因此只应在可丢弃的 CI 虚拟机内运行不受信任的软件包测试。 + +## 参数 + +```bash +sudo ./testcase/command-execute-chroot.sh [options] \ + pkg.list desktop-white.pkg package_has_service.list +``` + +三个位置参数均为必填: + +1. 待测软件包列表,取每个非注释行的第一列; +2. 桌面白名单,匹配包的命令测试标记为 `SKIP`; +3. 需特殊配置的服务包列表,匹配包的服务启动测试标记为 `SKIP`。 + +常用选项: + +```text +-j N 并发度 +-t SECONDS 单个包完整流程超时 +-c SECONDS 单条命令探测超时 +-s SECONDS 单次 systemd 操作超时 +-w DIR 工作目录 +-o DIR 报告目录 +-b DIR 复用已有基础 rootfs +-r VERSION 指定 releasever +-S MODE auto | skip | systemd +-k 保留工作目录(仍会卸载挂载点) +``` + +未指定 `-b` 时,脚本先用 `dnf/yum --installroot` 创建包含 `bash/coreutils/rpm/strace/file/systemd` 的基础 rootfs,再为每个包复制一份。单包失败或超时只影响自己的结果文件,其他 worker 继续运行。 + +## 命令判定 + +命令发现与现有 `test_package_command` 保持同一范围:只扫描包内 `/usr/bin` 与 `/usr/sbin` 的可执行 ELF,排除共享库及 `nde-*`。每条命令先后使用 `--help`、`--version` 短时探测,并用 strace 确认 `execve` 成功;缺少动态库、命令不存在、段错误或 core dump 均判为失败。没有可执行 ELF 的包,命令阶段记为 `PASS` 并说明原因。 + +## 服务策略与局限 + +安装后通过 RPM 文件清单识别 `.service`、`.socket`、`.target` unit。 + +- 参数 3 中的包:`SKIP`,原因固定为“需特殊配置”; +- `-S auto`(默认):仅在 chroot 内 systemd 可达时依次检查 `start`、`is-active`、`stop`;普通 chroot 没有 systemd/PID1,因此明确记为 `SKIP`,不误判为失败; +- `-S skip`:所有发现的 unit 均记为 `SKIP`; +- `-S systemd`:强制执行 `start/is-active/stop`,适用于调用方已经准备好完整 systemd 运行环境的 rootfs。 + +脚本不会 bind 挂载宿主机 `/run`,避免 chroot 中的 `systemctl` 误操作宿主机 systemd。若需要真实服务启停测试,应使用 systemd-nspawn 或独立 QEMU 虚拟机作为后续扩展。 + +## 输出 + +输出目录中生成: + +- `package-command-result-chroot-*.txt`:与原 `package-command-result-*.txt` 同样以 source/binary/install/command/remove 为核心列,并增加 `service`、`overall` 与详细原因; +- `package-command-summary-chroot-*.txt`:分别列出安装失败、卸载失败、命令失败、服务失败和跳过项; +- 工作目录 `logs/`:每个包独立日志和命令 strace 日志。 + +总体状态优先级为 `FAIL > SKIP > PASS`,因此保证: + +```text +通过 + 失败 + 跳过 = 待测包总数 +``` + +只要存在任一失败包,脚本最终返回非零;只有通过和跳过时返回 0。 + +## 自动化验证 + +```bash +make -C testcase check-command-execute-chroot +``` + +测试使用 PATH 注入模拟 dnf、rpm、strace、chroot 和 mount,不安装软件或创建真实挂载,验证以下场景: + +- 3 个必填列表参数和非法参数返回码; +- `-j 3` worker 实际重叠执行; +- 普通包完整通过; +- 安装、卸载、命令探测分别失败并进入对应分类; +- 桌面白名单、特殊服务包、无 systemd/PID1 三种跳过原因; +- 强制 systemd 模式下 start/status/stop 全通过,以及 status 失败的明确结论; +- 每包日志、chroot 清理、报告行数和 `通过 + 失败 + 跳过 = 总数` 对账。 diff --git a/testcase/command-execute-chroot.sh b/testcase/command-execute-chroot.sh new file mode 100755 index 0000000..2086d96 --- /dev/null +++ b/testcase/command-execute-chroot.sh @@ -0,0 +1,746 @@ +#!/bin/bash +############################################################################### +# @用例名称: command-execute-chroot +# @用例类型: 功能测试 +# @自动化: 1 +# +# 在独立 chroot 中并发完成软件包安装、命令、服务和卸载测试。 +# 旧的 command-execute-whitelist.sh 保持不变。 +############################################################################### + +set -o pipefail + +SCRIPT_PATH="$(realpath "$0")" +SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" +[ -z "${TST_TS_TOPDIR:-}" ] && TST_TS_TOPDIR="$(realpath "$SCRIPT_DIR/..")" +export TST_TS_TOPDIR + +# 复用测试套已有的日志设施;脚本仍可在精简环境中独立输出日志。 +if [ -f "${TST_TS_TOPDIR}/tst_common/lib/common.sh" ]; then + # shellcheck source=../tst_common/lib/common.sh + source "${TST_TS_TOPDIR}/tst_common/lib/common.sh" || exit 1 +fi + +usage() { + cat <<'EOF' +Usage: + command-execute-chroot.sh [options] + +Required arguments: + pkg.list packages to test, one package in the first column of each line + desktop.list desktop packages whose command test must be skipped + service.list service packages requiring configuration; service start is skipped + +Options: + -j N concurrent chroot workers (default: number of CPUs) + -t SECONDS timeout for one complete package test (default: 300) + -c SECONDS timeout for one command probe (default: 3) + -s SECONDS timeout for one systemd operation (default: 30) + -w DIR work directory (default: /var/tmp/yum-ci-chroot.TIMESTAMP) + -o DIR report directory (default: current directory) + -b DIR reuse an exported base rootfs instead of bootstrapping one + -r VERSION dnf/yum --releasever value (default: detect from host) + -S MODE service mode: auto, skip, or systemd (default: auto) + -k keep work directory after the test (mounts are still unmounted) + -h show this help + +Service policy: + auto run start/is-active/stop only when systemd is reachable in the chroot; + otherwise report SKIP instead of FAIL + skip always report discovered units as SKIP + systemd force start/is-active/stop checks (for a prepared systemd chroot only) +EOF +} + +log_msg() { + if declare -F msg >/dev/null 2>&1; then + msg "$*" + else + printf '[%s] %s\n' "$(date '+%F %T')" "$*" + fi +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 2 +} + +is_positive_integer() { + [[ "$1" =~ ^[1-9][0-9]*$ ]] +} + +safe_filename() { + printf '%s' "$1" | sed 's/[^A-Za-z0-9_.-]/_/g' +} + +sanitize_field() { + printf '%s' "$1" | tr '\t\r\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' +} + +last_reason() { + local file="$1" + local fallback="$2" + local reason="" + if [ -s "$file" ]; then + reason="$(grep -v '^[[:space:]]*$' "$file" | tail -n 1)" + fi + [ -z "$reason" ] && reason="$fallback" + sanitize_field "$reason" +} + +list_contains_package() { + local list_file="$1" + local requested="$2" + local resolved="$3" + local candidate + + [ -s "$list_file" ] || return 1 + while read -r candidate; do + [ -z "$candidate" ] && continue + if [ "$candidate" = "$requested" ] || [ "$candidate" = "$resolved" ]; then + return 0 + fi + # Accept a conventional NAME-VERSION or NAME.ARCH whitelist entry. + if [[ "$candidate" == "$resolved"-[0-9]* ]] || [[ "$candidate" == "$resolved".* ]]; then + return 0 + fi + done < <(awk '!/^[[:space:]]*(#|$)/ {print $1}' "$list_file") + return 1 +} + +dnf_root_args() { + local root="$1" + printf '%s\n' "--installroot=$root" + printf '%s\n' '--setopt=install_weak_deps=False' + printf '%s\n' '--setopt=clean_requirements_on_remove=False' + [ -n "${CHROOT_RELEASEVER:-}" ] && printf '%s\n' "--releasever=${CHROOT_RELEASEVER}" +} + +run_root_pkg_manager() { + local root="$1" + local timeout_seconds="$2" + shift 2 + local -a root_args=() + mapfile -t root_args < <(dnf_root_args "$root") + timeout --signal=TERM --kill-after=10 "$timeout_seconds" \ + "$CHROOT_PKG_MANAGER" -y "${root_args[@]}" "$@" +} + +copy_host_configuration() { + local root="$1" + local path + for path in /etc/yum.repos.d /etc/dnf/vars /etc/yum/vars /etc/pki/rpm-gpg; do + if [ -e "$path" ]; then + mkdir -p "$root$path" + cp -a "$path/." "$root$path/" + fi + done + if [ -f /etc/resolv.conf ]; then + mkdir -p "$root/etc" + cp -L /etc/resolv.conf "$root/etc/resolv.conf" + fi +} + +prepare_base_root() { + if [ -n "${CHROOT_BASE_ROOT_INPUT:-}" ]; then + CHROOT_BASE_ROOT="$(realpath "$CHROOT_BASE_ROOT_INPUT")" + [ -d "$CHROOT_BASE_ROOT" ] || die "base rootfs is not a directory: $CHROOT_BASE_ROOT" + [ -x "$CHROOT_BASE_ROOT/bin/bash" ] || [ -x "$CHROOT_BASE_ROOT/bin/sh" ] || \ + die "base rootfs has no /bin/bash or /bin/sh: $CHROOT_BASE_ROOT" + export CHROOT_BASE_ROOT + return 0 + fi + + CHROOT_BASE_ROOT="$CHROOT_WORK_DIR/base-root" + export CHROOT_BASE_ROOT + mkdir -p "$CHROOT_BASE_ROOT" + copy_host_configuration "$CHROOT_BASE_ROOT" + log_msg "BOOTSTRAP: preparing base rootfs in $CHROOT_BASE_ROOT" + if ! run_root_pkg_manager "$CHROOT_BASE_ROOT" "$CHROOT_BASE_TIMEOUT" install \ + bash coreutils rpm strace file findutils grep systemd; then + die "failed to bootstrap base rootfs; check repository configuration" + fi +} + +copy_base_root() { + local root="$1" + mkdir -p "$root" + if ! cp -a --reflink=auto "$CHROOT_BASE_ROOT/." "$root/" 2>/dev/null; then + cp -a "$CHROOT_BASE_ROOT/." "$root/" + fi +} + +mount_chroot_filesystems() { + local root="$1" + mkdir -p "$root/dev" "$root/proc" "$root/sys" + + mount --rbind /dev "$root/dev" || return 1 + mount --make-rslave "$root/dev" || return 1 + mount -t proc proc "$root/proc" || return 1 + mount --rbind /sys "$root/sys" || return 1 + mount --make-rslave "$root/sys" || return 1 +} + +cleanup_mounts() { + local root="$1" + local target + [ -n "$root" ] || return 0 + [ -d "$root" ] || return 0 + + while read -r target; do + [ -n "$target" ] && umount -l "$target" >/dev/null 2>&1 || true + done < <(findmnt -rn -o TARGET | awk -v root="$root" '$0 == root || index($0, root "/") == 1' | sort -r) +} + +cleanup_worker_root() { + local root="$1" + cleanup_mounts "$root" + if [ "${CHROOT_KEEP_WORKDIR:-0}" -ne 1 ] && [ -n "$root" ]; then + case "$root" in + "$CHROOT_WORK_DIR"/roots/*) rm -rf -- "$root" ;; + esac + fi +} + +query_installed_metadata() { + local root="$1" + local requested="$2" + CHROOT_BINARY_NAME="$(rpm --root "$root" -q --qf '%{NAME}\n' "$requested" 2>/dev/null | head -n 1)" + [ -z "$CHROOT_BINARY_NAME" ] && CHROOT_BINARY_NAME="$requested" + CHROOT_BINARY_NEVRA="$(rpm --root "$root" -q --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' "$requested" 2>/dev/null | head -n 1)" + [ -z "$CHROOT_BINARY_NEVRA" ] && CHROOT_BINARY_NEVRA="$requested" + CHROOT_SOURCE_PACKAGE="$(rpm --root "$root" -q --qf '%{SOURCERPM}\n' "$requested" 2>/dev/null | head -n 1)" + [ -z "$CHROOT_SOURCE_PACKAGE" ] && CHROOT_SOURCE_PACKAGE="UNKNOWN" +} + +discover_commands() { + local root="$1" + local package="$2" + local output_file="$3" + local path + : >"$output_file" + + while read -r path; do + [[ "$path" =~ ^/usr/(bin|sbin)/ ]] || continue + [[ "$path" =~ \.so($|\.) ]] && continue + [[ "$path" =~ ^/usr/bin/nde- ]] && continue + if chroot "$root" test -x "$path" >/dev/null 2>&1 && \ + chroot "$root" file -b "$path" 2>/dev/null | grep -qw ELF; then + printf '%s\n' "$path" >>"$output_file" + fi + done < <(rpm --root "$root" -ql "$package" 2>/dev/null) + sort -u -o "$output_file" "$output_file" +} + +probe_one_command() { + local root="$1" + local command="$2" + local log_prefix="$3" + local argument rc trace_file output_file + local saw_exec + + for argument in --help --version; do + saw_exec=0 + trace_file="${log_prefix}.$(safe_filename "$command").${argument#--}.strace" + output_file="${log_prefix}.$(safe_filename "$command").${argument#--}.log" + : >"$trace_file" + timeout --foreground --signal=TERM --kill-after=2 "$CHROOT_COMMAND_TIMEOUT" \ + strace -f -qq -e trace=execve -o "$trace_file" \ + chroot "$root" "$command" "$argument" >"$output_file" 2>&1 + rc=$? + + if grep -F "execve(\"$command\"" "$trace_file" | grep -q '= 0'; then + saw_exec=1 + fi + if grep -Eqi 'error while loading shared libraries|No such file or directory|command not found' "$output_file"; then + continue + fi + if [ "$rc" -eq 126 ] || [ "$rc" -eq 127 ] || [ "$rc" -eq 139 ] || \ + grep -Eqi 'segmentation fault|core dumped' "$output_file"; then + continue + fi + # As in test_command_execute, a successful execve is sufficient even if + # --help exits non-zero or the probe reaches its short timeout. + if [ "$saw_exec" -eq 1 ]; then + return 0 + fi + done + return 1 +} + +test_package_commands() { + local root="$1" + local package="$2" + local log_prefix="$3" + local command_list="${log_prefix}.commands.list" + local command failed=0 + local -a failures=() + + discover_commands "$root" "$package" "$command_list" + if [ ! -s "$command_list" ]; then + CHROOT_COMMAND_REASON="no executable ELF command in /usr/bin or /usr/sbin" + return 0 + fi + + while read -r command; do + [ -z "$command" ] && continue + if ! probe_one_command "$root" "$command" "$log_prefix"; then + failures+=("$command: exec probe failed") + failed=$((failed + 1)) + fi + done <"$command_list" + + if [ "$failed" -ne 0 ]; then + CHROOT_COMMAND_REASON="$(IFS='; '; echo "${failures[*]}")" + return 1 + fi + CHROOT_COMMAND_REASON="all $(wc -l <"$command_list") command probes passed" + return 0 +} + +discover_units() { + local root="$1" + local package="$2" + local output_file="$3" + rpm --root "$root" -ql "$package" 2>/dev/null | \ + grep -E '^/(usr/)?lib/systemd/system/.*\.(service|socket|target)$' | \ + awk -F/ '{print $NF}' | grep -v '@' | sort -u >"$output_file" || true +} + +chroot_systemd_available() { + local root="$1" + local state + state="$(timeout --signal=TERM --kill-after=2 "$CHROOT_SERVICE_TIMEOUT" \ + chroot "$root" systemctl is-system-running 2>/dev/null || true)" + case "$state" in + running | degraded | starting) return 0 ;; + *) return 1 ;; + esac +} + +test_package_services() { + local root="$1" + local package="$2" + local log_prefix="$3" + local unit_file="${log_prefix}.units.list" + local unit failed=0 start_rc active_rc stop_rc + local -a failures=() + + discover_units "$root" "$package" "$unit_file" + if [ ! -s "$unit_file" ]; then + CHROOT_SERVICE_REASON="package provides no systemd unit" + return 0 + fi + + if [ "$CHROOT_SERVICE_MODE" = "skip" ]; then + CHROOT_SERVICE_REASON="chroot service policy is skip (no systemd/PID1)" + return 2 + fi + if [ "$CHROOT_SERVICE_MODE" = "auto" ] && ! chroot_systemd_available "$root"; then + CHROOT_SERVICE_REASON="systemd/PID1 is unavailable in chroot; start/status/stop skipped" + return 2 + fi + + while read -r unit; do + [ -z "$unit" ] && continue + timeout --signal=TERM --kill-after=2 "$CHROOT_SERVICE_TIMEOUT" \ + chroot "$root" systemctl --no-pager start "$unit" \ + >"${log_prefix}.${unit}.start.log" 2>&1 + start_rc=$? + timeout --signal=TERM --kill-after=2 "$CHROOT_SERVICE_TIMEOUT" \ + chroot "$root" systemctl --no-pager is-active --quiet "$unit" \ + >"${log_prefix}.${unit}.status.log" 2>&1 + active_rc=$? + timeout --signal=TERM --kill-after=2 "$CHROOT_SERVICE_TIMEOUT" \ + chroot "$root" systemctl --no-pager stop "$unit" \ + >"${log_prefix}.${unit}.stop.log" 2>&1 + stop_rc=$? + + if [ "$start_rc" -ne 0 ] || [ "$active_rc" -ne 0 ] || [ "$stop_rc" -ne 0 ]; then + failures+=("$unit: start=$start_rc status=$active_rc stop=$stop_rc") + failed=$((failed + 1)) + fi + done <"$unit_file" + + if [ "$failed" -ne 0 ]; then + CHROOT_SERVICE_REASON="$(IFS='; '; echo "${failures[*]}")" + return 1 + fi + CHROOT_SERVICE_REASON="all units passed start/status/stop" + return 0 +} + +write_worker_result() { + local result_file="$1" + shift + local tmp_file="${result_file}.tmp.$$" + local field + : >"$tmp_file" + for field in "$@"; do + printf '%s\t' "$(sanitize_field "$field")" >>"$tmp_file" + done + # Replace the final tab with a newline. + sed -i 's/\t$/\n/' "$tmp_file" + mv -f "$tmp_file" "$result_file" +} + +worker_main() { + local index="$1" + local requested="$2" + local safe_pkg root log_prefix install_log remove_log result_file + local install_status="FAIL" command_status="SKIP" service_status="SKIP" remove_status="SKIP" + local overall="FAIL" install_reason="" command_reason="install failed" + local service_reason="install failed" remove_reason="install failed" skip_reason="" + local service_rc=0 + + safe_pkg="$(safe_filename "$requested")" + root="$CHROOT_WORK_DIR/roots/${index}-${safe_pkg}" + CHROOT_WORKER_ROOT="$root" + log_prefix="$CHROOT_LOG_DIR/${index}-${safe_pkg}" + install_log="${log_prefix}.install.log" + remove_log="${log_prefix}.remove.log" + result_file="$CHROOT_RESULT_DIR/${index}.tsv" + + trap 'cleanup_worker_root "$CHROOT_WORKER_ROOT"' EXIT + trap 'exit 124' TERM INT HUP + + log_msg "WORKER[$index]: package=$requested root=$root" + if ! copy_base_root "$root"; then + install_reason="copy base rootfs failed" + write_worker_result "$result_file" UNKNOWN "$requested" "$install_status" \ + "$command_status" "$service_status" "$remove_status" "$overall" \ + "$install_reason" "$command_reason" "$service_reason" "$remove_reason" "$skip_reason" + return 0 + fi + + if run_root_pkg_manager "$root" "$CHROOT_INSTALL_TIMEOUT" install "$requested" \ + >"$install_log" 2>&1; then + install_status="PASS" + install_reason="installed successfully" + else + install_reason="$(last_reason "$install_log" "package manager install returned non-zero")" + write_worker_result "$result_file" UNKNOWN "$requested" "$install_status" \ + "$command_status" "$service_status" "$remove_status" "$overall" \ + "$install_reason" "$command_reason" "$service_reason" "$remove_reason" "$skip_reason" + return 0 + fi + + query_installed_metadata "$root" "$requested" + + if ! mount_chroot_filesystems "$root"; then + command_status="FAIL" + service_status="FAIL" + command_reason="failed to bind mount /dev, /proc, or /sys" + service_reason="$command_reason" + else + if list_contains_package "$CHROOT_DESKTOP_LIST" "$requested" "$CHROOT_BINARY_NAME"; then + command_status="SKIP" + command_reason="desktop whitelist: command test skipped" + skip_reason="$command_reason" + elif test_package_commands "$root" "$requested" "$log_prefix"; then + command_status="PASS" + command_reason="$CHROOT_COMMAND_REASON" + else + command_status="FAIL" + command_reason="$CHROOT_COMMAND_REASON" + fi + + if list_contains_package "$CHROOT_SERVICE_LIST" "$requested" "$CHROOT_BINARY_NAME"; then + service_status="SKIP" + service_reason="service package requires special configuration" + skip_reason="${skip_reason:+$skip_reason; }$service_reason" + else + test_package_services "$root" "$requested" "$log_prefix" + service_rc=$? + case "$service_rc" in + 0) service_status="PASS" ;; + 2) + service_status="SKIP" + skip_reason="${skip_reason:+$skip_reason; }$CHROOT_SERVICE_REASON" + ;; + *) service_status="FAIL" ;; + esac + service_reason="$CHROOT_SERVICE_REASON" + fi + fi + + if run_root_pkg_manager "$root" "$CHROOT_REMOVE_TIMEOUT" remove "$requested" \ + >"$remove_log" 2>&1; then + if grep -q 'scriptlet failed' "$remove_log"; then + remove_status="FAIL" + remove_reason="remove scriptlet failed" + else + remove_status="PASS" + remove_reason="removed successfully" + fi + else + remove_status="FAIL" + remove_reason="$(last_reason "$remove_log" "package manager remove returned non-zero")" + fi + + if [ "$install_status" = "FAIL" ] || [ "$command_status" = "FAIL" ] || \ + [ "$service_status" = "FAIL" ] || [ "$remove_status" = "FAIL" ]; then + overall="FAIL" + elif [ "$command_status" = "SKIP" ] || [ "$service_status" = "SKIP" ]; then + overall="SKIP" + else + overall="PASS" + fi + + write_worker_result "$result_file" "$CHROOT_SOURCE_PACKAGE" "$CHROOT_BINARY_NEVRA" \ + "$install_status" "$command_status" "$service_status" "$remove_status" "$overall" \ + "$install_reason" "$command_reason" "$service_reason" "$remove_reason" "$skip_reason" +} + +active_jobs() { + jobs -pr | wc -l +} + +wait_for_slot() { + while [ "$(active_jobs)" -ge "$CHROOT_JOBS" ]; do + wait -n || true + done +} + +write_missing_worker_result() { + local index="$1" + local package="$2" + local result_file="$CHROOT_RESULT_DIR/${index}.tsv" + [ -s "$result_file" ] && return 0 + write_worker_result "$result_file" UNKNOWN "$package" FAIL SKIP SKIP SKIP FAIL \ + "package test timed out or worker exited unexpectedly (> ${CHROOT_PACKAGE_TIMEOUT}s)" \ + "install did not complete" "install did not complete" "install did not complete" "" +} + +append_summary_section() { + local output="$1" + local label="$2" + local status_column="$3" + local reason_column="$4" + local count + count="$(awk -F '\t' -v column="$status_column" '$column == "FAIL" {count++} END {print count+0}' "$CHROOT_MERGED_TSV")" + printf '【%s】(%s)\n' "$label" "$count" >>"$output" + if [ "$count" -eq 0 ]; then + printf ' (无)\n' >>"$output" + else + awk -F '\t' -v column="$status_column" -v reason="$reason_column" \ + '$column == "FAIL" {printf " - %-36s 原因: %s\n", $2, $reason}' \ + "$CHROOT_MERGED_TSV" >>"$output" + fi +} + +generate_reports() { + local timestamp="$1" + local total="$2" + local result_file="$CHROOT_OUTPUT_DIR/package-command-result-chroot-${timestamp}.txt" + local summary_file="$CHROOT_OUTPUT_DIR/package-command-summary-chroot-${timestamp}.txt" + local index package result details + local passed failed skipped skip_count + + CHROOT_MERGED_TSV="$CHROOT_WORK_DIR/all-results.tsv" + export CHROOT_MERGED_TSV + : >"$CHROOT_MERGED_TSV" + for index in $(seq 1 "$total"); do + package="$(sed -n "${index}p" "$CHROOT_PACKAGE_LIST")" + write_missing_worker_result "$index" "$package" + cat "$CHROOT_RESULT_DIR/${index}.tsv" >>"$CHROOT_MERGED_TSV" + done + + printf '# source-package\tbinary_package\tinstall\tcommand\tservice\tremove\toverall\tdetails\n' >"$result_file" + while IFS=$'\t' read -r source binary install command service remove overall \ + install_reason command_reason service_reason remove_reason skip_reason; do + details="install=${install_reason}; command=${command_reason}; service=${service_reason}; remove=${remove_reason}" + [ -n "$skip_reason" ] && details="$details; skipped=${skip_reason}" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$source" "$binary" "$install" "$command" "$service" "$remove" "$overall" "$details" \ + >>"$result_file" + done <"$CHROOT_MERGED_TSV" + + passed="$(awk -F '\t' '$7 == "PASS" {count++} END {print count+0}' "$CHROOT_MERGED_TSV")" + failed="$(awk -F '\t' '$7 == "FAIL" {count++} END {print count+0}' "$CHROOT_MERGED_TSV")" + skipped="$(awk -F '\t' '$7 == "SKIP" {count++} END {print count+0}' "$CHROOT_MERGED_TSV")" + + { + printf '========== 测试失败汇总 (%s) ==========\n' "$(date '+%F %T')" + printf '环境: chroot | 并发度: %s | 待测包总数: %s | 通过: %s | 失败: %s | 跳过: %s\n' \ + "$CHROOT_JOBS" "$total" "$passed" "$failed" "$skipped" + } >"$summary_file" + append_summary_section "$summary_file" "安装失败" 3 8 + append_summary_section "$summary_file" "卸载失败" 6 11 + append_summary_section "$summary_file" "命令测试失败" 4 9 + append_summary_section "$summary_file" "服务测试失败" 5 10 + + skip_count="$(awk -F '\t' '$7 == "SKIP" && $12 != "" {count++} END {print count+0}' "$CHROOT_MERGED_TSV")" + printf '【跳过项】(%s)\n' "$skip_count" >>"$summary_file" + if [ "$skip_count" -eq 0 ]; then + printf ' (无)\n' >>"$summary_file" + else + awk -F '\t' '$7 == "SKIP" && $12 != "" {printf " - %-36s (%s)\n", $2, $12}' \ + "$CHROOT_MERGED_TSV" >>"$summary_file" + fi + printf '=======================================================\n' >>"$summary_file" + + cat "$summary_file" + log_msg "result report: $result_file" + log_msg "summary report: $summary_file" + CHROOT_FINAL_FAILED="$failed" +} + +cleanup_parent() { + local root + if [ -d "${CHROOT_WORK_DIR:-}/roots" ]; then + for root in "$CHROOT_WORK_DIR"/roots/*; do + [ -e "$root" ] || continue + cleanup_mounts "$root" + done + fi + if [ "${CHROOT_KEEP_WORKDIR:-0}" -ne 1 ] && [ -n "${CHROOT_WORK_DIR:-}" ]; then + case "$CHROOT_WORK_DIR" in + /var/tmp/yum-ci-chroot.* | /tmp/yum-ci-chroot.*) rm -rf -- "$CHROOT_WORK_DIR" ;; + esac + fi +} + +interrupt_parent() { + local pid + for pid in $(jobs -pr); do + kill "$pid" >/dev/null 2>&1 || true + done + wait >/dev/null 2>&1 || true + exit 130 +} + +detect_releasever() { + local value="" + for file in /etc/dnf/vars/releasever /etc/yum/vars/releasever; do + [ -s "$file" ] && value="$(head -n 1 "$file")" && break + done + if [ -z "$value" ] && command -v rpm >/dev/null 2>&1; then + value="$(rpm -E '%{?rhel}' 2>/dev/null)" + [ "$value" = '%{?rhel}' ] && value="" + fi + printf '%s' "$value" +} + +validate_environment() { + local command + [ "$(id -u)" -eq 0 ] || die "must run as root to create chroot and bind mounts" + for command in bash chroot cp file findmnt grep mount rpm sed sort strace timeout umount; do + command -v "$command" >/dev/null 2>&1 || die "required command not found: $command" + done + if command -v dnf >/dev/null 2>&1; then + CHROOT_PKG_MANAGER="$(command -v dnf)" + elif command -v yum >/dev/null 2>&1; then + CHROOT_PKG_MANAGER="$(command -v yum)" + else + die "dnf or yum is required" + fi + export CHROOT_PKG_MANAGER +} + +main() { + local default_jobs timestamp index package total + default_jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)" + CHROOT_JOBS="${CHROOT_JOBS:-$default_jobs}" + CHROOT_PACKAGE_TIMEOUT="${CHROOT_PACKAGE_TIMEOUT:-300}" + CHROOT_COMMAND_TIMEOUT="${CHROOT_COMMAND_TIMEOUT:-3}" + CHROOT_SERVICE_TIMEOUT="${CHROOT_SERVICE_TIMEOUT:-30}" + CHROOT_INSTALL_TIMEOUT="${CHROOT_INSTALL_TIMEOUT:-240}" + CHROOT_REMOVE_TIMEOUT="${CHROOT_REMOVE_TIMEOUT:-120}" + CHROOT_BASE_TIMEOUT="${CHROOT_BASE_TIMEOUT:-900}" + CHROOT_SERVICE_MODE="${CHROOT_SERVICE_MODE:-auto}" + CHROOT_KEEP_WORKDIR="${CHROOT_KEEP_WORKDIR:-0}" + CHROOT_WORK_DIR="${CHROOT_WORK_DIR:-}" + CHROOT_OUTPUT_DIR="${CHROOT_OUTPUT_DIR:-$(pwd)}" + CHROOT_BASE_ROOT_INPUT="${CHROOT_BASE_ROOT_INPUT:-}" + CHROOT_RELEASEVER="${CHROOT_RELEASEVER:-}" + + while getopts ':j:t:c:s:w:o:b:r:S:kh' option; do + case "$option" in + j) CHROOT_JOBS="$OPTARG" ;; + t) CHROOT_PACKAGE_TIMEOUT="$OPTARG" ;; + c) CHROOT_COMMAND_TIMEOUT="$OPTARG" ;; + s) CHROOT_SERVICE_TIMEOUT="$OPTARG" ;; + w) CHROOT_WORK_DIR="$OPTARG" ;; + o) CHROOT_OUTPUT_DIR="$OPTARG" ;; + b) CHROOT_BASE_ROOT_INPUT="$OPTARG" ;; + r) CHROOT_RELEASEVER="$OPTARG" ;; + S) CHROOT_SERVICE_MODE="$OPTARG" ;; + k) CHROOT_KEEP_WORKDIR=1 ;; + h) usage; return 0 ;; + :) die "option -$OPTARG requires a value" ;; + \?) die "unknown option: -$OPTARG" ;; + esac + done + shift $((OPTIND - 1)) + + [ "$#" -eq 3 ] || { usage >&2; die "expected exactly 3 list arguments"; } + CHROOT_PACKAGE_LIST_INPUT="$1" + CHROOT_DESKTOP_LIST="$2" + CHROOT_SERVICE_LIST="$3" + for file in "$CHROOT_PACKAGE_LIST_INPUT" "$CHROOT_DESKTOP_LIST" "$CHROOT_SERVICE_LIST"; do + [ -f "$file" ] || die "list file not found: $file" + done + CHROOT_PACKAGE_LIST_INPUT="$(realpath "$CHROOT_PACKAGE_LIST_INPUT")" + CHROOT_DESKTOP_LIST="$(realpath "$CHROOT_DESKTOP_LIST")" + CHROOT_SERVICE_LIST="$(realpath "$CHROOT_SERVICE_LIST")" + + for value in "$CHROOT_JOBS" "$CHROOT_PACKAGE_TIMEOUT" "$CHROOT_COMMAND_TIMEOUT" \ + "$CHROOT_SERVICE_TIMEOUT" "$CHROOT_INSTALL_TIMEOUT" "$CHROOT_REMOVE_TIMEOUT"; do + is_positive_integer "$value" || die "concurrency and timeout values must be positive integers" + done + case "$CHROOT_SERVICE_MODE" in auto | skip | systemd) ;; *) die "invalid service mode: $CHROOT_SERVICE_MODE" ;; esac + + validate_environment + [ -z "$CHROOT_RELEASEVER" ] && CHROOT_RELEASEVER="$(detect_releasever)" + timestamp="$(date '+%Y%m%d-%H%M%S')" + if [ -z "$CHROOT_WORK_DIR" ]; then + CHROOT_WORK_DIR="/var/tmp/yum-ci-chroot.${timestamp}.$$" + fi + mkdir -p "$CHROOT_WORK_DIR" "$CHROOT_OUTPUT_DIR" + CHROOT_WORK_DIR="$(realpath "$CHROOT_WORK_DIR")" + CHROOT_OUTPUT_DIR="$(realpath "$CHROOT_OUTPUT_DIR")" + CHROOT_PACKAGE_LIST="$CHROOT_WORK_DIR/pkg.list" + # Logs are reports too: keep them outside the disposable chroot work tree. + CHROOT_LOG_DIR="$CHROOT_OUTPUT_DIR/package-command-logs-chroot-${timestamp}" + CHROOT_RESULT_DIR="$CHROOT_WORK_DIR/results" + mkdir -p "$CHROOT_LOG_DIR" "$CHROOT_RESULT_DIR" "$CHROOT_WORK_DIR/roots" + awk '!/^[[:space:]]*(#|$)/ {print $1}' "$CHROOT_PACKAGE_LIST_INPUT" | sort -u >"$CHROOT_PACKAGE_LIST" + total="$(wc -l <"$CHROOT_PACKAGE_LIST")" + + export CHROOT_JOBS CHROOT_PACKAGE_TIMEOUT CHROOT_COMMAND_TIMEOUT CHROOT_SERVICE_TIMEOUT + export CHROOT_INSTALL_TIMEOUT CHROOT_REMOVE_TIMEOUT CHROOT_BASE_TIMEOUT CHROOT_SERVICE_MODE + export CHROOT_KEEP_WORKDIR CHROOT_WORK_DIR CHROOT_OUTPUT_DIR CHROOT_DESKTOP_LIST + export CHROOT_SERVICE_LIST CHROOT_PACKAGE_LIST CHROOT_LOG_DIR CHROOT_RESULT_DIR CHROOT_RELEASEVER + + trap cleanup_parent EXIT + trap interrupt_parent INT TERM HUP + if [ "$total" -eq 0 ]; then + log_msg "no package to test" + return 0 + fi + + prepare_base_root + log_msg "START: total=$total jobs=$CHROOT_JOBS package-timeout=${CHROOT_PACKAGE_TIMEOUT}s" + index=0 + while read -r package; do + [ -z "$package" ] && continue + index=$((index + 1)) + wait_for_slot + timeout --signal=TERM --kill-after=10 "$CHROOT_PACKAGE_TIMEOUT" \ + bash "$SCRIPT_PATH" --worker "$index" "$package" \ + >"$CHROOT_LOG_DIR/${index}-$(safe_filename "$package").log" 2>&1 & + done <"$CHROOT_PACKAGE_LIST" + wait || true + + generate_reports "$timestamp" "$total" + [ "$CHROOT_FINAL_FAILED" -eq 0 ] +} + +if [ "${1:-}" = "--worker" ]; then + [ "$#" -eq 3 ] || exit 2 + worker_main "$2" "$3" + exit 0 +fi + +main "$@" diff --git a/testcase/tests/test-command-execute-chroot.sh b/testcase/tests/test-command-execute-chroot.sh new file mode 100755 index 0000000..e42a46d --- /dev/null +++ b/testcase/tests/test-command-execute-chroot.sh @@ -0,0 +1,325 @@ +#!/bin/bash + +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$TEST_DIR/../.." && pwd)" +SCRIPT="$REPO_ROOT/testcase/command-execute-chroot.sh" +TMPDIR_TEST="$(mktemp -d /tmp/yum-ci-chroot-test.XXXXXX)" +MOCK_BIN="$TMPDIR_TEST/bin" +MOCK_STATE="$TMPDIR_TEST/state" +OUTPUT_DIR="$TMPDIR_TEST/output" +WORK_DIR="$TMPDIR_TEST/work" +OUTPUT_SYSTEMD="$TMPDIR_TEST/output-systemd" +WORK_SYSTEMD="$TMPDIR_TEST/work-systemd" +OUTPUT_TIMEOUT="$TMPDIR_TEST/output-timeout" +WORK_TIMEOUT="$TMPDIR_TEST/work-timeout" + +cleanup() { + if [ "${KEEP_TEST_TMP:-0}" -eq 1 ]; then + echo "KEEP_TEST_TMP: $TMPDIR_TEST" >&2 + else + rm -rf "$TMPDIR_TEST" + fi +} +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_contains() { + local file="$1" + local pattern="$2" + grep -F -- "$pattern" "$file" >/dev/null || fail "$file does not contain: $pattern" +} + +mkdir -p "$MOCK_BIN" "$MOCK_STATE" "$OUTPUT_DIR" "$OUTPUT_SYSTEMD" "$OUTPUT_TIMEOUT" + +cat >"$MOCK_BIN/mock-tool" <<'MOCK' +#!/bin/bash +set -u + +tool="$(basename "$0")" + +lock_state() { + while ! mkdir "$MOCK_STATE/lock" 2>/dev/null; do sleep 0.01; done +} + +unlock_state() { + rmdir "$MOCK_STATE/lock" +} + +track_begin() { + lock_state + touch "$MOCK_STATE/active.$$" + local current maximum + current="$(find "$MOCK_STATE" -name 'active.*' | wc -l)" + maximum="$(cat "$MOCK_STATE/max" 2>/dev/null || echo 0)" + [ "$current" -gt "$maximum" ] && echo "$current" >"$MOCK_STATE/max" + unlock_state +} + +track_end() { + lock_state + rm -f "$MOCK_STATE/active.$$" + unlock_state +} + +case "$tool" in + id) + [ "${1:-}" = "-u" ] && echo 0 || echo root + ;; + timeout) + exec /usr/bin/timeout "$@" + ;; + dnf | yum) + root="" + action="" + while [ "$#" -gt 0 ]; do + case "$1" in + --installroot=*) root="${1#*=}"; shift ;; + install | remove) action="$1"; shift; break ;; + *) shift ;; + esac + done + items=("$@") + mkdir -p "$root/bin" "$root/dev" "$root/proc" "$root/sys" \ + "$root/usr/bin" "$root/usr/lib/systemd/system" + touch "$root/bin/bash" + chmod +x "$root/bin/bash" + if [ "$action" = "install" ] && [ "${#items[@]}" -gt 1 ]; then + exit 0 + fi + package="${items[${#items[@]}-1]}" + track_begin + if [ "$package" = "timeout-pkg" ]; then + sleep 3 + fi + sleep 0.15 + if [ "$action" = "install" ]; then + if [ "$package" = "install-fail" ]; then + echo "missing dependency for install-fail" >&2 + track_end + exit 1 + fi + echo "$package" >"$root/.pkg" + touch "$root/usr/bin/$package" + chmod +x "$root/usr/bin/$package" + if [ "$package" = "special-svc" ] || [ "$package" = "auto-svc" ] || \ + [ "$package" = "systemd-ok" ] || [ "$package" = "systemd-fail" ]; then + touch "$root/usr/lib/systemd/system/$package.service" + fi + track_end + exit 0 + fi + if [ "$package" = "remove-fail" ]; then + echo "remove transaction failed for remove-fail" >&2 + track_end + exit 1 + fi + rm -f "$root/.pkg" + track_end + ;; + rpm) + if [ "${1:-}" = "-E" ]; then + echo 9 + exit 0 + fi + root="" + if [ "${1:-}" = "--root" ]; then + root="$2" + shift 2 + fi + operation="${1:-}" + shift || true + package="$(cat "$root/.pkg" 2>/dev/null || true)" + case "$operation" in + -ql) + echo "/usr/bin/$package" + if [ "$package" = "special-svc" ] || [ "$package" = "auto-svc" ] || \ + [ "$package" = "systemd-ok" ] || [ "$package" = "systemd-fail" ]; then + echo "/usr/lib/systemd/system/$package.service" + fi + ;; + -q) + format="" + if [ "${1:-}" = "--qf" ]; then + format="$2" + fi + case "$format" in + *SOURCERPM*) echo "$package-1.0-1.src.rpm" ;; + *VERSION*) echo "$package-1.0-1.x86_64" ;; + *) echo "$package" ;; + esac + ;; + esac + ;; + chroot) + root="$1" + shift + case "${1:-}" in + test) + [ "${2:-}" = "-x" ] && [ -x "$root${3:-}" ] + ;; + file) + echo "ELF 64-bit LSB executable" + ;; + systemctl) + action="" + for argument in "$@"; do + case "$argument" in + start | stop | is-active | is-system-running) action="$argument" ;; + esac + done + unit="${@: -1}" + if [ "$action" = "is-system-running" ]; then + # The default auto-mode run models a chroot without PID1. + exit 1 + fi + if [ "$unit" = "systemd-fail.service" ] && [ "$action" = "is-active" ]; then + exit 1 + fi + exit 0 + ;; + *) exit 0 ;; + esac + ;; + strace) + trace="" + command="" + while [ "$#" -gt 0 ]; do + case "$1" in + -o) trace="$2"; shift 2 ;; + chroot) command="$3"; break ;; + *) shift ;; + esac + done + echo "execve(\"$command\", [\"$command\"], 0x0) = 0" >"$trace" + if [[ "$command" == *cmd-fail ]]; then + echo "Segmentation fault (core dumped)" >&2 + exit 139 + fi + exit 0 + ;; + mount | umount | findmnt) + exit 0 + ;; + *) + echo "unexpected mock tool: $tool" >&2 + exit 99 + ;; +esac +MOCK +chmod +x "$MOCK_BIN/mock-tool" +for tool in id timeout dnf yum rpm chroot strace mount umount findmnt; do + ln -s mock-tool "$MOCK_BIN/$tool" +done + +cat >"$TMPDIR_TEST/pkg.list" <<'EOF' +okpkg +desktop-pkg +special-svc +auto-svc +install-fail +remove-fail +cmd-fail +EOF +echo desktop-pkg >"$TMPDIR_TEST/desktop.list" +echo special-svc >"$TMPDIR_TEST/service.list" + +export MOCK_STATE +export PATH="$MOCK_BIN:$PATH" +export CHROOT_BASE_TIMEOUT=30 +export CHROOT_INSTALL_TIMEOUT=30 +export CHROOT_REMOVE_TIMEOUT=30 + +set +e +bash "$SCRIPT" -j 3 -t 30 -c 2 -s 2 -w "$WORK_DIR" -o "$OUTPUT_DIR" \ + "$TMPDIR_TEST/pkg.list" "$TMPDIR_TEST/desktop.list" "$TMPDIR_TEST/service.list" \ + >"$TMPDIR_TEST/run.log" 2>&1 +run_rc=$? +set -e +[ "$run_rc" -eq 1 ] || fail "expected test run to report package failures with rc=1, got $run_rc" + +result_file="$(find "$OUTPUT_DIR" -maxdepth 1 -name 'package-command-result-chroot-*.txt' -print -quit)" +summary_file="$(find "$OUTPUT_DIR" -maxdepth 1 -name 'package-command-summary-chroot-*.txt' -print -quit)" +[ -s "$result_file" ] || fail "result report was not created" +[ -s "$summary_file" ] || fail "summary report was not created" + +assert_contains "$result_file" $'# source-package\tbinary_package\tinstall\tcommand\tservice\tremove\toverall\tdetails' +assert_contains "$summary_file" '待测包总数: 7 | 通过: 1 | 失败: 3 | 跳过: 3' +assert_contains "$summary_file" '【安装失败】(1)' +assert_contains "$summary_file" '【卸载失败】(1)' +assert_contains "$summary_file" '【命令测试失败】(1)' +assert_contains "$summary_file" '【服务测试失败】(0)' +assert_contains "$summary_file" '【跳过项】(3)' +assert_contains "$summary_file" 'desktop whitelist: command test skipped' +assert_contains "$summary_file" 'service package requires special configuration' +assert_contains "$summary_file" 'systemd/PID1 is unavailable in chroot' +assert_contains "$result_file" $'okpkg-1.0-1.x86_64\tPASS\tPASS\tPASS\tPASS\tPASS' +assert_contains "$result_file" $'desktop-pkg-1.0-1.x86_64\tPASS\tSKIP\tPASS\tPASS\tSKIP' +assert_contains "$result_file" $'special-svc-1.0-1.x86_64\tPASS\tPASS\tSKIP\tPASS\tSKIP' +assert_contains "$result_file" $'auto-svc-1.0-1.x86_64\tPASS\tPASS\tSKIP\tPASS\tSKIP' +assert_contains "$result_file" $'install-fail\tFAIL\tSKIP\tSKIP\tSKIP\tFAIL' +assert_contains "$result_file" $'remove-fail-1.0-1.x86_64\tPASS\tPASS\tPASS\tFAIL\tFAIL' +assert_contains "$result_file" $'cmd-fail-1.0-1.x86_64\tPASS\tFAIL\tPASS\tPASS\tFAIL' + +row_count="$(grep -vc '^#' "$result_file")" +[ "$row_count" -eq 7 ] || fail "expected 7 result rows, got $row_count" +max_concurrency="$(cat "$MOCK_STATE/max")" +[ "$max_concurrency" -ge 2 ] || fail "workers did not overlap; max concurrency=$max_concurrency" +remaining_roots="$(find "$WORK_DIR/roots" -mindepth 1 -maxdepth 1 -type d | wc -l)" +[ "$remaining_roots" -eq 0 ] || fail "worker chroot directories were not cleaned" +log_count="$(find "$OUTPUT_DIR" -path '*package-command-logs-chroot-*/*.log' | wc -l)" +[ "$log_count" -ge 7 ] || fail "expected per-package logs, got $log_count" + +set +e +bash "$SCRIPT" "$TMPDIR_TEST/pkg.list" "$TMPDIR_TEST/desktop.list" \ + >"$TMPDIR_TEST/args.log" 2>&1 +args_rc=$? +set -e +[ "$args_rc" -eq 2 ] || fail "invalid argument count should return 2" +assert_contains "$TMPDIR_TEST/args.log" 'expected exactly 3 list arguments' + +cat >"$TMPDIR_TEST/systemd.list" <<'EOF' +systemd-ok +systemd-fail +EOF +: >"$TMPDIR_TEST/empty.list" +set +e +bash "$SCRIPT" -j 2 -t 30 -c 2 -s 2 -S systemd \ + -w "$WORK_SYSTEMD" -o "$OUTPUT_SYSTEMD" \ + "$TMPDIR_TEST/systemd.list" "$TMPDIR_TEST/empty.list" "$TMPDIR_TEST/empty.list" \ + >"$TMPDIR_TEST/systemd-run.log" 2>&1 +systemd_rc=$? +set -e +[ "$systemd_rc" -eq 1 ] || fail "forced systemd run should expose one failing unit" +systemd_result="$(find "$OUTPUT_SYSTEMD" -maxdepth 1 -name 'package-command-result-chroot-*.txt' -print -quit)" +systemd_summary="$(find "$OUTPUT_SYSTEMD" -maxdepth 1 -name 'package-command-summary-chroot-*.txt' -print -quit)" +assert_contains "$systemd_summary" '待测包总数: 2 | 通过: 1 | 失败: 1 | 跳过: 0' +assert_contains "$systemd_summary" '【服务测试失败】(1)' +assert_contains "$systemd_summary" 'systemd-fail.service: start=0 status=1 stop=0' +assert_contains "$systemd_result" $'systemd-ok-1.0-1.x86_64\tPASS\tPASS\tPASS\tPASS\tPASS' +assert_contains "$systemd_result" $'systemd-fail-1.0-1.x86_64\tPASS\tPASS\tFAIL\tPASS\tFAIL' + +echo timeout-pkg >"$TMPDIR_TEST/timeout.list" +set +e +bash "$SCRIPT" -j 1 -t 1 -c 2 -s 2 \ + -w "$WORK_TIMEOUT" -o "$OUTPUT_TIMEOUT" \ + "$TMPDIR_TEST/timeout.list" "$TMPDIR_TEST/empty.list" "$TMPDIR_TEST/empty.list" \ + >"$TMPDIR_TEST/timeout-run.log" 2>&1 +timeout_rc=$? +set -e +[ "$timeout_rc" -eq 1 ] || fail "timed-out package should make the run fail" +timeout_result="$(find "$OUTPUT_TIMEOUT" -maxdepth 1 -name 'package-command-result-chroot-*.txt' -print -quit)" +timeout_summary="$(find "$OUTPUT_TIMEOUT" -maxdepth 1 -name 'package-command-summary-chroot-*.txt' -print -quit)" +assert_contains "$timeout_summary" '待测包总数: 1 | 通过: 0 | 失败: 1 | 跳过: 0' +assert_contains "$timeout_summary" '【安装失败】(1)' +assert_contains "$timeout_result" 'package test timed out or worker exited unexpectedly (> 1s)' + +bash "$SCRIPT" -h >/dev/null +bash -n "$SCRIPT" + +echo "PASS: command-execute-chroot integration tests" -- Gitee