- 新增 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
171 lines
5.1 KiB
Bash
Executable File
171 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 真实 40250 服务器端到端冒烟测试。
|
||
#
|
||
# 用法:
|
||
# ./script/live_smoke_test.sh
|
||
# ./script/live_smoke_test.sh --no-build
|
||
# ./script/live_smoke_test.sh --no-mutations
|
||
# ./script/live_smoke_test.sh --no-reconnect
|
||
#
|
||
# 账号和密码优先从环境变量读取;未提供时安全地交互输入,不写入脚本。
|
||
# 服务器地址与客户端一样经 project/net/serverinfo.gd 解析,不在脚本里写死。
|
||
# 不要在本脚本中启用 set -x。
|
||
set -euo pipefail
|
||
set +x
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
APP="${MT_SMOKE_APP:-$REPO/build/export/mtgodot-poc.app}"
|
||
EXECUTABLE="$APP/Contents/MacOS/mtgodot-poc"
|
||
ARTIFACT_DIR="${MT_SMOKE_OUTPUT:-$REPO/build/live-smoke}"
|
||
LOG="$ARTIFACT_DIR/live-smoke.log"
|
||
REPORT="$ARTIFACT_DIR/live-smoke-report.json"
|
||
CONFIG_FILE="${MT_SMOKE_CONFIG:-$REPO/.env.live-smoke.local}"
|
||
|
||
BUILD=1
|
||
MUTATIONS=1
|
||
RECONNECT=1
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法: ./script/live_smoke_test.sh [选项]
|
||
|
||
选项:
|
||
--no-build 使用现有 APP,不重新打包
|
||
--no-mutations 只做登录、进游戏和数据读取,不发送移动/聊天/重连包
|
||
--no-reconnect 做移动和聊天,但跳过重连
|
||
--reconnect-only 只测试登录/主动重连,不发送移动、聊天或施法
|
||
--help 显示帮助
|
||
|
||
环境变量:
|
||
MT_ACCOUNT / MT_PASSWORD 可预先提供账号密码,未提供时交互输入
|
||
MT_CHAR_SLOT=N 指定角色槽位;不设置时自动选择第一个角色
|
||
MT_GODOT 解析 serverinfo 用的 godot,默认 godot
|
||
EOF
|
||
}
|
||
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--no-build) BUILD=0 ;;
|
||
--no-mutations) MUTATIONS=0; RECONNECT=0 ;;
|
||
--no-reconnect) RECONNECT=0 ;;
|
||
--reconnect-only) MUTATIONS=0; RECONNECT=1 ;;
|
||
--help) usage; exit 0 ;;
|
||
*) echo "未知选项: $1" >&2; usage >&2; exit 2 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
cd "$REPO"
|
||
mkdir -p "$ARTIFACT_DIR"
|
||
|
||
if [ -f "$CONFIG_FILE" ]; then
|
||
# shellcheck disable=SC1090
|
||
. "$CONFIG_FILE"
|
||
fi
|
||
|
||
if [ -z "${MT_ACCOUNT:-}" ]; then
|
||
read -r -s -p "账号: " MT_ACCOUNT
|
||
echo
|
||
fi
|
||
if [ -z "${MT_PASSWORD:-}" ]; then
|
||
read -r -s -p "密码: " MT_PASSWORD
|
||
echo
|
||
fi
|
||
|
||
if [ -z "${MT_ACCOUNT:-}" ] || [ -z "${MT_PASSWORD:-}" ]; then
|
||
echo "账号和密码不能为空" >&2
|
||
exit 2
|
||
fi
|
||
|
||
if [ "$BUILD" -eq 1 ]; then
|
||
echo "== build release =="
|
||
./build-macos-client.sh release
|
||
fi
|
||
|
||
if [ ! -x "$EXECUTABLE" ]; then
|
||
echo "找不到可执行 APP:$EXECUTABLE" >&2
|
||
echo "请先运行 ./build-macos-client.sh release" >&2
|
||
exit 2
|
||
fi
|
||
|
||
echo "== check server ports =="
|
||
# 登录页默认 server 0 / 第一个频道;与 AppFlow 同一 ServerInfo 解析。
|
||
ADDRESS_LINE="$("${MT_GODOT:-godot}" --headless --path project --script print_server_address.gd 2>/dev/null | grep '^SERVER_ADDRESS ' || true)"
|
||
if [ -z "$ADDRESS_LINE" ]; then
|
||
echo "无法从 serverinfo 解析服务器地址" >&2
|
||
exit 2
|
||
fi
|
||
read -r AUTH_HOST AUTH_PORT GAME_HOST GAME_PORT <<<"$(node -e '
|
||
const a = JSON.parse(process.argv[1].slice("SERVER_ADDRESS ".length));
|
||
process.stdout.write([a.auth_host, a.auth_port, a.game_host, a.game_port].join(" "));
|
||
' "$ADDRESS_LINE")"
|
||
for endpoint in "$AUTH_HOST:$AUTH_PORT" "$GAME_HOST:$GAME_PORT"; do
|
||
if ! nc -G 3 -z "${endpoint%:*}" "${endpoint##*:}" >/dev/null 2>&1; then
|
||
echo "服务器端口不可达:$endpoint" >&2
|
||
exit 3
|
||
fi
|
||
done
|
||
|
||
if pgrep -x mtgodot-poc >/dev/null; then
|
||
echo "已有客户端运行,请先关闭后再执行独立登录测试。" >&2
|
||
exit 2
|
||
fi
|
||
|
||
export MT_PROTOCOL=classic
|
||
export MT_AUTOLOGIN=1
|
||
export MT_TEST_MODE=smoke
|
||
export MT_ACCOUNT
|
||
export MT_PASSWORD
|
||
export MT_SMOKE_MUTATIONS="$MUTATIONS"
|
||
export MT_SMOKE_RECONNECT="$RECONNECT"
|
||
export MT_TEST_REPORT="$REPORT"
|
||
rm -f "$REPORT"
|
||
|
||
echo "== run live smoke =="
|
||
echo "log: $LOG"
|
||
echo "report: $REPORT"
|
||
set +e
|
||
# 日志落盘前脱敏;PIPESTATUS[0] 仍是 APP 的真实退出码。
|
||
env -u MT_ASSETS "$EXECUTABLE" 2>&1 | node script/redact_stream.mjs | tee "$LOG"
|
||
APP_STATUS=${PIPESTATUS[0]}
|
||
set -e
|
||
|
||
if [ ! -f "$REPORT" ]; then
|
||
echo "未生成本次测试报告,APP 可能在测试状态机启动前崩溃。" >&2
|
||
echo "LIVE_SMOKE RESULT: FAIL (missing report)" >&2
|
||
exit 4
|
||
fi
|
||
|
||
echo "== report =="
|
||
if command -v jq >/dev/null 2>&1; then
|
||
REPORT_STATUS=$(jq -r '.status // empty' "$REPORT")
|
||
jq -r '"status=" + .status, "duration=" + (.duration_seconds|tostring) + "s", ( .failures[]? | "failure=" + . )' "$REPORT"
|
||
else
|
||
REPORT_STATUS=""
|
||
if grep -q '"status"[[:space:]]*:[[:space:]]*"PASS"' "$REPORT"; then
|
||
REPORT_STATUS=PASS
|
||
fi
|
||
grep -E '"status"|"failures"' "$REPORT" || true
|
||
fi
|
||
|
||
if [ "$APP_STATUS" -ne 0 ]; then
|
||
echo "LIVE_SMOKE RESULT: FAIL (APP exit=$APP_STATUS)" >&2
|
||
echo "最近的崩溃报告:"
|
||
ls -1t "$HOME/Library/Logs/DiagnosticReports/mtgodot-poc-"*.ips 2>/dev/null | head -n 3 || true
|
||
exit "$APP_STATUS"
|
||
fi
|
||
|
||
# 与 package_render_test.sh / validate_playable_report.mjs 相同的完整日志门禁。
|
||
if grep -nE 'SCRIPT ERROR|Parse Error|^ERROR:|leaked at exit|shaders of type .* were never freed' "$LOG"; then
|
||
echo "LIVE_SMOKE RESULT: FAIL (log exit gate)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$REPORT_STATUS" != "PASS" ]; then
|
||
echo "LIVE_SMOKE RESULT: FAIL (report status=$REPORT_STATUS)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
echo "LIVE_SMOKE RESULT: PASS"
|