Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion .github/workflows/test-audience-sample-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ jobs:
unity: 2021.3.45f2
changeset: 88f88f591b2e
runner: [self-hosted, macOS, ARM64]
- target: StandaloneWindows64
backend: IL2CPP
unity: 6000.4.0f1
changeset: 8cf496087c8f
runner: [self-hosted, Windows, X64]
- target: StandaloneWindows64
backend: Mono2x
unity: 6000.4.0f1
changeset: 8cf496087c8f
runner: [self-hosted, Windows, X64]
- target: StandaloneOSX
backend: IL2CPP
unity: 6000.4.0f1
changeset: 8cf496087c8f
runner: [self-hosted, macOS, ARM64]
- target: StandaloneOSX
backend: Mono2x
unity: 6000.4.0f1
changeset: 8cf496087c8f
runner: [self-hosted, macOS, ARM64]
runs-on: ${{ matrix.runner }}
timeout-minutes: 60

Expand Down Expand Up @@ -243,13 +263,19 @@ jobs:
run: |
set -euo pipefail
mkdir -p artifacts
# Tee Unity's stdout to artifacts/unity.log so the annotation step has a
# file to scan, while still streaming progress to the job log. pipefail
# propagates Unity's exit code through tee. The annotation step reads this
# file in-job; the actions/upload-artifact step below also uploads it so
# compile failures retain a full post-mortem (annotations are matched-line
# only and drop IL2CPP linker output, build config dumps, etc).
"$UNITY_PATH" \
-batchmode -nographics \
-projectPath examples/audience \
-runTests \
-testPlatform ${{ matrix.target }} \
-testResults "$(pwd)/artifacts/test-results.xml" \
-logFile -
-logFile - 2>&1 | tee "$(pwd)/artifacts/unity.log"

