feat: harden isolated Tornado test workflow

This commit is contained in:
2026-07-10 11:27:40 +09:00
parent 5a8c7028dc
commit fc932b27f6
36 changed files with 3919 additions and 226 deletions

View File

@@ -21,14 +21,27 @@ public sealed class DynamicK3dSessionTests
new FakeScene(log),
connectResult);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
var exception = await Assert.ThrowsAnyAsync<Exception>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator);
session.Connect(options);
using var session = new DynamicK3dSession(activator, releaser);
try
{
session.Connect(options);
}
catch
{
Assert.False(session.IsConnected);
throw;
}
return true;
},
TimeSpan.FromSeconds(5),
@@ -38,6 +51,114 @@ public sealed class DynamicK3dSessionTests
Assert.DoesNotContain(scenes.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(connectResult.ToString(), exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain("GetScenePlayer", log.Names);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"Disconnect",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Disconnect"));
Assert.Single(log.ThreadIds.Distinct());
Assert.All(log.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
}
[Fact]
public async Task Connect_WhenPlayerInitializationFails_DisconnectsOnceAndPreservesOriginalFailure()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var originalFailure = new InvalidOperationException("fake original player failure");
var engine = new FakeEngine(
log,
new FakePlayer(log),
new FakeScene(log),
playerFailure: originalFailure,
disconnectFailure: new InvalidOperationException("fake cleanup failure"));
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None));
Assert.Same(originalFailure, exception);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"GetScenePlayerOnChannel:9",
"Disconnect",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Disconnect"));
Assert.Single(log.ThreadIds.Distinct());
Assert.All(log.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
}
[Fact]
public async Task Connect_WhenKtapInvocationThrows_AttemptsDisconnectAndPreservesOriginalFailure()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var originalFailure = new InvalidOperationException("fake original KTAP failure");
var engine = new FakeEngine(
log,
new FakePlayer(log),
new FakeScene(log),
connectFailure: originalFailure);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None));
Assert.Same(originalFailure, exception);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"Disconnect",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Disconnect"));
}
[Theory]
@@ -115,6 +236,40 @@ public sealed class DynamicK3dSessionTests
Assert.Equal(typeof(object), create.ReturnType);
}
[Fact]
public async Task Abandon_ReleasesEveryComReferenceWithoutDisconnect()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene = new FakeScene(log);
var engine = new FakeEngine(log, player, scene);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Abandon();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.DoesNotContain("Disconnect", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Release:Player"));
Assert.Equal(1, log.Names.Count(name => name == "Release:Engine"));
Assert.Equal(1, log.Names.Count(name => name == "Release:EventHandler"));
}
private static PlayoutOptions TestOptions(string sceneDirectory) => new()
{
Mode = PlayoutMode.Test,
@@ -156,6 +311,8 @@ public sealed class DynamicK3dSessionTests
_engine = engine;
}
public object EventHandler => _eventHandler;
public object Create(Guid classId)
{
_log.Add($"Create:{classId:B}");
@@ -174,17 +331,26 @@ public sealed class DynamicK3dSessionTests
private readonly FakePlayer _player;
private readonly FakeScene _scene;
private readonly int _connectResult;
private readonly Exception? _playerFailure;
private readonly Exception? _disconnectFailure;
private readonly Exception? _connectFailure;
public FakeEngine(
FakeComLog log,
FakePlayer player,
FakeScene scene,
int connectResult = 1)
int connectResult = 1,
Exception? playerFailure = null,
Exception? disconnectFailure = null,
Exception? connectFailure = null)
{
_log = log;
_player = player;
_scene = scene;
_connectResult = connectResult;
_playerFailure = playerFailure;
_disconnectFailure = disconnectFailure;
_connectFailure = connectFailure;
}
public int KTAPConnect(
@@ -196,12 +362,22 @@ public sealed class DynamicK3dSessionTests
{
Assert.NotNull(eventHandler);
_log.Add($"KTAPConnect:{tcpMode}:{host}:{hostPort}:{clientPort}");
if (_connectFailure is not null)
{
throw _connectFailure;
}
return _connectResult;
}
public object GetScenePlayerOnChannel(int channel)
{
_log.Add($"GetScenePlayerOnChannel:{channel}");
if (_playerFailure is not null)
{
throw _playerFailure;
}
return _player;
}
@@ -216,7 +392,34 @@ public sealed class DynamicK3dSessionTests
public void EndTransaction() => _log.Add("EndTransaction");
public void Disconnect() => _log.Add("Disconnect");
public void Disconnect()
{
_log.Add("Disconnect");
if (_disconnectFailure is not null)
{
throw _disconnectFailure;
}
}
}
private sealed class FakeReleaser : ILateBoundComObjectReleaser
{
private readonly FakeComLog _log;
private readonly (object Value, string Name)[] _objects;
public FakeReleaser(
FakeComLog log,
params (object Value, string Name)[] objects)
{
_log = log;
_objects = objects;
}
public void Release(object value)
{
var match = Assert.Single(_objects, item => ReferenceEquals(item.Value, value));
_log.Add($"Release:{match.Name}");
}
}
public sealed class FakePlayer