Files
mtgodot-poc/script/forest_map_render_test.sh
T

205 lines
8.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"
source script/playable_process_cleanup.sh
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;超时 TERM10 秒后 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=$?
child_pid=""
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"