#!/usr/bin/env bash # finalize.sh is the last thing that runs in a match container, and anything it # does not collect is gone: the writable layer goes with the task. This covers # the part that is easy to get wrong by hand - which files under the client's # logs/ are offered for upload and which are not. # # The manifest here carries no upload targets, the same as the dev manifest # scripts/run.sh writes, so `upload` reports what it would have sent and returns # non-zero. That is what the assertions read: the file was found and offered, no # network involved. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT fail=0 check() { # if "${@:2}"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi } # The image collects container/ under ARENA_HOME; finalize.sh sources # lib/common.sh from there. mkdir -p "$TMP/home" "$TMP/run/state" "$TMP/run/spool" # The observer still spools a dump a phase; nothing collects them. What they # stood in for is kept properly now - raw/game_actions_N.tsv is the same match # structured and a thirtieth of the size, and raw/gamelog.html is MegaMek's own # account - and no reader for these ever existed. printf 'a turn report\n' > "$TMP/run/spool/007-r02-FIRING.txt" ln -s "$ROOT/container" "$TMP/home/container" echo '{"matchId":"test-match"}' > "$TMP/run/manifest.json" run_finalize() { # ARENA_HOME="$TMP/home" ARENA_RUN="$TMP/run" ARENA_MM_LOGDIR="$1" \ bash "$ROOT/container/exit/finalize.sh" 2>&1 } # --- a client that logged ----------------------------------------------------- LOGS="$TMP/logs" mkdir -p "$LOGS" # What MegaMek's shipped mmconf/log4j2.xml writes. unified_log rolls at 1MB, so a # long match leaves the early rounds - the tileset and camo work - in the gz. touch "$LOGS/megamek.log" \ "$LOGS/unified_log.log" \ "$LOGS/unified_log_1.log.gz" \ "$LOGS/unified_log_2.log.gz" \ "$LOGS/bot_path_ranker.log" \ "$LOGS/megameklab.log" out="$(run_finalize "$LOGS")"; status=$? check "runs clean" test "$status" -eq 0 [ "$status" -eq 0 ] || printf '%s\n' "$out" | sed 's/^/ /' # The logs ride the `diagnostics` key, the same one the stats table and the # profiles use. Neither of those exists in this fixture, so a diagnostics line # here is one of ours. offered() { grep -q "diagnostics.*$1" <<<"$out"; } check "the megamek logger's log is offered" offered "megamek.log" check "the unified log is offered" offered "unified_log.log" check "rolled unified logs are offered" bash -c \ "grep -q 'unified_log_1.log.gz' <<<\"\$1\" && grep -q 'unified_log_2.log.gz' <<<\"\$1\"" _ "$out" check "reports how many it sent" grep -q 'client log(s)' <<<"$out" # Spooled dumps stay in the container. This is the assertion that keeps the # decision from drifting back by accident: an upload path re-added here would # name the file. check "a spooled turn report is not uploaded" bash -c \ "! grep -q '007-r02-FIRING.txt' <<<\"\$1\"" _ "$out" # The two exclusions are the reason this test exists: both sit in the same # directory and both would come along with a wider glob. bot_path_ranker is # Princess's per-move ranking and is large; megameklab belongs to another # application and is always empty here. check "Princess's path ranking is left behind" bash -c "! grep -q 'bot_path_ranker' <<<\"\$1\"" _ "$out" check "another application's log is left behind" bash -c "! grep -q 'megameklab' <<<\"\$1\"" _ "$out" # --- a client that never started ---------------------------------------------- # A match that dies in init has no logs/ at all. Collecting nothing is correct; # failing the finalize, and losing the result upload with it, is not. EMPTY="$TMP/empty" mkdir -p "$EMPTY" out="$(run_finalize "$EMPTY")"; status=$? check "an empty log dir still finalizes" test "$status" -eq 0 check "and says it collected nothing" grep -q 'uploaded 0 client log(s)' <<<"$out" # --- the result, however the match ended -------------------------------------- # MatchHost keeps a provisional result on disk from the first tick of the game # and replaces it at victory, so a match that was abandoned, timed out or # signalled has one too. This pass must upload it either way, and must say which # it had: "no result" used to be the only record an undecided match left, and it # said nothing about how far the match got. RESULT="$TMP/run/state/result.json" check "a match with no document says so" bash -c \ "grep -q 'no result.json - the match host wrote none' <<<\"\$1\"" _ "$out" echo '{"ending": "no-victory-at-exit", "round": 4, "victoryTeam": 0, "victoryPlayerId": -1, "players": []}' > "$RESULT" out="$(run_finalize "$EMPTY")"; status=$? check "an undecided match still finalizes" test "$status" -eq 0 check "and its result is offered for upload" grep -q 'result-final.json' <<<"$out" check "and the log says how it ended" grep -q 'the match ended: no-victory-at-exit (round 4)' <<<"$out" # A decided one reads the same way and differs only in the ending, which is the # whole point of the field: one shape, one reader. rm -rf "$TMP/run/state/sent" "$TMP/run/state/result-final.json" echo '{"ending": "victory", "round": 9, "victoryTeam": 1, "victoryPlayerId": -1, "players": []}' > "$RESULT" out="$(run_finalize "$EMPTY")"; status=$? check "a decided match finalizes" test "$status" -eq 0 check "and says so" grep -q 'the match ended: victory (round 9)' <<<"$out" # An artifact that has already been accepted is never offered twice, whichever # pass got it through. rm -rf "$TMP/run/state/sent" mkdir -p "$TMP/run/state/sent" : > "$TMP/run/state/sent/result-final.json" out="$(run_finalize "$EMPTY")" check "a sent result is not offered again" bash -c \ "! grep -q 'result-final.json' <<<\"\$1\"" _ "$out" rm -rf "$TMP/run/state/sent" # --- counting the match ------------------------------------------------------- # Two operational properties, neither about whether the numbers are right: that # a container nobody configured stays silent, and that a counter which cannot be # reached does not take the match down with it. The third property - that the # hash here equals the one web/src/analytics.ts computes - cannot be tested from # this repo and is why the salt is passed in rather than written in both. cat > "$TMP/run/state/identity.json" <<'IDENTITY' {"matchId":"test-match","players":[ {"slot":"Alpha","control":"human","did":"did:plc:aaa","handle":"a.example"}, {"slot":"Princess1","control":"bot","did":null,"handle":null}]} IDENTITY out="$(run_finalize "$EMPTY")"; status=$? check "unconfigured, it counts nothing" bash -c "! grep -qi 'posthog\|counted the match' <<<\"\$1\"" _ "$out" check "and still finalizes" test "$status" -eq 0 # Port 1 refuses immediately, so this is the unreachable case rather than a slow # one. A match is not less finished because a counter did not answer. out="$(ARENA_POSTHOG_KEY=phc_test ARENA_POSTHOG_SALT=test-salt \ ARENA_POSTHOG_HOST=http://127.0.0.1:1 ARENA_POSTHOG_TIMEOUT=2 \ run_finalize "$EMPTY")"; status=$? check "an unreachable counter warns" grep -qi 'posthog did not accept' <<<"$out" check "and does not fail the finalize" test "$status" -eq 0 check "and the match still finalizes" grep -q 'done' <<<"$out" exit "$fail"