244 lines
10 KiB
C#
244 lines
10 KiB
C#
using NUnit.Framework;
|
|
using Unity.Pipeline.Config;
|
|
using Unity.Pipeline.Models;
|
|
using Unity.Pipeline.Runtime.Telemetry;
|
|
using Unity.Pipeline.Tests.Editor.ServerLifecyle;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Pipeline.Tests.Editor
|
|
{
|
|
/// <summary>
|
|
/// Tests for RuntimePipelineDriver, created automatically by RuntimePipelineBootstrap instead
|
|
/// of being authored in a scene.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Excluded from the default (dogfood) run: these tests start a real RuntimePipelineDriver /
|
|
/// runtime server and mutate global command discovery, which destabilizes the live editor
|
|
/// server agents drive over HTTP. Run them deliberately from the Test Runner window.
|
|
/// </remarks>
|
|
[Explicit("Starts a real RuntimePipelineDriver/server and mutates global state; would destabilize the live server. Run manually from the Test Runner window.")]
|
|
[Category("ServerLifecycle")]
|
|
class RuntimePipelineDriverTests
|
|
{
|
|
private GameObject m_TestGameObject;
|
|
private RuntimePipelineDriver m_Driver;
|
|
private RuntimePipelineConfig m_Config;
|
|
private bool m_OriginalRunInBackground;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
GlobalServerStateGuard.Capture();
|
|
|
|
m_OriginalRunInBackground = Application.runInBackground;
|
|
|
|
m_Config = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
|
|
|
m_TestGameObject = new GameObject("TestRuntimePipelineDriver");
|
|
m_Driver = m_TestGameObject.AddComponent<RuntimePipelineDriver>();
|
|
m_Driver.Initialize(m_Config, null);
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
if (m_Driver != null && m_Driver.IsServerRunning)
|
|
m_Driver.StopServer();
|
|
|
|
if (m_TestGameObject != null)
|
|
Object.DestroyImmediate(m_TestGameObject);
|
|
|
|
if (m_Config != null)
|
|
Object.DestroyImmediate(m_Config);
|
|
|
|
Application.runInBackground = m_OriginalRunInBackground;
|
|
|
|
GlobalServerStateGuard.Restore();
|
|
}
|
|
|
|
[Test]
|
|
public void ServerLifecycle_EnabledInBuilds_CanStartAndStop()
|
|
{
|
|
m_Config.enableInBuilds = true;
|
|
|
|
m_Driver.StartServer();
|
|
|
|
Assert.IsTrue(m_Driver.IsServerRunning, "Server should be running");
|
|
Assert.Greater(m_Driver.ActualPort, 0, "Should have assigned port");
|
|
Assert.NotNull(m_Driver.Server, "Server instance should exist");
|
|
|
|
m_Driver.StopServer();
|
|
|
|
Assert.IsFalse(m_Driver.IsServerRunning, "Server should be stopped");
|
|
}
|
|
|
|
[Test]
|
|
public void ServerLifecycle_StartStop_RestoresRunInBackground()
|
|
{
|
|
Application.runInBackground = false;
|
|
m_Config.enableInBuilds = true;
|
|
|
|
m_Driver.StartServer();
|
|
|
|
Assert.IsTrue(Application.runInBackground, "Starting the server should force runInBackground on");
|
|
|
|
m_Driver.StopServer();
|
|
|
|
Assert.IsFalse(Application.runInBackground, "Stopping the server should restore the previous runInBackground value");
|
|
}
|
|
|
|
[Test]
|
|
public void ServerLifecycle_StoppedExternallyBeforeStopServerCalled_StillRestoresRunInBackground()
|
|
{
|
|
Application.runInBackground = false;
|
|
m_Config.enableInBuilds = true;
|
|
|
|
m_Driver.StartServer();
|
|
Assert.IsTrue(Application.runInBackground, "Precondition: starting the server should force runInBackground on");
|
|
|
|
// Simulate the listener dying on its own (e.g. an unexpected native failure) rather
|
|
// than being stopped through the driver: stop the server directly, bypassing
|
|
// RuntimePipelineDriver.StopServer() entirely, so IsRunning goes false before the
|
|
// driver's own StopServer ever runs.
|
|
m_Driver.Server.Stop();
|
|
Assert.IsFalse(m_Driver.IsServerRunning, "Precondition: the server is no longer running");
|
|
|
|
m_Driver.StopServer();
|
|
|
|
Assert.IsFalse(Application.runInBackground,
|
|
"StopServer must still restore runInBackground even when the server had already stopped on its own");
|
|
}
|
|
|
|
[Test]
|
|
public void ServerStart_AlreadyOverriddenByCodeReloadPush_DoesNotClobberSavedValue()
|
|
{
|
|
// Simulates a code-reload push arriving over PlayerConnection (which needs no HTTP
|
|
// server) before StartServer ever ran, so DrainPendingReloads already forced
|
|
// runInBackground on and saved the real original value.
|
|
Application.runInBackground = false;
|
|
SetRunInBackgroundOverrideState(m_Driver, previousValue: false, overridden: true);
|
|
Application.runInBackground = true;
|
|
|
|
m_Config.enableInBuilds = true;
|
|
m_Driver.StartServer();
|
|
|
|
Assert.IsTrue(Application.runInBackground, "Precondition: still forced on while the server starts");
|
|
|
|
m_Driver.StopServer();
|
|
|
|
Assert.IsFalse(Application.runInBackground,
|
|
"StopServer must restore the value saved before the code-reload push overrode it, " +
|
|
"not the already-overridden 'true' StartServer would otherwise re-save on top of it");
|
|
}
|
|
|
|
[Test]
|
|
public void Configuration_ValidSettings_PassesValidation()
|
|
{
|
|
m_Config.enableInBuilds = true;
|
|
|
|
var result = m_Driver.ValidateConfiguration();
|
|
|
|
Assert.IsTrue(result.IsValid, $"Should pass validation: {result.Message}");
|
|
}
|
|
|
|
[Test]
|
|
public void Token_AutoGenerated_IsSecure()
|
|
{
|
|
var config = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
|
|
|
var descriptor = RuntimeInstanceDescriptor.CreateCurrent(7900, config);
|
|
|
|
Assert.IsNotEmpty(descriptor.EvalToken, "Token should not be empty");
|
|
Assert.GreaterOrEqual(descriptor.EvalToken.Length, 32, "Token should be at least 32 characters for security");
|
|
|
|
Object.DestroyImmediate(config);
|
|
}
|
|
|
|
[Test]
|
|
public void ServerStart_DisabledInBuilds_DoesNotStart()
|
|
{
|
|
m_Config.enableInBuilds = false;
|
|
|
|
m_Driver.StartServer();
|
|
|
|
Assert.IsFalse(m_Driver.IsServerRunning, "Server should not start when disabled");
|
|
}
|
|
|
|
[Test]
|
|
public void ServerStart_AlreadyStarted_DoesNotStartAgain()
|
|
{
|
|
m_Config.enableInBuilds = true;
|
|
m_Driver.StartServer();
|
|
|
|
var firstPort = m_Driver.ActualPort;
|
|
|
|
m_Driver.StartServer();
|
|
|
|
Assert.AreEqual(firstPort, m_Driver.ActualPort, "Port should not change when starting already started server");
|
|
}
|
|
|
|
[Test]
|
|
public void SecondDriver_DoesNotDisposeFirstDriversSampler()
|
|
{
|
|
// RuntimePipelineDriver has no [ExecuteAlways], so Unity never invokes Awake/OnDestroy
|
|
// automatically for it in Edit Mode (only in Play Mode or a real Player) — confirmed by
|
|
// every other test in this fixture driving StartServer/ValidateConfiguration directly
|
|
// instead of relying on Awake's side effects. Invoke them via reflection here to exercise
|
|
// the same sampler-ownership logic a running game would trigger on its own.
|
|
InvokeAwake(m_Driver); // m_Driver is from SetUp; this is its first Awake.
|
|
|
|
var sharedBeforeSecondDriver = FrameStatsSampler.Shared;
|
|
Assert.IsNotNull(sharedBeforeSecondDriver, "Precondition: the first driver should already own a sampler");
|
|
|
|
var secondConfig = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
|
var secondGo = new GameObject("SecondTestDriver");
|
|
var secondDriver = secondGo.AddComponent<RuntimePipelineDriver>();
|
|
secondDriver.Initialize(secondConfig, null);
|
|
InvokeAwake(secondDriver);
|
|
|
|
Assert.AreSame(sharedBeforeSecondDriver, FrameStatsSampler.Shared,
|
|
"A second driver's Awake must not replace an already-owned sampler");
|
|
|
|
InvokeOnDestroy(secondDriver);
|
|
Object.DestroyImmediate(secondGo);
|
|
|
|
Assert.AreSame(sharedBeforeSecondDriver, FrameStatsSampler.Shared,
|
|
"Destroying the second (non-owning) driver must not dispose the first driver's sampler");
|
|
|
|
Object.DestroyImmediate(secondConfig);
|
|
|
|
// m_Driver owns the sampler; TearDown's DestroyImmediate would not otherwise trigger its
|
|
// OnDestroy in Edit Mode, so do it explicitly to avoid leaking the sampler into other tests.
|
|
InvokeOnDestroy(m_Driver);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force a MonoBehaviour message that Unity would otherwise only dispatch automatically in
|
|
/// Play Mode (RuntimePipelineDriver has no [ExecuteAlways]).
|
|
/// </summary>
|
|
private static void InvokeMessage(RuntimePipelineDriver driver, string methodName)
|
|
{
|
|
var method = typeof(RuntimePipelineDriver).GetMethod(methodName,
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
method.Invoke(driver, null);
|
|
}
|
|
|
|
private static void InvokeAwake(RuntimePipelineDriver driver) => InvokeMessage(driver, "Awake");
|
|
|
|
private static void InvokeOnDestroy(RuntimePipelineDriver driver) => InvokeMessage(driver, "OnDestroy");
|
|
|
|
/// <summary>
|
|
/// Force the driver's runInBackground save/restore bookkeeping into a given state, to set up
|
|
/// scenarios (like a code-reload push racing StartServer) that would otherwise require decoding
|
|
/// a real PlayerConnection payload to reach through DrainPendingReloads.
|
|
/// </summary>
|
|
private static void SetRunInBackgroundOverrideState(RuntimePipelineDriver driver, bool previousValue, bool overridden)
|
|
{
|
|
const System.Reflection.BindingFlags flags =
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic;
|
|
typeof(RuntimePipelineDriver).GetField("m_PreviousRunInBackground", flags).SetValue(driver, previousValue);
|
|
typeof(RuntimePipelineDriver).GetField("m_RunInBackgroundOverridden", flags).SetValue(driver, overridden);
|
|
}
|
|
}
|
|
}
|