mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
395 lines
14 KiB
Bash
Executable File
395 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
cd "${REPO_ROOT}"
|
|
|
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
SMOKE_ROOT="${BAT_API_LIVE_SMOKE_ROOT:-/tmp/bat-api-local-live-${timestamp}}"
|
|
OUTPUT_DIR="${SMOKE_ROOT}/official"
|
|
LOCALIZED_DIR="${SMOKE_ROOT}/localized"
|
|
STATE_DIR="${SMOKE_ROOT}/state"
|
|
REPORT_DIR="${SMOKE_ROOT}/report"
|
|
FIXTURE_ROOT="${REPO_ROOT}/internal/api/testdata/release"
|
|
BAT_BIN="${BAT_BIN:-${REPO_ROOT}/target/debug/bat}"
|
|
BAT_API_BIN="${BAT_API_BIN:-${SMOKE_ROOT}/bat-api}"
|
|
PORT="${BAT_API_LIVE_PORT:-18181}"
|
|
BASE_URL="http://127.0.0.1:${PORT}"
|
|
SOCKET_PATH="${STATE_DIR}/bat.sock"
|
|
API_PID=""
|
|
|
|
fail() {
|
|
printf 'error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_command() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "required command is missing: $1"
|
|
}
|
|
|
|
require_command cargo
|
|
require_command curl
|
|
require_command go
|
|
require_command jq
|
|
require_command realpath
|
|
|
|
[[ -d "${FIXTURE_ROOT}" ]] || fail "release fixture directory is missing: ${FIXTURE_ROOT}"
|
|
[[ "${SMOKE_ROOT}" == /tmp/* ]] || fail "BAT_API_LIVE_SMOKE_ROOT must be under /tmp"
|
|
[[ ! -e "${SMOKE_ROOT}" ]] || fail "smoke root already exists: ${SMOKE_ROOT}"
|
|
[[ -x "${BAT_BIN}" || "${BAT_API_LIVE_SKIP_BUILD:-}" != "1" ]] || fail "bat binary is not executable: ${BAT_BIN}"
|
|
|
|
mkdir -p "${OUTPUT_DIR}" "${LOCALIZED_DIR}" "${STATE_DIR}" "${REPORT_DIR}"
|
|
|
|
if [[ "${BAT_API_LIVE_SKIP_BUILD:-}" != "1" ]]; then
|
|
cargo build -p bat-infrastructure --bin bat
|
|
GOCACHE="${GOCACHE:-/tmp/bat-go-cache}" go build -o "${BAT_API_BIN}" ./cmd/bat-api
|
|
fi
|
|
|
|
[[ -x "${BAT_BIN}" ]] || fail "bat binary is not executable: ${BAT_BIN}"
|
|
[[ -x "${BAT_API_BIN}" ]] || fail "bat-api binary is not executable: ${BAT_API_BIN}"
|
|
|
|
write_version_state() {
|
|
local id="$1"
|
|
local version_dir="$2"
|
|
local snapshot_path="${version_dir}/official-sync-snapshot.json"
|
|
local state_path="${OUTPUT_DIR}/official-version-state.json"
|
|
local temp_path="${state_path}.tmp.$$"
|
|
|
|
jq -n \
|
|
--arg id "${id}" \
|
|
--arg app_version "1.70.0" \
|
|
--arg bundle_version "s8tloc7lo3" \
|
|
--arg addressables_root "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture" \
|
|
--arg resource_root "${version_dir}" \
|
|
--arg snapshot_path "${snapshot_path}" \
|
|
'{
|
|
state_version: 1,
|
|
current_completed_version: {
|
|
id: $id,
|
|
app_version: $app_version,
|
|
bundle_version: $bundle_version,
|
|
addressables_root: $addressables_root,
|
|
resource_root: $resource_root,
|
|
snapshot_path: $snapshot_path,
|
|
staging_path: null,
|
|
version_path: $resource_root,
|
|
started_unix_seconds: null,
|
|
completed_unix_seconds: 1
|
|
},
|
|
in_progress_version: null,
|
|
previous_available_version: null,
|
|
failed_versions: [],
|
|
updated_unix_seconds: 1
|
|
}' >"${temp_path}"
|
|
mv -f "${temp_path}" "${state_path}"
|
|
}
|
|
|
|
write_empty_version_state() {
|
|
local state_path="${OUTPUT_DIR}/official-version-state.json"
|
|
local temp_path="${state_path}.tmp.$$"
|
|
jq -n '{
|
|
state_version: 1,
|
|
current_completed_version: null,
|
|
in_progress_version: null,
|
|
previous_available_version: null,
|
|
failed_versions: [],
|
|
updated_unix_seconds: 1
|
|
}' >"${temp_path}"
|
|
mv -f "${temp_path}" "${state_path}"
|
|
}
|
|
|
|
switch_current() {
|
|
local id="$1"
|
|
local link_path="${OUTPUT_DIR}/current"
|
|
local temp_link="${OUTPUT_DIR}/.current.$$"
|
|
ln -s "versions/${id}" "${temp_link}"
|
|
mv -Tf "${temp_link}" "${link_path}"
|
|
}
|
|
|
|
stop_daemon() {
|
|
if [[ -S "${SOCKET_PATH}" || -f "${STATE_DIR}/bat.pid" ]]; then
|
|
"${BAT_BIN}" stop \
|
|
--state-dir "${STATE_DIR}" \
|
|
--json \
|
|
--no-progress \
|
|
--no-banner \
|
|
>"${REPORT_DIR}/daemon-stop.json" 2>"${REPORT_DIR}/daemon-stop.log" || true
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "${API_PID}" ]] && kill -0 "${API_PID}" 2>/dev/null; then
|
|
kill "${API_PID}" 2>/dev/null || true
|
|
wait "${API_PID}" 2>/dev/null || true
|
|
fi
|
|
stop_daemon
|
|
printf 'Smoke artifacts: %s\n' "${SMOKE_ROOT}" >&2
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
mkdir -p "${OUTPUT_DIR}/versions/v-current"
|
|
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-current/"
|
|
write_version_state "v-current" "${OUTPUT_DIR}/versions/v-current"
|
|
switch_current "v-current"
|
|
|
|
# The daemon is real and owns the live RPC socket, but this smoke must never
|
|
# reach official endpoints. Its sync loop can fail and retry while the seeded
|
|
# published release remains available to bat-api.
|
|
LOCAL_CURL="${SMOKE_ROOT}/curl-network-blocked"
|
|
printf '%s\n' \
|
|
'#!/usr/bin/env bash' \
|
|
'printf "local live smoke blocks network access: curl %q\n" "$*" >&2' \
|
|
'exit 7' >"${LOCAL_CURL}"
|
|
chmod +x "${LOCAL_CURL}"
|
|
|
|
printf '==> start Rust bat daemon\n'
|
|
BAT_AUTO_DISCOVER=0 "${BAT_BIN}" \
|
|
--server-info-path "${FIXTURE_ROOT}/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
|
|
--connection-group Prod \
|
|
--app-version 1.70.0 \
|
|
--output "${OUTPUT_DIR}" \
|
|
--localized-output "${LOCALIZED_DIR}" \
|
|
--state-dir "${STATE_DIR}" \
|
|
--interval 1h \
|
|
--error-retry 1s \
|
|
--platforms Windows \
|
|
--curl "${LOCAL_CURL}" \
|
|
--daemon \
|
|
--json \
|
|
--no-progress \
|
|
--no-banner \
|
|
>"${REPORT_DIR}/daemon-start.json" 2>"${REPORT_DIR}/daemon-start.log"
|
|
|
|
for _ in $(seq 1 100); do
|
|
[[ -S "${SOCKET_PATH}" ]] && break
|
|
sleep 0.1
|
|
done
|
|
[[ -S "${SOCKET_PATH}" ]] || fail "Rust bat did not create ${SOCKET_PATH}; see ${REPORT_DIR}/daemon-start.log"
|
|
|
|
printf '==> start bat-api on the same host\n'
|
|
BAT_API_SKIP_ENV_FILE=1 "${BAT_API_BIN}" \
|
|
--listen "127.0.0.1:${PORT}" \
|
|
--public-base-url "${BASE_URL}" \
|
|
--state-dir "${STATE_DIR}" \
|
|
--socket "${SOCKET_PATH}" \
|
|
--server-info-file "${FIXTURE_ROOT}/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
|
|
--refresh-interval 100ms \
|
|
>"${REPORT_DIR}/bat-api.log" 2>&1 &
|
|
API_PID="$!"
|
|
|
|
wait_http_status() {
|
|
local path="$1"
|
|
local expected="$2"
|
|
local response_file="$3"
|
|
shift 3
|
|
local curl_args=("$@")
|
|
local status=""
|
|
for _ in $(seq 1 100); do
|
|
status="$(curl -sS "${curl_args[@]}" -o "${response_file}" -w '%{http_code}' "${BASE_URL}${path}" || true)"
|
|
if [[ "${status}" == "${expected}" ]]; then
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
fail "${path} expected HTTP ${expected}, got ${status}; response: $(tr '\n' ' ' <"${response_file}")"
|
|
}
|
|
|
|
wait_json() {
|
|
local path="$1"
|
|
local expression="$2"
|
|
local response_file="$3"
|
|
for _ in $(seq 1 100); do
|
|
curl -sS "${BASE_URL}${path}" >"${response_file}" 2>/dev/null || true
|
|
if jq -e "${expression}" "${response_file}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
fail "${path} did not satisfy ${expression}; response: $(tr '\n' ' ' <"${response_file}")"
|
|
}
|
|
|
|
wait_json_status() {
|
|
local path="$1"
|
|
local expected="$2"
|
|
local expression="$3"
|
|
local response_file="$4"
|
|
local status=""
|
|
for _ in $(seq 1 100); do
|
|
status="$(curl -sS -o "${response_file}" -w '%{http_code}' "${BASE_URL}${path}" || true)"
|
|
if [[ "${status}" == "${expected}" ]] &&
|
|
jq -e "${expression}" "${response_file}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
fail "${path} expected HTTP ${expected} and ${expression}, got ${status}; response: $(tr '\n' ' ' <"${response_file}")"
|
|
}
|
|
|
|
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-current"))' \
|
|
"${REPORT_DIR}/ready-initial.json"
|
|
curl -fsS "${BASE_URL}/v1/bootstrap" >"${REPORT_DIR}/bootstrap-initial.json"
|
|
jq -e '
|
|
.ready == true and
|
|
.policy.pull_owner == "rust_bat" and
|
|
.policy.resource_root_discovery == "bat_sock_rpc" and
|
|
.resource.release.version_id == "v-current"
|
|
' "${REPORT_DIR}/bootstrap-initial.json" >/dev/null
|
|
|
|
curl -fsS "${BASE_URL}/v1/launcher/bootstrap" >"${REPORT_DIR}/launcher-bootstrap-initial.json"
|
|
jq -e '
|
|
.ready == true and
|
|
.policy.package_update_manifest == false and
|
|
.policy.downloads_launcher_package == false and
|
|
.addressables_catalog_url_root ==
|
|
"http://127.0.0.1:'"${PORT}"'/prod-clientpatch.bluearchiveyostar.com/r93_fixture"
|
|
' "${REPORT_DIR}/launcher-bootstrap-initial.json" >/dev/null
|
|
|
|
curl -fsS "${BASE_URL}/api-launcher-jp.yo-star.com/api/launcher/game/config/json" \
|
|
>"${REPORT_DIR}/launcher-config-json-initial.json"
|
|
jq -e '
|
|
.code == 200 and
|
|
.data.package_update_manifest == false and
|
|
.data.scope == "resource_bootstrap_only"
|
|
' "${REPORT_DIR}/launcher-config-json-initial.json" >/dev/null
|
|
|
|
curl -fsS "${BASE_URL}/v1/server-info" >"${REPORT_DIR}/server-info-initial.json"
|
|
jq -e '
|
|
.ConnectionGroups[0].AddressablesCatalogUrlRoot ==
|
|
"http://127.0.0.1:'"${PORT}"'/prod-clientpatch.bluearchiveyostar.com/r93_fixture"
|
|
' "${REPORT_DIR}/server-info-initial.json" >/dev/null
|
|
|
|
CDN_PATH="/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
|
|
curl -fsS "${BASE_URL}${CDN_PATH}" >"${REPORT_DIR}/cdn-full.bytes"
|
|
[[ "$(cat "${REPORT_DIR}/cdn-full.bytes")" == "TABLE_CATALOG_FIXTURE" ]] || fail "CDN full body mismatch"
|
|
|
|
curl -fsSI "${BASE_URL}${CDN_PATH}" >"${REPORT_DIR}/cdn-head.headers"
|
|
grep -Fqi 'content-length: 21' "${REPORT_DIR}/cdn-head.headers" || fail "CDN HEAD content length missing"
|
|
grep -Fqi 'cache-control: public, max-age=31536000, immutable' "${REPORT_DIR}/cdn-head.headers" ||
|
|
fail "CDN cache header missing"
|
|
|
|
curl -fsS -D "${REPORT_DIR}/cdn-range.headers" \
|
|
-H 'Range: bytes=0-3' \
|
|
-o "${REPORT_DIR}/cdn-range.bytes" \
|
|
"${BASE_URL}${CDN_PATH}"
|
|
grep -Fq '206 Partial Content' "${REPORT_DIR}/cdn-range.headers" || fail "CDN range did not return 206"
|
|
[[ "$(cat "${REPORT_DIR}/cdn-range.bytes")" == "TABL" ]] || fail "CDN range body mismatch"
|
|
|
|
wait_http_status "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/missing.bin" "404" \
|
|
"${REPORT_DIR}/cdn-missing.response"
|
|
wait_http_status \
|
|
"/prod-clientpatch.bluearchiveyostar.com/%2e%2e/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
|
|
"404" \
|
|
"${REPORT_DIR}/cdn-escape.response" \
|
|
--path-as-is
|
|
|
|
printf '==> switch current release through the local published state\n'
|
|
mkdir -p "${OUTPUT_DIR}/versions/v-next"
|
|
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-next/"
|
|
write_version_state "v-next" "${OUTPUT_DIR}/versions/v-next"
|
|
switch_current "v-next"
|
|
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-next"))' \
|
|
"${REPORT_DIR}/ready-after-switch.json"
|
|
curl -fsS "${BASE_URL}/v1/bootstrap" >"${REPORT_DIR}/bootstrap-after-switch.json"
|
|
jq -e '.ready == true and .resource.release.version_id == "v-next"' \
|
|
"${REPORT_DIR}/bootstrap-after-switch.json" >/dev/null
|
|
|
|
printf '==> publish a size-mismatched release and require not-ready\n'
|
|
mkdir -p "${OUTPUT_DIR}/versions/v-broken"
|
|
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-broken/"
|
|
printf 'intentional size mismatch\n' >"${OUTPUT_DIR}/versions/v-broken/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
|
|
write_version_state "v-broken" "${OUTPUT_DIR}/versions/v-broken"
|
|
switch_current "v-broken"
|
|
wait_json_status "/readyz" "503" \
|
|
'.ready == false and .present_count == 1 and .missing_count == 1 and (.resource_root | endswith("/versions/v-broken"))' \
|
|
"${REPORT_DIR}/ready-size-mismatch.json"
|
|
wait_http_status "${CDN_PATH}" "404" "${REPORT_DIR}/cdn-size-mismatch.response"
|
|
|
|
printf '==> publish a manifest-missing release and require not-ready\n'
|
|
mkdir -p "${OUTPUT_DIR}/versions/v-no-manifest"
|
|
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-no-manifest/"
|
|
mv "${OUTPUT_DIR}/versions/v-no-manifest/official-download-manifest.json" \
|
|
"${OUTPUT_DIR}/versions/v-no-manifest/official-download-manifest.json.disabled"
|
|
write_version_state "v-no-manifest" "${OUTPUT_DIR}/versions/v-no-manifest"
|
|
switch_current "v-no-manifest"
|
|
wait_json_status "/readyz" "503" \
|
|
'.ready == false and .resource_root == ""' \
|
|
"${REPORT_DIR}/ready-manifest-missing.json"
|
|
|
|
printf '==> publish no-release state and require not-ready\n'
|
|
write_empty_version_state
|
|
wait_json_status "/readyz" "503" '.ready == false and .resource_root == ""' \
|
|
"${REPORT_DIR}/ready-no-release.json"
|
|
switch_current "v-next"
|
|
write_version_state "v-next" "${OUTPUT_DIR}/versions/v-next"
|
|
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-next"))' \
|
|
"${REPORT_DIR}/ready-after-restore.json"
|
|
|
|
printf '==> disconnect and reconnect bat.sock\n'
|
|
stop_daemon
|
|
for _ in $(seq 1 100); do
|
|
[[ ! -S "${SOCKET_PATH}" ]] && break
|
|
sleep 0.1
|
|
done
|
|
wait_json "/healthz" '.rpc_available == false and .ready == false' \
|
|
"${REPORT_DIR}/health-rpc-disconnected.json"
|
|
wait_http_status "/readyz" "503" "${REPORT_DIR}/ready-rpc-disconnected.json"
|
|
|
|
BAT_AUTO_DISCOVER=0 "${BAT_BIN}" \
|
|
--server-info-path "${FIXTURE_ROOT}/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
|
|
--connection-group Prod \
|
|
--app-version 1.70.0 \
|
|
--output "${OUTPUT_DIR}" \
|
|
--localized-output "${LOCALIZED_DIR}" \
|
|
--state-dir "${STATE_DIR}" \
|
|
--interval 1h \
|
|
--error-retry 1s \
|
|
--platforms Windows \
|
|
--curl "${LOCAL_CURL}" \
|
|
--daemon \
|
|
--json \
|
|
--no-progress \
|
|
--no-banner \
|
|
>"${REPORT_DIR}/daemon-restart.json" 2>"${REPORT_DIR}/daemon-restart.log"
|
|
for _ in $(seq 1 100); do
|
|
[[ -S "${SOCKET_PATH}" ]] && break
|
|
sleep 0.1
|
|
done
|
|
[[ -S "${SOCKET_PATH}" ]] || fail "Rust bat did not recreate ${SOCKET_PATH}"
|
|
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-next"))' \
|
|
"${REPORT_DIR}/ready-after-reconnect.json"
|
|
|
|
curl -fsS "${BASE_URL}/healthz" >"${REPORT_DIR}/health-final.json"
|
|
jq -e '.ok == true and .rpc_available == true and .ready == true' \
|
|
"${REPORT_DIR}/health-final.json" >/dev/null
|
|
|
|
cat >"${REPORT_DIR}/SMOKE_REPORT.md" <<EOF
|
|
# bat-api Local Live Smoke Report
|
|
|
|
- generated_at_utc: ${timestamp}
|
|
- bat: ${BAT_BIN}
|
|
- bat_api: ${BAT_API_BIN}
|
|
- state_dir: ${STATE_DIR}
|
|
- official_output: ${OUTPUT_DIR}
|
|
- socket: ${SOCKET_PATH}
|
|
- listen: ${BASE_URL}
|
|
|
|
## Covered
|
|
|
|
- same-host Rust \`bat --daemon\` and Go \`bat-api\` over \`bat.sock\`
|
|
- initial ready and resource bootstrap
|
|
- server-info Addressables root rewrite
|
|
- CDN GET, HEAD, Range, ETag/cache semantics and missing/escape paths
|
|
- atomic \`current\` release switch
|
|
- size-mismatched or manifest-missing release -> \`/readyz\` 503 and no stale index
|
|
- no published release -> \`/readyz\` 503 and empty index
|
|
- RPC disconnect -> \`/healthz\` reports unavailable and \`/readyz\` 503
|
|
- daemon restart and release rediscovery
|
|
|
|
All inputs are isolated under \`${SMOKE_ROOT}\`; no official network or client
|
|
directory is used.
|
|
EOF
|
|
|
|
printf 'LOCAL_BAT_API_LIVE_SMOKE_OK\n'
|
|
printf 'Smoke report: %s\n' "${REPORT_DIR}/SMOKE_REPORT.md"
|