- name: Run PlayMode tests (Windows)
if: runner.os == 'Windows'
Expand Down Expand Up @@ -283,6 +309,51 @@ jobs:
run: |
git config --global --add safe.directory $env:GITHUB_WORKSPACE.Replace('\','/')

- name: Surface Unity compile errors as annotations (macOS)
if: always() && runner.os == 'macOS'
shell: bash
run: |
set -uo pipefail
# Unity writes compile errors as 'error CS####:' or 'Compilation failed: <n>'.
# When a cell fails compile (vs fails a test), the test-results.xml is empty
# and the only signal otherwise is the artifact zip. Promote those lines to
# ::error:: annotations so the PR UI shows the cause inline.
LOG_FILE="artifacts/unity.log"
if [ ! -f "$LOG_FILE" ]; then
echo "::notice::No Unity log file at $LOG_FILE."
exit 0
fi
# `|| true` guards the success path: with `pipefail`, grep exits 1 when no
# matches (the clean-build case), which would otherwise propagate as the
# step's exit code and falsely mark every green cell red.
grep -E '(error CS[0-9]+:|Compilation failed:)' "$LOG_FILE" | sort -u | while IFS= read -r line; do
trimmed="${line#"${line%%[![:space:]]*}"}"
# Sanitize '::' so log lines containing workflow commands (e.g. ::endgroup::)
# cannot terminate the annotation early or inject other commands.
sanitized="${trimmed//::/%3A%3A}"
echo "::error::$sanitized"
done || true

- name: Surface Unity compile errors as annotations (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
$logFile = "artifacts\unity.log"
if (-not (Test-Path $logFile)) {
Write-Host "::notice::No Unity log file at $logFile."
exit 0
}
Get-Content $logFile |
Select-String -Pattern '(error CS\d+:|Compilation failed:)' |
ForEach-Object { $_.Line.Trim() } |
Sort-Object -Unique |
ForEach-Object {
# Sanitize '::' so log lines containing workflow commands cannot
# terminate the annotation early or inject other commands.
$sanitized = $_ -replace '::', '%3A%3A'
Write-Host "::error::$sanitized"
}

- name: Publish test report
uses: dorny/test-reporter@v3
if: always()
Expand All @@ -298,4 +369,5 @@ jobs:
name: playmode-${{ matrix.backend }}-${{ matrix.target }}-${{ matrix.unity }}
path: |
artifacts/test-results.xml
artifacts/unity.log
examples/audience/Logs/**
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "Immutable.Audience.Samples.SampleApp.Editor",
"rootNamespace": "Immutable.Audience.Samples.SampleApp.Editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ namespace Immutable.Audience.Samples.SampleApp.Tests
[TestFixture]
internal class SampleAppLiveFireTests
{
// Cached allocations for fixed-delay yields (SDK needs time to flush
// rather than "wait up to N seconds for a condition"). Static readonly
// so the WaitForSecondsRealtime instance is created once per class load.
private static readonly WaitForSecondsRealtime _twoSeconds = new(2f);

private VisualElement? _root;
private string _key = "";

Expand Down Expand Up @@ -84,7 +89,11 @@ private IEnumerator LoadSceneOnly()
yield return SceneManager.LoadSceneAsync(SampleAppUi.SceneName, LoadSceneMode.Single);
yield return null; // one extra frame for Awake/InitializeUi

var sample = UnityEngine.Object.FindObjectOfType<AudienceSample>();
#if UNITY_2023_1_OR_NEWER
var sample = UnityEngine.Object.FindFirstObjectByType<AudienceSample>(FindObjectsInactive.Include);
#else
var sample = UnityEngine.Object.FindObjectOfType<AudienceSample>(includeInactive: true);
#endif
Assume.That(sample, Is.Not.Null, "AudienceSample MonoBehaviour expected in scene");

_root = sample!.GetComponent<UIDocument>().rootVisualElement;
Expand Down Expand Up @@ -226,7 +235,7 @@ public IEnumerator SetConsent_None_PurgesQueueAndPersists()

// Flushing after revocation should be a no-op (queue purged) — no error.
_root.Q<Button>(SampleAppUi.Buttons.Flush).Click();
yield return new WaitForSecondsRealtime(2f);
yield return _twoSeconds;

AssertNoErrors();
}
Expand Down Expand Up @@ -329,11 +338,12 @@ public IEnumerator ReInit_AfterShutdown_AcceptsTrack()

_root.Q<Button>(SampleAppUi.Buttons.Init).Click();
// Two INIT@Ok rows now; WaitForLogEntry returns on the first
// match it sees, but the original is already in the log so we
// wait one frame for the second click to land then assert by
// count instead.
// match it sees, but the original is already in the log so poll
// by count until the second Ok row appears or the deadline elapses.
yield return null;
yield return new WaitForSecondsRealtime(0.5f);
yield return SampleAppTestHelpers.WaitForCondition(
() => SampleAppTestHelpers.CountLogEntriesAtLevel(_root, LogLevels.Ok) > 1,
2f, "second INIT@Ok row after re-init");
Assert.Greater(SampleAppTestHelpers.CountLogEntriesAtLevel(_root, LogLevels.Ok), 1,
"expected a second INIT@Ok row after re-init");

Expand Down Expand Up @@ -554,7 +564,11 @@ public IEnumerator PersistedConsent_OverridesDropdownOnReInit()

// Dropdown is still at default Anonymous; re-Init must read disk.
_root.Q<Button>(SampleAppUi.Buttons.Init).Click();
yield return new WaitForSecondsRealtime(0.5f); // OnSdkStateChanged + RefreshStatusBar
// Poll until OnSdkStateChanged + RefreshStatusBar has run and the
// label reflects the re-loaded consent level.
yield return SampleAppTestHelpers.WaitForCondition(
() => _root.Q<Label>(SampleAppUi.StatusBar.Consent).text == SampleAppUi.Consent.Full,
2f, "status-consent label to show Full after re-Init from persisted consent");

var statusConsent = _root.Q<Label>(SampleAppUi.StatusBar.Consent).text;
Assert.AreEqual(SampleAppUi.Consent.Full, statusConsent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.UIElements;

Expand All @@ -14,6 +15,23 @@ namespace Immutable.Audience.Samples.SampleApp.Tests
// the log pane via the userData stash on each log row.
internal static class SampleAppTestHelpers
{
// Polls predicate once per frame until it returns true or the deadline
// elapses. Calls Assert.Fail with description when the deadline is hit.
// Use this instead of WaitForSecondsRealtime when a test is waiting
// "at most N seconds for X to become true" — the polling exits as soon
// as the condition is satisfied rather than burning the full N seconds.
internal static IEnumerator WaitForCondition(
Func<bool> predicate, float timeoutSeconds, string description)
{
var deadline = Time.realtimeSinceStartup + timeoutSeconds;
while (Time.realtimeSinceStartup < deadline)
{
if (predicate()) yield break;
yield return null;
}
Assert.Fail($"Timed out after {timeoutSeconds:F1}s waiting for: {description}");
}

// Wait until the log pane contains an entry whose label matches `label`
// and whose level matches `level`. Yields one frame per check.
// Throws TimeoutException on deadline.
Expand Down
7 changes: 1 addition & 6 deletions examples/audience/Packages/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@
"dependencies": {
"com.immutable.audience": "file:../../../src/Packages/Audience",
"com.unity.collab-proxy": "2.5.2",
"com.unity.feature.development": "1.0.1",
"com.unity.ide.rider": "3.0.31",
"com.unity.ide.visualstudio": "2.0.22",
"com.unity.ide.vscode": "1.2.5",
"com.unity.test-framework": "1.1.33",
"com.unity.test-framework": "1.4.5",
"com.unity.textmeshpro": "3.0.6",
"com.unity.timeline": "1.6.5",
"com.unity.toolchain.macos-arm64-linux-x86_64": "2.0.5",
"com.unity.ugui": "1.0.0",
"com.unity.visualscripting": "1.9.4",
"com.unity.modules.ai": "1.0.0",
Expand Down
Loading
Loading