From f25c79dabfa57e816939e523b5b98d936a4fe4c3 Mon Sep 17 00:00:00 2001 From: jacksonbai Date: Fri, 31 Jul 2026 17:40:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Docker=20=E5=B9=B6?= =?UTF-8?q?=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 --- docs/command-execute-docker.md | 111 +++ testcase/command-execute-docker.sh | 648 ++++++++++++++++++ testcase/lib/command-execute-docker-worker.sh | 371 ++++++++++ testcase/tests/test-command-execute-docker.sh | 235 +++++++ 4 files changed, 1365 insertions(+) create mode 100644 docs/command-execute-docker.md create mode 100755 testcase/command-execute-docker.sh create mode 100755 testcase/lib/command-execute-docker-worker.sh create mode 100755 testcase/tests/test-command-execute-docker.sh diff --git a/docs/command-execute-docker.md b/docs/command-execute-docker.md new file mode 100644 index 0000000..9905514 --- /dev/null +++ b/docs/command-execute-docker.md @@ -0,0 +1,111 @@ +# Docker 并发软件包测试 + +`testcase/command-execute-docker.sh` 在相互隔离的 OpenCloudOS 容器中并发执行 +软件包安装、命令验证、服务验证和卸载。该脚本独立于现有 +`command-execute-whitelist.sh`,不会改变原测试用例的行为。 + +## 使用方法 + +脚本固定接收三个位置参数: + +```bash +testcase/command-execute-docker.sh -j 4 \ + testcase/pkg-to-test.list \ + testcase/desktop-white.pkg \ + testcase/service-white.pkg +``` + +- 第一个参数是待测软件包列表。 +- 第二个参数是桌面软件包白名单,命中的包跳过命令测试。 +- 第三个参数是需要特殊配置的服务软件包列表,命中的包跳过服务启动测试。 + 跳过会记录原因,不会误判为失败。 + +每个列表忽略空行和以 `#` 开头的行,并读取每行第一列。与现有逻辑一致, +`livepatch-kernel`、`debuginfo` 和 `debugsource` 包不进入测试。 +命令验证沿用 `tst_lib/ts_common.sh` 的判定思路:依次尝试 `--help` 和 +`--version`,检查缺失动态库及异常信号,并用 `strace` 确认可执行文件能够 +完成 `execve`;脚本文件继续按原逻辑忽略。 + +常用选项: + +```text +-j N 并发容器数 +-i IMAGE OpenCloudOS 镜像 +-t SECONDS 单包超时 +--cpus N 单容器 CPU 限额 +--memory SIZE 单容器内存限额 +-o DIR 结果目录 +--repo-dir DIR 使用指定的软件源配置 +--service-mode MODE systemd 或 skip +``` + +默认镜像为官方 `opencloudos/opencloudos9-init:latest`。它带有 `dnf` 和 +systemd,镜像内的软件源与 OpenCloudOS 9 对应。测试其他 OpenCloudOS +版本时应通过 `-i` 选择对应镜像;测试自定义或待发布仓库时,通过 +`--repo-dir` 只读挂载与被测版本匹配的 `.repo` 文件。 + +## 服务测试和限制 + +默认 `--service-mode systemd` 会使用 `--privileged` 启动 +`/sbin/init`,并挂载宿主机 cgroup。工作脚本通过 +`rpm -ql | grep -E '\.(service|socket|target)$'` 识别 unit,然后依次执行: + +1. `systemctl start` +2. `systemctl is-active`,状态必须为 `active` +3. `systemctl stop` + +模板 unit(名称含 `@`)不会直接启动。拒绝手动启停的 unit 会写明原因并 +记为跳过。某些 CI 环境不允许 privileged 容器或不提供可写 cgroup,此时 +可使用 `--service-mode skip`;服务项会明确记录“systemd 不可用”,不会 +记为失败。特殊服务软件包列表的优先级更高,无论 systemd 是否可用都会跳过 +启停并记录“需额外配置”。 + +## 隔离、清理和结果 + +每个软件包使用一个独立容器,单包失败或超时不会阻塞其他包。脚本使用 +`--rm`,并用本次运行的唯一 label 在退出时清理由自己创建的残留容器。 +结果使用 bind mount 写回宿主机,不创建 Docker volume,也不构建新镜像, +因此不会遗留 volume 或镜像中间层。作为输入的基础镜像不会被删除。 + +每个包有独立日志目录。总报告名为 +`package-command-result-*.txt`,与现有报告一样是纯 TSV,字段为: + +```text +source-package binary_package install command service remove +``` + +分类失败清单另存为同一时间戳的 `failure-summary-*.txt`,并同时打印到标准 +输出,因此总报告不会混入非 TSV 内容: + +```text +========== 测试失败汇总 (2026-07-31 16:31:44) ========== +环境: docker | 并发度: 3 | 待测包总数: 7 | 通过: 1 | 失败: 4 | 跳过: 2 +校验: 通过 1 + 失败 4 + 跳过 2 = 7 + +【安装失败】(1) + - install-fail 原因: 模拟安装失败 + +【卸载失败】(1) + - remove-fail 原因: 卸载后软件包仍存在 + +【命令测试失败】(1) + - command-fail 原因: /usr/bin/bad 执行失败 + +【服务测试失败】(1) + - service-fail 原因: bad.service 启动失败 + +【跳过项】(2) + - desktop-pkg (跳过命令测试: 命中桌面软件包白名单) + - service-pkg (跳过服务测试: 命中特殊服务软件包列表,需额外配置) +======================================================= +``` + +每个包只归入一类:任一阶段失败记为失败,无失败但有阶段被跳过记为跳过, +其余记为通过,因此始终满足: + +```text +通过 + 失败 + 跳过 = 待测包总数 +``` + +任一包出现失败时脚本返回 `1`;只有通过和明确跳过项时返回 `0`;参数或运行 +环境错误返回 `2`。 diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh new file mode 100755 index 0000000..07258c0 --- /dev/null +++ b/testcase/command-execute-docker.sh @@ -0,0 +1,648 @@ +#!/bin/bash +# +# Run the package install/command/service/remove checks in isolated containers. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +readonly WORKER_SCRIPT="${SCRIPT_DIR}/lib/command-execute-docker-worker.sh" + +JOBS=4 +IMAGE="opencloudos/opencloudos9-init:latest" +CONTAINER_TIMEOUT=600 +CPUS="1" +MEMORY="1g" +OUTPUT_DIR="" +SERVICE_MODE="systemd" +REPO_DIR="" +DOCKER_BIN="${DOCKER_BIN:-docker}" +RUN_ID="yum-ci-pkg-test-$(date +%Y%m%d%H%M%S)-$$" +readonly MAIN_SUBSHELL_LEVEL="$BASH_SUBSHELL" +RUN_DIR="" +REPORT_FILE="" +SUMMARY_FILE="" + +usage() { + cat <<'EOF' +用法: + command-execute-docker.sh [选项] <软件包列表> <桌面软件包白名单> <特殊服务软件包列表> + +必选位置参数: + 软件包列表 每行一个待测二进制包 + 桌面软件包白名单 命中的包跳过命令执行测试 + 特殊服务软件包列表 命中的包跳过服务启动测试,并记录原因 + +选项: + -j N 并发容器数,默认 4 + -i IMAGE OpenCloudOS 基础镜像,默认 + opencloudos/opencloudos9-init:latest + -t SECONDS 单包测试超时,默认 600 秒 + --cpus N 单容器 CPU 限额,默认 1 + --memory SIZE 单容器内存限额,默认 1g + -o DIR 结果目录,默认 ./docker-package-result-时间戳 + --repo-dir DIR 将指定 repo 目录只读挂载到容器 + --service-mode MODE systemd 或 skip,默认 systemd + -h, --help 显示帮助 +EOF +} + +die() { + echo "错误: $*" >&2 + exit 2 +} + +is_positive_integer() { + [[ "$1" =~ ^[1-9][0-9]*$ ]] +} + +absolute_path() { + local path="$1" + local directory + local basename + + directory="$(cd "$(dirname "$path")" 2>/dev/null && pwd)" || return 1 + basename="$(basename "$path")" + printf '%s/%s\n' "$directory" "$basename" +} + +validate_input_file() { + local path="$1" + local description="$2" + + [[ -f "$path" ]] || die "${description}不存在: ${path}" + [[ -r "$path" ]] || die "${description}不可读: ${path}" +} + +package_matches_list() { + local package="$1" + local list_file="$2" + + awk -v package="$package" ' + /^[[:space:]]*#/ || NF == 0 { next } + { + item = $1 + if (package == item) { + found = 1 + exit + } + } + END { exit(found ? 0 : 1) } + ' "$list_file" +} + +safe_name() { + printf '%s' "$1" | tr -c 'A-Za-z0-9_.-' '_' +} + +# shellcheck disable=SC2329 # 由 EXIT trap 调用。 +cleanup_containers() { + local container_id + + command -v "$DOCKER_BIN" >/dev/null 2>&1 || return 0 + while IFS= read -r container_id; do + [[ -n "$container_id" ]] || continue + "$DOCKER_BIN" rm -f "$container_id" >/dev/null 2>&1 || true + done < <("$DOCKER_BIN" ps -aq \ + --filter "label=opencloudos.yum-ci-pkg-test.run=${RUN_ID}" 2>/dev/null) +} + +# shellcheck disable=SC2329 # 由 trap 调用。 +on_exit() { + [[ "$BASH_SUBSHELL" -eq "$MAIN_SUBSHELL_LEVEL" ]] || return 0 + cleanup_containers +} + +trap on_exit EXIT +trap 'exit 130' INT TERM + +write_synthetic_result() { + local result_dir="$1" + local package="$2" + local install_status="$3" + local install_reason="$4" + local command_status="$5" + local command_reason="$6" + local service_status="$7" + local service_reason="$8" + local remove_status="$9" + local remove_reason="${10}" + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$package" "$package" \ + "$install_status" "$install_reason" \ + "$command_status" "$command_reason" \ + "$service_status" "$service_reason" \ + "$remove_status" "$remove_reason" >"${result_dir}/result.tsv" +} + +run_with_timeout() { + local limit="$1" + local marker + local command_pid + local watchdog_pid + local rc + shift + + marker="$(mktemp "${RUN_DIR}/.timeout.XXXXXX")" || return 125 + rm -f "$marker" + "$@" & + command_pid=$! + ( + sleep "$limit" + if kill -0 "$command_pid" >/dev/null 2>&1; then + : >"$marker" + kill -TERM "$command_pid" >/dev/null 2>&1 || true + sleep 15 + kill -KILL "$command_pid" >/dev/null 2>&1 || true + fi + ) & + watchdog_pid=$! + + wait "$command_pid" + rc=$? + kill "$watchdog_pid" >/dev/null 2>&1 || true + wait "$watchdog_pid" >/dev/null 2>&1 || true + if [[ -f "$marker" ]]; then + rm -f "$marker" + return 124 + fi + rm -f "$marker" + return "$rc" +} + +run_direct_container() { + local container_name="$1" + local package="$2" + local skip_command="$3" + local special_service="$4" + local result_dir="$5" + local -a docker_args + + docker_args=( + run --rm + --name "$container_name" + --label "opencloudos.yum-ci-pkg-test.run=${RUN_ID}" + --cpus "$CPUS" + --memory "$MEMORY" + -e "TEST_PACKAGE=${package}" + -e "SKIP_COMMAND=${skip_command}" + -e "SPECIAL_SERVICE=${special_service}" + -e "SERVICE_MODE=skip" + -v "${WORKER_SCRIPT}:/opt/yum-ci-pkg-test/worker.sh:ro" + -v "${result_dir}:/results" + ) + if [[ -n "$REPO_DIR" ]]; then + docker_args+=(-v "${REPO_DIR}:/etc/yum.repos.d:ro") + fi + docker_args+=("$IMAGE" /bin/bash /opt/yum-ci-pkg-test/worker.sh /results) + + run_with_timeout "$CONTAINER_TIMEOUT" \ + "$DOCKER_BIN" "${docker_args[@]}" +} + +wait_for_systemd() { + local container_name="$1" + local _ + + for _ in {1..30}; do + if "$DOCKER_BIN" exec "$container_name" \ + systemctl is-system-running 2>/dev/null | + grep -Eq '^(running|degraded)$'; then + return 0 + fi + if ! "$DOCKER_BIN" inspect -f '{{.State.Running}}' "$container_name" \ + 2>/dev/null | grep -qx true; then + return 1 + fi + sleep 1 + done + return 1 +} + +run_systemd_container() { + local container_name="$1" + local package="$2" + local skip_command="$3" + local special_service="$4" + local result_dir="$5" + local -a docker_args + local rc + + docker_args=( + run -d --rm + --name "$container_name" + --label "opencloudos.yum-ci-pkg-test.run=${RUN_ID}" + --privileged + --cgroupns=host + --stop-timeout 15 + --cpus "$CPUS" + --memory "$MEMORY" + -v "/sys/fs/cgroup:/sys/fs/cgroup:rw" + -v "${WORKER_SCRIPT}:/opt/yum-ci-pkg-test/worker.sh:ro" + -v "${result_dir}:/results" + ) + if [[ -n "$REPO_DIR" ]]; then + docker_args+=(-v "${REPO_DIR}:/etc/yum.repos.d:ro") + fi + docker_args+=("$IMAGE" /sbin/init) + + if ! "$DOCKER_BIN" "${docker_args[@]}" >"${result_dir}/container.id" \ + 2>"${result_dir}/container-start.log"; then + return 125 + fi + + if ! wait_for_systemd "$container_name"; then + echo "systemd 未能在容器中就绪" >>"${result_dir}/container-start.log" + "$DOCKER_BIN" rm -f "$container_name" >/dev/null 2>&1 || true + return 125 + fi + + run_with_timeout "$CONTAINER_TIMEOUT" \ + "$DOCKER_BIN" exec \ + -e "TEST_PACKAGE=${package}" \ + -e "SKIP_COMMAND=${skip_command}" \ + -e "SPECIAL_SERVICE=${special_service}" \ + -e "SERVICE_MODE=systemd" \ + "$container_name" /bin/bash /opt/yum-ci-pkg-test/worker.sh /results + rc=$? + "$DOCKER_BIN" stop -t 10 "$container_name" >/dev/null 2>&1 || true + return "$rc" +} + +run_one_package() { + local index="$1" + local package="$2" + local skip_command="$3" + local special_service="$4" + local directory_name + local result_dir + local container_name + local rc + + directory_name="$(printf '%04d-%s' "$index" "$(safe_name "$package")")" + result_dir="${RUN_DIR}/packages/${directory_name}" + container_name="${RUN_ID}-${index}" + mkdir -p "$result_dir" + + echo "[$index] 开始测试 ${package}" + if [[ "$SERVICE_MODE" == "systemd" ]]; then + run_systemd_container "$container_name" "$package" \ + "$skip_command" "$special_service" "$result_dir" \ + >"${result_dir}/container.log" 2>&1 + rc=$? + else + run_direct_container "$container_name" "$package" \ + "$skip_command" "$special_service" "$result_dir" \ + >"${result_dir}/container.log" 2>&1 + rc=$? + fi + + if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then + "$DOCKER_BIN" rm -f "$container_name" >/dev/null 2>&1 || true + fi + if [[ ! -s "${result_dir}/result.tsv" ]]; then + if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then + write_synthetic_result "$result_dir" "$package" \ + FAIL "容器测试超过 ${CONTAINER_TIMEOUT} 秒" \ + SKIP "安装阶段未完成" SKIP "安装阶段未完成" \ + SKIP "安装阶段未完成" + else + write_synthetic_result "$result_dir" "$package" \ + FAIL "容器执行失败,退出码 ${rc}" \ + SKIP "安装阶段未完成" SKIP "安装阶段未完成" \ + SKIP "安装阶段未完成" + fi + fi + printf '%s\t%s\t%s\n' "$index" "$package" "$result_dir" \ + >"${RUN_DIR}/metadata/$(printf '%06d' "$index").tsv" + echo "[$index] 完成测试 ${package}" +} + +worker_loop() { + local queue_file="$1" + local index + local package + local skip_command + local special_service + + while IFS=$'\t' read -r index package skip_command special_service; do + [[ -n "$package" ]] || continue + run_one_package "$index" "$package" "$skip_command" "$special_service" + done <"$queue_file" +} + +append_failure() { + local file="$1" + local package="$2" + local reason="$3" + + printf -- ' - %-28s 原因: %s\n' "$package" "$reason" >>"$file" +} + +append_skipped() { + local file="$1" + local package="$2" + local detail="$3" + + printf -- ' - %-28s (%s)\n' "$package" "$detail" >>"$file" +} + +join_stages() { + local separator="" + local stage + + for stage in "$@"; do + printf '%s%s' "$separator" "$stage" + separator="; " + done +} + +# 按验收标准的格式输出一个分类块:标题带条目数,空类别显示“(无)”。 +print_category() { + local title="$1" + local file="$2" + local count=0 + + [[ ! -s "$file" ]] || count="$(wc -l <"$file" | tr -d ' ')" + printf '\n【%s】(%s)\n' "$title" "$count" + if [[ -s "$file" ]]; then + cat "$file" + else + printf ' (无)\n' + fi +} + +aggregate_results() { + local total=0 + local passed=0 + local failed=0 + local skipped=0 + local metadata_file + local index + local requested_package + local result_dir + local source_package + local binary_package + local install_status + local install_reason + local command_status + local command_reason + local service_status + local service_reason + local remove_status + local remove_reason + local final_status + local -a result_fields + local -a skipped_stages + local install_failures="${RUN_DIR}/summary.install" + local command_failures="${RUN_DIR}/summary.command" + local service_failures="${RUN_DIR}/summary.service" + local remove_failures="${RUN_DIR}/summary.remove" + local skipped_items="${RUN_DIR}/summary.skipped" + + : >"$install_failures" + : >"$command_failures" + : >"$service_failures" + : >"$remove_failures" + : >"$skipped_items" + printf '# source-package\tbinary_package\tinstall\tcommand\tservice\tremove\n' \ + >"$REPORT_FILE" + + while IFS= read -r metadata_file; do + IFS=$'\t' read -r index requested_package result_dir <"$metadata_file" + IFS=$'\t' read -r -a result_fields <"${result_dir}/result.tsv" + source_package="${result_fields[0]:-$requested_package}" + binary_package="${result_fields[1]:-$requested_package}" + install_status="${result_fields[2]:-FAIL}" + install_reason="${result_fields[3]:-结果字段缺失}" + command_status="${result_fields[4]:-SKIP}" + command_reason="${result_fields[5]:-结果字段缺失}" + service_status="${result_fields[6]:-SKIP}" + service_reason="${result_fields[7]:-结果字段缺失}" + remove_status="${result_fields[8]:-SKIP}" + remove_reason="${result_fields[9]:-结果字段缺失}" + + printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$source_package" "$binary_package" \ + "$install_status" "$command_status" \ + "$service_status" "$remove_status" >>"$REPORT_FILE" + + final_status=PASS + if [[ "$install_status" == FAIL || "$command_status" == FAIL || + "$service_status" == FAIL || "$remove_status" == FAIL ]]; then + final_status=FAIL + elif [[ "$install_status" == SKIP || "$command_status" == SKIP || + "$service_status" == SKIP || "$remove_status" == SKIP ]]; then + final_status=SKIP + fi + + total=$((total + 1)) + case "$final_status" in + PASS) passed=$((passed + 1)) ;; + FAIL) failed=$((failed + 1)) ;; + SKIP) skipped=$((skipped + 1)) ;; + esac + + if [[ "$final_status" == FAIL ]]; then + # 只列真正失败的阶段;失败包的后续阶段虽被跳过,但不属于跳过项。 + [[ "$install_status" != FAIL ]] || + append_failure "$install_failures" "$requested_package" "$install_reason" + [[ "$command_status" != FAIL ]] || + append_failure "$command_failures" "$requested_package" "$command_reason" + [[ "$service_status" != FAIL ]] || + append_failure "$service_failures" "$requested_package" "$service_reason" + [[ "$remove_status" != FAIL ]] || + append_failure "$remove_failures" "$requested_package" "$remove_reason" + elif [[ "$final_status" == SKIP ]]; then + # 每个跳过包只占一行,保证【跳过项】条目数与统计中的“跳过”一致。 + skipped_stages=() + [[ "$install_status" != SKIP ]] || + skipped_stages+=("跳过安装: ${install_reason}") + [[ "$command_status" != SKIP ]] || + skipped_stages+=("跳过命令测试: ${command_reason}") + [[ "$service_status" != SKIP ]] || + skipped_stages+=("跳过服务测试: ${service_reason}") + [[ "$remove_status" != SKIP ]] || + skipped_stages+=("跳过卸载: ${remove_reason}") + append_skipped "$skipped_items" "$requested_package" \ + "$(join_stages "${skipped_stages[@]}")" + fi + done < <(find "${RUN_DIR}/metadata" -type f -name '*.tsv' | sort) + + # 分类汇总单独成文,保证总报告本身与现有 package-command-result-*.txt + # 一样是纯 TSV,可直接被既有工具解析。 + { + printf '========== 测试失败汇总 (%s) ==========\n' "$(date '+%F %T')" + printf '环境: docker | 并发度: %d | 待测包总数: %d | 通过: %d | 失败: %d | 跳过: %d\n' \ + "$JOBS" "$total" "$passed" "$failed" "$skipped" + printf '校验: 通过 %d + 失败 %d + 跳过 %d = %d\n' \ + "$passed" "$failed" "$skipped" "$total" + print_category 安装失败 "$install_failures" + print_category 卸载失败 "$remove_failures" + print_category 命令测试失败 "$command_failures" + print_category 服务测试失败 "$service_failures" + print_category 跳过项 "$skipped_items" + printf '=======================================================\n' + } >"$SUMMARY_FILE" + + cat "$SUMMARY_FILE" + echo "测试报告: ${REPORT_FILE}" + echo "失败汇总: ${SUMMARY_FILE}" + [[ "$failed" -eq 0 ]] +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -j) + [[ $# -ge 2 ]] || die "-j 缺少参数" + JOBS="$2" + shift 2 + ;; + -i) + [[ $# -ge 2 ]] || die "-i 缺少参数" + IMAGE="$2" + shift 2 + ;; + -t) + [[ $# -ge 2 ]] || die "-t 缺少参数" + CONTAINER_TIMEOUT="$2" + shift 2 + ;; + --cpus) + [[ $# -ge 2 ]] || die "--cpus 缺少参数" + CPUS="$2" + shift 2 + ;; + --memory) + [[ $# -ge 2 ]] || die "--memory 缺少参数" + MEMORY="$2" + shift 2 + ;; + -o) + [[ $# -ge 2 ]] || die "-o 缺少参数" + OUTPUT_DIR="$2" + shift 2 + ;; + --repo-dir) + [[ $# -ge 2 ]] || die "--repo-dir 缺少参数" + REPO_DIR="$2" + shift 2 + ;; + --service-mode) + [[ $# -ge 2 ]] || die "--service-mode 缺少参数" + SERVICE_MODE="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + -*) + die "未知选项: $1" + ;; + *) + break + ;; + esac +done + +[[ $# -eq 3 ]] || { + usage >&2 + exit 2 +} + +is_positive_integer "$JOBS" || die "并发数必须是正整数" +is_positive_integer "$CONTAINER_TIMEOUT" || die "超时必须是正整数秒" +[[ "$SERVICE_MODE" == systemd || "$SERVICE_MODE" == skip ]] || + die "--service-mode 只能是 systemd 或 skip" +command -v "$DOCKER_BIN" >/dev/null 2>&1 || die "未找到 Docker 命令: ${DOCKER_BIN}" +[[ -f "$WORKER_SCRIPT" ]] || die "容器工作脚本不存在: ${WORKER_SCRIPT}" + +PACKAGE_LIST="$(absolute_path "$1")" || die "无法解析软件包列表路径: $1" +DESKTOP_WHITELIST="$(absolute_path "$2")" || die "无法解析桌面白名单路径: $2" +SERVICE_PACKAGE_LIST="$(absolute_path "$3")" || die "无法解析服务软件包列表路径: $3" +validate_input_file "$PACKAGE_LIST" "软件包列表" +validate_input_file "$DESKTOP_WHITELIST" "桌面白名单" +validate_input_file "$SERVICE_PACKAGE_LIST" "特殊服务软件包列表" + +if [[ -n "$REPO_DIR" ]]; then + REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)" || + die "repo 目录不存在: ${REPO_DIR}" +fi + +if [[ -z "$OUTPUT_DIR" ]]; then + OUTPUT_DIR="${PWD}/docker-package-result-$(date +%Y%m%d-%H%M%S)" +fi +mkdir -p "$OUTPUT_DIR" +RUN_DIR="$(cd "$OUTPUT_DIR" && pwd)" +RUN_TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +REPORT_FILE="${RUN_DIR}/package-command-result-${RUN_TIMESTAMP}.txt" +SUMMARY_FILE="${RUN_DIR}/failure-summary-${RUN_TIMESTAMP}.txt" +mkdir -p "${RUN_DIR}/packages" "${RUN_DIR}/metadata" "${RUN_DIR}/queues" + +declare -a queue_files +for ((worker = 0; worker < JOBS; worker++)); do + queue_files[worker]="${RUN_DIR}/queues/${worker}.tsv" + : >"${queue_files[worker]}" +done + +package_index=0 +while IFS= read -r package; do + [[ -n "$package" ]] || continue + [[ "$package" =~ ^[A-Za-z0-9_.+~:-]+$ ]] || + die "软件包名称含不安全字符: ${package}" + package_index=$((package_index + 1)) + skip_command=0 + special_service=0 + package_matches_list "$package" "$DESKTOP_WHITELIST" && skip_command=1 + package_matches_list "$package" "$SERVICE_PACKAGE_LIST" && special_service=1 + queue_index=$(((package_index - 1) % JOBS)) + printf '%s\t%s\t%s\t%s\n' \ + "$package_index" "$package" "$skip_command" "$special_service" \ + >>"${queue_files[queue_index]}" +done < <( + awk ' + /^[[:space:]]*#/ || NF == 0 { next } + { + package = $1 + if (package ~ /livepatch-kernel-/ || + package ~ /-debuginfo-/ || + package ~ /-debugsource-/) { + next + } + if (!seen[package]++) { + print package + } + } + ' "$PACKAGE_LIST" +) + +if [[ "$package_index" -eq 0 ]]; then + echo "软件包列表中没有可测试的软件包" + aggregate_results + exit 0 +fi + +echo "测试镜像: ${IMAGE}" +echo "软件包数: ${package_index},并发数: ${JOBS}" +echo "结果目录: ${RUN_DIR}" + +declare -a worker_pids +for queue_file in "${queue_files[@]}"; do + worker_loop "$queue_file" & + worker_pids+=("$!") +done + +for worker_pid in "${worker_pids[@]}"; do + wait "$worker_pid" || true +done + +if aggregate_results; then + exit 0 +fi +exit 1 diff --git a/testcase/lib/command-execute-docker-worker.sh b/testcase/lib/command-execute-docker-worker.sh new file mode 100755 index 0000000..e316462 --- /dev/null +++ b/testcase/lib/command-execute-docker-worker.sh @@ -0,0 +1,371 @@ +#!/bin/bash +# +# Execute one package lifecycle inside an isolated OpenCloudOS container. + +set -uo pipefail + +RESULT_DIR="${1:-/results}" +TEST_PACKAGE="${TEST_PACKAGE:-}" +SKIP_COMMAND="${SKIP_COMMAND:-0}" +SPECIAL_SERVICE="${SPECIAL_SERVICE:-0}" +SERVICE_MODE="${SERVICE_MODE:-skip}" + +SOURCE_PACKAGE="${TEST_PACKAGE:-UNKNOWN}" +BINARY_PACKAGE="${TEST_PACKAGE:-UNKNOWN}" +INSTALLED_NAME="${TEST_PACKAGE:-UNKNOWN}" +INSTALL_STATUS=SKIP +INSTALL_REASON="尚未执行" +COMMAND_STATUS=SKIP +COMMAND_REASON="尚未执行" +SERVICE_STATUS=SKIP +SERVICE_REASON="尚未执行" +REMOVE_STATUS=SKIP +REMOVE_REASON="尚未执行" +RESULT_WRITTEN=0 +PACKAGE_WAS_INSTALLED=0 +PKG_MANAGER="" + +clean_reason() { + printf '%s' "$1" | tr '\t\r\n' ' ' +} + +write_result() { + mkdir -p "$RESULT_DIR" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$(clean_reason "$SOURCE_PACKAGE")" \ + "$(clean_reason "$BINARY_PACKAGE")" \ + "$INSTALL_STATUS" "$(clean_reason "$INSTALL_REASON")" \ + "$COMMAND_STATUS" "$(clean_reason "$COMMAND_REASON")" \ + "$SERVICE_STATUS" "$(clean_reason "$SERVICE_REASON")" \ + "$REMOVE_STATUS" "$(clean_reason "$REMOVE_REASON")" \ + >"${RESULT_DIR}/result.tsv" + RESULT_WRITTEN=1 +} + +on_exit() { + local rc=$? + + if [[ "$RESULT_WRITTEN" -eq 0 ]]; then + INSTALL_STATUS=FAIL + INSTALL_REASON="工作脚本异常退出,退出码 ${rc}" + COMMAND_STATUS=SKIP + COMMAND_REASON="安装阶段未完成" + SERVICE_STATUS=SKIP + SERVICE_REASON="安装阶段未完成" + REMOVE_STATUS=SKIP + REMOVE_REASON="安装阶段未完成" + write_result + fi +} + +trap on_exit EXIT + +log() { + printf '[%s] %s\n' "$(date '+%F %T')" "$*" +} + +find_package_manager() { + if command -v dnf >/dev/null 2>&1; then + PKG_MANAGER=dnf + elif command -v yum >/dev/null 2>&1; then + PKG_MANAGER=yum + else + return 1 + fi +} + +install_package() { + "$PKG_MANAGER" -y install "$TEST_PACKAGE" \ + >"${RESULT_DIR}/install.log" 2>&1 +} + +collect_package_metadata() { + local source_rpm + + INSTALLED_NAME="$(rpm -q --qf '%{NAME}\n' "$TEST_PACKAGE" 2>/dev/null | + head -n 1)" + [[ -n "$INSTALLED_NAME" ]] || INSTALLED_NAME="$TEST_PACKAGE" + BINARY_PACKAGE="$(rpm -q \ + --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' \ + "$TEST_PACKAGE" 2>/dev/null | head -n 1)" + [[ -n "$BINARY_PACKAGE" ]] || BINARY_PACKAGE="$TEST_PACKAGE" + source_rpm="$(rpm -q --qf '%{SOURCERPM}\n' "$TEST_PACKAGE" 2>/dev/null | + head -n 1)" + if [[ -n "$source_rpm" && "$source_rpm" != "(none)" ]]; then + SOURCE_PACKAGE="${source_rpm%.src.rpm}" + fi +} + +ensure_command_tools() { + "$PKG_MANAGER" -y install coreutils file strace \ + >"${RESULT_DIR}/test-tools.log" 2>&1 || true +} + +command_looks_successful() { + local command_path="$1" + local command_index="$2" + local option="$3" + local log_file="${RESULT_DIR}/command-${command_index}.log" + local trace_file="${RESULT_DIR}/command-${command_index}.strace" + local rc + + : >"$log_file" + timeout --signal=KILL 5 "$command_path" "$option" >"$log_file" 2>&1 + rc=$? + if [[ "$rc" -eq 0 || "$rc" -eq 124 || "$rc" -eq 137 ]]; then + return 0 + fi + if grep -Eiq 'usage|invalid|unknown|argument|option' "$log_file"; then + return 0 + fi + if grep -q 'error while loading shared libraries' "$log_file"; then + return 1 + fi + if [[ "$rc" -gt 128 ]]; then + return 1 + fi + if command -v strace >/dev/null 2>&1; then + timeout --signal=KILL 5 strace -f -e trace=execve \ + -o "$trace_file" "$command_path" "$option" \ + >>"$log_file" 2>&1 || true + grep -Eq 'execve\(.*\) += 0$' "$trace_file" 2>/dev/null + return + fi + return 1 +} + +test_commands() { + local command_list="${RESULT_DIR}/commands.list" + local command_path + local command_index=0 + local failed_commands=() + + if [[ "$SKIP_COMMAND" -eq 1 ]]; then + COMMAND_STATUS=SKIP + COMMAND_REASON="命中桌面软件包白名单" + return + fi + + ensure_command_tools + rpm -ql "$INSTALLED_NAME" 2>/dev/null | + grep -E '^/(usr/)?s?bin/' | + grep -Ev '(\.so([.0-9]*)?$|/usr/bin/nde-)' | + while IFS= read -r path; do + [[ -f "$path" && -x "$path" ]] && printf '%s\n' "$path" + done | sort -u >"$command_list" + + if [[ ! -s "$command_list" ]]; then + COMMAND_STATUS=PASS + COMMAND_REASON="软件包不包含可执行命令" + return + fi + + while IFS= read -r command_path; do + command_index=$((command_index + 1)) + if command -v file >/dev/null 2>&1 && + file -L "$command_path" 2>/dev/null | grep -Eqi 'script|text executable'; then + log "忽略脚本命令: ${command_path}" + continue + fi + if command -v ldd >/dev/null 2>&1 && + ldd "$command_path" 2>&1 | grep -q 'not found'; then + failed_commands+=("${command_path}(缺少动态库)") + continue + fi + if command_looks_successful "$command_path" "$command_index" --help || + command_looks_successful "$command_path" "$command_index" --version; then + continue + fi + failed_commands+=("${command_path}(执行失败)") + done <"$command_list" + + if [[ "${#failed_commands[@]}" -eq 0 ]]; then + COMMAND_STATUS=PASS + COMMAND_REASON="${command_index} 个命令均可执行" + else + COMMAND_STATUS=FAIL + COMMAND_REASON="${failed_commands[*]}" + fi +} + +systemd_is_available() { + [[ "$SERVICE_MODE" == systemd ]] && + [[ -d /run/systemd/system ]] && + [[ "$(cat /proc/1/comm 2>/dev/null)" == systemd ]] && + command -v systemctl >/dev/null 2>&1 +} + +test_services() { + local unit_list="${RESULT_DIR}/units.list" + local unit + local unit_log + local active_state + local tested=0 + local skipped_units=() + local failed_units=() + + if [[ "$SPECIAL_SERVICE" -eq 1 ]]; then + SERVICE_STATUS=SKIP + SERVICE_REASON="命中特殊服务软件包列表,需额外配置" + return + fi + + rpm -ql "$INSTALLED_NAME" 2>/dev/null | + grep -E '\.(service|socket|target)$' | + sed 's|.*/||' | + grep -v '@' | + sort -u >"$unit_list" + + if [[ ! -s "$unit_list" ]]; then + SERVICE_STATUS=PASS + SERVICE_REASON="软件包不包含 systemd unit" + return + fi + if ! systemd_is_available; then + SERVICE_STATUS=SKIP + SERVICE_REASON="容器中 systemd 不可用,未执行服务启停" + return + fi + + while IFS= read -r unit; do + [[ -n "$unit" ]] || continue + tested=$((tested + 1)) + unit_log="${RESULT_DIR}/service-${unit}.log" + if ! timeout --signal=KILL 30 systemctl --no-pager start "$unit" \ + >"$unit_log" 2>&1; then + if grep -Eqi 'refuse manual start|operation not supported' "$unit_log"; then + skipped_units+=("${unit}(拒绝手动启动)") + else + failed_units+=("${unit}(启动失败)") + fi + continue + fi + + active_state="$(systemctl is-active "$unit" 2>>"$unit_log" || true)" + if [[ "$active_state" != active ]]; then + failed_units+=("${unit}(状态为 ${active_state:-unknown})") + systemctl --no-pager stop "$unit" >>"$unit_log" 2>&1 || true + continue + fi + + if ! timeout --signal=KILL 30 systemctl --no-pager stop "$unit" \ + >>"$unit_log" 2>&1; then + if grep -Eqi 'refuse manual stop|operation not supported' "$unit_log"; then + skipped_units+=("${unit}(拒绝手动停止)") + else + failed_units+=("${unit}(停止失败)") + fi + else + active_state="$(systemctl is-active "$unit" 2>>"$unit_log" || true)" + if [[ "$active_state" == active || "$active_state" == activating ]]; then + failed_units+=("${unit}(停止后状态仍为 ${active_state})") + fi + fi + done <"$unit_list" + + if [[ "${#failed_units[@]}" -gt 0 ]]; then + SERVICE_STATUS=FAIL + SERVICE_REASON="${failed_units[*]}" + elif [[ "${#skipped_units[@]}" -gt 0 ]]; then + SERVICE_STATUS=SKIP + SERVICE_REASON="${skipped_units[*]}" + else + SERVICE_STATUS=PASS + SERVICE_REASON="${tested} 个 unit 已通过 start/is-active/stop" + fi +} + +remove_package() { + if [[ "$PACKAGE_WAS_INSTALLED" -eq 1 ]]; then + REMOVE_STATUS=SKIP + REMOVE_REASON="软件包在基础镜像中已预装" + return + fi + + if "$PKG_MANAGER" -y remove \ + --setopt=clean_requirements_on_remove=False "$INSTALLED_NAME" \ + >"${RESULT_DIR}/remove.log" 2>&1; then + if grep -q 'scriptlet failed' "${RESULT_DIR}/remove.log"; then + REMOVE_STATUS=FAIL + REMOVE_REASON="卸载 scriptlet 执行失败" + elif rpm -q "$INSTALLED_NAME" >/dev/null 2>&1; then + REMOVE_STATUS=FAIL + REMOVE_REASON="卸载后软件包仍存在" + else + REMOVE_STATUS=PASS + REMOVE_REASON="卸载成功" + fi + elif grep -iqw \ + 'The operation would result in removing the following protected packages' \ + "${RESULT_DIR}/remove.log"; then + # 与 tst_lib/ts_common.sh 的判定保持一致:受保护的包无法卸载不算失败。 + REMOVE_STATUS=PASS + REMOVE_REASON="软件包受保护,不可卸载" + else + REMOVE_STATUS=FAIL + REMOVE_REASON="包管理器卸载失败" + fi +} + +main() { + mkdir -p "$RESULT_DIR" + [[ -n "$TEST_PACKAGE" ]] || { + INSTALL_STATUS=FAIL + INSTALL_REASON="未传入 TEST_PACKAGE" + COMMAND_REASON="安装未成功" + SERVICE_REASON="安装未成功" + REMOVE_REASON="安装未成功" + write_result + return 1 + } + [[ "$TEST_PACKAGE" =~ ^[A-Za-z0-9_.+~:-]+$ ]] || { + INSTALL_STATUS=FAIL + INSTALL_REASON="软件包名称含不安全字符" + COMMAND_REASON="安装未成功" + SERVICE_REASON="安装未成功" + REMOVE_REASON="安装未成功" + write_result + return 1 + } + if ! find_package_manager; then + INSTALL_STATUS=FAIL + INSTALL_REASON="容器内未找到 dnf 或 yum" + COMMAND_REASON="安装未成功" + SERVICE_REASON="安装未成功" + REMOVE_REASON="安装未成功" + write_result + return 1 + fi + + if rpm -q "$TEST_PACKAGE" >/dev/null 2>&1; then + PACKAGE_WAS_INSTALLED=1 + fi + + log "安装 ${TEST_PACKAGE}" + if ! install_package; then + INSTALL_STATUS=FAIL + INSTALL_REASON="包管理器安装失败" + COMMAND_STATUS=SKIP + COMMAND_REASON="安装未成功" + SERVICE_STATUS=SKIP + SERVICE_REASON="安装未成功" + REMOVE_STATUS=SKIP + REMOVE_REASON="安装未成功" + write_result + return 1 + fi + + INSTALL_STATUS=PASS + INSTALL_REASON="安装成功" + collect_package_metadata + test_commands + test_services + remove_package + write_result + + [[ "$COMMAND_STATUS" != FAIL && + "$SERVICE_STATUS" != FAIL && + "$REMOVE_STATUS" != FAIL ]] +} + +main diff --git a/testcase/tests/test-command-execute-docker.sh b/testcase/tests/test-command-execute-docker.sh new file mode 100755 index 0000000..fe04f87 --- /dev/null +++ b/testcase/tests/test-command-execute-docker.sh @@ -0,0 +1,235 @@ +#!/bin/bash + +set -uo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly TEST_DIR +TESTCASE_DIR="$(cd "${TEST_DIR}/.." && pwd)" +readonly TESTCASE_DIR +readonly SCRIPT="${TESTCASE_DIR}/command-execute-docker.sh" +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_contains() { + local file="$1" + local expected="$2" + + grep -Fq "$expected" "$file" || + fail "${file} 中未找到: ${expected}" +} + +assert_equals() { + local expected="$1" + local actual="$2" + + [[ "$expected" == "$actual" ]] || + fail "期望 '${expected}',实际 '${actual}'" +} + +mkdir -p "${TMP_DIR}/bin" + +cat >"${TMP_DIR}/bin/docker" <<'EOF' +#!/bin/bash +set -u + +if [[ "${1:-}" == ps ]]; then + exit 0 +fi +if [[ "${1:-}" == rm ]]; then + exit 0 +fi +[[ "${1:-}" == run ]] || exit 2 +shift + +package="" +skip_command=0 +special_service=0 +result_dir="" +while [[ $# -gt 0 ]]; do + case "$1" in + -e) + case "$2" in + TEST_PACKAGE=*) package="${2#TEST_PACKAGE=}" ;; + SKIP_COMMAND=*) skip_command="${2#SKIP_COMMAND=}" ;; + SPECIAL_SERVICE=*) special_service="${2#SPECIAL_SERVICE=}" ;; + esac + shift 2 + ;; + -v) + [[ "$2" != *:/results ]] || result_dir="${2%:/results}" + shift 2 + ;; + --name|--label|--cpus|--memory) + shift 2 + ;; + --rm) + shift + ;; + *) + shift + ;; + esac +done + +[[ -n "$package" && -n "$result_dir" ]] || exit 2 +printf '%s\t%s\t%s\n' "$package" "$skip_command" "$special_service" \ + >>"${FAKE_DOCKER_TRACE}" + +if [[ "$package" == timeout-pkg ]]; then + exec sleep 30 +fi + +source_package="${package}-source" +binary_package="${package}-1.0-1.x86_64" +install_status=PASS +install_reason="安装成功" +command_status=PASS +command_reason="命令通过" +service_status=PASS +service_reason="服务通过" +remove_status=PASS +remove_reason="卸载成功" + +case "$package" in + desktop-pkg) + [[ "$skip_command" -eq 1 ]] || exit 3 + command_status=SKIP + command_reason="命中桌面软件包白名单" + ;; + service-pkg) + [[ "$special_service" -eq 1 ]] || exit 3 + service_status=SKIP + service_reason="命中特殊服务软件包列表,需额外配置" + ;; + install-fail) + install_status=FAIL + install_reason="模拟安装失败" + command_status=SKIP + command_reason="安装未成功" + service_status=SKIP + service_reason="安装未成功" + remove_status=SKIP + remove_reason="安装未成功" + ;; + command-fail) + command_status=FAIL + command_reason="/usr/bin/bad 执行失败" + ;; + service-fail) + service_status=FAIL + service_reason="bad.service 启动失败" + ;; + remove-fail) + remove_status=FAIL + remove_reason="卸载后软件包仍存在" + ;; +esac + +mkdir -p "$result_dir" +printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$source_package" "$binary_package" \ + "$install_status" "$install_reason" \ + "$command_status" "$command_reason" \ + "$service_status" "$service_reason" \ + "$remove_status" "$remove_reason" >"${result_dir}/result.tsv" +EOF + +chmod +x "${TMP_DIR}/bin/docker" + +cat >"${TMP_DIR}/packages.list" <<'EOF' +# 注释 +pass-pkg +desktop-pkg +service-pkg +install-fail +command-fail +service-fail +remove-fail +pass-pkg +ignored-debuginfo-1.0 +EOF +printf 'desktop-pkg\n' >"${TMP_DIR}/desktop.list" +printf 'service-pkg\n' >"${TMP_DIR}/service.list" + +export FAKE_DOCKER_TRACE="${TMP_DIR}/docker.trace" +set +e +DOCKER_BIN="${TMP_DIR}/bin/docker" \ + "$SCRIPT" -j 3 --service-mode skip -o "${TMP_DIR}/result" \ + "${TMP_DIR}/packages.list" "${TMP_DIR}/desktop.list" \ + "${TMP_DIR}/service.list" +rc=$? +set -e +assert_equals 1 "$rc" + +report="$(find "${TMP_DIR}/result" -maxdepth 1 \ + -name 'package-command-result-*.txt' -print -quit)" +[[ -n "$report" ]] || fail "未生成总报告" +assert_contains "$report" $'# source-package\tbinary_package\tinstall\tcommand\tservice\tremove' +assert_contains "$report" $'desktop-pkg-source\tdesktop-pkg-1.0-1.x86_64\tPASS\tSKIP\tPASS\tPASS' +assert_contains "$report" $'service-pkg-source\tservice-pkg-1.0-1.x86_64\tPASS\tPASS\tSKIP\tPASS' + +# 总报告必须保持纯 TSV,分类汇总另存一份,格式按验收标准。 +grep -qv $'\t' <(tail -n +2 "$report") && + fail "总报告中混入了非 TSV 行" +summary="$(find "${TMP_DIR}/result" -maxdepth 1 \ + -name 'failure-summary-*.txt' -print -quit)" +[[ -n "$summary" ]] || fail "未生成失败汇总" +assert_contains "$summary" "========== 测试失败汇总 (" +assert_contains "$summary" "环境: docker | 并发度: 3 | 待测包总数: 7 | 通过: 1 | 失败: 4 | 跳过: 2" +assert_contains "$summary" "校验: 通过 1 + 失败 4 + 跳过 2 = 7" +assert_contains "$summary" "【安装失败】(1)" +assert_contains "$summary" "【卸载失败】(1)" +assert_contains "$summary" "【命令测试失败】(1)" +assert_contains "$summary" "【服务测试失败】(1)" +assert_contains "$summary" "【跳过项】(2)" +assert_contains "$summary" "install-fail 原因: 模拟安装失败" +assert_contains "$summary" "command-fail 原因: /usr/bin/bad 执行失败" +assert_contains "$summary" "service-fail 原因: bad.service 启动失败" +assert_contains "$summary" "remove-fail 原因: 卸载后软件包仍存在" +assert_contains "$summary" "desktop-pkg (跳过命令测试: 命中桌面软件包白名单)" +assert_contains "$summary" "service-pkg (跳过服务测试: 命中特殊服务软件包列表,需额外配置)" +assert_contains "$summary" "=======================================================" +assert_equals 7 "$(wc -l <"${TMP_DIR}/docker.trace" | tr -d ' ')" +! grep -Fq ignored-debuginfo "${TMP_DIR}/docker.trace" || + fail "debuginfo 软件包不应进入测试" + +: >"${TMP_DIR}/desktop.list" +: >"${TMP_DIR}/service.list" +printf 'pass-pkg\n' >"${TMP_DIR}/success.list" +DOCKER_BIN="${TMP_DIR}/bin/docker" \ + "$SCRIPT" --service-mode skip -o "${TMP_DIR}/success-result" \ + "${TMP_DIR}/success.list" "${TMP_DIR}/desktop.list" \ + "${TMP_DIR}/service.list" + +printf 'timeout-pkg\n' >"${TMP_DIR}/timeout.list" +set +e +DOCKER_BIN="${TMP_DIR}/bin/docker" \ + "$SCRIPT" -t 1 --service-mode skip -o "${TMP_DIR}/timeout-result" \ + "${TMP_DIR}/timeout.list" "${TMP_DIR}/desktop.list" \ + "${TMP_DIR}/service.list" >/dev/null +rc=$? +set -e +assert_equals 1 "$rc" +timeout_summary="$(find "${TMP_DIR}/timeout-result" -maxdepth 1 \ + -name 'failure-summary-*.txt' -print -quit)" +[[ -n "$timeout_summary" ]] || fail "超时用例未生成失败汇总" +assert_contains "$timeout_summary" "容器测试超过 1 秒" + +set +e +DOCKER_BIN="${TMP_DIR}/bin/docker" \ + "$SCRIPT" "${TMP_DIR}/success.list" "${TMP_DIR}/desktop.list" \ + >/dev/null 2>&1 +rc=$? +set -e +assert_equals 2 "$rc" + +echo "PASS: command-execute-docker.sh" -- Gitee