Skip to content

Commit cf5e5d9

Browse files
ci(audience): Linux PlayMode under xvfb (SDK-317 / SDK-318)
- Adds playmode-linux job using docker + xvfb. game-ci/unity-test-runner@v4 hardcodes -nographics, so PlayMode tests came back inconclusive and silently passed. - Watchdog SIGTERMs Unity 30s after "Test run completed" so cells exit on suite finish; handles Unity 6's known shutdown hang. - Replaces cartesian + partial-include matrix with explicit per-cell entries; partial includes failed to merge the runner column and spawned zero tests. - Forces StandaloneLinux64 to OpenGLCore-only at build to skip Vulkan shader variants and runtime negotiation. - Suppresses in-app log pane on Unity 6 Linux to skip llvmpipe rasterising UI Toolkit triangles per frame. - Stamps CI provenance into Player.log on player startup; gated to CI runs only. - Mirrors SDK output and OnError fires to Debug.Log so failures land in Player.log. - Captures Unity profiler binary log when AUDIENCE_PLAYER_PROFILE_PATH is set. - DiskStore.ReadBatch treats missing queue dir as empty (matches existing guards). - Trims 30 unused packages from the sample-app manifest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b3a13ba commit cf5e5d9

12 files changed

Lines changed: 453 additions & 61 deletions

File tree

.github/workflows/test-audience-sample-app.yml

Lines changed: 225 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ concurrency:
2525
group: ${{ github.workflow }}-${{ github.ref }}
2626
cancel-in-progress: true
2727

28+
# CI run id stamped into the player for CDP filtering. Per-cell id set on jobs below.
29+
env:
30+
AUDIENCE_TEST_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
31+
2832
jobs:
2933
# Reduced matrix on pull_request, full matrix on schedule and
3034
# workflow_dispatch. The self-hosted Windows runner pool is small, so
@@ -39,27 +43,31 @@ jobs:
3943
strategy:
4044
fail-fast: false
4145
matrix:
42-
unity: ['2021.3.45f2', '6000.4.0f1', '2022.3.62f2']
43-
target: [StandaloneWindows64, StandaloneOSX]
44-
backend: [IL2CPP, Mono2x]
46+
# Explicit per-cell entries instead of cartesian + partial-key include.
47+
# Partial-key includes failed to merge the runner column onto generated combos,
48+
# so every cell ran with no runner and the job silently spawned zero tests.
4549
include:
46-
- unity: '2021.3.45f2'
47-
changeset: 88f88f591b2e
48-
- unity: '6000.4.0f1'
49-
changeset: 8cf496087c8f
50-
- unity: '2022.3.62f2'
51-
changeset: 7670c08855a9
52-
- target: StandaloneWindows64
53-
runner: [self-hosted, Windows, X64]
54-
- target: StandaloneOSX
55-
runner: [self-hosted, macOS, ARM64]
50+
- { target: StandaloneWindows64, backend: IL2CPP, unity: '2021.3.45f2', changeset: 88f88f591b2e, runner: [self-hosted, Windows, X64] }
51+
- { target: StandaloneWindows64, backend: Mono2x, unity: '2021.3.45f2', changeset: 88f88f591b2e, runner: [self-hosted, Windows, X64] }
52+
- { target: StandaloneOSX, backend: IL2CPP, unity: '2021.3.45f2', changeset: 88f88f591b2e, runner: [self-hosted, macOS, ARM64] }
53+
- { target: StandaloneOSX, backend: Mono2x, unity: '2021.3.45f2', changeset: 88f88f591b2e, runner: [self-hosted, macOS, ARM64] }
54+
- { target: StandaloneWindows64, backend: IL2CPP, unity: '6000.4.0f1', changeset: 8cf496087c8f, runner: [self-hosted, Windows, X64] }
55+
- { target: StandaloneWindows64, backend: Mono2x, unity: '6000.4.0f1', changeset: 8cf496087c8f, runner: [self-hosted, Windows, X64] }
56+
- { target: StandaloneOSX, backend: IL2CPP, unity: '6000.4.0f1', changeset: 8cf496087c8f, runner: [self-hosted, macOS, ARM64] }
57+
- { target: StandaloneOSX, backend: Mono2x, unity: '6000.4.0f1', changeset: 8cf496087c8f, runner: [self-hosted, macOS, ARM64] }
58+
- { target: StandaloneWindows64, backend: IL2CPP, unity: '2022.3.62f2', changeset: 7670c08855a9, runner: [self-hosted, Windows, X64] }
59+
- { target: StandaloneWindows64, backend: Mono2x, unity: '2022.3.62f2', changeset: 7670c08855a9, runner: [self-hosted, Windows, X64] }
60+
- { target: StandaloneOSX, backend: IL2CPP, unity: '2022.3.62f2', changeset: 7670c08855a9, runner: [self-hosted, macOS, ARM64] }
61+
- { target: StandaloneOSX, backend: Mono2x, unity: '2022.3.62f2', changeset: 7670c08855a9, runner: [self-hosted, macOS, ARM64] }
5662
exclude: ${{ fromJSON(github.event_name == 'pull_request' && '[{"unity":"2022.3.62f2"}]' || '[]') }}
5763
runs-on: ${{ matrix.runner }}
5864
# Healthy cells finish in ~10 min. 30 min covers cold caches +
5965
# IL2CPP + Unity 6 startup; anything past that is a hang. Capping
6066
# short releases the self-hosted runner sooner so queued cells can
6167
# progress instead of waiting 60 min on a stuck job.
6268
timeout-minutes: 30
69+
env:
70+
AUDIENCE_TEST_CELL_ID: ${{ matrix.target }}-${{ matrix.backend }}-${{ matrix.unity }}
6371

