test(worker): write-completion correlation executor coverage
This commit is contained in:
@@ -897,6 +897,9 @@ public sealed class MxAccessCommandExecutorTests
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||
// No completion source in this test — disable the bounded reply wait
|
||||
// so the forwarding assertions don't pay the default 1.5 s timeout.
|
||||
session.WriteCompletionTimeout = TimeSpan.Zero;
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
||||
@@ -919,6 +922,8 @@ public sealed class MxAccessCommandExecutorTests
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||
// Same rationale as the WriteSecured forwarding test above.
|
||||
session.WriteCompletionTimeout = TimeSpan.Zero;
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
DateTime timestamp = new(2026, 5, 19, 13, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
@@ -934,6 +939,181 @@ public sealed class MxAccessCommandExecutorTests
|
||||
Assert.Equal(44, fakeComObject.WriteVerifierUserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the fast-completion ordering edge: a completion recorded while
|
||||
/// the WriteSecured COM call is still on the stack (MXAccess committing
|
||||
/// synchronously) is newer than the pre-call baseline and lands on the
|
||||
/// reply — the wait never misses a callback that beat it.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DispatchAsync_WriteSecured_WhenCompletionArrivesDuringComCall_ReturnsStatuses()
|
||||
{
|
||||
FakeMxAccessComObject fakeComObject = new(registerHandle: 82);
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
CompletionCacheEventSink sink = new();
|
||||
fakeComObject.OnWriteSecuredCallback = () =>
|
||||
sink.WriteCompletionCache.Record(82, 820, CreateCompletionRows(detail: 4321));
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, sink);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
||||
"write-secured-fast", serverHandle: 82, itemHandle: 820, value: 1, currentUserId: 11, verifierUserId: 22));
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.True(reply.HasHresult);
|
||||
Assert.Equal(0, reply.Hresult);
|
||||
MxStatusProxy row = Assert.Single(reply.Statuses);
|
||||
Assert.Equal(4321, row.Detail);
|
||||
Assert.Equal(MxStatusCategory.Ok, row.Category);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the pump-wait path: the completion arrives after the COM call
|
||||
/// returned, while the executor is pump-waiting, and still lands on the
|
||||
/// reply.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DispatchAsync_WriteSecured_WhenCompletionArrivesWhileWaiting_ReturnsStatuses()
|
||||
{
|
||||
FakeMxAccessComObject fakeComObject = new(registerHandle: 83);
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
CompletionCacheEventSink sink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, sink);
|
||||
session.WriteCompletionTimeout = TimeSpan.FromSeconds(10);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
Task<MxCommandReply> pending = session.DispatchAsync(CreateWriteSecuredCommand(
|
||||
"write-secured-waiting", serverHandle: 83, itemHandle: 830, value: 1, currentUserId: 11, verifierUserId: 22));
|
||||
await Task.Delay(50);
|
||||
sink.WriteCompletionCache.Record(83, 830, CreateCompletionRows(detail: 99));
|
||||
|
||||
MxCommandReply reply = await pending;
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.Equal(99, Assert.Single(reply.Statuses).Detail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the timeout fallback: no completion within the bounded wait
|
||||
/// returns today's reply shape — protocol OK with EMPTY statuses (the
|
||||
/// consumer's honest-unconfirmed path), never a synthesized failure row.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DispatchAsync_WriteSecured_WhenNoCompletion_TimesOutWithEmptyStatusesAndOkProtocol()
|
||||
{
|
||||
FakeMxAccessComObject fakeComObject = new(registerHandle: 84);
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
CompletionCacheEventSink sink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, sink);
|
||||
session.WriteCompletionTimeout = TimeSpan.FromMilliseconds(100);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
||||
"write-secured-timeout", serverHandle: 84, itemHandle: 840, value: 1, currentUserId: 11, verifierUserId: 22));
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.True(reply.HasHresult);
|
||||
Assert.Equal(0, reply.Hresult);
|
||||
Assert.Empty(reply.Statuses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the version-baseline rule end to end: a completion recorded
|
||||
/// BEFORE the write was dispatched is stale and must not be misattributed
|
||||
/// to this write — the reply times out empty instead.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DispatchAsync_WriteSecured_IgnoresStaleCompletionFromBeforeTheCall()
|
||||
{
|
||||
FakeMxAccessComObject fakeComObject = new(registerHandle: 85);
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
CompletionCacheEventSink sink = new();
|
||||
sink.WriteCompletionCache.Record(85, 850, CreateCompletionRows(detail: 1111));
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, sink);
|
||||
session.WriteCompletionTimeout = TimeSpan.FromMilliseconds(100);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
||||
"write-secured-stale", serverHandle: 85, itemHandle: 850, value: 1, currentUserId: 11, verifierUserId: 22));
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.Empty(reply.Statuses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WriteSecured2 correlates the same way as WriteSecured
|
||||
/// (fast-completion edge).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DispatchAsync_WriteSecured2_WhenCompletionArrivesDuringComCall_ReturnsStatuses()
|
||||
{
|
||||
FakeMxAccessComObject fakeComObject = new(registerHandle: 86);
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
CompletionCacheEventSink sink = new();
|
||||
fakeComObject.OnWriteSecuredCallback = () =>
|
||||
sink.WriteCompletionCache.Record(86, 860, CreateCompletionRows(detail: 2222));
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, sink);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2Command(
|
||||
"write-secured2-fast", serverHandle: 86, itemHandle: 860, value: 1,
|
||||
timestamp: new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc), currentUserId: 33, verifierUserId: 44));
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.Equal(2222, Assert.Single(reply.Statuses).Detail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies plain Write stays fire-and-forget: even with a huge completion
|
||||
/// timeout configured and no completion source, the reply returns
|
||||
/// immediately (guarded well under the configured wait) with empty
|
||||
/// statuses — only the secured write kinds enter the bounded wait.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DispatchAsync_Write_DoesNotWaitForCompletion()
|
||||
{
|
||||
FakeMxAccessComObject fakeComObject = new(registerHandle: 87);
|
||||
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||
CompletionCacheEventSink sink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, sink);
|
||||
session.WriteCompletionTimeout = TimeSpan.FromSeconds(30);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
Task<MxCommandReply> pending = session.DispatchAsync(CreateWriteCommand(
|
||||
"plain-write-no-wait", serverHandle: 87, itemHandle: 870, value: 1, userId: 5));
|
||||
Task completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(5)));
|
||||
|
||||
Assert.Same(pending, completed);
|
||||
MxCommandReply reply = await pending;
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.Empty(reply.Statuses);
|
||||
}
|
||||
|
||||
private static Google.Protobuf.Collections.RepeatedField<MxStatusProxy> CreateCompletionRows(int detail)
|
||||
{
|
||||
return new Google.Protobuf.Collections.RepeatedField<MxStatusProxy>
|
||||
{
|
||||
new MxStatusProxy
|
||||
{
|
||||
Success = 1,
|
||||
Category = MxStatusCategory.Ok,
|
||||
Detail = detail,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Write without a payload returns an invalid request error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -1775,6 +1955,28 @@ public sealed class MxAccessCommandExecutorTests
|
||||
TimeSpan.FromMilliseconds(25));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test sink that owns a real write-completion cache without touching the
|
||||
/// MXAccess COM RCW (Attach is a no-op). Implements the provider seam so
|
||||
/// <see cref="MxAccessSession.Create"/> shares this cache with the write
|
||||
/// executor, letting tests record completions the executor's bounded
|
||||
/// wait then observes.
|
||||
/// </summary>
|
||||
private sealed class CompletionCacheEventSink : IMxAccessEventSink, IWriteCompletionCacheProvider
|
||||
{
|
||||
public MxAccessWriteCompletionCache WriteCompletionCache { get; } = new MxAccessWriteCompletionCache();
|
||||
|
||||
public void Attach(
|
||||
object mxAccessComObject,
|
||||
string sessionId)
|
||||
{
|
||||
}
|
||||
|
||||
public void Detach()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeMxAccessComObject : IMxAccessServer
|
||||
{
|
||||
private readonly int registerHandle;
|
||||
@@ -1790,6 +1992,14 @@ public sealed class MxAccessCommandExecutorTests
|
||||
private readonly IReadOnlyDictionary<int, Exception> writeExceptionByItemHandle;
|
||||
private readonly List<string> operationNames = new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked at the end of a successful WriteSecured/WriteSecured2 —
|
||||
/// stands in for MXAccess committing synchronously and delivering
|
||||
/// OnWriteComplete while the COM call is still on the stack, so
|
||||
/// tests can exercise the fast-completion ordering edge.
|
||||
/// </summary>
|
||||
public Action? OnWriteSecuredCallback { get; set; }
|
||||
|
||||
/// <summary>Initializes a fake MXAccess COM object with the given handles and optional exceptions.</summary>
|
||||
/// <param name="registerHandle">Return value for Register method.</param>
|
||||
/// <param name="addItemHandle">Return value for AddItem method.</param>
|
||||
@@ -2100,6 +2310,7 @@ public sealed class MxAccessCommandExecutorTests
|
||||
WriteValue = value;
|
||||
WriteThreadId = Environment.CurrentManagedThreadId;
|
||||
ThrowIfWriteFailureConfigured(itemHandle);
|
||||
OnWriteSecuredCallback?.Invoke();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -2120,6 +2331,7 @@ public sealed class MxAccessCommandExecutorTests
|
||||
WriteTimestamp = timestamp;
|
||||
WriteThreadId = Environment.CurrentManagedThreadId;
|
||||
ThrowIfWriteFailureConfigured(itemHandle);
|
||||
OnWriteSecuredCallback?.Invoke();
|
||||
}
|
||||
|
||||
private void ThrowIfWriteFailureConfigured(int itemHandle)
|
||||
|
||||
Reference in New Issue
Block a user