Files
SVSimServer/SVSim.BattleEngine.Tests/BattleAmbientTests.cs
gamer147 c789d836f1 feat(engine-ambient): delete static fallbacks; add MultiInstanceEngineTests
Step 8 (final) of multi-instancing migration. All per-battle statics now
require a BattleAmbient scope — unwrapped writes throw InvalidOperationException
(fail-fast forcing function). MultiInstanceEngineTests proves correctness:
two parallel battles resolve independently, N=4/8/16 stress matches sequential
baseline, GameMgr.GetIns throws without scope.

Migration complete. EngineSessionGate gone. Suite fully green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 23:19:37 -04:00

247 lines
8.9 KiB
C#

#nullable enable
using SVSim.BattleEngine.Ambient;
using NUnit.Framework;
using System.Runtime.Serialization;
using System.Threading.Tasks;
namespace SVSim.BattleEngine.Tests;
[TestFixture, Parallelizable(ParallelScope.Self)]
public class BattleAmbientTests
{
[Test]
public void Current_IsNull_WhenNoScope()
{
Assert.That(BattleAmbient.Current, Is.Null);
}
[Test]
public void Require_Throws_WhenNoScope()
{
Assert.Throws<System.InvalidOperationException>(() => BattleAmbient.Require());
}
[Test]
public void Enter_SetsCurrent_RestoresOnDispose()
{
var ctx = new BattleAmbientContext { ViewerId = 42 };
Assert.That(BattleAmbient.Current, Is.Null);
using (var _ = BattleAmbient.Enter(ctx))
{
Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
Assert.That(BattleAmbient.Require().ViewerId, Is.EqualTo(42));
}
Assert.That(BattleAmbient.Current, Is.Null);
}
[Test]
public void Enter_Nested_RestoresPriorOnDispose()
{
var outer = new BattleAmbientContext { ViewerId = 1 };
var inner = new BattleAmbientContext { ViewerId = 2 };
using (var _o = BattleAmbient.Enter(outer))
{
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
using (var _i = BattleAmbient.Enter(inner))
{
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(2));
}
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
}
}
[Test]
public async Task Enter_FlowsAcrossAwait()
{
var ctx = new BattleAmbientContext { ViewerId = 99 };
using (var _ = BattleAmbient.Enter(ctx))
{
await Task.Yield();
Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
}
}
[Test]
public async Task Enter_IsolatedBetweenConcurrentTasks()
{
var ctxA = new BattleAmbientContext { ViewerId = 100 };
var ctxB = new BattleAmbientContext { ViewerId = 200 };
var taskA = Task.Run(async () => {
using var _ = BattleAmbient.Enter(ctxA);
await Task.Delay(20);
return BattleAmbient.Current!.ViewerId;
});
var taskB = Task.Run(async () => {
using var _ = BattleAmbient.Enter(ctxB);
await Task.Delay(20);
return BattleAmbient.Current!.ViewerId;
});
var results = await Task.WhenAll(taskA, taskB);
Assert.That(results[0], Is.EqualTo(100));
Assert.That(results[1], Is.EqualTo(200));
}
[Test]
public void IsForecast_ReadsAmbient_WhenScopeActive()
{
var ctx = new BattleAmbientContext { IsForecast = false };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(BattleManagerBase.IsForecast, Is.False);
ctx.IsForecast = true;
Assert.That(BattleManagerBase.IsForecast, Is.True);
}
[Test]
public void IsForecast_WriteInsideScope_WritesAmbient_NotFallback()
{
var ctx = new BattleAmbientContext { IsForecast = false };
using (var _ = BattleAmbient.Enter(ctx))
{
BattleManagerBase.IsForecast = true;
Assert.That(ctx.IsForecast, Is.True);
}
}
[Test]
public void IsForecast_OutsideScope_GetAndSetThrow()
{
// Post-Task-8: fallback is gone. Both get and set go through BattleAmbient.Require(),
// which throws when no scope is active. This is the forcing function — any unwrapped
// engine code that touches IsForecast fails fast instead of silently writing a static.
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsForecast; });
Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsForecast = true);
}
[Test]
public void IsRandomDraw_OutsideScope_GetAndSetThrow_InsideScope_Roundtrips()
{
// Post-Task-8: get/set both Require() a scope. Inside a scope, writes land on the ctx.
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsRandomDraw = true);
var ctx = new BattleAmbientContext { IsRandomDraw = false };
using (var _ = BattleAmbient.Enter(ctx))
{
Assert.That(BattleManagerBase.IsRandomDraw, Is.False);
BattleManagerBase.IsRandomDraw = true;
Assert.That(ctx.IsRandomDraw, Is.True);
}
// Scope disposed -> back to throwing on access.
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
}
[Test]
public void GetIns_ReadsAmbient_WhenScopeActive()
{
var fakeMgr = (BattleManagerBase)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(BattleManagerBase));
var ctx = new BattleAmbientContext { Mgr = fakeMgr };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(BattleManagerBase.GetIns(), Is.SameAs(fakeMgr));
}
[Test]
public void GetIns_OutsideScope_ReturnsNull()
{
// Post-Task-8: fallback is gone. GetIns() reads Current?.Mgr (soft, kept null-tolerant so
// engine call sites that pattern `GetIns()?.Foo ?? default` still compose). With no scope
// active, Current is null, so GetIns() returns null.
Assert.That(BattleAmbient.Current, Is.Null);
Assert.That(BattleManagerBase.GetIns(), Is.Null);
}
[Test]
public void ViewerId_ReadsAmbient_WhenScopeActive()
{
var ctx = new BattleAmbientContext { ViewerId = 12345 };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(Cute.Certification.ViewerId, Is.EqualTo(12345));
}
[Test]
public void RealTimeNetworkAgent_ReadsAmbient_WhenScopeActive()
{
var ctx = new BattleAmbientContext();
using var _ = BattleAmbient.Enter(ctx);
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Null);
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
ctx.NetworkAgent = agent;
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.SameAs(agent));
}
[Test]
public void SetRealTimeNetworkBattle_InsideScope_WritesAmbient()
{
var ctx = new BattleAmbientContext();
using var _ = BattleAmbient.Enter(ctx);
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
Assert.That(ctx.NetworkAgent, Is.SameAs(agent));
}
[Test]
public void BattleRecoveryInfo_ReadsAmbient_WhenScopeActive()
{
var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
var ctx = new BattleAmbientContext { RecoveryInfo = info };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(Wizard.Data.BattleRecoveryInfo, Is.SameAs(info));
}
[Test]
public void BattleRecoveryInfo_SetInsideScope_WritesAmbient()
{
var ctx = new BattleAmbientContext();
using var _ = BattleAmbient.Enter(ctx);
var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
Wizard.Data.BattleRecoveryInfo = info;
Assert.That(ctx.RecoveryInfo, Is.SameAs(info));
}
[Test]
public void GameMgr_GetIns_InsideScope_ReturnsScopeInstance()
{
var mgr = new GameMgr();
var ctx = new BattleAmbientContext { GameMgr = mgr };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(GameMgr.GetIns(), Is.SameAs(mgr));
}
[Test]
public void GameMgr_GetIns_OutsideScope_Throws()
{
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => GameMgr.GetIns());
}
[Test]
public async Task GameMgr_GetIns_IsolatedBetweenConcurrentTasks()
{
var mgrA = new GameMgr();
var mgrB = new GameMgr();
var taskA = Task.Run(async () => {
using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrA });
await Task.Delay(20);
return GameMgr.GetIns();
});
var taskB = Task.Run(async () => {
using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrB });
await Task.Delay(20);
return GameMgr.GetIns();
});
var results = await Task.WhenAll(taskA, taskB);
Assert.That(results[0], Is.SameAs(mgrA));
Assert.That(results[1], Is.SameAs(mgrB));
}
}