6472
steps:
6573
- name: Kill stale Unity processes (Windows pre-checkout)
@@ -412,6 +420,10 @@ jobs:
412420
|| github.event_name == 'workflow_dispatch'
413421
name: ${{ matrix.target }} / ${{ matrix.backend }} / Unity ${{ matrix.unity }}
414422
runs-on: ubuntu-latest-8-cores
423+
# 45 min cap covers the inner 40 min watchdog plus post-Unity steps.
424+
timeout-minutes: 45
425+
env:
426+
AUDIENCE_TEST_CELL_ID: ${{ matrix.target }}-${{ matrix.backend }}-${{ matrix.unity }}
415427
strategy:
416428
fail-fast: false
417429
matrix:
@@ -433,35 +445,225 @@ jobs:
433445
Library-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}-
434446
Library-${{ matrix.backend }}-${{ matrix.target }}-
435447
436-
- uses: game-ci/unity-test-runner@v4
437-
id: playmode
448+
- name: Run PlayMode tests under xvfb
449+
# Manual docker run because game-ci/unity-test-runner@v4 hardcodes -nographics.
450+
# Without a virtual display every PlayMode test comes back inconclusive,
451+
# and the action's USE_EXIT_CODE=false suppresses Unity exit 2, so cells went silently green.
452+
shell: bash
438453
env:
439454
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
440455
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
441456
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
442457
AUDIENCE_TEST_PUBLISHABLE_KEY: ${{ secrets.AUDIENCE_TEST_PUBLISHABLE_KEY }}
443458
AUDIENCE_SCRIPTING_BACKEND: ${{ matrix.backend }}
444-
with:
445-
unityVersion: ${{ matrix.unity }}
446-
targetPlatform: ${{ matrix.target }}
447-
projectPath: examples/audience
448-
testMode: playmode
449-
githubToken: ${{ secrets.GITHUB_TOKEN }}
459+
UNITY_VERSION: ${{ matrix.unity }}
460+
AUDIENCE_LINUX_GLCORE_ONLY: "1"
461+
AUDIENCE_PLAYER_PROFILE_PATH: "/github/workspace/artifacts/player-profile.raw"
462+
run: |
463+
set -uo pipefail
464+
mkdir -p artifacts
465+
466+
# il2cpp-3 image ships both Mono and IL2CPP playback engines, plus xvfb.
467+
image="unityci/editor:ubuntu-${UNITY_VERSION}-linux-il2cpp-3"
468+
469+
docker run --rm \
470+
--workdir /github/workspace \
471+
--env UNITY_EMAIL --env UNITY_PASSWORD --env UNITY_SERIAL \
472+
--env AUDIENCE_TEST_PUBLISHABLE_KEY --env AUDIENCE_SCRIPTING_BACKEND \
473+
--env AUDIENCE_TEST_RUN_ID --env AUDIENCE_TEST_CELL_ID \
474+
--env AUDIENCE_LINUX_GLCORE_ONLY \
475+
--env AUDIENCE_PLAYER_PROFILE_PATH \
476+
--volume "$PWD":/github/workspace:z \
477+
--cpus=8 --memory=30487m \
478+
"$image" \
479+
/bin/bash -c '
480+
set -uo pipefail
481+
482+
# Per-run license activation.
483+
# Unity sometimes exits non-zero on a successful activation; assert the log marker instead.
484+
unity-editor -batchmode -nographics -quit \
485+
-username "$UNITY_EMAIL" \
486+
-password "$UNITY_PASSWORD" \
487+
-serial "$UNITY_SERIAL" \
488+
-logFile - 2>&1 | tee /github/workspace/artifacts/activation.log || true
489+
if grep -qE "License activation has failed|\[Licensing::Client\] Error: Code [0-9]+" \
490+
/github/workspace/artifacts/activation.log; then
491+
echo "::error::Unity license activation failed."
492+
exit 1
493+
fi
494+
if ! grep -qE "Successfully activated the entitlement license" \
495+
/github/workspace/artifacts/activation.log; then
496+
echo "::error::Unity license activation: no success marker in log."
497+
exit 1
498+
fi
499+
500+
# xvfb-run gives Unity a virtual X display. UI Toolkit needs GLX + render;
501+
# llvmpipe in the image provides software OpenGL so no GPU is needed.
502+
#
503+
# Watchdog (vs fixed timeout) because per-version run length varies wildly:
504+
# Unity 2021.3 cells finish in ~2 min, Unity 6 in ~22 min, and Unity 6 has a
505+
# known post-test shutdown hang. The watchdog SIGTERMs 30 s after seeing
506+
# "Test run completed" so each cell exits as soon as its suite finishes.
507+
# 40 min hard cap covers the case where that line never appears.
508+
#
509+
# Player runs at 320x240 (-screen-width / -screen-height below); xvfb desktop size doesn't matter.
510+
# -force-glcore skips Unity 6's Vulkan init and matches the Unity 2021.3 default path.
511+
log=/github/workspace/artifacts/playmode.log
512+
xvfb-run -a --server-args="-ac +extension GLX +render -noreset" -- \
513+
unity-editor \
514+
-batchmode \
515+
-force-glcore \
516+
-screen-fullscreen 0 \
517+
-screen-width 320 \
518+
-screen-height 240 \
519+
-projectPath /github/workspace/examples/audience \
520+
-runTests \
521+
-testPlatform StandaloneLinux64 \
522+
-testResults /github/workspace/artifacts/playmode-results.xml \
523+
-logFile "$log" &
524+
unity_pid=$!
525+
526+
# Stream the log to job stdout for live visibility while the
527+
# editor is alive. tail --pid exits when unity_pid does.
528+
tail --pid=$unity_pid -F "$log" 2>/dev/null &
529+
530+
deadline=$((SECONDS + 2400)) # 40 min hard cap
531+
flush_deadline=0
532+
kill_reason=""
533+
while kill -0 $unity_pid 2>/dev/null; do
534+
if [ "$SECONDS" -ge "$deadline" ]; then
535+
kill_reason="hard-cap-40m"
536+
break
537+
fi
538+
if [ "$flush_deadline" -eq 0 ] && grep -q "Test run completed" "$log" 2>/dev/null; then
539+
flush_deadline=$((SECONDS + 30))
540+
echo "[watchdog] saw \"Test run completed\" at ${SECONDS}s; SIGTERM after 30s flush window"
541+
fi
542+
if [ "$flush_deadline" -gt 0 ] && [ "$SECONDS" -ge "$flush_deadline" ]; then
543+
kill_reason="flush-window-elapsed"
544+
break
545+
fi
546+
sleep 5
547+
done
548+
549+
if [ -n "$kill_reason" ]; then
550+
echo "[watchdog] sending SIGTERM to Unity (reason: $kill_reason)"
551+
kill -TERM $unity_pid 2>/dev/null || true
552+
# 15 s grace, then SIGKILL if still alive.
553+
for _ in 1 2 3; do
554+
kill -0 $unity_pid 2>/dev/null || break
555+
sleep 5
556+
done
557+
if kill -0 $unity_pid 2>/dev/null; then
558+
echo "[watchdog] SIGTERM not honored, sending SIGKILL"
559+
kill -KILL $unity_pid 2>/dev/null || true
560+
fi
561+
fi
562+
563+
wait $unity_pid 2>/dev/null
564+
test_rc=$?
565+
if [ "$kill_reason" = "hard-cap-40m" ]; then
566+
echo "::warning::Unity hit the 40 min hard cap without logging \"Test run completed\". The player may have hung mid-suite. Inspect Player.log to see how far it got."
567+
fi
568+
569+
# Player runs in a separate process from the editor; copy its Player.log so HTTP traces and OnError fires are captured.
570+
# Glob across companies / products so the capture survives renames.
571+
find /root/.config/unity3d -name "Player.log" 2>/dev/null | while IFS= read -r f; do
572+
co=$(basename "$(dirname "$(dirname "$f")")")
573+
pr=$(basename "$(dirname "$f")")
574+
cp "$f" "/github/workspace/artifacts/Player-${co}-${pr}.log" 2>/dev/null || true
575+
done
576+
577+
# Always return the seat to keep the activation pool from exhausting on reruns.
578+
unity-editor -batchmode -nographics -quit -returnlicense -logFile - 2>&1 || true
579+
580+
# Unity exits 2 on test failure or inconclusive; propagate so the step actually fails.
581+
exit $test_rc
582+
'
583+
584+
- name: Fail when no tests actually executed
585+
# Defense in depth: NUnit marks tests "inconclusive" (not "failed") when the player never starts,
586+
# and dorny/test-reporter doesn't flag inconclusive. Without this guard, broken display silently passes.
587+
if: always()
588+
shell: bash
589+
run: |
590+
set -euo pipefail
591+
xml="artifacts/playmode-results.xml"
592+
if [ ! -f "$xml" ]; then
593+
echo "::error::No test-results.xml at $xml. Unity did not produce results."
594+
exit 1
595+
fi
596+
# Grep parses the <test-run> summary line. Avoids xmllint (not preinstalled on ubuntu-latest-8-cores).
597+
line=$(grep -m1 '<test-run ' "$xml" || true)
598+
if [ -z "$line" ]; then
599+
echo "::error::No <test-run> element in $xml. The XML is malformed."
600+
exit 1
601+
fi
602+
extract() {
603+
printf %s "$1" | grep -oE " $2=\"[0-9]+\"" | head -1 | grep -oE '[0-9]+'
604+
}
605+
passed=$(extract "$line" passed)
606+
failed=$(extract "$line" failed)
607+
inconclusive=$(extract "$line" inconclusive)
608+
echo "passed=${passed:-?} failed=${failed:-?} inconclusive=${inconclusive:-?}"
609+
if [ "${inconclusive:-0}" -gt 0 ]; then
610+
echo "::error::$inconclusive test(s) came back inconclusive. Unity could not actually execute them. Check that xvfb is running and the player launches."
611+
exit 1
612+
fi
613+
if [ "${passed:-0}" -eq 0 ] && [ "${failed:-0}" -eq 0 ]; then
614+
echo "::error::Zero tests passed and zero failed. The suite did not execute."
615+
exit 1
616+
fi
617+
618+
- name: Surface CI provenance to job summary
619+
# Greps the [CI] line from Player.log into the job summary so the buildGuid is copy-pasteable from the Actions UI.
620+
if: always()
621+
shell: bash
622+
run: |
623+
set -uo pipefail
624+
log=""
625+
for candidate in artifacts/Player-*.log; do
626+
[ -f "$candidate" ] && { log="$candidate"; break; }
627+
done
628+
{
629+
echo "## CI provenance"
630+
if [ -z "$log" ]; then
631+
echo "_No Player.log captured for this cell._"
632+
exit 0
633+
fi
634+
line=$(grep -m1 '\[CI\]' "$log" || true)
635+
if [ -z "$line" ]; then
636+
echo "_No [CI] line found in \`$log\`. The SampleApp's RuntimeInitializeOnLoad hook may not have fired._"
637+
exit 0
638+
fi
639+
echo
640+
echo '```'
641+
echo "$line"
642+
echo '```'
643+
echo
644+
echo "Filter CDP for events with this \`buildGuid\` to see the rows from this cell."
645+
} >> "$GITHUB_STEP_SUMMARY"
450646
451647
- name: Publish test report
452648
uses: dorny/test-reporter@v3
453649
if: always()
454650
with:
455651
name: PlayMode (${{ matrix.backend }} / ${{ matrix.target }})
456-
path: ${{ steps.playmode.outputs.artifactsPath }}/playmode-results.xml
652+
path: artifacts/playmode-results.xml
457653
reporter: dotnet-nunit
458654
fail-on-error: true
459655

