using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using MxGateway.Worker.Sta; namespace MxGateway.Worker.Tests.Sta; public sealed class StaRuntimeTests { [Fact] public async Task InvokeAsync_ExecutesCommandOnStaThread() { RecordingComApartmentInitializer initializer = new(); using StaRuntime runtime = CreateRuntime(initializer); runtime.Start(); StaCommandObservation observation = await runtime.InvokeAsync( () => new StaCommandObservation( Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.GetApartmentState())); Assert.Equal(runtime.StaThreadId, observation.ThreadId); Assert.Equal(initializer.InitializeThreadId, observation.ThreadId); Assert.Equal(ApartmentState.STA, observation.ApartmentState); } [Fact] public async Task InvokeAsync_WakesIdlePumpForQueuedCommand() { RecordingComApartmentInitializer initializer = new(); using StaRuntime runtime = new( initializer, new StaMessagePump(), TimeSpan.FromSeconds(30)); runtime.Start(); Stopwatch stopwatch = Stopwatch.StartNew(); int threadId = await runtime.InvokeAsync(() => Thread.CurrentThread.ManagedThreadId); stopwatch.Stop(); Assert.Equal(runtime.StaThreadId, threadId); Assert.True( stopwatch.Elapsed < TimeSpan.FromSeconds(2), $"Command took {stopwatch.Elapsed} to execute, so the command wake event did not wake the STA promptly."); } [Fact] public void Shutdown_StopsThreadAndUninitializesComApartment() { RecordingComApartmentInitializer initializer = new(); using StaRuntime runtime = CreateRuntime(initializer); runtime.Start(); bool stopped = runtime.Shutdown(TimeSpan.FromSeconds(2)); Assert.True(stopped); Assert.False(runtime.IsRunning); Assert.Equal(1, initializer.InitializeCount); Assert.Equal(1, initializer.UninitializeCount); Assert.Equal(initializer.InitializeThreadId, initializer.UninitializeThreadId); } [Fact] public void LastActivityUtc_UpdatesWhilePumpIsIdle() { RecordingComApartmentInitializer initializer = new(); using StaRuntime runtime = CreateRuntime(initializer); runtime.Start(); DateTimeOffset firstActivity = runtime.LastActivityUtc; bool updated = SpinWait.SpinUntil( () => runtime.LastActivityUtc > firstActivity, TimeSpan.FromSeconds(2)); Assert.True(updated); } [Fact] public async Task InvokeAsync_CommandException_FaultsReturnedTaskWithoutStoppingRuntime() { RecordingComApartmentInitializer initializer = new(); using StaRuntime runtime = CreateRuntime(initializer); runtime.Start(); InvalidOperationException exception = await Assert.ThrowsAsync( () => runtime.InvokeAsync(() => throw new InvalidOperationException("command failed"))); int threadId = await runtime.InvokeAsync(() => Thread.CurrentThread.ManagedThreadId); Assert.Equal("command failed", exception.Message); Assert.Equal(runtime.StaThreadId, threadId); } [Fact] public async Task InvokeAsync_AfterShutdown_ReturnsFaultedTask() { RecordingComApartmentInitializer initializer = new(); using StaRuntime runtime = CreateRuntime(initializer); runtime.Start(); runtime.Shutdown(TimeSpan.FromSeconds(2)); InvalidOperationException exception = await Assert.ThrowsAsync( () => runtime.InvokeAsync(() => Thread.CurrentThread.ManagedThreadId)); Assert.Contains("shutting down", exception.Message); } private static StaRuntime CreateRuntime(RecordingComApartmentInitializer initializer) { return new StaRuntime( initializer, new StaMessagePump(), TimeSpan.FromMilliseconds(25)); } private sealed class StaCommandObservation { public StaCommandObservation(int threadId, ApartmentState apartmentState) { ThreadId = threadId; ApartmentState = apartmentState; } public int ThreadId { get; } public ApartmentState ApartmentState { get; } } private sealed class RecordingComApartmentInitializer : IStaComApartmentInitializer { public int InitializeCount { get; private set; } public int UninitializeCount { get; private set; } public int? InitializeThreadId { get; private set; } public int? UninitializeThreadId { get; private set; } public void Initialize() { InitializeCount++; InitializeThreadId = Thread.CurrentThread.ManagedThreadId; } public void Uninitialize() { UninitializeCount++; UninitializeThreadId = Thread.CurrentThread.ManagedThreadId; } } }