diff --git a/README.md b/README.md index 8597cde318ca878902ff2980c0e1c9c1ef26c70a..9f687f22f6fa6862dd0b9fb37ba28c28030b800f 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,9 @@ - KABI - 软件包安装 - 内核模块加载 -- systemd服务启动 \ No newline at end of file +- systemd服务启动 + +## Docker 并发软件包测试 + +新用例可在隔离容器中并发完成软件包安装、命令、systemd 服务和卸载测试。 +使用方法和 systemd 容器限制见 [Docker 并发软件包测试](docs/command-execute-docker.md)。 diff --git a/docs/command-execute-docker.md b/docs/command-execute-docker.md new file mode 100644 index 0000000000000000000000000000000000000000..50c0c01b5e71d144d4776b7e8e6f24cb70b48238 --- /dev/null +++ b/docs/command-execute-docker.md @@ -0,0 +1,80 @@ +# Docker 并发软件包测试 + +`testcase/command-execute-docker.sh` 是独立于现有 QEMU 用例 +`command-execute-whitelist.sh` 的 Docker 实现。它复用 `tst_lib/ts_common.sh` +中的安装、卸载、ELF 可执行文件发现和命令判定函数,并为每个软件包创建隔离容器。 + +## 调用方式 + +```bash +OPENCLOUDOS_IMAGE='registry/repository:tag' # 替换为与待测仓库匹配的镜像 +sudo testcase/command-execute-docker.sh \ + --image "$OPENCLOUDOS_IMAGE" \ + --pull -j 8 --timeout 15m --phase-timeout 5m \ + --cpus 1.0 --memory 1g \ + packages.list testcase/desktop-white.pkg testcase/service-white.pkg +``` + +三个位置参数必须依次为: + +1. 待测软件包列表; +2. 桌面软件包白名单(安装、服务、卸载仍测试,命令执行记为 `SKIP`); +3. 需要额外配置的服务软件包列表(服务启动检查记为 `SKIP` 并写明原因)。 + +列表按第一列解析,忽略空行、注释、重复项、debuginfo/debugsource、livepatch +和 i686 等 32 位包。`--repo-dir DIR` 可将与目标版本匹配的 repo 文件只读挂载到 +容器的 `/etc/yum.repos.d`。使用前应选择与待测仓库版本和架构一致的 OpenCloudOS +基础镜像。 + +常用选项: + +- `-j N`:最多并行 N 个软件包容器; +- `--timeout 15m`:每个软件包任务的总超时; +- `--phase-timeout 5m`:安装、命令、服务和卸载阶段的超时; +- `--cpus 1.0 --memory 1g`:单容器 CPU、内存上限; +- `--service-mode auto|systemd|skip`:systemd 策略,默认为 `auto`; +- `--output-dir DIR`:指定一个不存在或为空的输出目录; +- `--dry-run`:只校验输入并显示执行计划。 + +## systemd 策略与限制 + +`auto` 和 `systemd` 模式先以 `/sbin/init` 启动特权容器,挂载 cgroup,并为 +`/run`、`/run/lock` 创建 tmpfs。工作脚本从已安装 RPM 的文件列表发现 +`.service`、`.socket`、`.target` 单元,对非模板单元依次执行 start、is-active、stop; +逐单元结果写入 `service-units.tsv`。模板单元因为缺少实例名而单独记为 `SKIP`。 + +部分 Docker 主机禁用特权容器、cgroup 挂载或 systemd,某些基础镜像也不包含 +`/sbin/init`。此时任务回退到普通容器,安装、命令和卸载仍执行,服务阶段明确记为 +`SKIP`,原因是 systemd 在当前 Docker 环境中不可用。`--service-mode skip` 可主动 +使用这一行为。服务白名单中的软件包同样只跳过服务阶段,不会被记作失败。 + +命令阶段会在目标包安装成功后安装测试工具 `file`、`findutils`、`strace`;这些工具 +仅存在于该包的临时容器中。`test_command_execute` 原有的 core pattern 写入在 Docker +模式下关闭,因为 `kernel.core_pattern` 不是容器命名空间资源,修改它会污染宿主机; +命令退出、缺库、帮助/版本输出和 strace 执行等其余判定保持不变。 + +## 结果和清理 + +每个软件包日志位于 `packages/<序号-包名>/`。总表文件名为 +`package-command-result-docker-*.txt`,字段为: + +```text +source-package binary_package install command service remove +``` + +汇总文件 `package-command-summary-docker-*.txt` 分别列出安装、卸载、命令、服务失败 +以及跳过项,并保证 `PASS + FAIL + SKIP = 总包数`。单个任务失败或超时不会终止其他 +容器。 + +容器使用运行 ID 标签和 `--rm`,正常、失败、超时及信号退出路径都会强制清理。 +脚本不执行 `docker build`,不创建匿名卷,也不产生临时镜像;用户显式 `--pull` +得到的基础镜像会保留,供后续测试复用。 + +## 调度器回归测试 + +无需 Docker 即可验证参数解析、并发上限、失败隔离、超时和汇总对账: + +```bash +testcase/tests/test-command-execute-docker.sh +testcase/tests/test-package-worker.sh +``` diff --git a/testcase/command-execute-docker.sh b/testcase/command-execute-docker.sh new file mode 100755 index 0000000000000000000000000000000000000000..791e6ad798f92a6afa14fea793ee1ce99c05a0f4 --- /dev/null +++ b/testcase/command-execute-docker.sh @@ -0,0 +1,458 @@ +#!/usr/bin/env bash +# Run package command/service tests concurrently in isolated Docker containers. + +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TST_TS_TOPDIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +export TST_TS_TOPDIR + +# Reuse the suite's message and package judgement helpers when available. +# shellcheck source=../tst_common/lib/common.sh +source "${TST_TS_TOPDIR}/tst_common/lib/common.sh" || exit 1 + +usage() { + cat <<'EOF' +Usage: + command-execute-docker.sh [options] PACKAGE_LIST DESKTOP_WHITELIST SPECIAL_SERVICE_LIST + +Required: + -i, --image IMAGE OpenCloudOS base image with matching repositories + +Options: + -j, --jobs N Maximum concurrent containers (default: 4) + --timeout DURATION Per-package container timeout (default: 15m) + --phase-timeout DUR Install/command/service/remove phase timeout (default: 5m) + --cpus NUMBER Docker CPU limit per container (default: 1.0) + --memory SIZE Docker memory limit per container (default: 1g) + --service-mode MODE auto, systemd, or skip (default: auto) + --repo-dir DIR Mount repository files at /etc/yum.repos.d read-only + --output-dir DIR Result directory (must be empty or absent) + --pull Pull IMAGE before the run + --dry-run Validate inputs and print the execution plan + -h, --help Show this help + +The three positional lists contain, respectively: packages to test, packages +whose commands should be skipped, and service packages requiring special +configuration whose service start/status/stop checks should be skipped. +EOF +} + +log_msg() { + if declare -F msg >/dev/null 2>&1; then + msg "$*" + else + printf '[yum-ci-docker] %s\n' "$*" + fi +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 2 +} + +require_value() { + [ "$#" -ge 2 ] && [ -n "$2" ] || die "option $1 requires a value" +} + +validate_positive_integer() { + [[ "$2" =~ ^[1-9][0-9]*$ ]] || die "$1 must be a positive integer: $2" +} + +validate_duration() { + [[ "$2" =~ ^[1-9][0-9]*([smhd])?$ ]] || die "$1 must be a positive GNU timeout duration: $2" +} + +sanitize_field() { + printf '%s' "$1" | tr '\t\r\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' +} + +JOBS=4 +IMAGE="" +CONTAINER_TIMEOUT="15m" +PHASE_TIMEOUT="5m" +CPUS="1.0" +MEMORY="1g" +SERVICE_MODE="auto" +REPO_DIR="" +OUTPUT_DIR="" +PULL_IMAGE=0 +DRY_RUN=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + -i|--image) + require_value "$@" + IMAGE="$2" + shift 2 + ;; + -j|--jobs) + require_value "$@" + JOBS="$2" + shift 2 + ;; + --timeout) + require_value "$@" + CONTAINER_TIMEOUT="$2" + shift 2 + ;; + --phase-timeout) + require_value "$@" + PHASE_TIMEOUT="$2" + shift 2 + ;; + --cpus) + require_value "$@" + CPUS="$2" + shift 2 + ;; + --memory) + require_value "$@" + MEMORY="$2" + shift 2 + ;; + --service-mode) + require_value "$@" + SERVICE_MODE="$2" + shift 2 + ;; + --repo-dir) + require_value "$@" + REPO_DIR="$2" + shift 2 + ;; + --output-dir) + require_value "$@" + OUTPUT_DIR="$2" + shift 2 + ;; + --pull) + PULL_IMAGE=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + -*) + die "unknown option: $1" + ;; + *) + break + ;; + esac +done + +[ "$#" -eq 3 ] || { + usage >&2 + die "exactly three list files are required" +} + +PACKAGE_LIST="$1" +DESKTOP_WHITELIST="$2" +SPECIAL_SERVICE_LIST="$3" + +[ -n "$IMAGE" ] || die "--image is required" +validate_positive_integer "--jobs" "$JOBS" +validate_duration "--timeout" "$CONTAINER_TIMEOUT" +validate_duration "--phase-timeout" "$PHASE_TIMEOUT" +[[ "$CPUS" =~ ^[0-9]+([.][0-9]+)?$ ]] || die "--cpus must be numeric: $CPUS" +case "$SERVICE_MODE" in + auto|systemd|skip) ;; + *) die "--service-mode must be auto, systemd, or skip" ;; +esac + +for input_file in "$PACKAGE_LIST" "$DESKTOP_WHITELIST" "$SPECIAL_SERVICE_LIST"; do + [ -f "$input_file" ] || die "list file does not exist: $input_file" +done +PACKAGE_LIST="$(realpath "$PACKAGE_LIST")" +DESKTOP_WHITELIST="$(realpath "$DESKTOP_WHITELIST")" +SPECIAL_SERVICE_LIST="$(realpath "$SPECIAL_SERVICE_LIST")" + +if [ -n "$REPO_DIR" ]; then + [ -d "$REPO_DIR" ] || die "repo directory does not exist: $REPO_DIR" + REPO_DIR="$(realpath "$REPO_DIR")" +fi + +mapfile -t PACKAGES < <( + awk ' + NF && $1 !~ /^#/ && + $1 !~ /livepatch-kernel-/ && + $1 !~ /-debuginfo-/ && + $1 !~ /-debugsource-/ && + $1 !~ /[.]i[3-6]86$/ && !seen[$1]++ { print $1 } + ' "$PACKAGE_LIST" +) + +[ "${#PACKAGES[@]}" -gt 0 ] || die "package list contains no testable package" + +if [ "$DRY_RUN" -eq 1 ]; then + printf 'image=%s\n' "$IMAGE" + printf 'jobs=%s\n' "$JOBS" + printf 'container_timeout=%s\n' "$CONTAINER_TIMEOUT" + printf 'phase_timeout=%s\n' "$PHASE_TIMEOUT" + printf 'cpus=%s\n' "$CPUS" + printf 'memory=%s\n' "$MEMORY" + printf 'service_mode=%s\n' "$SERVICE_MODE" + printf 'packages=%s\n' "${#PACKAGES[@]}" + printf '%s\n' "${PACKAGES[@]}" + exit 0 +fi + +command -v timeout >/dev/null 2>&1 || die "GNU timeout is required" + +JOB_RUNNER="${YUM_CI_JOB_RUNNER:-${SCRIPT_DIR}/docker/package-job.sh}" +[ -x "$JOB_RUNNER" ] || die "package job runner is not executable: $JOB_RUNNER" + +if [ -z "${YUM_CI_JOB_RUNNER:-}" ]; then + command -v docker >/dev/null 2>&1 || die "docker is required" + docker info >/dev/null 2>&1 || die "Docker daemon is unavailable" + if [ "$PULL_IMAGE" -eq 1 ]; then + docker pull "$IMAGE" || die "failed to pull image: $IMAGE" + else + docker image inspect "$IMAGE" >/dev/null 2>&1 || + die "image is not present locally (use --pull): $IMAGE" + fi +fi + +RUN_ID="$(date '+%Y%m%d-%H%M%S')-$$" +if [ -z "$OUTPUT_DIR" ]; then + OUTPUT_DIR="$(pwd)/docker-command-results-${RUN_ID}" +fi +mkdir -p "$OUTPUT_DIR" || die "cannot create output directory: $OUTPUT_DIR" +OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" +if find "$OUTPUT_DIR" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then + die "output directory must be empty: $OUTPUT_DIR" +fi + +mkdir -p "$OUTPUT_DIR/input" "$OUTPUT_DIR/packages" +awk 'NF && $1 !~ /^#/ {print $1}' "$PACKAGE_LIST" >"$OUTPUT_DIR/input/packages.list" +awk 'NF && $1 !~ /^#/ {print $1}' "$DESKTOP_WHITELIST" >"$OUTPUT_DIR/input/desktop.list" +awk 'NF && $1 !~ /^#/ {print $1}' "$SPECIAL_SERVICE_LIST" >"$OUTPUT_DIR/input/special-service.list" + +MANIFEST="$OUTPUT_DIR/manifest.tsv" +: >"$MANIFEST" + +declare -a ACTIVE_PIDS=() + +cleanup_children() { + local pid + for pid in "${ACTIVE_PIDS[@]}"; do + kill -TERM "$pid" >/dev/null 2>&1 || true + done + for pid in "${ACTIVE_PIDS[@]}"; do + wait "$pid" >/dev/null 2>&1 || true + done + ACTIVE_PIDS=() +} + +cleanup_run_containers() { + local container_ids + [ -z "${YUM_CI_JOB_RUNNER:-}" ] || return 0 + command -v docker >/dev/null 2>&1 || return 0 + container_ids="$(docker ps -aq --filter "label=org.opencloudos.yum-ci.run=$RUN_ID" 2>/dev/null)" + [ -z "$container_ids" ] || docker rm -f $container_ids >/dev/null 2>&1 || true +} + +on_signal() { + log_msg "interrupted; stopping package jobs" + cleanup_children + cleanup_run_containers + exit 130 +} +trap on_signal INT TERM HUP +trap cleanup_run_containers EXIT + +write_synthetic_result() { + local package="$1" + local output_rel="$2" + local reason="$3" + reason="$(sanitize_field "$reason")" + [ -n "$reason" ] || reason="container job failed" + printf -- '-\t%s\tFAIL\tSKIP\tSKIP\tSKIP\tFAIL\t%s\t%s\tinstall not completed\tservice not reached\tremove not reached\n' \ + "$package" "$reason" "$reason" >"$OUTPUT_DIR/$output_rel/result.tsv" +} + +run_job_wrapper() { + local index="$1" + local package="$2" + local task_name="$3" + local output_rel="packages/$task_name" + local job_log="$OUTPUT_DIR/$output_rel/job.log" + local rc + mkdir -p "$OUTPUT_DIR/$output_rel" + + env \ + YUM_CI_RUN_ID="$RUN_ID" \ + YUM_CI_TASK_INDEX="$index" \ + YUM_CI_PACKAGE="$package" \ + YUM_CI_IMAGE="$IMAGE" \ + YUM_CI_REPO_ROOT="$TST_TS_TOPDIR" \ + YUM_CI_RUN_DIR="$OUTPUT_DIR" \ + YUM_CI_OUTPUT_REL="$output_rel" \ + YUM_CI_REPO_DIR="$REPO_DIR" \ + YUM_CI_CPUS="$CPUS" \ + YUM_CI_MEMORY="$MEMORY" \ + YUM_CI_SERVICE_MODE="$SERVICE_MODE" \ + YUM_CI_PHASE_TIMEOUT="$PHASE_TIMEOUT" \ + timeout --signal=TERM --kill-after=30s "$CONTAINER_TIMEOUT" "$JOB_RUNNER" \ + >"$job_log" 2>&1 + rc=$? + + if [ ! -s "$OUTPUT_DIR/$output_rel/result.tsv" ]; then + if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then + write_synthetic_result "$package" "$output_rel" "container timeout after $CONTAINER_TIMEOUT" + else + write_synthetic_result "$package" "$output_rel" "container job failed with exit code $rc" + fi + fi + printf '%s\n' "$rc" >"$OUTPUT_DIR/$output_rel/job.exit" + return 0 +} + +compact_pids() { + local pid + local -a remaining=() + for pid in "${ACTIVE_PIDS[@]}"; do + [ -n "$pid" ] && remaining+=("$pid") + done + ACTIVE_PIDS=("${remaining[@]}") +} + +wait_for_one() { + local i pid running_pids + while :; do + running_pids=" $(jobs -pr) " + for i in "${!ACTIVE_PIDS[@]}"; do + pid="${ACTIVE_PIDS[$i]}" + if [[ "$running_pids" != *" $pid "* ]]; then + wait "$pid" >/dev/null 2>&1 || true + unset 'ACTIVE_PIDS[i]' + compact_pids + return 0 + fi + done + sleep 0.1 + done +} + +log_msg "Docker package test: ${#PACKAGES[@]} packages, concurrency=$JOBS, image=$IMAGE" +index=0 +for package in "${PACKAGES[@]}"; do + index=$((index + 1)) + safe_package="$(printf '%s' "$package" | sed 's/[^A-Za-z0-9_.-]/_/g' | cut -c1-80)" + task_name="$(printf '%05d-%s' "$index" "$safe_package")" + printf '%s\t%s\t%s\n' "$index" "$package" "$task_name" >>"$MANIFEST" + + while [ "${#ACTIVE_PIDS[@]}" -ge "$JOBS" ]; do + wait_for_one + done + run_job_wrapper "$index" "$package" "$task_name" & + ACTIVE_PIDS+=("$!") +done + +while [ "${#ACTIVE_PIDS[@]}" -gt 0 ]; do + wait_for_one +done +trap - INT TERM HUP +cleanup_run_containers + +RESULT_REPORT="$OUTPUT_DIR/package-command-result-docker-${RUN_ID}.txt" +SUMMARY_REPORT="$OUTPUT_DIR/package-command-summary-docker-${RUN_ID}.txt" +INSTALL_FAILURES="$OUTPUT_DIR/.install-failures" +REMOVE_FAILURES="$OUTPUT_DIR/.remove-failures" +COMMAND_FAILURES="$OUTPUT_DIR/.command-failures" +SERVICE_FAILURES="$OUTPUT_DIR/.service-failures" +SKIPPED_ITEMS="$OUTPUT_DIR/.skipped-items" +: >"$INSTALL_FAILURES" +: >"$REMOVE_FAILURES" +: >"$COMMAND_FAILURES" +: >"$SERVICE_FAILURES" +: >"$SKIPPED_ITEMS" + +printf '# source-package\tbinary_package\tinstall\tcommand\tservice\tremove\n' >"$RESULT_REPORT" + +total=0 +passed=0 +failed=0 +skipped=0 +while IFS=$'\t' read -r item_index package task_name; do + result_file="$OUTPUT_DIR/packages/$task_name/result.tsv" + if [ ! -s "$result_file" ]; then + write_synthetic_result "$package" "packages/$task_name" "missing result file" + fi + IFS=$'\t' read -r source_pkg binary_pkg install_status command_status service_status remove_status \ + overall_status overall_reason install_reason command_reason service_reason remove_reason <"$result_file" + + printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$source_pkg" "$binary_pkg" "$install_status" "$command_status" "$service_status" "$remove_status" \ + >>"$RESULT_REPORT" + + total=$((total + 1)) + case "$overall_status" in + PASS) passed=$((passed + 1)) ;; + FAIL) failed=$((failed + 1)) ;; + SKIP) skipped=$((skipped + 1)) ;; + *) + failed=$((failed + 1)) + overall_reason="invalid overall status: $overall_status" + ;; + esac + + [ "$install_status" = "FAIL" ] && printf ' - %s 原因: %s\n' "$binary_pkg" "$install_reason" >>"$INSTALL_FAILURES" + [ "$remove_status" = "FAIL" ] && printf ' - %s 原因: %s\n' "$binary_pkg" "$remove_reason" >>"$REMOVE_FAILURES" + [ "$command_status" = "FAIL" ] && printf ' - %s 原因: %s\n' "$binary_pkg" "$command_reason" >>"$COMMAND_FAILURES" + [ "$service_status" = "FAIL" ] && printf ' - %s 原因: %s\n' "$binary_pkg" "$service_reason" >>"$SERVICE_FAILURES" + if [ "$command_status" = "SKIP" ] && [ "$command_reason" != "install failed" ] && + [ "$command_reason" != "install not completed" ]; then + printf ' - %s command: %s\n' "$binary_pkg" "$command_reason" >>"$SKIPPED_ITEMS" + fi + if [ "$service_status" = "SKIP" ] && [ "$service_reason" != "install failed" ] && + [ "$service_reason" != "install not completed" ] && [ "$service_reason" != "service not reached" ]; then + printf ' - %s service: %s\n' "$binary_pkg" "$service_reason" >>"$SKIPPED_ITEMS" + fi +done <"$MANIFEST" + +[ $((passed + failed + skipped)) -eq "$total" ] || die "result accounting mismatch" + +print_category() { + local title="$1" + local file="$2" + local count + count="$(wc -l <"$file")" + printf '\n【%s】(%s)\n' "$title" "$count" + if [ "$count" -eq 0 ]; then + printf ' (无)\n' + else + cat "$file" + fi +} + +{ + printf '========== 测试失败汇总 (%s) ==========\n' "$(date '+%Y-%m-%d %H:%M:%S')" + printf '环境: docker | 镜像: %s | 并发度: %s | 待测包总数: %s | 通过: %s | 失败: %s | 跳过: %s\n' \ + "$IMAGE" "$JOBS" "$total" "$passed" "$failed" "$skipped" + print_category "安装失败" "$INSTALL_FAILURES" + print_category "卸载失败" "$REMOVE_FAILURES" + print_category "命令测试失败" "$COMMAND_FAILURES" + print_category "服务测试失败" "$SERVICE_FAILURES" + print_category "跳过项" "$SKIPPED_ITEMS" + printf '\n对账: %s + %s + %s = %s\n' "$passed" "$failed" "$skipped" "$total" + printf '=======================================================\n' +} >"$SUMMARY_REPORT" + +rm -f "$INSTALL_FAILURES" "$REMOVE_FAILURES" "$COMMAND_FAILURES" "$SERVICE_FAILURES" "$SKIPPED_ITEMS" + +cat "$SUMMARY_REPORT" +log_msg "result report: $RESULT_REPORT" +log_msg "summary report: $SUMMARY_REPORT" + +[ "$failed" -eq 0 ] diff --git a/testcase/docker/package-job.sh b/testcase/docker/package-job.sh new file mode 100755 index 0000000000000000000000000000000000000000..56d01dae6ba8e4088a25714bffa882a5de68b234 --- /dev/null +++ b/testcase/docker/package-job.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Start one isolated container, execute package-worker.sh, and always remove it. + +set -o pipefail + +required=( + YUM_CI_RUN_ID YUM_CI_TASK_INDEX YUM_CI_PACKAGE YUM_CI_IMAGE + YUM_CI_REPO_ROOT YUM_CI_RUN_DIR YUM_CI_OUTPUT_REL + YUM_CI_CPUS YUM_CI_MEMORY YUM_CI_SERVICE_MODE YUM_CI_PHASE_TIMEOUT +) +for variable in "${required[@]}"; do + [ -n "${!variable:-}" ] || { + printf 'missing required environment variable: %s\n' "$variable" >&2 + exit 2 + } +done + +safe_run_id="$(printf '%s' "$YUM_CI_RUN_ID" | sed 's/[^A-Za-z0-9_.-]/-/g' | cut -c1-48)" +container_name="yum-ci-${safe_run_id}-${YUM_CI_TASK_INDEX}" +container_id="" + +cleanup() { + if [ -n "$container_id" ]; then + docker rm -f "$container_id" >/dev/null 2>&1 || true + fi +} +on_signal() { + trap - EXIT + cleanup + exit 143 +} +trap cleanup EXIT +trap on_signal INT TERM HUP + +common_args=( + --detach + --rm + --name "$container_name" + --label "org.opencloudos.yum-ci=true" + --label "org.opencloudos.yum-ci.run=$YUM_CI_RUN_ID" + --cpus "$YUM_CI_CPUS" + --memory "$YUM_CI_MEMORY" + --volume "$YUM_CI_REPO_ROOT:/opt/yum-ci:ro" + --volume "$YUM_CI_RUN_DIR/$YUM_CI_OUTPUT_REL:/output:rw" + --volume "$YUM_CI_RUN_DIR/input:/input:ro" +) + +if [ -n "${YUM_CI_REPO_DIR:-}" ]; then + common_args+=(--volume "$YUM_CI_REPO_DIR:/etc/yum.repos.d:ro") +fi + +start_plain_container() { + docker run "${common_args[@]}" "$YUM_CI_IMAGE" \ + /bin/sh -c 'trap : TERM INT; while :; do sleep 3600; done' +} + +start_systemd_container() { + docker run "${common_args[@]}" \ + --privileged \ + --cgroupns=host \ + --tmpfs /run \ + --tmpfs /run/lock \ + --volume /sys/fs/cgroup:/sys/fs/cgroup:rw \ + "$YUM_CI_IMAGE" /sbin/init +} + +systemd_available=0 +service_skip_reason="" + +case "$YUM_CI_SERVICE_MODE" in + skip) + container_id="$(start_plain_container)" || exit 1 + service_skip_reason="service checks disabled by --service-mode skip" + ;; + auto|systemd) + if container_id="$(start_systemd_container 2>/dev/null)"; then + ready=0 + for ((_attempt = 1; _attempt <= 30; _attempt++)); do + systemd_state="$(docker exec "$container_id" systemctl is-system-running 2>/dev/null || true)" + case "$systemd_state" in + running|degraded) + ready=1 + break + ;; + esac + [ "$(docker inspect --format '{{.State.Running}}' "$container_id" 2>/dev/null)" = "true" ] || break + sleep 1 + done + if [ "$ready" -eq 1 ]; then + systemd_available=1 + else + docker rm -f "$container_id" >/dev/null 2>&1 || true + container_id="" + fi + fi + + if [ "$systemd_available" -eq 0 ]; then + container_id="$(start_plain_container)" || exit 1 + service_skip_reason="systemd could not run in this Docker environment" + fi + ;; +esac + +docker exec \ + --env "YUM_CI_PACKAGE=$YUM_CI_PACKAGE" \ + --env "YUM_CI_PHASE_TIMEOUT=$YUM_CI_PHASE_TIMEOUT" \ + --env "YUM_CI_SYSTEMD_AVAILABLE=$systemd_available" \ + --env "YUM_CI_SERVICE_SKIP_REASON=$service_skip_reason" \ + "$container_id" \ + /bin/bash /opt/yum-ci/testcase/docker/package-worker.sh /output /input/desktop.list /input/special-service.list diff --git a/testcase/docker/package-worker.sh b/testcase/docker/package-worker.sh new file mode 100755 index 0000000000000000000000000000000000000000..dc1f0550bfbacbabc67238a140f2097b34d01795 --- /dev/null +++ b/testcase/docker/package-worker.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# Execute install -> command -> service -> remove for one package in a container. + +set -o pipefail + +OUTPUT_DIR="${1:-}" +DESKTOP_LIST="${2:-}" +SPECIAL_SERVICE_LIST="${3:-}" +PACKAGE="${YUM_CI_PACKAGE:-}" +PHASE_TIMEOUT="${YUM_CI_PHASE_TIMEOUT:-5m}" +SYSTEMD_AVAILABLE="${YUM_CI_SYSTEMD_AVAILABLE:-0}" +SERVICE_SKIP_REASON="${YUM_CI_SERVICE_SKIP_REASON:-systemd is unavailable}" +export TST_COMMAND_SKIP_CORE_PATTERN_WRITE=1 + +[ -n "$OUTPUT_DIR" ] && [ -n "$PACKAGE" ] || { + printf 'usage: package-worker.sh OUTPUT_DIR DESKTOP_LIST SPECIAL_SERVICE_LIST\n' >&2 + exit 2 +} + +mkdir -p "$OUTPUT_DIR" +TST_TS_TOPDIR="${YUM_CI_REPO_ROOT:-/opt/yum-ci}" +export TST_TS_TOPDIR +# shellcheck source=../../tst_common/lib/common.sh +source "$TST_TS_TOPDIR/tst_common/lib/common.sh" || exit 1 + +sanitize_field() { + printf '%s' "$1" | tr '\t\r\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' +} + +listed() { + local package_name="$1" + local list_file="$2" + awk -v pkg="$package_name" ' + NF && $1 !~ /^#/ && ($1 == pkg || index(pkg, $1 ".") == 1) {found=1} + END {exit !found} + ' "$list_file" +} + +run_common_function() { + local log_file="$1" + local function_name="$2" + shift 2 + export -f "$function_name" msg + timeout --signal=TERM --kill-after=10s "$PHASE_TIMEOUT" \ + bash -c ' + function_name="$1" + shift + "$function_name" "$@" + ' _ "$function_name" "$@" >"$log_file" 2>&1 +} + +write_result() { + local temporary="$OUTPUT_DIR/result.tsv.tmp" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$(sanitize_field "$source_package")" \ + "$(sanitize_field "$binary_package")" \ + "$install_status" "$command_status" "$service_status" "$remove_status" "$overall_status" \ + "$(sanitize_field "$overall_reason")" \ + "$(sanitize_field "$install_reason")" \ + "$(sanitize_field "$command_reason")" \ + "$(sanitize_field "$service_reason")" \ + "$(sanitize_field "$remove_reason")" >"$temporary" + mv -f "$temporary" "$OUTPUT_DIR/result.tsv" +} + +source_package="-" +binary_package="$PACKAGE" +install_status="FAIL" +command_status="SKIP" +service_status="SKIP" +remove_status="SKIP" +overall_status="FAIL" +overall_reason="worker terminated before completion" +install_reason="worker terminated before install completed" +command_reason="install not completed" +service_reason="install not completed" +remove_reason="install not completed" +finished=0 + +on_exit() { + if [ "$finished" -eq 0 ]; then + write_result + fi +} +on_signal() { + on_exit + trap - EXIT + exit 143 +} +trap on_exit EXIT +trap on_signal INT TERM HUP + +rpm -qa --qf '%{NAME}.%{ARCH}\t%{VERSION}\n' >"$OUTPUT_DIR/installed-before.list" 2>"$OUTPUT_DIR/installed-before.log" || true + +if run_common_function "$OUTPUT_DIR/install.log" test_single_rpm_install "" "$PACKAGE" "$OUTPUT_DIR/package"; then + install_status="PASS" + install_reason="installed successfully" +else + install_rc=$? + install_status="FAIL" + if [ "$install_rc" -eq 124 ] || [ "$install_rc" -eq 137 ]; then + install_reason="install timed out after $PHASE_TIMEOUT" + else + install_reason="package installation failed (exit $install_rc)" + fi +fi + +if [ "$install_status" = "PASS" ]; then + package_metadata="$(rpm -q --qf '%{NAME}.%{ARCH}\t%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\t%{SOURCERPM}\n' "$PACKAGE" 2>/dev/null | head -n 1)" + if [ -n "$package_metadata" ]; then + IFS=$'\t' read -r package_short binary_package source_rpm <<<"$package_metadata" + source_package="${source_rpm%.src.rpm}" + source_package="${source_package%.nosrc.rpm}" + else + package_short="$PACKAGE" + install_status="FAIL" + install_reason="installed package metadata could not be queried" + fi +fi + +if [ "$install_status" = "PASS" ]; then + rpm -ql "$package_short" >"$OUTPUT_DIR/package-files.list" 2>"$OUTPUT_DIR/package-files.log" || : + : >"$OUTPUT_DIR/commands.list" + + if listed "$package_short" "$DESKTOP_LIST" || listed "$PACKAGE" "$DESKTOP_LIST"; then + command_status="SKIP" + command_reason="desktop whitelist: command execution is intentionally skipped" + elif timeout --signal=TERM --kill-after=10s "$PHASE_TIMEOUT" \ + yum install -y file findutils strace >"$OUTPUT_DIR/harness-dependencies.log" 2>&1; then + get_elf_executable "$OUTPUT_DIR/package-files.list" "$OUTPUT_DIR/commands.list" \ + >"$OUTPUT_DIR/command-discovery.log" 2>&1 || true + if [ ! -s "$OUTPUT_DIR/commands.list" ]; then + command_status="PASS" + command_reason="package contains no ELF executable" + elif run_common_function "$OUTPUT_DIR/command.log" test_command_execute \ + "$OUTPUT_DIR/command-work" "$OUTPUT_DIR/commands.list"; then + command_status="PASS" + command_reason="all discovered ELF commands passed" + else + command_rc=$? + command_status="FAIL" + if [ "$command_rc" -eq 124 ] || [ "$command_rc" -eq 137 ]; then + command_reason="command validation timed out after $PHASE_TIMEOUT" + else + command_reason="one or more discovered commands failed (exit $command_rc)" + fi + fi + else + command_status="FAIL" + command_reason="could not install command-test dependencies: file, findutils, strace" + fi + + awk '/^\/(usr\/)?lib\/systemd\/system\/[^/]+[.](service|socket|target)$/ || + /^\/etc\/systemd\/system\/[^/]+[.](service|socket|target)$/ {print $0}' \ + "$OUTPUT_DIR/package-files.list" | sed 's#^.*/##' | sort -u >"$OUTPUT_DIR/service-units.list" + : >"$OUTPUT_DIR/service-units.tsv" + + if listed "$package_short" "$SPECIAL_SERVICE_LIST" || listed "$PACKAGE" "$SPECIAL_SERVICE_LIST"; then + service_status="SKIP" + service_reason="special-service list: service requires configuration and was not started" + elif [ ! -s "$OUTPUT_DIR/service-units.list" ]; then + service_status="PASS" + service_reason="package installs no service, socket, or target unit" + elif [ "$SYSTEMD_AVAILABLE" != "1" ]; then + service_status="SKIP" + service_reason="$SERVICE_SKIP_REASON" + else + service_status="PASS" + service_reason="all discovered systemd units passed start/status/stop" + service_unit_skipped=0 + while IFS= read -r unit; do + [ -n "$unit" ] || continue + if [[ "$unit" == *@.* ]]; then + printf '%s\tSKIP\ttemplate unit requires an instance name\n' "$unit" >>"$OUTPUT_DIR/service-units.tsv" + service_unit_skipped=1 + continue + fi + unit_log="$OUTPUT_DIR/service-${unit//\//_}.log" + if timeout --signal=TERM --kill-after=10s "$PHASE_TIMEOUT" \ + bash -c 'systemctl start "$1" && systemctl is-active --quiet "$1" && systemctl stop "$1"' \ + _ "$unit" >"$unit_log" 2>&1; then + printf '%s\tPASS\tstart, active status, and stop succeeded\n' "$unit" >>"$OUTPUT_DIR/service-units.tsv" + else + unit_rc=$? + systemctl stop "$unit" >>"$unit_log" 2>&1 || true + printf '%s\tFAIL\tstart/status/stop failed (exit %s)\n' "$unit" "$unit_rc" >>"$OUTPUT_DIR/service-units.tsv" + service_status="FAIL" + service_reason="one or more systemd units failed; see service-units.tsv" + fi + done <"$OUTPUT_DIR/service-units.list" + if [ "$service_status" = "PASS" ] && [ "$service_unit_skipped" -eq 1 ]; then + service_status="SKIP" + service_reason="one or more template units require an instance name; see service-units.tsv" + fi + fi + + if run_common_function "$OUTPUT_DIR/remove.log" test_single_rpm_remove \ + "$package_short" "$binary_package" "$OUTPUT_DIR/package" "$OUTPUT_DIR/installed-before.list"; then + remove_status="PASS" + remove_reason="removed successfully (or package was present before the test)" + else + remove_rc=$? + remove_status="FAIL" + if [ "$remove_rc" -eq 124 ] || [ "$remove_rc" -eq 137 ]; then + remove_reason="remove timed out after $PHASE_TIMEOUT" + else + remove_reason="package removal failed (exit $remove_rc)" + fi + fi +else + command_status="SKIP" + command_reason="install failed" + service_status="SKIP" + service_reason="install failed" + remove_status="SKIP" + remove_reason="install failed" +fi + +if [ "$install_status" = "FAIL" ] || [ "$command_status" = "FAIL" ] || + [ "$service_status" = "FAIL" ] || [ "$remove_status" = "FAIL" ]; then + overall_status="FAIL" + overall_reason="one or more test phases failed" +elif [ "$command_status" = "SKIP" ] || [ "$service_status" = "SKIP" ] || [ "$remove_status" = "SKIP" ]; then + overall_status="SKIP" + overall_reason="one or more test phases were intentionally skipped" +else + overall_status="PASS" + overall_reason="all test phases passed" +fi + +finished=1 +write_result +trap - EXIT INT TERM HUP +[ "$overall_status" != "FAIL" ] diff --git a/testcase/tests/fake-package-job.sh b/testcase/tests/fake-package-job.sh new file mode 100755 index 0000000000000000000000000000000000000000..2111d803e2395d20122e304cc634c1dd629557cd --- /dev/null +++ b/testcase/tests/fake-package-job.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Deterministic package-job replacement used by test-command-execute-docker.sh. + +set -o pipefail + +state_dir="${YUM_CI_FAKE_STATE:?}" +output_dir="$YUM_CI_RUN_DIR/$YUM_CI_OUTPUT_REL" +mkdir -p "$state_dir" "$output_dir" + +lock() { + while ! mkdir "$state_dir/lock" 2>/dev/null; do + sleep 0.01 + done +} + +unlock() { + rmdir "$state_dir/lock" +} + +adjust_count() { + local delta="$1" current maximum + lock + current="$(cat "$state_dir/current" 2>/dev/null || printf '0')" + maximum="$(cat "$state_dir/max" 2>/dev/null || printf '0')" + current=$((current + delta)) + printf '%s\n' "$current" >"$state_dir/current" + if [ "$current" -gt "$maximum" ]; then + printf '%s\n' "$current" >"$state_dir/max" + fi + unlock +} + +adjust_count 1 +on_signal() { + adjust_count -1 + trap - EXIT + exit 143 +} +trap 'adjust_count -1' EXIT +trap on_signal INT TERM HUP +# Keep successful jobs overlapped long enough to exercise the -j limit. SECONDS +# and the loop body are shell builtins, avoiding slow process startup on MSYS2. +overlap_until=$((SECONDS + 1)) +while [ "$SECONDS" -lt "$overlap_until" ]; do :; done + +package="$YUM_CI_PACKAGE" +source_package="${package}-source" +install=PASS +command=PASS +service=PASS +remove=PASS +overall=PASS +overall_reason="all test phases passed" +install_reason="installed" +command_reason="commands passed" +service_reason="services passed" +remove_reason="removed" + +case "$package" in + installfail) + install=FAIL; command=SKIP; service=SKIP; remove=SKIP; overall=FAIL + overall_reason="install failed"; install_reason="simulated install failure" + command_reason="install failed"; service_reason="install failed"; remove_reason="install failed" + ;; + commandfail) + command=FAIL; overall=FAIL; overall_reason="command failed"; command_reason="simulated command failure" + ;; + servicefail) + service=FAIL; overall=FAIL; overall_reason="service failed"; service_reason="simulated service failure" + ;; + removefail) + remove=FAIL; overall=FAIL; overall_reason="remove failed"; remove_reason="simulated remove failure" + ;; + desktoppkg) + command=SKIP; overall=SKIP; overall_reason="desktop command skipped" + command_reason="desktop whitelist: command execution is intentionally skipped" + ;; + specialsvc) + service=SKIP; overall=SKIP; overall_reason="special service skipped" + service_reason="special-service list: service requires configuration" + ;; + timeoutpkg) + sleep 20 + ;; +esac + +printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$source_package" "$package" "$install" "$command" "$service" "$remove" "$overall" \ + "$overall_reason" "$install_reason" "$command_reason" "$service_reason" "$remove_reason" \ + >"$output_dir/result.tsv" diff --git a/testcase/tests/test-command-execute-docker.sh b/testcase/tests/test-command-execute-docker.sh new file mode 100755 index 0000000000000000000000000000000000000000..54b2e25d80da44ac4d85d195e7f543089710b7bb --- /dev/null +++ b/testcase/tests/test-command-execute-docker.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ORCHESTRATOR="$REPO_ROOT/testcase/command-execute-docker.sh" +FAKE_RUNNER="$SCRIPT_DIR/fake-package-job.sh" +TMPDIR_TEST="$(mktemp -d)" +cleanup() { + if [ -n "${YUM_CI_TEST_KEEP_TMP:-}" ]; then + printf 'test artifacts kept at %s\n' "$TMPDIR_TEST" >&2 + else + rm -rf "$TMPDIR_TEST" + fi +} +trap cleanup EXIT INT TERM HUP + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +assert_contains() { + local needle="$1" file="$2" + grep -Fq "$needle" "$file" || fail "missing '$needle' in $file" +} + +cat >"$TMPDIR_TEST/packages.list" <<'EOF' +# duplicates and excluded debug packages exercise list normalization +passpkg +installfail +commandfail +servicefail +removefail +desktoppkg +specialsvc +timeoutpkg +passpkg +ignored-debuginfo-test +EOF +printf 'desktoppkg\n' >"$TMPDIR_TEST/desktop.list" +printf 'specialsvc\n' >"$TMPDIR_TEST/special.list" + +dry_run="$TMPDIR_TEST/dry-run.txt" +"$ORCHESTRATOR" --image fake -j 2 --dry-run \ + "$TMPDIR_TEST/packages.list" "$TMPDIR_TEST/desktop.list" "$TMPDIR_TEST/special.list" >"$dry_run" +assert_contains 'packages=8' "$dry_run" +assert_contains 'jobs=2' "$dry_run" + +if "$ORCHESTRATOR" --image fake "$TMPDIR_TEST/packages.list" "$TMPDIR_TEST/desktop.list" \ + >"$TMPDIR_TEST/bad-args.log" 2>&1; then + fail 'two positional list arguments should be rejected' +fi + +mkdir -p "$TMPDIR_TEST/state" +run_log="$TMPDIR_TEST/run.log" +if YUM_CI_JOB_RUNNER="$FAKE_RUNNER" YUM_CI_FAKE_STATE="$TMPDIR_TEST/state" \ + "$ORCHESTRATOR" --image fake -j 2 --timeout 15s --phase-timeout 1s \ + --output-dir "$TMPDIR_TEST/results" \ + "$TMPDIR_TEST/packages.list" "$TMPDIR_TEST/desktop.list" "$TMPDIR_TEST/special.list" \ + >"$run_log" 2>&1; then + fail 'mixed simulated results should return a non-zero status' +fi + +[ -s "$TMPDIR_TEST/state/max" ] || { + cat "$run_log" >&2 + fail 'fake package runner did not record concurrency' +} +[ "$(cat "$TMPDIR_TEST/state/max")" -eq 2 ] || fail 'concurrency limit was not exercised at exactly two jobs' + +result_report="$(find "$TMPDIR_TEST/results" -maxdepth 1 -name 'package-command-result-docker-*.txt' -print -quit)" +summary_report="$(find "$TMPDIR_TEST/results" -maxdepth 1 -name 'package-command-summary-docker-*.txt' -print -quit)" +[ -n "$result_report" ] || fail 'result report was not created' +[ -n "$summary_report" ] || fail 'summary report was not created' + +[ "$(grep -vc '^#' "$result_report")" -eq 8 ] || fail 'result report must have one row per normalized package' +assert_contains $'# source-package\tbinary_package\tinstall\tcommand\tservice\tremove' "$result_report" +assert_contains $'desktoppkg\tPASS\tSKIP\tPASS\tPASS' "$result_report" +assert_contains $'specialsvc\tPASS\tPASS\tSKIP\tPASS' "$result_report" +assert_contains $'timeoutpkg\tFAIL\tSKIP\tSKIP\tSKIP' "$result_report" + +assert_contains '待测包总数: 8' "$summary_report" +assert_contains '通过: 1' "$summary_report" +assert_contains '失败: 5' "$summary_report" +assert_contains '跳过: 2' "$summary_report" +assert_contains 'simulated install failure' "$summary_report" +assert_contains 'simulated command failure' "$summary_report" +assert_contains 'simulated service failure' "$summary_report" +assert_contains 'simulated remove failure' "$summary_report" +assert_contains 'container timeout after 15s' "$summary_report" +assert_contains 'desktop whitelist' "$summary_report" +assert_contains 'special-service list' "$summary_report" +assert_contains '对账: 1 + 5 + 2 = 8' "$summary_report" + +for package_dir in "$TMPDIR_TEST/results/packages"/*; do + [ -f "$package_dir/job.log" ] || fail "missing per-package job log: $package_dir" + [ -s "$package_dir/result.tsv" ] || fail "missing per-package result: $package_dir" +done + +printf 'PASS: command-execute-docker orchestration tests\n' diff --git a/testcase/tests/test-package-worker.sh b/testcase/tests/test-package-worker.sh new file mode 100755 index 0000000000000000000000000000000000000000..3e97bdf287a2ddaf7df319ff6cfd50f7f6345005 --- /dev/null +++ b/testcase/tests/test-package-worker.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +WORKER="$REPO_ROOT/testcase/docker/package-worker.sh" +TMPDIR_TEST="$(mktemp -d)" +trap 'rm -rf "$TMPDIR_TEST"' EXIT INT TERM HUP + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +mkdir -p "$TMPDIR_TEST/bin" + +cat >"$TMPDIR_TEST/bin/yum" <<'EOF' +#!/usr/bin/env bash +if [ "${MOCK_INSTALL_FAIL:-0}" = "1" ] && [ "$1" = "install" ]; then + exit 1 +fi +if [ "${MOCK_REMOVE_FAIL:-0}" = "1" ] && [ "$1" = "remove" ]; then + exit 1 +fi +exit 0 +EOF + +cat >"$TMPDIR_TEST/bin/rpm" <<'EOF' +#!/usr/bin/env bash +case "$1" in + -qa) + printf 'base.x86_64\t1\n' + ;; + -ql) + printf '%s\n' "$MOCK_EXECUTABLE" + printf '/usr/lib/systemd/system/%s.service\n' "$YUM_CI_PACKAGE" + ;; + -q) + printf '%s.x86_64\t%s-1-1.x86_64\t%s-source-1-1.src.rpm\n' \ + "$YUM_CI_PACKAGE" "$YUM_CI_PACKAGE" "$YUM_CI_PACKAGE" + ;; + *) + exit 1 + ;; +esac +EOF + +cat >"$TMPDIR_TEST/bin/file" <<'EOF' +#!/usr/bin/env bash +for item in "$@"; do + printf '%s: ELF 64-bit executable\n' "$item" +done +EOF + +cat >"$TMPDIR_TEST/bin/systemctl" <<'EOF' +#!/usr/bin/env bash +if [ "${MOCK_SERVICE_FAIL:-0}" = "1" ] && [ "$1" = "start" ]; then + exit 1 +fi +exit 0 +EOF + +cat >"$TMPDIR_TEST/fake-command" <<'EOF' +#!/usr/bin/env bash +case "${1:-}" in + --help|--version) exit 0 ;; + *) exit 1 ;; +esac +EOF +chmod +x "$TMPDIR_TEST/bin/"* "$TMPDIR_TEST/fake-command" + +run_case() { + local name="$1" desktop_entry="$2" special_entry="$3" service_fail="$4" + local output="$TMPDIR_TEST/$name" + mkdir -p "$output" + printf '%s\n' "$desktop_entry" >"$output/desktop.list" + printf '%s\n' "$special_entry" >"$output/special.list" + PATH="$TMPDIR_TEST/bin:$PATH" \ + YUM_CI_REPO_ROOT="$REPO_ROOT" \ + YUM_CI_PACKAGE="$name" \ + YUM_CI_PHASE_TIMEOUT=15s \ + YUM_CI_SYSTEMD_AVAILABLE=1 \ + MOCK_EXECUTABLE="$TMPDIR_TEST/fake-command" \ + MOCK_SERVICE_FAIL="$service_fail" \ + "$WORKER" "$output" "$output/desktop.list" "$output/special.list" \ + >"$output/worker.log" 2>&1 || true + [ -s "$output/result.tsv" ] || fail "$name did not write result.tsv" +} + +run_case normal '' '' 0 +run_case desktop desktop '' 0 +run_case special '' special 0 +run_case servicefail '' '' 1 + +IFS=$'\t' read -r _ _ install command service remove overall _ \ + <"$TMPDIR_TEST/normal/result.tsv" +[ "$install/$command/$service/$remove/$overall" = 'PASS/PASS/PASS/PASS/PASS' ] || + fail "normal package statuses: $install/$command/$service/$remove/$overall" + +IFS=$'\t' read -r _ _ install command service remove overall _ \ + <"$TMPDIR_TEST/desktop/result.tsv" +[ "$install/$command/$service/$remove/$overall" = 'PASS/SKIP/PASS/PASS/SKIP' ] || + fail "desktop package statuses: $install/$command/$service/$remove/$overall" + +IFS=$'\t' read -r _ _ install command service remove overall _ \ + <"$TMPDIR_TEST/special/result.tsv" +[ "$install/$command/$service/$remove/$overall" = 'PASS/PASS/SKIP/PASS/SKIP' ] || + fail "special service package statuses: $install/$command/$service/$remove/$overall" + +IFS=$'\t' read -r _ _ install command service remove overall _ \ + <"$TMPDIR_TEST/servicefail/result.tsv" +[ "$install/$command/$service/$remove/$overall" = 'PASS/PASS/FAIL/PASS/FAIL' ] || + fail "service failure package statuses: $install/$command/$service/$remove/$overall" + +printf 'PASS: package-worker phase tests\n' diff --git a/tst_lib/ts_common.sh b/tst_lib/ts_common.sh index df39d24fff8c7b2d7c0cf3c3ff66204436bcb12c..03cb05edddb9b13499be96a11dcc7f04f812a0d9 100644 --- a/tst_lib/ts_common.sh +++ b/tst_lib/ts_common.sh @@ -565,7 +565,13 @@ test_command_execute() { [ -z "$test_command" ] && continue # 设置core dump路径,用来判断命令是否生成了core文件 - echo "$tmpdir/${index}.core" >/proc/sys/kernel/core_pattern + # Docker containers share this non-namespaced kernel setting with the + # host. The Docker package runner disables the write to avoid cross-job + # and host contamination; existing VM-based callers keep the old + # behavior by default. + if [ "${TST_COMMAND_SKIP_CORE_PATTERN_WRITE:-0}" != "1" ]; then + echo "$tmpdir/${index}.core" >/proc/sys/kernel/core_pattern + fi # 先尝试执行命令,成功则通过 if timeout -s SIGKILL 1 "$test_command" --help >"$tmpdir/${index}.log" 2>&1; then