Skip to content
Open
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
1 change: 1 addition & 0 deletions client/Packages/com.beamable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Content fields with attribute `MustReferenceContent` will have display content picker inspector in Editor
- Added a QA Smoke Kit sample (internal smoke-test harness).

### Fixed

Expand Down
8 changes: 8 additions & 0 deletions client/Packages/com.beamable/Samples/QASmokeKit.meta

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

169 changes: 169 additions & 0 deletions client/Packages/com.beamable/Samples/QASmokeKit/QASmokeKit.unity
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &519420028
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 519420029}
- component: {fileID: 519420030}
m_Layer: 0
m_Name: QA Smoke Runner
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &519420029
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &519420030
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a1b2c3d4e5f60718293a4b5c6d7e8f90, type: 3}
m_Name:
m_EditorClassIdentifier:

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

98 changes: 98 additions & 0 deletions client/Packages/com.beamable/Samples/QASmokeKit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# QA Smoke Kit

A fast smoke-test harness bundled with the Beamable Unity SDK. It exercises the most
load-bearing SDK surfaces — `BeamContext` readiness, `Accounts.OnReady` resilience, the
`BeamContext` retry buffer, and the `MustReferenceContent` content picker — so a tester can
confirm a release behaves correctly in a few minutes.

Because the kit ships inside `com.beamable`, it tracks the code it tests: when the SDK
changes, the harness imported alongside it stays in sync.

> Always run against a disposable QA realm with no real player or content data. Several of
> the recipes below plant bad tokens and force failures.

## How to read results

Every line the harness emits is prefixed with `[SMOKE]`, and each check logs either
`[SMOKE] PASS ...` or `[SMOKE] FAIL ...`. Read the verdicts straight from the Unity Editor
log rather than relaying the Console by hand:

```
grep "\[SMOKE\]" <Unity Editor log>
```

The Editor log lives at the platform-standard path (for example
`~/Library/Logs/Unity/Editor.log` on macOS).

## The scripts

A play-mode component does nothing until it lives on an active GameObject in the loaded
scene. The included `QASmokeKit.unity` scene is pre-wired with a single GameObject named
**QA Smoke Runner** that already has `SmokeRunner` attached — open it and press Play.

### `SmokeRunner.cs`

The happy-path readiness harness. Attach it to a GameObject and press Play (or just open the
included scene). It awaits `BeamContext.Default.OnReady`, logs the resolved
`cid` / `pid` / `playerId`, then awaits `Accounts.OnReady`. Touches only the most stable SDK
surface, so it compiles against the current release without edits. Extend the marked section
with stat / inventory / microservice assertions as needed.

### `SmokeRetryWatcher.cs`

A passive log watcher for fault-injection recipe B. While attached, it listens for
exceptions or errors during `BeamContext` init and emits `[SMOKE] FAIL` if it sees an
`IndexOutOfRangeException` or an overflow.

### `SmokeReferenceContent.cs`

Defines a `smoke_ref` content type that gives the `MustReferenceContent` picker an obvious,
top-level test surface. See the content-picker test below.

## Fault-injection recipe A — `Accounts.OnReady` stale-token resilience

`Accounts.OnReady` must resolve even when a stale or invalid **remembered** device token is
present, while still strictly rejecting an invalid **active** token. The relevant code is in
`Runtime/Player/PlayerAccounts.cs`. A happy-path pass will not exercise this — you must
plant a bad remembered token.

1. Sign in normally so the SDK persists a remembered token. It is stored in `PlayerPrefs`
under a key of the form `{prefix}{cid}.{pid}.access_token` (and a matching `...refresh`
key).
2. Plant a bad remembered token: overwrite that `PlayerPrefs` entry with garbage, or sign in
a second account and then invalidate its server-side refresh token.
3. Re-init: restart the Editor, or re-enter Play with `SmokeRunner` attached.
4. **PASS** = `[SMOKE] PASS Accounts.OnReady resolved` appears while a genuinely bad
**active** token is still rejected. **FAIL** = `OnReady` hangs or throws.

## Fault-injection recipe B — `BeamContext` retry-buffer overflow

Pre-fix, `BeamContext.Try()` wrote `errors[attempt]` into a buffer sized to
`CoreConfiguration.ContextRetryDelays.Length`. With `EnableInfiniteContextRetries = true`,
once `attempt` passed the array length the buffer overflowed into an
`IndexOutOfRangeException`. The fix clamps the index to the last slot. The relevant code is
in `Runtime/BeamContext.cs`.

