stride3d/stride · error · InvalidOperationException

Stride.Games.AutoTesting

Error message

Stride.Games.AutoTesting: '{RegisteredTest.GetType().FullName}' is already registered; exactly one [ScreenshotTest] per sample is allowed (tried to register '{test.GetType().FullName}').

What it means

ScreenshotTestRunner.RegisterTest enforces that exactly one IScreenshotTest instance type is registered per process. If a test with a different type is registered while one is already held, it throws InvalidOperationException naming both types.

Solutions

  1. Remove [ScreenshotTest] from all but one test class in the project
  2. Delete the redundant/obsolete test class
  3. If both tests are needed, split them into separate sample projects/executables

Example fix

// before
[ScreenshotTest]
public class ScreenATest : IScreenshotTest { ... }
[ScreenshotTest]
public class ScreenBTest : IScreenshotTest { ... }
// after
[ScreenshotTest]
public class ScreenATest : IScreenshotTest { ... }
public class ScreenBTest : IScreenshotTest { ... } // attribute removed
Defensive patterns

Strategy: validation

Validate before calling

if (ScreenshotTestRunner.RegisteredTest is not null && ScreenshotTestRunner.RegisteredTest.GetType() != test.GetType())
    throw new InvalidOperationException("Multiple [ScreenshotTest] classes in this project");
ScreenshotTestRunner.RegisterTest(test);

Type guard

bool IsFirstOrSame(IScreenshotTest t) => ScreenshotTestRunner.RegisteredTest is null || ScreenshotTestRunner.RegisteredTest.GetType() == t.GetType();

Try / catch

try { ScreenshotTestRunner.RegisterTest(test); } catch (InvalidOperationException ex) { /* log duplicate test type and skip */ }

Prevention

When it happens

Trigger: An app assembly containing two or more classes marked [ScreenshotTest], each triggering static registration on startup.

Common situations: Adding a second sample screen with [ScreenshotTest] to an existing sample project; copy-pasting a test class without removing the old one; merging branches that each added a screenshot test.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/aef8fe4ecb1abfff. Report an issue: GitHub.

Appendix: source

Thrown at sources/engine/Stride.Games.AutoTesting/ScreenshotTestRunner.cs:43

/// before exiting the game.
/// </summary>
public static class AutoTestingBootstrap
{
    /// <summary>The test registered for this run, or null if none (non-test build).</summary>
    internal static IScreenshotTest? RegisteredTest { get; private set; }

    /// <summary>
    /// Registers the screenshot-test driver for this sample. Called from a [ModuleInitializer] in
    /// the test fixture: invoking a method here force-loads this assembly (so the
    /// <see cref="ScreenshotTestRunner"/>'s [ModuleInitializer] runs and hooks
    /// <see cref="Game.GameStarted"/>), and hands over the concrete instance directly — no reflection,
    /// keeping the discovery path trim/AOT-clean.
    /// </summary>
    public static void RegisterTest(IScreenshotTest test)
    {
        ArgumentNullException.ThrowIfNull(test);
        if (RegisteredTest is not null && RegisteredTest.GetType() != test.GetType())
            throw new InvalidOperationException(
                $"Stride.Games.AutoTesting: '{RegisteredTest.GetType().FullName}' is already registered; " +
                $"exactly one [ScreenshotTest] per sample is allowed (tried to register '{test.GetType().FullName}').");
        RegisteredTest = test;
    }
}

/// <summary>
/// Installs native-crash diagnostics for the sample run from a [ModuleInitializer].
/// See <see cref="NativeCrashHandler"/> (shared with Stride.Graphics.Regression).
/// </summary>
internal static class CrashDiagnostics
{
    [ModuleInitializer]
    internal static void Initialize()
    {
        NativeCrashHandler.Install();

        // Crash-report popups must never interrupt a test run: unless the caller chose a mode, tools

View on GitHub (pinned to 96fad776d2)