diff --git a/command-execute-chroot.sh b/command-execute-chroot.sh new file mode 100644 index 0000000000000000000000000000000000000000..4914bc90b4909b9e4d6f9ed4e1f3c75cc8a701fc --- /dev/null +++ b/command-execute-chroot.sh @@ -0,0 +1,494 @@ +#!/bin/bash +# +# command-execute-chroot.sh +# +# 基于 chroot 对软件包进行"安装 -> 命令可执行性 -> 服务 -> 卸载"的并发隔离测试。 +# +# 本用例是 command-execute-whitelist.sh 的 chroot 隔离 + 并发版本: +# - 每个软件包在独立的 dnf --installroot chroot 中完成整条测试链,互不干扰; +# - 复用 tst_lib/ts_common.sh 的判定规则与结果格式(安装成功判定、命令可执行性 +# 判定思路、结果列语义 install/command/service/remove); +# - 通过 -j / CHROOT_JOBS 控制并发度; +# - chroot 内无 systemd/PID1,service 用 ExecStart 等价生命周期(start/status/stop) +# 测试:可测服务给 PASS 结论,不可测的记 SKIP 并注明原因,绝不误判为 FAIL; +# - 汇总按 安装失败/卸载失败/命令测试失败/服务测试失败/跳过项 分类,每项附原因, +# 并保证 通过 + 失败 + 跳过 == 待测总数。 +# +# 用法(三个位置参数 + 可选并发/超时旗标): +# command-execute-chroot.sh [-j N] [-t 秒] +# $1 pkg.list 待测软件包清单(每行一个二进制包名;为空则用发行版默认白名单) +# $2 desktop.whitelist 桌面白名单:命中则跳过命令测试(command=SKIP) +# $3 service.list 含服务的包清单:命中则跳过服务测试并记 skipped(原因:需特殊配置) +# +# -j N 并发度(同时测试的包数),亦可用环境变量 CHROOT_JOBS,默认 4 +# -t 秒 单包 install/remove 超时,亦可用环境变量 CHROOT_PKG_TIMEOUT,默认 900 +# +# 说明:chroot 内服务从不判 FAIL(起不来一律 skipped 附原因),故分类汇总中 +# 【服务测试失败】恒为 0 —— 设计使然、非漏实现(见验收标准2"不得误判为 fail")。 +# + +[ -z "$TST_TS_TOPDIR" ] && { TST_TS_TOPDIR="$(realpath "$(dirname "$0")/..")"; export TST_TS_TOPDIR; } +source "${TST_TS_TOPDIR}/tst_common/lib/common.sh" || exit 1 + +g_tmpdir="$(mktemp -d)" +g_jobs="${CHROOT_JOBS:-4}" +g_pkg_timeout="${CHROOT_PKG_TIMEOUT:-900}" +g_releasever="${CHROOT_RELEASEVER:-9}" + +# --------------------------------------------------------------------------- +# chroot 生命周期辅助 +# --------------------------------------------------------------------------- + +# 挂载一个 chroot 所需的伪文件系统。注意:不 bind /dev/pts, +# 并发场景下 umount -l 清理会破坏宿主 pty(曾导致 sudo 无法分配 pty)。 +_chroot_mount() { + local root="$1" + mkdir -p "$root/proc" "$root/sys" "$root/dev" + mount -t proc proc "$root/proc" 2>/dev/null + mount -t sysfs sysfs "$root/sys" 2>/dev/null + mount --bind /dev "$root/dev" 2>/dev/null +} + +# 卸载并删除 chroot。umount -l 兜底,避免残留挂载点阻塞 rm。 +_chroot_umount() { + local root="$1" + local m + for m in dev sys proc; do + mountpoint -q "$root/$m" && umount "$root/$m" 2>/dev/null + mountpoint -q "$root/$m" && umount -l "$root/$m" 2>/dev/null + done +} + +_chroot_cleanup() { + local root="$1" + # 先杀掉仍在使用该 root 的进程(服务测试可能留下后台进程),否则 umount 被占用 + fuser -k "$root" >/dev/null 2>&1 && sleep 1 + _chroot_umount "$root" + # --one-file-system:万一 umount 全部失败,rm 也绝不穿透仍挂着的 + # bind mount(如 /dev)删到宿主文件系统 + [ -n "$CHROOT_KEEP" ] || rm -rf --one-file-system "$root" 2>/dev/null +} + +# 在 chroot 内对包的主服务做"等价生命周期"测试(chroot 无 systemd)。 +# 结论写入全局 _svc_res / 原因写入 _svc_reason: +# PASS —— 后台启动 -> 存活(active) -> SIGTERM 停止 全走通; +# SKIP —— 该包"是服务"但环境所限测不了(无 ExecStart/二进制缺失/起不来/在 service.list), +# 附原因,绝不 FAIL;这类会计入【跳过项】。 +# NA —— 该包根本不含服务(无 .service unit),服务维度不适用,不影响整包判定(可算通过)。 +_chroot_test_service() { + local root="$1" pkg="$2" service_list="$3" + _svc_res="NA"; _svc_reason="" + + if grep -qxF "$pkg" "$service_list" 2>/dev/null; then + _svc_res="SKIP"; _svc_reason="含服务包, 需特殊配置" + echo "[$pkg] in service.list -> service SKIP (declared)" + return 0 + fi + + # 找主 unit:只看 systemd system 目录下的 .service,跳过 @ 模板;优先与包同名者 + local units u base main_unit="" + units="$(rpm --root "$root" -ql "$pkg" 2>/dev/null \ + | grep -E '/systemd/system/[^/]+\.service$' | grep -v '@')" + if [ -z "$units" ]; then + # 不含服务:NA,服务维度不适用,不拖累整包判定 + echo "[$pkg] no systemd .service unit -> service NA (not a service)" + return 0 + fi + for u in $units; do + base="$(basename "$u")" + [ "$base" = "$pkg.service" ] && { main_unit="$u"; break; } + done + [ -n "$main_unit" ] || main_unit="$(echo "$units" | head -1)" + echo "[$pkg] service main unit: $main_unit" + + # 取 ExecStart,去掉 systemd 的前缀修饰符(-/+/!/@/:) + local unit_file="$root$main_unit" execstart mainbin + execstart="$(grep -m1 '^ExecStart=' "$unit_file" 2>/dev/null | sed 's/^ExecStart=//')" + if [ -z "$execstart" ]; then + _svc_res="SKIP"; _svc_reason="无法从 unit 解析启动命令" + echo "[$pkg] no ExecStart in unit -> service SKIP (cannot derive start command)" + return 0 + fi + execstart="${execstart#[-+!@:]}" + mainbin="$(echo "$execstart" | awk '{print $1}')" + echo "[$pkg] ExecStart: $execstart" + if [ ! -x "$root$mainbin" ]; then + _svc_res="SKIP"; _svc_reason="服务二进制缺失/不可执行 ($mainbin)" + echo "[$pkg] ExecStart binary missing/not exec ($mainbin) -> service SKIP (env-limited)" + return 0 + fi + + # 等价生命周期:后台 chroot 启动(依赖宿主分词,chroot 内无需 /bin/sh) + echo "[$pkg] service start (equivalent lifecycle, no systemd in chroot)..." + chroot "$root" $execstart >/dev/null 2>&1 & + local spid=$! rc i + sleep 2 + if kill -0 "$spid" 2>/dev/null; then + echo "[$pkg] service status=active (process alive)" + kill -TERM "$spid" 2>/dev/null + for i in 1 2 3 4 5; do kill -0 "$spid" 2>/dev/null || break; sleep 1; done + kill -KILL "$spid" 2>/dev/null + wait "$spid" 2>/dev/null + echo "[$pkg] service stop=ok -> service PASS (start+status+stop 全走通)" + _svc_res="PASS" + else + wait "$spid" 2>/dev/null; rc=$? + echo "[$pkg] service exited immediately rc=$rc -> service SKIP (环境所限,非包缺陷)" + _svc_res="SKIP"; _svc_reason="启动后立即退出 rc=$rc (环境所限, 非包缺陷)" + fi + return 0 +} + +# --------------------------------------------------------------------------- +# 单个软件包的完整测试链(在子进程中运行,互相隔离) +# +# 参数:$1=二进制包名 $2=desktop白名单文件 $3=service清单文件 +# 产出:把一行 6 列 tab 结果写到 $g_tmpdir/results/.line +# 把整包日志写到 $g_tmpdir/logs/.log +# 结果行格式: 源包 \t 二进制包 \t install \t command \t service \t remove +# --------------------------------------------------------------------------- +_test_one_pkg() { + local pkg="$1" desktop_list="$2" service_list="$3" + local root="$g_tmpdir/roots/$pkg" + local log="$g_tmpdir/logs/$pkg.log" + local line="$g_tmpdir/results/$pkg.line" + local verdict="$g_tmpdir/results/$pkg.verdict" + local cmd_fail_detail="" + local yum_opts="--installroot=$root --releasever=$g_releasever --nogpgcheck --setopt=install_weak_deps=False" + + mkdir -p "$root" "$g_tmpdir/logs" "$g_tmpdir/results" + + # 源包名(复用框架能力;取不到就留空占位,不影响判定) + local pkg_src + pkg_src="$(get_src_pkg_by_bin "$pkg" 2>/dev/null)" + [ -n "$pkg_src" ] || pkg_src="$pkg" + + { + echo "==== [$pkg] begin $(date '+%F %T') ====" + + # --- 1) 安装 --------------------------------------------------------- + # 适配 chroot 的安装成功判定:不看 dnf 退出码/输出关键词,直接查目标 root 的 + # rpm 数据库里包是否真的装上。原因:在 --installroot chroot 内,安装后脚本 + # 够不到 systemd 常打印噪音、甚至令 dnf 返回非 0,但事务其实已完成、rpm 已落库。 + # 以 "rpm 是否已安装" 为唯一事实标准,最稳。 + local install_out install_rc + install_out="$(timeout -s KILL "$g_pkg_timeout" yum $yum_opts install -y "$pkg" 2>&1)" + install_rc=$? + echo "$install_out" + echo "[$pkg] dnf exit rc=$install_rc" + if ! rpm --root "$root" -q "$pkg" >/dev/null 2>&1; then + local ins_reason + if [ "$install_rc" -eq 137 ]; then + ins_reason="安装超时 (>${g_pkg_timeout}s)" + elif echo "$install_out" | grep -qiE "No match for argument|Unable to find a match|No package .* available"; then + ins_reason="未找到软件包 (yum install 无匹配)" + elif echo "$install_out" | grep -qi "conflicts with"; then + ins_reason="包冲突 ($(echo "$install_out" | grep -i 'conflicts with' | head -1 | sed 's/^[[:space:]]*//'))" + elif echo "$install_out" | grep -qi "nothing provides\|needed by"; then + ins_reason="依赖缺失 ($(echo "$install_out" | grep -i 'nothing provides' | head -1 | sed 's/^[[:space:]]*//'))" + else + ins_reason="安装未完成 (dnf rc=$install_rc, rpm 未入库)" + fi + echo "==== [$pkg] install FAIL: $ins_reason ====" + echo -e "${pkg_src}\t${pkg}\tFAIL\tSKIP\tSKIP\tSKIP" >"$line" + echo -e "install_fail\t${pkg}\t${ins_reason}" >"$verdict" + _chroot_cleanup "$root" + return 0 + fi + + _chroot_mount "$root" + + # --- 2) 命令可执行性 ------------------------------------------------- + local command_res="PASS" + if grep -qxF "$pkg" "$desktop_list" 2>/dev/null; then + command_res="SKIP" # 桌面白名单:跳过命令测试 + echo "[$pkg] in desktop whitelist -> command SKIP" + else + # 取该包安装的可执行文件(chroot 内 bin/sbin 下、且为 ELF 可执行) + local f rel_path abs_path exes="" + while read -r f; do + case "$f" in + */bin/*|*/sbin/*) : ;; + *) continue ;; + esac + abs_path="$root$f" + [ -x "$abs_path" ] || continue + file "$abs_path" 2>/dev/null | grep -q "ELF" || continue + exes="$exes $f" + done < <(rpm --root "$root" -ql "$pkg" 2>/dev/null) + + if [ -z "$exes" ]; then + echo "[$pkg] no executable command -> command PASS(none)" + else + for rel_path in $exes; do + if _chroot_run_cmd "$root" "$rel_path"; then + echo "[$pkg] cmd OK: $rel_path" + else + echo "[$pkg] cmd FAIL: $rel_path ($_cmd_fail_reason)" + command_res="FAIL" + [ -n "$cmd_fail_detail" ] || \ + cmd_fail_detail="命令: $(basename "$rel_path") 原因: $_cmd_fail_reason" + fi + done + fi + fi + + # --- 3) 服务测试 ----------------------------------------------------- + # chroot 无 systemd/PID1,systemctl 不可用。改用等价生命周期:从 ExecStart 取 + # 主二进制,后台 chroot 启动 -> 存活即 active -> SIGTERM 停止;全走通记 PASS, + # 起不来/无法测一律 SKIP 附原因,绝不判 FAIL(验收:不得误判为 fail)。 + local service_res _svc_res _svc_reason + _chroot_test_service "$root" "$pkg" "$service_list" + service_res="$_svc_res" + + # --- 4) 卸载 --------------------------------------------------------- + # 复用框架卸载规则:--setopt=clean_requirements_on_remove=False 只卸目标包; + # "scriptlet failed" 判失败;"protected packages" 视为通过。 + local remove_out remove_rc remove_res="PASS" remove_reason="" + remove_out="$(timeout -s KILL "$g_pkg_timeout" yum $yum_opts remove -y \ + --setopt=clean_requirements_on_remove=False "$pkg" 2>&1)" + remove_rc=$? + echo "$remove_out" + if [ "$remove_rc" -eq 137 ]; then + remove_res="FAIL"; remove_reason="卸载超时 (>${g_pkg_timeout}s)" + elif echo "$remove_out" | grep -q "scriptlet failed"; then + remove_res="FAIL"; remove_reason="卸载脚本失败 (scriptlet failed)" + elif echo "$remove_out" | grep -q "removing the following protected packages"; then + remove_res="PASS" + elif [ "$remove_rc" -ne 0 ]; then + remove_res="FAIL"; remove_reason="yum remove 返回非0 (rc=$remove_rc)" + fi + echo "[$pkg] remove $remove_res" + + echo -e "${pkg_src}\t${pkg}\tPASS\t${command_res}\t${service_res}\t${remove_res}" >"$line" + + # 计算包级归类 + 原因,写 verdict(供分类汇总,一包一票,保证对账)。 + # 优先级:卸载失败 > 命令测试失败 > (声明式)跳过 > 服务不可测跳过 > 通过。 + # 声明式跳过 = 命中桌面白名单(跳命令) 或 服务清单(跳服务)。 + # 服务不可测跳过 = 该包含服务但环境所限起不来(service_res=SKIP); + # 不含服务(service_res=NA)不算跳过,走通过。 + local vbucket vreason + if [ "$remove_res" = "FAIL" ]; then + vbucket="remove_fail"; vreason="$remove_reason" + elif [ "$command_res" = "FAIL" ]; then + vbucket="command_fail"; vreason="$cmd_fail_detail" + elif grep -qxF "$pkg" "$desktop_list" 2>/dev/null; then + vbucket="skip"; vreason="桌面白名单, 跳过命令测试" + elif grep -qxF "$pkg" "$service_list" 2>/dev/null; then + vbucket="skip"; vreason="含服务包, 需特殊配置, 跳过服务启动" + elif [ "$service_res" = "SKIP" ]; then + vbucket="skip"; vreason="服务不可测: ${_svc_reason:-环境所限}" + else + vbucket="pass"; vreason="" + fi + echo -e "${vbucket}\t${pkg}\t${vreason}" >"$verdict" + echo "==== [$pkg] end $(date '+%F %T') ====" + } >"$log" 2>&1 + + _chroot_cleanup "$root" + return 0 +} + +# 在 chroot 内执行一个命令并判定可执行性。 +# 判定思路复用 test_command_execute:优先 --help,退回 --version,短超时; +# 退出码 127(命令未找到)/126(不可执行) 或 崩溃(>=128,排除 124超时/130/143信号) +# 判为不可执行(FAIL);"error while loading shared libraries" 判 FAIL;其余判可执行。 +_chroot_run_cmd() { + local root="$1" cmd="$2" out rc + local arg + _cmd_fail_reason="" # 全局:失败原因,供上层写入汇总 + for arg in --help --version; do + out="$(timeout -s SIGKILL 5 chroot "$root" "$cmd" "$arg" 2>&1)" + rc=$? + if echo "$out" | grep -q "error while loading shared libraries"; then + _cmd_fail_reason="缺少共享库 (error while loading shared libraries)" + return 1 + fi + case "$rc" in + 127|126) continue ;; # 换下一个参数再试 + 124|137|130|143) return 0 ;; # 超时/被信号杀死:进程能起来,算可执行 + 0) return 0 ;; + *) + if [ "$rc" -ge 128 ]; then + _cmd_fail_reason="崩溃/core dumped (退出码 $rc)" + return 1 # 崩溃(段错误等) + fi + return 0 # 非零退出但有响应,视为可执行 + ;; + esac + done + _cmd_fail_reason="命令未找到/不可执行 (退出码 127/126)" + return 1 # --help / --version 均报命令未找到/不可执行 +} + +# --------------------------------------------------------------------------- +# 并发调度 + 结果汇总 +# --------------------------------------------------------------------------- +test_package_command_chroot() { + local pkg_list="$1" desktop_list="$2" service_list="$3" + local result_file="$g_tmpdir/package-command-result-chroot.txt" + local summary_file="$g_tmpdir/summary-chroot.txt" + + mkdir -p "$g_tmpdir/roots" "$g_tmpdir/logs" "$g_tmpdir/results" + + # 清洗待测清单(复用框架 clean_package_list_to_test;无则退化为去空行/注释) + local pkgs + if declare -F clean_package_list_to_test >/dev/null 2>&1; then + pkgs="$(clean_package_list_to_test "$pkg_list" 2>/dev/null)" + fi + [ -n "$pkgs" ] || pkgs="$(grep -vE '^\s*(#|$)' "$pkg_list" | awk '{print $1}')" + # 排除 debuginfo/debugsource(与框架一致),并按包名去重 + # (verdict 文件按包名 keyed,重复包名会互相覆盖导致票数<总数、对账误报)。 + pkgs="$(echo "$pkgs" | grep -vE 'debuginfo$|debugsource$' | awk 'NF && !seen[$0]++')" + + local total=0 p + for p in $pkgs; do total=$((total + 1)); done + msg "chroot pkg test: total=$total jobs=$g_jobs releasever=$g_releasever" + + # 并发调度:始终以"实际在跑的后台任务数"为节流基准(jobs -rp), + # 避免自维护计数器与真实并发漂移。达到上限就回收一个再继续投放。 + for p in $pkgs; do + _test_one_pkg "$p" "$desktop_list" "$service_list" & + while [ "$(jobs -rp | wc -l)" -ge "$g_jobs" ]; do + wait -n 2>/dev/null || sleep 0.2 + done + done + wait + + # 合并结果(保持清单顺序) + echo -e "# source-package\tbinary_package\tinstall\tcommand\tservice\tremove" >"$result_file" + for p in $pkgs; do + if [ -f "$g_tmpdir/results/$p.line" ]; then + cat "$g_tmpdir/results/$p.line" >>"$result_file" + else + # 单包整体异常(超时/被杀)也要有一行,且不丢账 + echo -e "$p\t$p\tFAIL\tSKIP\tSKIP\tSKIP" >>"$result_file" + fi + # 每包必须有一票 verdict,缺失(异常退出)按失败计,保证对账 + [ -f "$g_tmpdir/results/$p.verdict" ] || \ + echo -e "install_fail\t$p\t任务异常 (超时/被杀, 无结果)" >"$g_tmpdir/results/$p.verdict" + done + + _summarize "$summary_file" "$total" + + msg "结果明细: $result_file" + msg "分类汇总: $summary_file" + cat "$summary_file" + return 0 +} + +# 读各包 verdict(bucket \t bin \t reason),按 安装/卸载/命令/服务失败 + 跳过项 +# 分类汇总,每项附包名与原因,并做 通过+失败+跳过=待测总数 对账。 +# 格式对齐 issue 验收标准中的"测试失败汇总"样式。 +_summarize() { + local summary_file="$1" total="$2" + local pass=0 fail=0 skip=0 + local n_install=0 n_remove=0 n_command=0 n_service=0 n_skip=0 + local L_install="" L_remove="" L_command="" L_service="" L_skip="" + local vf bucket bin reason nl=$'\n' + + for vf in "$g_tmpdir"/results/*.verdict; do + [ -f "$vf" ] || continue + IFS=$'\t' read -r bucket bin reason <"$vf" + case "$bucket" in + pass) pass=$((pass + 1)) ;; + install_fail) fail=$((fail + 1)); n_install=$((n_install + 1)) + L_install="${L_install}${bin} 原因: ${reason}${nl}" ;; + remove_fail) fail=$((fail + 1)); n_remove=$((n_remove + 1)) + L_remove="${L_remove}${bin} 原因: ${reason}${nl}" ;; + command_fail) fail=$((fail + 1)); n_command=$((n_command + 1)) + L_command="${L_command}${bin} ${reason}${nl}" ;; + service_fail) fail=$((fail + 1)); n_service=$((n_service + 1)) + L_service="${L_service}${bin} 原因: ${reason}${nl}" ;; + skip) skip=$((skip + 1)); n_skip=$((n_skip + 1)) + L_skip="${L_skip}${bin} (${reason})${nl}" ;; + esac + done + + { + echo "========== 测试失败汇总 ($(date '+%F %T')) ==========" + echo "环境: chroot | 并发度: $g_jobs | 待测包总数: $total | 通过: $pass | 失败: $fail | 跳过: $skip" + echo "对账: 通过($pass) + 失败($fail) + 跳过($skip) = $((pass + fail + skip)) (待测总数 $total)" + echo "【安装失败】($n_install)"; if [ "$n_install" -gt 0 ]; then printf '%s' "$L_install"; else echo "(无)"; fi + echo "【卸载失败】($n_remove)"; if [ "$n_remove" -gt 0 ]; then printf '%s' "$L_remove"; else echo "(无)"; fi + echo "【命令测试失败】($n_command)"; if [ "$n_command" -gt 0 ]; then printf '%s' "$L_command"; else echo "(无)"; fi + echo "【服务测试失败】($n_service)"; if [ "$n_service" -gt 0 ]; then printf '%s' "$L_service"; else echo "(无)"; fi + echo "【跳过项】($n_skip)"; if [ "$n_skip" -gt 0 ]; then printf '%s' "$L_skip"; else echo "(无)"; fi + echo "=======================================================" + if [ $((pass + fail + skip)) -ne "$total" ]; then + echo "!!! 对账不平:verdict 票数($((pass + fail + skip))) 与待测总数($total) 不一致,请查 $g_tmpdir/logs/" + fi + } >"$summary_file" +} + +# --------------------------------------------------------------------------- +# 框架钩子 +# --------------------------------------------------------------------------- +tc_setup() { + msg "this is tc_setup (chroot)" + msg "g_tmpdir=$g_tmpdir jobs=$g_jobs" + assert_true yum clean all + assert_true yum makecache + return 0 +} + +do_test() { + local pkg_list desktop_whitelist service_whitelist + local -a pos=() + + # 先解析可选旗标 -j N(并发度) / -t 秒(单包超时),其余作为位置参数。 + # 旗标优先于同名环境变量 CHROOT_JOBS / CHROOT_PKG_TIMEOUT。 + while [ $# -gt 0 ]; do + case "$1" in + -j) [ $# -ge 2 ] && { g_jobs="$2"; shift; }; shift ;; + -j*) g_jobs="${1#-j}"; shift ;; + -t) [ $# -ge 2 ] && { g_pkg_timeout="$2"; shift; }; shift ;; + -t*) g_pkg_timeout="${1#-t}"; shift ;; + --) shift; while [ $# -gt 0 ]; do pos+=("$1"); shift; done ;; + *) pos+=("$1"); shift ;; + esac + done + set -- "${pos[@]}" + + # 数值校验:-j/-t 传了非正整数(typo)会导致节流条件恒假、并发失控, + # 这里回退默认并告警,绝不带病往下跑。 + case "$g_jobs" in + ''|*[!0-9]*|0) msg "无效并发度 '-j $g_jobs',回退默认 4"; g_jobs=4 ;; + esac + case "$g_pkg_timeout" in + ''|*[!0-9]*|0) msg "无效超时 '-t $g_pkg_timeout',回退默认 900"; g_pkg_timeout=900 ;; + esac + + # 位置参数:$1=pkg.list $2=桌面白名单 $3=含服务包清单 + if [ -n "$1" ]; then + pkg_list="$(realpath "$1")" + else + local ts_release_type + ts_release_type="$(get_ts_release_type)" + pkg_list="$(realpath "./yum/${ts_release_type}.package.whitelist")" + fi + # 白名单路径若传了但不存在,必须报错而非静默失效(realpath 对不存在路径 + # 输出空串,后续 grep 白名单会形同虚设、GUI 包被误测命令)。 + [ -z "$2" ] || assert_true [ -f "$2" ] + [ -z "$3" ] || assert_true [ -f "$3" ] + desktop_whitelist="$(realpath "${2:-/dev/null}")" + service_whitelist="$(realpath "${3:-/dev/null}")" + + assert_true [ -f "$pkg_list" ] + assert_true test_package_command_chroot \ + "$pkg_list" "$desktop_whitelist" "$service_whitelist" + return 0 +} + +tc_teardown() { + msg "this is tc_teardown (chroot)" + # 兜底清理所有残留挂载 + local m + for m in "$g_tmpdir"/roots/*/dev "$g_tmpdir"/roots/*/sys "$g_tmpdir"/roots/*/proc; do + [ -d "$m" ] || continue + mountpoint -q "$m" && umount -l "$m" 2>/dev/null + done + return 0 +} + +tst_main "$@"