1. In the Core Configuration asset (Beamable > Core Configuration, or the SDK settings
window): set `EnableInfiniteContextRetries = true` and shorten `ContextRetryDelays` to
e.g. `[1, 1]` so the array is exhausted in seconds.
2. Induce failure: turn off networking, or point config defaults at an unreachable host, so
init keeps failing.
3. Attach `SmokeRetryWatcher` to a GameObject, press Play, and let it retry well past the
array length (a dozen or more attempts).
4. **PASS** = retries keep cycling, the error buffer stays bounded, and no `[SMOKE] FAIL`
line appears. **FAIL** = an `IndexOutOfRangeException` surfaces during init.

## Content-picker test — `MustReferenceContent`

Once `SmokeReferenceContent.cs` compiles, the Content Manager offers a new `smoke_ref`
content type. Create one and inspect its two fields:

- **`currency`** (`CurrencyRef`, `[MustReferenceContent]`) — the Editor shows a content
picker. Confirm the dropdown filters to `CurrencyContent` (e.g. the default
`currency.gems` / `currency.coins`). Test a valid pick, a cleared pick, and confirm
wrong-type content is not offered.
- **`currencyId`** (`string`, `[MustReferenceContent(true, typeof(CurrencyContent))]`) — type
a bogus id such as `currency.does_not_exist`. The invalid value must be **flagged** by
validation and **not** silently auto-rewritten or cleared. Confirm the typed text stays
put.

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
@@ -0,0 +1,35 @@
using Beamable.Common.Content;
using Beamable.Common.Content.Validation;
using Beamable.Common.Inventory;
using UnityEngine;

// Staged by the agent to give the 5.1.1 MustReferenceContent picker an obvious,
// top-level test surface (the built-in CurrencyContent only carries the attribute on a
// field buried inside a nested CurrencyReward list).
//
// After this compiles, the Content Manager should offer a new "smoke_ref" content type.
// Create one and inspect it:
//
// currency (CurrencyRef) -> [MustReferenceContent] picker. Expect a dropdown that
// filters to CurrencyContent (currency.gems / currency.coins).
// Test: valid pick, clear the pick, confirm wrong types are
// not offered.
//
// currencyId (string) -> [MustReferenceContent] on a string field. Type a bogus id
// (e.g. "currency.does_not_exist"). 5.1.1 change: the invalid
// value must be FLAGGED by validation and NOT silently
// auto-rewritten/cleared. Confirm your typed text stays put.
namespace BeamQA
{
[ContentType("smoke_ref")]
public class SmokeReferenceContent : ContentObject
{
[Tooltip("Picker should filter to CurrencyContent.")]
[MustReferenceContent]
public CurrencyRef currency;

[Tooltip("Invalid ids should be flagged, not auto-rewritten.")]
[MustReferenceContent(true, typeof(CurrencyContent))]
public string currencyId;
}
}

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
@@ -0,0 +1,38 @@
using System;
using UnityEngine;

// Fault-injection watcher for the 5.1.1 BeamContext retry-buffer fix.
//
// Pre-fix, BeamContext.Try() wrote errors[attempt] into a buffer sized to
// CoreConfiguration.ContextRetryDelays.Length. With EnableInfiniteContextRetries = true,
// once `attempt` passed the array length it overflowed -> IndexOutOfRangeException.
// The fix clamps the index to the last slot.
//
// HUMAN setup (Beamable > Core Configuration asset, or the SDK settings window):
// 1. EnableInfiniteContextRetries = true
// 2. Shorten ContextRetryDelays to e.g. [1, 1] so the array is exhausted in seconds.
// 3. Induce failure: turn off networking, or point config-defaults.txt at an
// unreachable host, so InitProcedure() keeps failing.
// 4. Attach this script to a GameObject and press Play. Let it retry well past the
// array length (a dozen+ attempts).
//
// PASS -> no [SMOKE] FAIL line below; retries keep cycling, error buffer stays bounded.
// FAIL -> an IndexOutOfRangeException (or "overflow") surfaces during init.
public class SmokeRetryWatcher : MonoBehaviour
{
void OnEnable() => Application.logMessageReceived += OnLog;
void OnDisable() => Application.logMessageReceived -= OnLog;

void Start() => Debug.Log("[SMOKE] SmokeRetryWatcher armed: watching init retries for buffer overflow");

void OnLog(string condition, string stackTrace, LogType type)
{
if (type != LogType.Exception && type != LogType.Error) return;
var text = condition + "\n" + stackTrace;
if (text.IndexOf("IndexOutOfRange", StringComparison.OrdinalIgnoreCase) >= 0 ||
text.IndexOf("overflow", StringComparison.OrdinalIgnoreCase) >= 0)
{
Debug.LogError($"[SMOKE] FAIL retry-buffer overflow during BeamContext init: {condition}");
}
}
}
Loading
Loading