136 lines
5.6 KiB
Bash
Executable File
136 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Public entry for the first playable client gate (INF-02 / REL-01).
|
||
#
|
||
# Runs N independent client processes through run_client_gate.sh into
|
||
# ROOT/run-<n>, writes ROOT/release-manifest.json listing exactly those runs and
|
||
# the candidate package hashes, then aggregates ONLY the listed runs. Historical
|
||
# PASS reports elsewhere are never scanned. Credentials are asked once (hidden
|
||
# TTY prompt) or taken from MT_ACCOUNT/MT_PASSWORD; never from arguments/files.
|
||
# Do not add `set -x` to this script.
|
||
#
|
||
# exit: 0 PASS, 1 FAIL, 2 BLOCKED, 124 a run hit the wall-clock timeout.
|
||
set -euo pipefail
|
||
set +x
|
||
cd "$(dirname "$0")/.."
|
||
repo="$PWD"
|
||
process_cleanup_grace_seconds=15
|
||
source script/playable_process_cleanup.sh
|
||
app="${MT_PLAYABLE_APP:-}"
|
||
config=""
|
||
output=""
|
||
mode="playable"
|
||
timeout="900"
|
||
allow=0
|
||
repeat=1
|
||
suite="full"
|
||
assets=""
|
||
serverlist="${MT_PLAYABLE_SERVERLIST:-}"
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法: script/playable_test.sh --app APP --config CONFIG [选项]
|
||
--app APP 导出包路径(候选包;所有 run 必须是同一哈希)
|
||
--output DIR 本批次根目录,必须不存在或为空(默认 build/playable/suite-<时间>)
|
||
--suite playable|full 必测用例集合,默认 full
|
||
--repeat N 独立进程运行次数,默认 1(发布要求见 docs/FIRST-MAC-PLAYABLE-IMPLEMENTATION.md)
|
||
--timeout-seconds N 每次墙钟超时,默认 900
|
||
--assets DIR 资源预检根目录(透传 run_client_gate.sh)
|
||
--serverlist FILE 测试模式 serverlist 覆盖(透传)
|
||
--allow-gameplay 允许专用测试账号发送移动/战斗/拾取/施法请求
|
||
退出码: 0 PASS / 1 FAIL / 2 BLOCKED / 124 超时
|
||
EOF
|
||
}
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--app) app="${2:-}"; shift 2 ;;
|
||
--config) config="${2:-}"; shift 2 ;;
|
||
--output) output="${2:-}"; shift 2 ;;
|
||
--suite) suite="${2:-}"; shift 2 ;;
|
||
--repeat) repeat="${2:-}"; shift 2 ;;
|
||
--timeout-seconds) timeout="${2:-}"; shift 2 ;;
|
||
--assets) assets="${2:-}"; shift 2 ;;
|
||
--serverlist) serverlist="${2:-}"; shift 2 ;;
|
||
--allow-gameplay) allow=1; shift ;;
|
||
--help) usage; exit 0 ;;
|
||
*) echo "未知选项: $1" >&2; usage >&2; exit 2 ;;
|
||
esac
|
||
done
|
||
if [ -z "$app" ] || [ ! -d "$app" ]; then echo "PLAYABLE SUITE: BLOCKED 需要 --app 指向候选导出包" >&2; exit 2; fi
|
||
if [ -z "$config" ] || [ ! -f "$config" ]; then echo "PLAYABLE SUITE: BLOCKED 需要 --config CONFIG" >&2; exit 2; fi
|
||
if [ "$suite" != "full" ] && [ "$suite" != "playable" ]; then echo "--suite 只支持 full/playable" >&2; exit 2; fi
|
||
if ! [[ "$repeat" =~ ^[1-9][0-9]*$ ]]; then echo "--repeat 必须是正整数" >&2; exit 2; fi
|
||
|
||
root="$output"
|
||
if [ -z "$root" ]; then root="$repo/build/playable/suite-$(date +%Y%m%d-%H%M%S)-$$"; fi
|
||
if [ -e "$root" ] && [ -n "$(find "$root" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||
echo "PLAYABLE SUITE: BLOCKED 输出目录已有内容,拒绝混入历史运行:$root" >&2
|
||
exit 2
|
||
fi
|
||
mkdir -p "$root"
|
||
root="$(cd "$root" && pwd)"
|
||
|
||
# Ask once for the whole batch; child runners inherit the environment only.
|
||
if [ -z "${MT_ACCOUNT:-}" ] || [ -z "${MT_PASSWORD:-}" ]; then
|
||
if [ -t 0 ]; then
|
||
if [ -z "${MT_ACCOUNT:-}" ]; then read -r -s -p "测试账号: " MT_ACCOUNT; echo >&2; fi
|
||
if [ -z "${MT_PASSWORD:-}" ]; then read -r -s -p "测试密码: " MT_PASSWORD; echo >&2; fi
|
||
fi
|
||
fi
|
||
if [ -z "${MT_ACCOUNT:-}" ] || [ -z "${MT_PASSWORD:-}" ]; then
|
||
echo "PLAYABLE SUITE: BLOCKED 缺少 MT_ACCOUNT/MT_PASSWORD,且不是交互终端" >&2
|
||
exit 2
|
||
fi
|
||
export MT_ACCOUNT MT_PASSWORD
|
||
|
||
# The candidate identity is computed once, before any run, from the same package.
|
||
bash script/playable_build_info.sh "$app" >"$root/candidate.json"
|
||
|
||
runs_json="[]"
|
||
timed_out=0
|
||
for index in $(seq 1 "$repeat"); do
|
||
run_dir="$root/run-$index"
|
||
args=(--app "$app" --config "$config" --output "$run_dir" --suite "$suite" --mode "$mode" --timeout-seconds "$timeout")
|
||
if [ -n "$assets" ]; then args+=(--assets "$assets"); fi
|
||
if [ -n "$serverlist" ]; then args+=(--serverlist "$serverlist"); fi
|
||
if [ "$allow" -eq 1 ]; then args+=(--allow-gameplay); fi
|
||
set +e
|
||
bash script/run_client_gate.sh "${args[@]}" </dev/null &
|
||
child_pid=$!
|
||
wait "$child_pid"
|
||
code=$?
|
||
child_pid=""
|
||
set -e
|
||
if [ "$code" -eq 124 ]; then timed_out=1; fi
|
||
# A run that never produced a sealed report is still listed so the release fails loudly.
|
||
runs_json="$(node -e '
|
||
const fs = require("node:fs");
|
||
const [runs, dir, name, suite] = process.argv.slice(1);
|
||
let runId = `${name}-unsealed`;
|
||
try { runId = JSON.parse(fs.readFileSync(`${dir}/report.json`, "utf8")).run_id || runId; } catch {}
|
||
const list = JSON.parse(runs);
|
||
list.push({ run_id: runId, suite, path: name });
|
||
process.stdout.write(JSON.stringify(list));
|
||
' "$runs_json" "$run_dir" "run-$index" "$suite")"
|
||
done
|
||
|
||
node -e '
|
||
const fs = require("node:fs");
|
||
const [root, suite, repeat, runs] = process.argv.slice(1);
|
||
const candidate = JSON.parse(fs.readFileSync(`${root}/candidate.json`, "utf8"));
|
||
fs.writeFileSync(`${root}/release-manifest.json`, JSON.stringify({
|
||
schema_version: 1, candidate, required_runs: { [suite]: Number(repeat) }, runs: JSON.parse(runs),
|
||
}, null, 2) + "\n");
|
||
' "$root" "$suite" "$repeat" "$runs_json"
|
||
|
||
set +e
|
||
node script/validate_playable_report.mjs --release-dir "$root"
|
||
release_code=$?
|
||
set -e
|
||
if [ "$timed_out" -eq 1 ]; then echo "PLAYABLE SUITE: TIMEOUT root=$root" >&2; exit 124; fi
|
||
case "$release_code" in
|
||
0) echo "PLAYABLE SUITE: PASS runs=$repeat root=$root" ;;
|
||
2) echo "PLAYABLE SUITE: BLOCKED root=$root" >&2 ;;
|
||
*) echo "PLAYABLE SUITE: FAIL root=$root" >&2; release_code=1 ;;
|
||
esac
|
||
exit "$release_code"
|