- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay 只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续 运行并记 BLOCKED;写本批次 release-manifest.json 后聚合 - run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理); soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数) - 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试 - forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别 - 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求 - .gitignore 排除本地场景配置、凭据文件与运行输出 离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS, node 夹具测试 PASS。未联网运行,未重建候选包。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
203 lines
8.2 KiB
Bash
Executable File
203 lines
8.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# MAP-02: run the packaged client in MT_TEST_MODE=forest_render and seal its report.
|
||
#
|
||
# - Offline: no server, no credentials. MT_ACCOUNT/MT_PASSWORD are removed from the
|
||
# child environment; nothing here reads or writes them. Do not add `set -x`.
|
||
# - Owns exactly one child PID; TERM, 10 s grace, KILL, then wait. Never pkills.
|
||
# - Maps come only from --maps or the config map_key (filled from the
|
||
# audit_playable_maps result); no map path is guessed here.
|
||
# - The client writes client-report.json/events/screenshots; this script writes
|
||
# report.json only after the real process exit and the full-log gate.
|
||
# - Metal screenshot sign-off stays a separate manual release item.
|
||
#
|
||
# exit: 0 PASS, 1 assert/exit-gate FAIL, 2 config/precondition BLOCKED, 124 wall-clock timeout.
|
||
set -euo pipefail
|
||
set +x
|
||
|
||
cd "$(dirname "$0")/.."
|
||
repo="$PWD"
|
||
app_path=""
|
||
maps=""
|
||
config_path=""
|
||
viewpoints=""
|
||
races=""
|
||
output_dir=""
|
||
timeout_seconds=900
|
||
required_arch="${MT_PLAYABLE_REQUIRED_ARCH:-arm64}"
|
||
kill_grace_seconds=10
|
||
suite="forest_render"
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法: script/forest_map_render_test.sh --app APP (--maps KEYS | --config CONFIG) [选项]
|
||
选项:
|
||
--app APP 已签名的 arm64 包(build/export/mtgodot-poc.app);新测试模式必须重新构建后运行
|
||
--maps K1[,K2] map-assets.json 里的 map_key,逗号分隔(优先于 --config 的 map_key)
|
||
--config FILE playable 配置;只读取 map_key/scenario_id
|
||
--viewpoints FILE 机位夹具(test/playable/forest-viewpoints.<name>.local.json);不给则只有自动候选
|
||
--races R1[,R2] 覆盖夹具里的怪物 vnum;不给且夹具为空时放 12 个候选树怪
|
||
--output DIR 本次运行目录,必须不存在或为空(默认 build/forest/run-<时间>-<pid>)
|
||
--timeout-seconds N 墙钟超时,默认 900;超时 TERM,10 秒后 KILL
|
||
--help 显示本帮助
|
||
说明:
|
||
自动候选机位、未确认机位、缺失传送点都会使对应用例 BLOCKED;
|
||
即使自动用例 PASS,Metal 截图仍需人工签核(发布清单单独的 manual 项)。
|
||
环境变量:
|
||
MT_PLAYABLE_REQUIRED_ARCH 包必须包含的架构,默认 arm64
|
||
退出码: 0 PASS / 1 FAIL / 2 BLOCKED(配置或前置条件)/ 124 墙钟超时
|
||
EOF
|
||
}
|
||
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--app) app_path="${2:-}"; shift 2 ;;
|
||
--maps) maps="${2:-}"; shift 2 ;;
|
||
--config) config_path="${2:-}"; shift 2 ;;
|
||
--viewpoints) viewpoints="${2:-}"; shift 2 ;;
|
||
--races) races="${2:-}"; shift 2 ;;
|
||
--output) output_dir="${2:-}"; shift 2 ;;
|
||
--timeout-seconds) timeout_seconds="${2:-}"; shift 2 ;;
|
||
--help) usage; exit 0 ;;
|
||
*) echo "未知选项: $1" >&2; usage >&2; exit 2 ;;
|
||
esac
|
||
done
|
||
|
||
output_ready=0
|
||
blocked() {
|
||
echo "FOREST RENDER GATE: BLOCKED $*" >&2
|
||
if [ "$output_ready" -eq 1 ]; then echo "BLOCKED $*" >>"$output_dir/gate.log"; fi
|
||
exit 2
|
||
}
|
||
|
||
abs_file() { echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"; }
|
||
|
||
[ -n "$app_path" ] || blocked "需要 --app"
|
||
[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || blocked "--timeout-seconds 必须是正整数"
|
||
engine="$app_path/Contents/MacOS/mtgodot-poc"
|
||
[ -x "$engine" ] || blocked "找不到可执行包:$app_path"
|
||
if [ -n "$config_path" ]; then
|
||
[ -f "$config_path" ] || blocked "配置不存在:$config_path"
|
||
config_path="$(abs_file "$config_path")"
|
||
fi
|
||
if [ -n "$viewpoints" ]; then
|
||
[ -f "$viewpoints" ] || blocked "机位夹具不存在:$viewpoints"
|
||
viewpoints="$(abs_file "$viewpoints")"
|
||
fi
|
||
if [ -n "$races" ] && ! [[ "$races" =~ ^[1-9][0-9]*(,[1-9][0-9]*)*$ ]]; then
|
||
blocked "--races 必须是逗号分隔的正整数 vnum"
|
||
fi
|
||
|
||
# Resolve the map list exactly as the client does (MT_FOREST_MAPS, else config map_key).
|
||
map_list="$(node -e '
|
||
const fs = require("node:fs");
|
||
const [maps, config] = process.argv.slice(1);
|
||
let keys = maps.split(",").map((s) => s.trim()).filter(Boolean);
|
||
if (keys.length === 0 && config) {
|
||
try { const key = String(JSON.parse(fs.readFileSync(config, "utf8")).map_key ?? "").trim(); if (key) keys = [key]; } catch {}
|
||
}
|
||
process.stdout.write(keys.join(","));
|
||
' "$maps" "$config_path")"
|
||
[ -n "$map_list" ] || blocked "没有地图:给 --maps,或在配置里填 map-assets.json 审计出的 map_key"
|
||
|
||
if [ -z "$output_dir" ]; then
|
||
output_dir="$repo/build/forest/run-$(date +%Y%m%d-%H%M%S)-$$"
|
||
fi
|
||
if [ -e "$output_dir" ] && [ -n "$(find "$output_dir" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||
blocked "输出目录已有内容,拒绝复用历史报告:$output_dir"
|
||
fi
|
||
mkdir -p "$output_dir"
|
||
output_dir="$(cd "$output_dir" && pwd)"
|
||
output_ready=1
|
||
|
||
if ! codesign --verify --strict "$app_path" >/dev/null 2>&1; then
|
||
blocked "签名校验失败:codesign --verify --strict $app_path"
|
||
fi
|
||
archs="$(lipo -archs "$engine" 2>/dev/null || true)"
|
||
case " $archs " in
|
||
*" $required_arch "*) ;;
|
||
*) blocked "包架构 [$archs] 不含 $required_arch" ;;
|
||
esac
|
||
|
||
run_id="$(basename "$output_dir")-$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')"
|
||
log="$output_dir/client.log"
|
||
client_report="$output_dir/client-report.json"
|
||
final_report="$output_dir/report.json"
|
||
events="$output_dir/events.jsonl"
|
||
|
||
# Same formula as forest_map_render_test.gd required_case_ids(); the validator
|
||
# fails the run if the client declares a different list.
|
||
node -e '
|
||
const [file, suite, maps] = process.argv.slice(1);
|
||
const cases = ["MAP-FOREST-CONFIG"];
|
||
for (const key of maps.split(",")) {
|
||
const id = key.split("/").pop();
|
||
cases.push(`MAP-FOREST-LOAD-${id}`);
|
||
for (const kind of ["flat", "slope", "dense", "warp"]) cases.push(`MAP-FOREST-VP-${id}-${kind}`);
|
||
cases.push(`MAP-FOREST-MOTION-${id}`);
|
||
}
|
||
require("node:fs").writeFileSync(file, JSON.stringify({ schema_version: 1, suite, cases }, null, 2) + "\n");
|
||
' "$output_dir/required-cases.json" "$suite" "$map_list"
|
||
|
||
bash script/playable_build_info.sh "$app_path" >"$output_dir/build.json"
|
||
build_field() { node -e 'process.stdout.write(String(JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"))[process.argv[2]]))' "$output_dir/build.json" "$1"; }
|
||
|
||
started_ms="$(($(date +%s) * 1000))"
|
||
set +e
|
||
# No credentials reach this child, so its log needs no redactor.
|
||
env -u MT_ASSETS -u MT_ACCOUNT -u MT_PASSWORD \
|
||
MT_TEST_MODE=forest_render \
|
||
MT_FOREST_MAPS="$map_list" \
|
||
MT_FOREST_VIEWPOINTS="$viewpoints" \
|
||
MT_FOREST_RACES="$races" \
|
||
MT_FOREST_TIMEOUT_SECONDS="$timeout_seconds" \
|
||
MT_PLAYABLE_CONFIG="$config_path" \
|
||
MT_PLAYABLE_RUN_ID="$run_id" \
|
||
MT_TEST_REPORT="$client_report" \
|
||
MT_TEST_EVENTS="$events" \
|
||
MT_TEST_OUTPUT="$output_dir" \
|
||
MT_BUILD_ENGINE_SHA256="$(build_field engine_sha256)" \
|
||
MT_BUILD_EXTENSION_SHA256="$(build_field extension_sha256)" \
|
||
MT_BUILD_PCK_SHA256="$(build_field pck_sha256)" \
|
||
MT_BUILD_ARCH="$(build_field arch)" \
|
||
"$engine" >"$log" 2>&1 </dev/null &
|
||
child_pid=$!
|
||
SECONDS=0
|
||
timed_out=0
|
||
while kill -0 "$child_pid" 2>/dev/null; do
|
||
if [ "$SECONDS" -ge "$timeout_seconds" ]; then
|
||
timed_out=1
|
||
kill -TERM "$child_pid" 2>/dev/null
|
||
grace=0
|
||
while kill -0 "$child_pid" 2>/dev/null && [ "$grace" -lt "$kill_grace_seconds" ]; do
|
||
sleep 1
|
||
grace=$((grace + 1))
|
||
done
|
||
killed=0
|
||
if kill -0 "$child_pid" 2>/dev/null; then kill -KILL "$child_pid" 2>/dev/null; killed=1; fi
|
||
echo "TIMEOUT term_grace_s=$grace killed=$killed" >>"$output_dir/gate.log"
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
wait "$child_pid"
|
||
process_code=$?
|
||
|
||
node script/validate_playable_report.mjs --report "$client_report" --output "$final_report" \
|
||
--log "$log" --process-code "$process_code" --timed-out "$timed_out" --run-id "$run_id" \
|
||
--suite "$suite" --required-cases "$output_dir/required-cases.json" --build "$output_dir/build.json" \
|
||
--events "$events" --started-ms "$started_ms" --require-pass
|
||
gate_status=$?
|
||
set -e
|
||
|
||
echo "FOREST RENDER GATE: Metal 截图人工签核仍是单独的 manual 项($output_dir/forest-*.png + forest-map-evidence.json)"
|
||
if [ "$timed_out" -eq 1 ]; then
|
||
echo "FOREST RENDER GATE: TIMEOUT raw_exit=$process_code report=$final_report log=$log" >&2
|
||
exit 124
|
||
fi
|
||
case "$gate_status" in
|
||
0) echo "FOREST RENDER GATE: PASS run_id=$run_id report=$final_report" ;;
|
||
2) echo "FOREST RENDER GATE: BLOCKED raw_exit=$process_code report=$final_report" >&2 ;;
|
||
*) echo "FOREST RENDER GATE: FAIL raw_exit=$process_code report=$final_report log=$log" >&2; gate_status=1 ;;
|
||
esac
|
||
exit "$gate_status"
|