460656
- uses: actions/upload-artifact@v4
461657
if: always()
462658
with:
463659
name: playmode-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}
464-
path: ${{ steps.playmode.outputs.artifactsPath }}
660+
path: |
661+
artifacts/playmode-results.xml
662+
artifacts/playmode.log
663+
artifacts/activation.log
664+
artifacts/Player-*.log
665+
artifacts/player-profile.raw
666+
examples/audience/Logs/**
465667
466668
# Mobile IL2CPP build validation — runs on GitHub-hosted Ubuntu via GameCI Docker
467669
# containers so self-hosted macOS/Windows machines are not occupied.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#nullable enable
2+
3+
using System;
4+
using UnityEditor;
5+
using UnityEditor.Build;
6+
using UnityEditor.Build.Reporting;
7+
using UnityEngine;
8+
using UnityEngine.Rendering;
9+
10+
namespace Immutable.Audience.Samples.SampleApp.Editor
11+
{
12+
// Forces StandaloneLinux64 player to OpenGLCore-only at build time.
13+
// Skips Vulkan shader variants the runtime never uses.
14+
internal sealed class GraphicsApisLinuxOverride : IPreprocessBuildWithReport
15+
{
16+
private const string EnvVar = "AUDIENCE_LINUX_GLCORE_ONLY";
17+
18+
public int callbackOrder => 1;
19+
20+
public void OnPreprocessBuild(BuildReport report)
21+
{
22+
if (report.summary.platform != BuildTarget.StandaloneLinux64) return;
23+
24+
var requested = Environment.GetEnvironmentVariable(EnvVar);
25+
if (string.IsNullOrEmpty(requested)) return;
26+
27+
var current = PlayerSettings.GetGraphicsAPIs(BuildTarget.StandaloneLinux64);
28+
if (current.Length == 1 && current[0] == GraphicsDeviceType.OpenGLCore)
29+
{
30+
Debug.Log($"[{nameof(GraphicsApisLinuxOverride)}] StandaloneLinux64 already at OpenGLCore only.");
31+
return;
32+
}
33+
34+
PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneLinux64, false);
35+
PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneLinux64, new[] { GraphicsDeviceType.OpenGLCore });
36+
Debug.Log($"[{nameof(GraphicsApisLinuxOverride)}] StandaloneLinux64 graphics APIs forced to OpenGLCore. Vulkan shader variants will be skipped.");
37+
}
38+
}
39+
}

examples/audience/Assets/Editor/GraphicsApisLinuxOverride.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)