| name | polling-patterns |
| description | Bounded polling and waiting patterns for EGM state changes. Use when writing async poll loops, waiting for EGM conditions, handling stuck/blocked EGM states, dismissing intermediate screens, or implementing timeout logic with CancellationToken. Covers: bounded poll pattern, linked CancellationTokenSource, unblock pattern with StartGame button, diagnostic screenshots. stages: test-implementation, test-validation |
Polling Patterns for EGM State Changes
When to Use
- Writing any
while loop that waits for an EGM condition
- Implementing timeout logic for state transitions
- Handling stuck or blocked EGM screens
- Creating helper methods that poll StatusDatabase or UTP for state changes
Async Conventions
- All I/O operations use
async/await
- Methods accept
CancellationToken parameter (typically ct = default)
- Async methods: suffix with
Async
CRITICAL: Never Use Unbounded Loops
NEVER write an unbounded while (true) or while (!condition) loop to wait for EGM state changes. An unbounded loop will hang indefinitely if the condition is never met and the only recovery is killing the process.
Canonical Bounded-Poll Pattern
The canonical pattern (from Tests/Features/Attract/AttractTests.cs → WaitForAttractState()) is:
Step-by-Step
- Declare two
private const int fields — a timeout (e.g., XxxTimeoutMs = 5000) and a poll interval (e.g., XxxPollIntervalMs = 200).
- Create a linked token source in the helper:
using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); cts.CancelAfter(timeoutMs);
- Loop on
!cts.Token.IsCancellationRequested, check condition, then await Task.Delay(pollIntervalMs, cts.Token).
- Catch
OperationCanceledException and return false.
- The helper returns
Task<bool>; the test asserts Assert.That(reached, Is.True, "descriptive timeout message").
Code Template
private const int XxxTimeoutMs = 5000;
private const int XxxPollIntervalMs = 200;
private async Task<bool> WaitForXxxAsync()
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
cts.CancelAfter(XxxTimeoutMs);
try
{
while (!cts.Token.IsCancellationRequested)
{
var conditionMet = await CheckConditionAsync();
if (conditionMet)
{
return true;
}
await Task.Delay(XxxPollIntervalMs, cts.Token);
}
}
catch (OperationCanceledException)
{
var lastValue = await CheckConditionAsync();
await TestContext.Out.WriteLineAsync(
$"[Timeout] WaitForXxx timed out after {XxxTimeoutMs}ms. Last value: {lastValue}");
return false;
}
return false;
}
Caller Pattern
var reached = await WaitForXxxAsync();
Assert.That(reached, Is.True, "Descriptive timeout message explaining what was expected");
This pattern enables partial diagnostic output: log the last observed value before returning false, so failures show what was actually seen.
Two-Phase Transition Pattern (State Toggle)
Some EGM operations are two-phase transitions: a flag goes false → true (initiated) then true → false (completed). The most critical example is a game cycle (GameCycleActive), but this applies to any toggle-style state.
CRITICAL: Both Phases Must Be Polled
A single while (flag) loop only covers Phase 2 (wait for completion). If Phase 1 (initiation) is skipped, the loop exits immediately when the flag was never set — silently producing a false positive.
Pattern
Phase 1: Press button → poll until flag becomes TRUE (with retry)
Phase 2: Poll until flag becomes FALSE (servicing side-effects)
Code Template
private const int InitiationPollIntervalMs = 200;
private const int InitiationTimeoutMs = 5_000;
private const int InitiationMaxRetries = 3;
private const int CompletionPollIntervalMs = 500;
private const int CompletionTimeoutMs = 120_000;
private async Task PressAndVerifyInitiatedAsync(
Func<Task> pressAction,
Func<Task<bool>> checkInitiated,
string actionName)
{
for (var attempt = 1; attempt <= InitiationMaxRetries; attempt++)
{
await pressAction();
var initiated = await WaitForConditionAsync(
checkInitiated,
timeoutMs: InitiationTimeoutMs,
pollIntervalMs: InitiationPollIntervalMs);
if (initiated)
{
return;
}
await TestContext.Out.WriteLineAsync(
$"[Retry] {actionName} attempt {attempt}: "
+ "flag did not become true. Re-pressing...");
}
Assert.Fail(
$"{actionName} did not initiate after "
+ $"{InitiationMaxRetries} attempts.");
}
private async Task WaitForCompletionAsync(
Func<Task<bool>> checkStillActive,
Func<Task>? onEachIteration = null)
{
using var cts = CancellationTokenSource
.CreateLinkedTokenSource(CancellationToken);
cts.CancelAfter(CompletionTimeoutMs);
try
{
while (await checkStillActive())
{
if (onEachIteration is not null)
{
await onEachIteration();
}
await Task.Delay(CompletionPollIntervalMs, cts.Token);
}
}
catch (OperationCanceledException)
{
Assert.Fail(
$"Completion did not occur within {CompletionTimeoutMs}ms");
}
}
Key Rules
- Never skip Phase 1. A
while (active) loop alone is not sufficient — if the action never initiated, the loop exits on the first iteration.
- Phase 1 uses the same press → poll → re-press retry pattern as bet/denom selection.
- Phase 2 may service side-effects on each iteration (e.g.,
HandleAllActiveFeaturesAsync during a game cycle).
- Both phases use bounded timeouts via linked
CancellationTokenSource.
Handling Stuck / Blocked EGM State
Some game transitions require the player to press a button (typically Repeat Bet / Start Game) to dismiss an intermediate screen (e.g., a "You have won X free games!" banner or a feature chooser) before the game advances.
Unblock Pattern
- Take a diagnostic screenshot via
ScreenshotFacade immediately before asserting timeout.
- Press
ButtonNames.GameplayButtons.StartGame via ButtonsFacade to advance past the blocked screen.
- Resume polling for the original condition.
Code Template (insert after the first failed poll pass)
var diagnosticScreenshots = await screenshotFacade.GetScreenshotsAsync();
TestContext.Out.WriteLine($"[Diagnostics] EGM screenshot count: {diagnosticScreenshots.Length}. Attempting to advance with StartGame button.");
await buttonsFacade.EmulateButtonPressByNameAsync(ButtonNames.GameplayButtons.StartGame);
await Task.Delay(500, CancellationToken);
Key Rules
- Only attempt the button-press unblock after at least one full polling window has passed without the condition being met. Do not press blindly on the first iteration.
- After pressing the button, reset the timeout so the polling window is measured from the press, not from the original start.
- Log a
TestContext.Out.WriteLine message every time the unblock button is pressed so test output records the recovery attempt.
ButtonNames.GameplayButtons.StartGame ("StartGame") is the correct constant for the Repeat Bet / Start Game / Continue button. Do not hard-code the string "StartGame" directly — always use the constant.