docs(src): add missing XML docs and strip tracking-ID comments

Sweep of 203 source files resolving CommentChecker findings: add
<summary>/<param>/<returns>/<inheritdoc> where missing, and remove
resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN,
Task N) from code comments. Comment/doc-only — no logic changes.
Server+Tests build clean under TreatWarningsAsErrors.
This commit is contained in:
Joseph Doherty
2026-07-07 14:09:49 -04:00
parent 8914472706
commit fca978de07
203 changed files with 1834 additions and 1383 deletions
@@ -143,10 +143,7 @@ public sealed class WorkerApplicationTests
private sealed class SucceedingPipeClient : IWorkerPipeClient
{
/// <summary>Runs the worker pipe client successfully.</summary>
/// <param name="options">Worker options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Completed task.</returns>
/// <inheritdoc />
public Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
@@ -166,10 +163,7 @@ public sealed class WorkerApplicationTests
_exception = exception;
}
/// <summary>Runs the worker pipe client and throws configured exception.</summary>
/// <param name="options">Worker options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Never completes; always throws.</returns>
/// <inheritdoc />
public Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
@@ -132,7 +132,7 @@ public sealed class VariantConverterTests
}
/// <summary>
/// Worker-010 regression: a 32-bit <see cref="uint"/> with an expected
/// A 32-bit <see cref="uint"/> with an expected
/// data type of <see cref="MxDataType.Time"/> must not be projected as a
/// Windows FILETIME. A uint can only hold the low 32 bits of a FILETIME,
/// which would silently render as a near-1601 timestamp; the converter
@@ -14,6 +14,7 @@ public sealed class WorkerFrameProtocolTests
private const string Nonce = "nonce-secret";
/// <summary>Verifies that valid envelopes round-trip through write and read.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame()
{
@@ -32,6 +33,7 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>Verifies that wrong protocol version throws mismatch error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch()
{
@@ -49,6 +51,7 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>Verifies that wrong session ID throws mismatch error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch()
{
@@ -72,6 +75,7 @@ public sealed class WorkerFrameProtocolTests
/// length prefix is the leading four bytes of the stream, so a four-zero-byte
/// stream is exactly a frame declaring a zero-length payload.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WithZeroLengthPayload_ThrowsMalformedLength()
{
@@ -93,6 +97,7 @@ public sealed class WorkerFrameProtocolTests
/// A small maximum is configured so the rejection is asserted without
/// allocating a multi-megabyte buffer.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WithPayloadAboveConfiguredMaximum_ThrowsMessageTooLarge()
{
@@ -115,6 +120,7 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>Verifies that malformed payload throws invalid envelope error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope()
{
@@ -130,7 +136,7 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>
/// Worker.Tests-021 (a): pins the <c>EndOfStream</c> branch of
/// Pins the <c>EndOfStream</c> branch of
/// <c>WorkerFrameReader.ReadExactlyOrThrowAsync</c>. The gateway
/// closing its end of the pipe during a partial-frame read is the
/// most common production transport failure; the reader must
@@ -140,6 +146,7 @@ public sealed class WorkerFrameProtocolTests
/// payload but only supplies 50 bytes, so the inner read loop sees
/// <c>bytesRead == 0</c> mid-frame.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WhenStreamEndsMidFrame_ThrowsEndOfStream()
{
@@ -157,16 +164,16 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>
/// Worker.Tests-021 (b): pins the writer-side
/// <c>MessageTooLarge</c> branch. A session that constructs an
/// envelope whose serialised size exceeds <c>MaxMessageBytes</c>
/// must be rejected by the writer before any bytes are sent down
/// the pipe, so a misbehaving producer cannot push the receiver
/// past its bounds. A small <c>MaxMessageBytes</c> is configured
/// so a modest <c>GatewayHello</c> payload — with its nonce
/// padded out to several hundred bytes — exceeds the limit
/// Pins the writer-side <c>MessageTooLarge</c> branch. A session that
/// constructs an envelope whose serialised size exceeds
/// <c>MaxMessageBytes</c> must be rejected by the writer before any
/// bytes are sent down the pipe, so a misbehaving producer cannot
/// push the receiver past its bounds. A small <c>MaxMessageBytes</c>
/// is configured so a modest <c>GatewayHello</c> payload — with its
/// nonce padded out to several hundred bytes — exceeds the limit
/// without allocating anything large.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteAsync_WithEnvelopeAboveConfiguredMaximum_ThrowsMessageTooLarge()
{
@@ -191,11 +198,10 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>
/// Worker.Tests-021 (c): documents that the writer-side
/// <c>InvalidEnvelope</c> branch (raised when
/// <c>WorkerEnvelope.CalculateSize()</c> returns 0) is unreachable
/// through public API. <c>WorkerEnvelopeValidator.Validate</c> (run
/// before the size check in <c>WorkerFrameWriter.WriteAsync</c>)
/// Documents that the writer-side <c>InvalidEnvelope</c> branch
/// (raised when <c>WorkerEnvelope.CalculateSize()</c> returns 0) is
/// unreachable through public API. <c>WorkerEnvelopeValidator.Validate</c>
/// (run before the size check in <c>WorkerFrameWriter.WriteAsync</c>)
/// rejects any envelope whose <c>BodyCase</c> is <c>None</c> with
/// <c>InvalidEnvelope</c>; a body-less envelope is therefore
/// intercepted before the empty-payload branch can fire. Any
@@ -209,6 +215,7 @@ public sealed class WorkerFrameProtocolTests
/// would weaken the writer against future serialisation
/// regressions; this test makes its rationale visible.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteAsync_WithEmptyEnvelope_ThrowsInvalidEnvelopeFromValidator()
{
@@ -233,6 +240,7 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>Verifies that concurrent writes produce complete serialized frames.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteAsync_WithConcurrentCalls_SerializesCompleteFrames()
{
@@ -256,12 +264,13 @@ public sealed class WorkerFrameProtocolTests
}
/// <summary>
/// Worker-009 regression: the reader rents its payload buffer from a
/// shared pool, so a rented buffer can be larger than the current frame
/// and may carry bytes from a previous, larger frame. Reading frames of
/// differing sizes back-to-back through one reader must parse each frame
/// using only its own payload length, never trailing pooled bytes.
/// The reader rents its payload buffer from a shared pool, so a rented
/// buffer can be larger than the current frame and may carry bytes from
/// a previous, larger frame. Reading frames of differing sizes
/// back-to-back through one reader must parse each frame using only its
/// own payload length, never trailing pooled bytes.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadAsync_WithVaryingFrameSizes_ParsesEachFrameExactly()
{
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.Ipc;
public sealed class WorkerPipeClientTests
{
/// <summary>Verifies that worker client connects and completes handshake.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_ConnectsToPipeAndCompletesHandshake()
{
@@ -83,6 +84,7 @@ public sealed class WorkerPipeClientTests
}
/// <summary>Verifies that worker client retries until pipe server becomes available.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_RetriesUntilPipeServerAppears()
{
@@ -127,6 +129,7 @@ public sealed class WorkerPipeClientTests
}
/// <summary>Verifies that worker client throws timeout if pipe never appears.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenPipeNeverAppears_ThrowsTimeoutException()
{
@@ -148,7 +151,7 @@ public sealed class WorkerPipeClientTests
/// <summary>
/// Reads frames until one matching the expected body case is found,
/// skipping interleaved heartbeats (the first heartbeat is emitted
/// immediately on entering the heartbeat loop — see Worker-002).
/// immediately on entering the heartbeat loop).
/// </summary>
private static async Task<WorkerEnvelope> ReadUntilAsync(
WorkerFrameReader reader,
@@ -20,6 +20,7 @@ public sealed class WorkerPipeSessionTests
private const string Nonce = "nonce-secret";
/// <summary>Verifies that valid gateway hello triggers worker hello and ready responses.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CompleteStartupHandshakeAsync_WithValidGatewayHello_SendsHelloThenReady()
{
@@ -51,6 +52,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that wrong nonce causes protocol violation fault before initialization.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CompleteStartupHandshakeAsync_WithWrongNonce_FaultsBeforeInitialization()
{
@@ -79,6 +81,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that unsupported protocol version causes mismatch fault before initialization.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CompleteStartupHandshakeAsync_WithWrongProtocol_FaultsBeforeInitialization()
{
@@ -106,6 +109,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that malformed frame causes protocol violation fault.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CompleteStartupHandshakeAsync_WithMalformedFrame_WritesWorkerFault()
{
@@ -132,6 +136,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that MXAccess COM creation failure produces fault instead of ready.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CompleteStartupHandshakeAsync_WhenMxAccessCreationFails_WritesFaultInsteadOfReady()
{
@@ -158,6 +163,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that heartbeat payload reflects current runtime snapshot.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_SendsHeartbeatPayloadFromRuntimeSnapshot()
{
@@ -211,6 +217,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that heartbeat reports current command correlation during execution.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenCommandIsExecuting_HeartbeatReportsCurrentCorrelation()
{
@@ -258,6 +265,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that worker events are written to the pipe.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenRuntimeHasEvents_WritesWorkerEventEnvelope()
{
@@ -293,6 +301,7 @@ public sealed class WorkerPipeSessionTests
/// (not dispatched to the STA) with an OK reply that echoes the ping
/// message into the reply's diagnostic field.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_PingControlCommand_RepliesOkAndEchoesMessage()
{
@@ -324,6 +333,7 @@ public sealed class WorkerPipeSessionTests
/// Verifies that GetSessionState reports the worker's lifecycle as the
/// proto SessionState — READY while the message loop is serving.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_GetSessionStateControlCommand_RepliesReady()
{
@@ -359,6 +369,7 @@ public sealed class WorkerPipeSessionTests
/// Verifies that GetWorkerInfo populates the worker process id, version,
/// and MXAccess ProgID/CLSID from the worker's own metadata.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_GetWorkerInfoControlCommand_PopulatesWorkerInfoFields()
{
@@ -398,6 +409,7 @@ public sealed class WorkerPipeSessionTests
/// Verifies that DrainEvents drains the runtime session's queued events
/// into the reply rather than streaming them as WorkerEvent envelopes.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_DrainEventsControlCommand_ReturnsQueuedEvents()
{
@@ -442,6 +454,7 @@ public sealed class WorkerPipeSessionTests
/// shutdown runs and disposes the runtime session, and that the message
/// loop then stops.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_ShutdownWorkerControlCommand_RepliesOkThenShutsDown()
{
@@ -491,12 +504,12 @@ public sealed class WorkerPipeSessionTests
/// <summary>
/// Verifies that stale STA activity with no command in flight triggers
/// the watchdog StaHung fault. Worker-017 changed the watchdog to skip
/// the fault while a command is in flight (the worker is busy
/// executing it, not hung), so this test deliberately leaves the
/// current-command correlation id empty to assert the genuine-hung
/// path still fires.
/// the watchdog StaHung fault. The watchdog skips the fault while a
/// command is in flight (the worker is busy executing it, not hung),
/// so this test deliberately leaves the current-command correlation
/// id empty to assert the genuine-hung path still fires.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenStaActivityIsStale_WritesWatchdogFault()
{
@@ -532,17 +545,18 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-017 regression: while a command is in flight (snapshot's
/// current command correlation id is non-empty), stale STA activity
/// must NOT trigger the watchdog StaHung fault. The STA is busy
/// executing the command, not hung; <c>StaRuntime.ProcessQueuedCommands</c>
/// only calls <c>MarkActivity()</c> before and after each work item,
/// so a synchronously long-running command (e.g. <c>ReadBulk</c>
/// waiting <c>timeout_ms</c> for OnDataChange) legitimately freezes
/// While a command is in flight (snapshot's current command
/// correlation id is non-empty), stale STA activity must NOT trigger
/// the watchdog StaHung fault. The STA is busy executing the command,
/// not hung; <c>StaRuntime.ProcessQueuedCommands</c> only calls
/// <c>MarkActivity()</c> before and after each work item, so a
/// synchronously long-running command (e.g. <c>ReadBulk</c> waiting
/// <c>timeout_ms</c> for OnDataChange) legitimately freezes
/// <c>LastActivityUtc</c>. The heartbeat already advertises the
/// in-flight correlation id so the gateway can apply its own per-command
/// timeout.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenStaActivityIsStaleWithCommandInFlight_DoesNotWriteWatchdogFault()
{
@@ -596,13 +610,13 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-004 regression: once the watchdog reports an StaHung fault,
/// subsequent heartbeats must report <see cref="WorkerState.Faulted"/>
/// rather than a non-faulted state that contradicts the fault. The
/// snapshot uses an empty current-command correlation id so the
/// heartbeat State is derived from the session state, not forced to
/// ExecutingCommand.
/// Once the watchdog reports an StaHung fault, subsequent heartbeats
/// must report <see cref="WorkerState.Faulted"/> rather than a
/// non-faulted state that contradicts the fault. The snapshot uses an
/// empty current-command correlation id so the heartbeat State is
/// derived from the session state, not forced to ExecutingCommand.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_AfterWatchdogFault_HeartbeatReportsFaultedState()
{
@@ -643,15 +657,16 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-023 regression: the in-flight-command suppression on the
/// <c>StaHung</c> watchdog (Worker-017) is bounded by
/// <c>WorkerPipeSessionOptions.HeartbeatStuckCeiling</c>. A truly
/// stuck synchronous STA command (e.g. a dead MXAccess provider) would
/// otherwise keep <c>CurrentCommandCorrelationId</c> non-empty forever
/// and permanently defeat the watchdog. Once <c>LastStaActivityUtc</c>
/// has been stale for longer than <c>HeartbeatStuckCeiling</c> the
/// watchdog DOES fire <c>StaHung</c> even with a command in flight.
/// The in-flight-command suppression on the <c>StaHung</c> watchdog is
/// bounded by <c>WorkerPipeSessionOptions.HeartbeatStuckCeiling</c>. A
/// truly stuck synchronous STA command (e.g. a dead MXAccess provider)
/// would otherwise keep <c>CurrentCommandCorrelationId</c> non-empty
/// forever and permanently defeat the watchdog. Once
/// <c>LastStaActivityUtc</c> has been stale for longer than
/// <c>HeartbeatStuckCeiling</c> the watchdog DOES fire <c>StaHung</c>
/// even with a command in flight.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenStaActivityIsStaleBeyondCeilingWithCommandInFlight_WritesWatchdogFault()
{
@@ -690,10 +705,11 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-025 regression: <c>RunAsync</c> must throw a diagnostic
/// exception if the runtime-session factory returns null, rather than
/// deferring the failure to an NRE on the next dereference.
/// <c>RunAsync</c> must throw a diagnostic exception if the
/// runtime-session factory returns null, rather than deferring the
/// failure to an NRE on the next dereference.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenRuntimeSessionFactoryReturnsNull_ThrowsDiagnosticException()
{
@@ -715,11 +731,11 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-006 regression: when graceful shutdown times out, RunAsync
/// must still dispose the runtime session in its finally block.
/// Skipping disposal on the timed-out path leaked the STA thread and
/// the MXAccess COM object.
/// When graceful shutdown times out, RunAsync must still dispose the
/// runtime session in its finally block. Skipping disposal on the
/// timed-out path leaked the STA thread and the MXAccess COM object.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenShutdownTimesOut_StillDisposesRuntimeSession()
{
@@ -783,6 +799,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that shutdown drops late replies and sends shutdown ack.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenShutdownArrivesDuringCommand_DropsLateReplyAndWritesShutdownAck()
{
@@ -823,6 +840,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Verifies that command exceptions after shutdown are dropped before ack.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenCommandThrowsAfterShutdown_DropsLateFaultAndWritesShutdownAck()
{
@@ -852,9 +870,9 @@ public sealed class WorkerPipeSessionTests
await pipePair.GatewayWriter
.WriteAsync(CreateShutdownEnvelope(), cancellation.Token);
// The first heartbeat is emitted immediately on entering the loop
// (Worker-002), so skip any interleaved heartbeats; the late fault
// must still be dropped — no WorkerFault may precede the ack.
// The first heartbeat is emitted immediately on entering the loop,
// so skip any interleaved heartbeats; the late fault must still be
// dropped — no WorkerFault may precede the ack.
WorkerEnvelope envelopeAfterShutdown;
do
{
@@ -873,7 +891,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker.Tests-017 regression: the <c>WorkerCancel</c> branch of
/// The <c>WorkerCancel</c> branch of
/// <see cref="WorkerPipeSession.DispatchGatewayEnvelopeAsync"/> must
/// forward the envelope's correlation id to the runtime session via
/// <see cref="IWorkerRuntimeSession.CancelCommand"/> and keep the
@@ -881,6 +899,7 @@ public sealed class WorkerPipeSessionTests
/// returns <c>true</c> (keep reading), so a subsequent
/// <c>WorkerShutdown</c> still produces the normal shutdown ack.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenGatewaySendsWorkerCancel_ForwardsCorrelationIdToRuntimeSession()
{
@@ -910,7 +929,7 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker.Tests-017 regression: the <c>default:</c> arm of
/// The <c>default:</c> arm of
/// <see cref="WorkerPipeSession.DispatchGatewayEnvelopeAsync"/> must
/// throw <see cref="WorkerFrameProtocolException"/> with
/// <see cref="WorkerFrameProtocolErrorCode.UnexpectedEnvelopeBody"/>
@@ -923,6 +942,7 @@ public sealed class WorkerPipeSessionTests
/// pre-handshake protocol violations); the contract this test pins
/// is the exception type/error-code and message-loop exit.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenGatewaySendsUnexpectedEnvelopeBodyAfterHandshake_ThrowsAndExitsMessageLoop()
{
@@ -956,11 +976,12 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-002 regression: the first heartbeat must be emitted
/// immediately on entering the heartbeat loop, not after a full
/// HeartbeatInterval. A long interval is configured so a delay-first
/// loop would fail to deliver a heartbeat inside the assertion window.
/// The first heartbeat must be emitted immediately on entering the
/// heartbeat loop, not after a full HeartbeatInterval. A long interval
/// is configured so a delay-first loop would fail to deliver a
/// heartbeat inside the assertion window.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_SendsFirstHeartbeatImmediatelyOnEnteringLoop()
{
@@ -984,7 +1005,7 @@ public sealed class WorkerPipeSessionTests
// if the first heartbeat is not received within 5s, ReadUntilAsync throws
// OperationCanceledException and the test fails. A redundant wall-clock
// elapsed < 5s assertion would add the same class of flakiness
// Workers.Tests-003/004/013/020 corrected elsewhere, so it is omitted here.
// corrected elsewhere, so it is omitted here.
using CancellationTokenSource heartbeatWait = CancellationTokenSource
.CreateLinkedTokenSource(cancellation.Token);
heartbeatWait.CancelAfter(TimeSpan.FromSeconds(5));
@@ -999,11 +1020,12 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>
/// Worker-003 regression: when a command completes after the worker
/// has transitioned out of a command-serving state, the dropped
/// reply must be logged with a diagnostic rather than discarded
/// silently, so a stuck gateway correlation wait can be traced.
/// When a command completes after the worker has transitioned out of
/// a command-serving state, the dropped reply must be logged with a
/// diagnostic rather than discarded silently, so a stuck gateway
/// correlation wait can be traced.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAsync_WhenReplyIsDroppedAfterShutdown_LogsDiagnostic()
{
@@ -1110,7 +1132,7 @@ public sealed class WorkerPipeSessionTests
// (default = position in the typical Hello/Command/Cancel/Shutdown
// ordering) so a multi-frame test that interleaves the helpers can
// assign monotonically increasing values and produce a wire trace
// that reads in ascending order — see Worker.Tests-030.
// that reads in ascending order.
private static WorkerEnvelope CreateGatewayHelloEnvelope(
string nonce = Nonce,
uint supportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
@@ -1346,17 +1368,13 @@ public sealed class WorkerPipeSessionTests
}
}
/// <summary>Records an informational log event.</summary>
/// <param name="eventName">The event name.</param>
/// <param name="fields">The event fields.</param>
/// <inheritdoc />
public void Information(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Record(eventName, fields);
}
/// <summary>Records an error log event.</summary>
/// <param name="eventName">The event name.</param>
/// <param name="fields">The event fields.</param>
/// <inheritdoc />
public void Error(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Record(eventName, fields);
@@ -1447,7 +1465,7 @@ public sealed class WorkerPipeSessionTests
return new PipePair(gatewayStream, workerStream);
}
/// <summary>Disposes pipe resources.</summary>
/// <inheritdoc />
public void Dispose()
{
WorkerStream.Dispose();
@@ -371,28 +371,20 @@ public sealed class AlarmCommandExecutorTests
/// <summary>Gets the last alarm filter prefix.</summary>
public string? LastFilterPrefix { get; private set; }
/// <summary>Records a subscription.</summary>
/// <param name="command">The subscribe-alarms command.</param>
/// <param name="sessionId">The session identifier.</param>
/// <inheritdoc />
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
{
LastSubscription = command.SubscriptionExpression;
LastSessionId = sessionId;
}
/// <summary>Records an unsubscribe request.</summary>
/// <inheritdoc />
public void Unsubscribe()
{
UnsubscribeCalled = true;
}
/// <summary>Records an acknowledge request.</summary>
/// <param name="alarmGuid">The alarm identifier.</param>
/// <param name="comment">The acknowledge comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
/// <inheritdoc />
public int Acknowledge(
Guid alarmGuid, string comment, string operatorUser,
string operatorNode, string operatorDomain, string operatorFullName)
@@ -406,15 +398,7 @@ public sealed class AlarmCommandExecutorTests
return AcknowledgeReturn;
}
/// <summary>Records an acknowledge by name request.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The group name.</param>
/// <param name="comment">The acknowledge comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
/// <inheritdoc />
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string comment, string operatorUser, string operatorNode,
@@ -428,8 +412,7 @@ public sealed class AlarmCommandExecutorTests
/// <summary>Gets the last acknowledge by name tuple.</summary>
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
/// <summary>Queries the active alarms with the given filter prefix.</summary>
/// <param name="alarmFilterPrefix">The alarm filter prefix for the query.</param>
/// <inheritdoc />
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
{
LastFilterPrefix = alarmFilterPrefix;
@@ -439,7 +422,7 @@ public sealed class AlarmCommandExecutorTests
/// <summary>Gets the number of poll calls.</summary>
public int PollCount { get; private set; }
/// <summary>Increments the poll count.</summary>
/// <inheritdoc />
public void PollOnce()
{
PollCount++;
@@ -42,7 +42,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker.Tests-024: pins both the disposal contract and the
/// Pins both the disposal contract and the
/// origin of the propagated exception. The fake throws
/// <c>InvalidOperationException("simulated wnwrap subscribe failure")</c>
/// from <c>Subscribe</c>; the handler must propagate that exact
@@ -199,7 +199,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker-024 regression: every method that touches the underlying
/// Every method that touches the underlying
/// <see cref="IMxAccessAlarmConsumer"/> must invoke the configured
/// STA-affinity guard. A guard that throws (simulating an off-STA
/// call) must propagate from every command-path entry point.
@@ -238,7 +238,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker-024 regression: a guard that throws must propagate from
/// A guard that throws must propagate from
/// every command-path entry point — proving the guard is not
/// swallowed by an inner try/catch.
/// </summary>
@@ -274,7 +274,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker-9: ForcedMode=Subtag builds a subtag consumer (via the
/// ForcedMode=Subtag builds a subtag consumer (via the
/// injected standby factory) and advises it — the primary
/// (alarmmgr) consumer is NOT created.
/// </summary>
@@ -312,7 +312,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker-9: ForcedMode=Unspecified + a non-empty watch list builds a
/// ForcedMode=Unspecified + a non-empty watch list builds a
/// failover composite (primary + subtag standby). Forcing the primary
/// to fail on subscribe with a threshold of 1 drives the composite to
/// switch to the subtag provider, which must enqueue an
@@ -359,7 +359,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker-9: a non-failover subscribe (alarmmgr-only) never enqueues a
/// A non-failover subscribe (alarmmgr-only) never enqueues a
/// provider-mode-changed event, and a subsequent Unsubscribe detaches
/// the handler so no event leaks.
/// </summary>
@@ -378,7 +378,7 @@ public sealed class AlarmCommandHandlerTests
}
/// <summary>
/// Worker-9: the mapper builds a well-formed OnAlarmProviderModeChanged
/// The mapper builds a well-formed OnAlarmProviderModeChanged
/// MxEvent — correct family and populated body fields.
/// </summary>
[Fact]
@@ -436,8 +436,7 @@ public sealed class AlarmCommandHandlerTests
/// <summary>Gets a value indicating whether the consumer has been disposed.</summary>
public bool Disposed { get; private set; }
/// <summary>Subscribes to alarms with the given subscription string.</summary>
/// <param name="subscription">The subscription reference.</param>
/// <inheritdoc />
public void Subscribe(string subscription)
{
LastSubscription = subscription;
@@ -447,13 +446,7 @@ public sealed class AlarmCommandHandlerTests
}
}
/// <summary>Acknowledges an alarm by GUID.</summary>
/// <param name="alarmGuid">The alarm GUID.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
/// <inheritdoc />
public int AcknowledgeByGuid(
Guid alarmGuid, string ackComment, string ackOperatorName,
string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName)
@@ -463,15 +456,7 @@ public sealed class AlarmCommandHandlerTests
return AcknowledgeReturn;
}
/// <summary>Acknowledges an alarm by name.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The alarm group name.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
/// <inheritdoc />
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string ackComment, string ackOperatorName, string ackOperatorNode,
@@ -485,13 +470,13 @@ public sealed class AlarmCommandHandlerTests
/// <summary>Gets the last acknowledge-by-name parameters.</summary>
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
/// <summary>Returns a snapshot of active alarms.</summary>
/// <inheritdoc />
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms() => SnapshotResult;
/// <summary>Gets the number of times polled.</summary>
public int PollCount { get; private set; }
/// <summary>Polls once for alarm updates.</summary>
/// <inheritdoc />
public void PollOnce()
{
PollCount++;
@@ -394,20 +394,13 @@ public sealed class AlarmDispatcherTests
AlarmTransitionEmitted?.Invoke(this, transition);
}
/// <summary>Records the subscription reference.</summary>
/// <param name="subscription">The subscription reference.</param>
/// <inheritdoc />
public void Subscribe(string subscription)
{
LastSubscription = subscription;
}
/// <summary>Records an acknowledge-by-GUID call with operator identity.</summary>
/// <param name="alarmGuid">The alarm GUID.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
/// <inheritdoc />
public int AcknowledgeByGuid(
Guid alarmGuid,
string ackComment,
@@ -425,15 +418,7 @@ public sealed class AlarmDispatcherTests
return AcknowledgeReturn;
}
/// <summary>Records an acknowledge-by-name call with alarm name, provider, and group.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The alarm group name.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
/// <inheritdoc />
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string ackComment, string ackOperatorName, string ackOperatorNode,
@@ -447,7 +432,7 @@ public sealed class AlarmDispatcherTests
/// <summary>Gets the last acknowledge-by-name tuple (alarm name, provider, group).</summary>
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
/// <summary>Returns the current snapshot result collection.</summary>
/// <inheritdoc />
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
{
return SnapshotResult;
@@ -456,7 +441,7 @@ public sealed class AlarmDispatcherTests
/// <summary>Gets the count of poll operations.</summary>
public int PollCount { get; private set; }
/// <summary>Increments the poll count.</summary>
/// <inheritdoc />
public void PollOnce()
{
PollCount++;
@@ -24,6 +24,7 @@ public sealed class FailoverAlarmConsumerTests
/// </summary>
private sealed class FlakyPrimary : IMxAccessAlarmConsumer
{
/// <summary>Raised when the fake forwards a simulated alarm transition via <see cref="Raise"/>.</summary>
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
public bool ThrowOnPoll = true;
@@ -32,7 +33,7 @@ public sealed class FailoverAlarmConsumerTests
/// When set, <see cref="PollOnce"/> throws
/// <see cref="OutOfMemoryException"/> instead of a
/// <see cref="System.Runtime.InteropServices.COMException"/>, to
/// exercise the OOM-safe exception filter (Worker.Tests-032).
/// exercise the OOM-safe exception filter.
/// </summary>
public bool ThrowOutOfMemoryOnPoll;
@@ -45,6 +46,7 @@ public sealed class FailoverAlarmConsumerTests
/// </summary>
public int SubscribeCount;
/// <inheritdoc />
public void Subscribe(string s)
{
SubscribeCount++;
@@ -54,6 +56,7 @@ public sealed class FailoverAlarmConsumerTests
}
}
/// <inheritdoc />
public void PollOnce()
{
Polls++;
@@ -68,14 +71,20 @@ public sealed class FailoverAlarmConsumerTests
}
}
/// <inheritdoc />
public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 11;
/// <inheritdoc />
public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 11;
/// <inheritdoc />
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms() => Array.Empty<MxAlarmSnapshotRecord>();
/// <inheritdoc />
public void Dispose() { }
/// <summary>Raises <see cref="AlarmTransitionEmitted"/> with the given event, simulating a COM-forwarded transition.</summary>
/// <param name="e">The transition event to forward.</param>
public void Raise(MxAlarmTransitionEvent e) => AlarmTransitionEmitted?.Invoke(this, e);
}
@@ -85,27 +94,33 @@ public sealed class FailoverAlarmConsumerTests
/// </summary>
private sealed class StubStandby : IMxAccessAlarmConsumer
{
/// <summary>Raised when the fake forwards a simulated alarm transition via <see cref="Raise"/>.</summary>
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
public bool Subscribed;
/// <summary>
/// When set, <see cref="SnapshotActiveAlarms"/> throws — modeling a
/// priming-snapshot failure during failover (Worker-026).
/// priming-snapshot failure during failover.
/// </summary>
public bool ThrowOnSnapshot;
/// <summary>Number of <see cref="SnapshotActiveAlarms"/> calls.</summary>
public int SnapshotCalls;
/// <inheritdoc />
public void Subscribe(string s) => Subscribed = true;
/// <inheritdoc />
public void PollOnce() { }
/// <inheritdoc />
public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 22;
/// <inheritdoc />
public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 22;
/// <inheritdoc />
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
{
SnapshotCalls++;
@@ -117,8 +132,11 @@ public sealed class FailoverAlarmConsumerTests
return Array.Empty<MxAlarmSnapshotRecord>();
}
/// <inheritdoc />
public void Dispose() { }
/// <summary>Raises <see cref="AlarmTransitionEmitted"/> with the given event, simulating a COM-forwarded transition.</summary>
/// <param name="e">The transition event to forward.</param>
public void Raise(MxAlarmTransitionEvent e) => AlarmTransitionEmitted?.Invoke(this, e);
}
@@ -128,6 +146,7 @@ public sealed class FailoverAlarmConsumerTests
PreviousState = MxAlarmStateKind.Unspecified,
};
/// <summary>Proves that the consumer switches to the subtag standby after the primary fails the configured threshold of consecutive times.</summary>
[Fact]
public void Primary_FailsThresholdTimes_SwitchesToSubtag()
{
@@ -154,6 +173,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(unchecked((int)0x80004005), changes[0].HResult);
}
/// <summary>Proves that once failed over, transitions raised by the standby are forwarded through the consumer's <c>AlarmTransitionEmitted</c> event.</summary>
[Fact]
public void AfterSwitch_StandbyTransitionsAreForwarded()
{
@@ -174,6 +194,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Same(transition, forwarded);
}
/// <summary>Proves that once the primary heals, the consumer fails back to it only after the configured number of consecutive clean probes.</summary>
[Fact]
public void WhileDegraded_PrimaryHeals_FailsBackAfterStableProbes()
{
@@ -212,6 +233,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(subscribeCountAfterFailover, primary.SubscribeCount);
}
/// <summary>Proves that before any failover, transitions raised by the primary are forwarded but transitions from the inactive standby are suppressed.</summary>
[Fact]
public void BeforeFailover_PrimaryTransitionsAreForwarded()
{
@@ -252,7 +274,7 @@ public sealed class FailoverAlarmConsumerTests
FailoverSettings settings = new FailoverSettings(threshold: 1, probeIntervalSeconds: 0, stableProbes: 3);
using FailoverAlarmConsumer sut = new FailoverAlarmConsumer(primary, standby, settings);
sut.Subscribe(@"\\HOST\Galaxy!Area"); // Subscribe attempt #1 (throws) → Subtag
sut.Subscribe(@"\\HOST\Galaxy!Area"); // Subscribe attempt (throws) → Subtag
// Capture how many Subscribe calls the initial setup caused (exactly 1:
// the attempt that threw and triggered failover).
@@ -272,6 +294,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(subscribeCountAfterSetup, primary.SubscribeCount);
}
/// <summary>Proves that acknowledgment calls delegate to whichever child (primary or standby) is currently active.</summary>
[Fact]
public void Acknowledge_DelegatesToActiveChild()
{
@@ -325,7 +348,7 @@ public sealed class FailoverAlarmConsumerTests
}
/// <summary>
/// Worker-026 regression: when the standby's priming
/// When the standby's priming
/// <c>SnapshotActiveAlarms</c> throws during failover, the switch must
/// still (a) fire <c>ProviderModeChanged</c> so the gateway learns the
/// feed went degraded, (b) leave <see cref="FailoverAlarmConsumer.Mode"/>
@@ -361,7 +384,7 @@ public sealed class FailoverAlarmConsumerTests
}
/// <summary>
/// Worker-026 regression: when a <c>ProviderModeChanged</c> subscriber's
/// When a <c>ProviderModeChanged</c> subscriber's
/// handler throws (modeling the AlarmCommandHandler's event-queue enqueue
/// overflowing at capacity), the switch must still take effect and the
/// exception must not escape the switch path into the poll loop.
@@ -389,7 +412,7 @@ public sealed class FailoverAlarmConsumerTests
}
/// <summary>
/// Worker.Tests-031 regression: with a non-zero
/// With a non-zero
/// <see cref="FailoverSettings.ProbeIntervalSeconds"/>, two back-to-back
/// <c>ProbeOnce</c> calls must throttle — the second falls inside the
/// interval and must NOT re-poll the primary. Two consecutive calls
@@ -422,7 +445,7 @@ public sealed class FailoverAlarmConsumerTests
}
/// <summary>
/// Worker.Tests-032 regression: <c>RunPrimary</c>'s
/// <c>RunPrimary</c>'s
/// <c>when (ex is not OutOfMemoryException)</c> filter must let an
/// <see cref="OutOfMemoryException"/> propagate rather than swallowing it
/// and counting it toward the failover threshold. No mode change must
@@ -447,11 +470,17 @@ public sealed class FailoverAlarmConsumerTests
}
/// <summary>
/// Worker.Tests-032 regression: <see cref="FailoverSettings"/> clamps
/// sub-1 <c>threshold</c> and <c>stableProbes</c> (and sub-0
/// Verifies that <see cref="FailoverSettings"/> clamps out-of-range
/// <c>threshold</c> and <c>stableProbes</c> values (and negative
/// <c>probeIntervalSeconds</c>) to their safe minimums so a misconfigured
/// bind cannot change failover semantics.
/// </summary>
/// <param name="threshold">The raw, possibly out-of-range threshold to construct with.</param>
/// <param name="probeInterval">The raw, possibly negative probe interval (seconds) to construct with.</param>
/// <param name="stableProbes">The raw, possibly out-of-range stable-probe count to construct with.</param>
/// <param name="expectedThreshold">The expected clamped <see cref="FailoverSettings.Threshold"/>.</param>
/// <param name="expectedProbeInterval">The expected clamped <see cref="FailoverSettings.ProbeIntervalSeconds"/>.</param>
/// <param name="expectedStableProbes">The expected clamped <see cref="FailoverSettings.StableProbes"/>.</param>
[Theory]
[InlineData(0, 0, 0, 1, 0, 1)]
[InlineData(-5, -5, -5, 1, 0, 1)]
@@ -199,26 +199,39 @@ public sealed class LmxSubtagAlarmSourceTests
private readonly Dictionary<string, int> handlesByAddress = new(StringComparer.Ordinal);
private int nextItemHandle = 100;
/// <summary>Item definitions passed to <see cref="AddItem"/> or <see cref="AddItem2"/>, in call order.</summary>
public List<string> AddedItems { get; } = new();
/// <summary>Number of times <see cref="Advise"/> has been called.</summary>
public int AdviseCount { get; private set; }
/// <summary>Server handles passed to <see cref="Advise"/>, in call order.</summary>
public List<int> AdvisedServerHandles { get; } = new();
/// <summary>Recorded <see cref="Write"/> calls, in call order.</summary>
public List<WriteRecord> Writes { get; } = new();
/// <summary>Item handles passed to <see cref="UnAdvise"/>, in call order.</summary>
public List<int> UnAdvisedItemHandles { get; } = new();
/// <summary>Item handles passed to <see cref="RemoveItem"/>, in call order.</summary>
public List<int> RemovedItemHandles { get; } = new();
/// <summary>Number of times <see cref="Unregister"/> has been called.</summary>
public int UnregisterCount { get; private set; }
/// <summary>Returns the item handle most recently assigned to the given item address.</summary>
/// <param name="itemAddress">The item definition/address to look up.</param>
/// <returns>The item handle assigned by <see cref="AddItem"/>.</returns>
public int LastItemHandleFor(string itemAddress) => handlesByAddress[itemAddress];
/// <inheritdoc />
public int Register(string clientName) => FakeServerHandle;
/// <inheritdoc />
public void Unregister(int serverHandle) => UnregisterCount++;
/// <inheritdoc />
public int AddItem(int serverHandle, string itemDefinition)
{
AddedItems.Add(itemDefinition);
@@ -227,55 +240,76 @@ public sealed class LmxSubtagAlarmSourceTests
return handle;
}
/// <inheritdoc />
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
=> AddItem(serverHandle, itemDefinition);
/// <inheritdoc />
public void RemoveItem(int serverHandle, int itemHandle) => RemovedItemHandles.Add(itemHandle);
/// <inheritdoc />
public void Advise(int serverHandle, int itemHandle)
{
AdviseCount++;
AdvisedServerHandles.Add(serverHandle);
}
/// <inheritdoc />
public void UnAdvise(int serverHandle, int itemHandle) => UnAdvisedItemHandles.Add(itemHandle);
/// <inheritdoc />
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
}
/// <inheritdoc />
public void Write(int serverHandle, int itemHandle, object? value, int userId)
=> Writes.Add(new WriteRecord(serverHandle, itemHandle, value, userId));
/// <inheritdoc />
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
{
}
/// <inheritdoc />
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
{
}
/// <inheritdoc />
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
{
}
/// <inheritdoc />
public object Suspend(int serverHandle, int itemHandle) => new object();
/// <inheritdoc />
public object Activate(int serverHandle, int itemHandle) => new object();
/// <inheritdoc />
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0;
/// <inheritdoc />
public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0;
/// <inheritdoc />
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
=> AddItem(serverHandle, itemDefinition);
/// <inheritdoc />
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
}
/// <summary>Immutable record of a single <see cref="Write"/> call captured by <see cref="Writes"/>.</summary>
internal sealed class WriteRecord
{
/// <summary>Initializes a new instance of the <see cref="WriteRecord"/> class from a recorded write.</summary>
/// <param name="serverHandle">The server handle the write targeted.</param>
/// <param name="itemHandle">The item handle the write targeted.</param>
/// <param name="value">The written value.</param>
/// <param name="userId">The writing user's identifier.</param>
public WriteRecord(int serverHandle, int itemHandle, object? value, int userId)
{
ServerHandle = serverHandle;
@@ -284,12 +318,16 @@ public sealed class LmxSubtagAlarmSourceTests
UserId = userId;
}
/// <summary>The server handle the write targeted.</summary>
public int ServerHandle { get; }
/// <summary>The item handle the write targeted.</summary>
public int ItemHandle { get; }
/// <summary>The written value.</summary>
public object? Value { get; }
/// <summary>The writing user's identifier.</summary>
public int UserId { get; }
}
}
@@ -5,7 +5,7 @@ using ZB.MOM.WW.MxGateway.Worker.MxAccess;
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
/// <summary>
/// Worker-007 regression tests for <see cref="MxAccessComServer"/>. The
/// Regression tests for <see cref="MxAccessComServer"/>. The
/// adapter no longer falls back to late-bound <c>Type.InvokeMember</c>
/// reflection: a COM object must implement either the typed
/// <c>ILMXProxyServer</c> COM interface family (production) or
@@ -124,8 +124,7 @@ public sealed class MxAccessComServerTests
/// <summary>Gets the recorded method calls as strings.</summary>
public IReadOnlyList<string> Calls => calls.ToArray();
/// <summary>Records a Register call and returns the configured handle.</summary>
/// <param name="clientName">The client name to record.</param>
/// <inheritdoc />
public int Register(string clientName)
{
calls.Add($"Register:{clientName}");
@@ -138,159 +137,111 @@ public sealed class MxAccessComServerTests
return registerHandle;
}
/// <summary>Records an Unregister call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <inheritdoc />
public void Unregister(int serverHandle)
{
calls.Add($"Unregister:{serverHandle}");
}
/// <summary>Records an AddItem call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemDefinition">The item definition string to record.</param>
/// <inheritdoc />
public int AddItem(int serverHandle, string itemDefinition)
{
calls.Add($"AddItem:{serverHandle}:{itemDefinition}");
return 0;
}
/// <summary>Records an AddItem2 call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemDefinition">The item definition string to record.</param>
/// <param name="itemContext">The item context string to record.</param>
/// <inheritdoc />
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
{
calls.Add($"AddItem2:{serverHandle}:{itemDefinition}:{itemContext}");
return 0;
}
/// <summary>Records a RemoveItem call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <inheritdoc />
public void RemoveItem(int serverHandle, int itemHandle)
{
calls.Add($"RemoveItem:{serverHandle}:{itemHandle}");
}
/// <summary>Records an Advise call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <inheritdoc />
public void Advise(int serverHandle, int itemHandle)
{
calls.Add($"Advise:{serverHandle}:{itemHandle}");
}
/// <summary>Records an UnAdvise call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <inheritdoc />
public void UnAdvise(int serverHandle, int itemHandle)
{
calls.Add($"UnAdvise:{serverHandle}:{itemHandle}");
}
/// <summary>Records an AdviseSupervisory call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <inheritdoc />
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
}
/// <summary>Records a Write call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="value">The value to write.</param>
/// <param name="userId">The user identifier.</param>
/// <inheritdoc />
public void Write(int serverHandle, int itemHandle, object? value, int userId)
{
calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}");
}
/// <summary>Records a Write2 call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="value">The value to write.</param>
/// <param name="timestamp">The timestamp value.</param>
/// <param name="userId">The user identifier.</param>
/// <inheritdoc />
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
{
calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}");
}
/// <summary>Records a WriteSecured call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="currentUserId">The current user identifier.</param>
/// <param name="verifierUserId">The verifier user identifier.</param>
/// <param name="value">The value to write.</param>
/// <inheritdoc />
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
{
calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}");
}
/// <summary>Records a WriteSecured2 call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="currentUserId">The current user identifier.</param>
/// <param name="verifierUserId">The verifier user identifier.</param>
/// <param name="value">The value to write.</param>
/// <param name="timestamp">The timestamp value.</param>
/// <inheritdoc />
public void WriteSecured2(
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
{
calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
}
/// <summary>Records a Suspend call and returns a canned status.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <inheritdoc />
public object Suspend(int serverHandle, int itemHandle)
{
calls.Add($"Suspend:{serverHandle}:{itemHandle}");
return new object();
}
/// <summary>Records an Activate call and returns a canned status.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <inheritdoc />
public object Activate(int serverHandle, int itemHandle)
{
calls.Add($"Activate:{serverHandle}:{itemHandle}");
return new object();
}
/// <summary>Records an AuthenticateUser call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="verifyUser">The user name to authenticate.</param>
/// <param name="verifyUserPassword">The credential; recorded only as a fixed marker, never echoed.</param>
/// <inheritdoc />
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
calls.Add($"AuthenticateUser:{serverHandle}:{verifyUser}");
return 0;
}
/// <summary>Records an ArchestrAUserToId call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
/// <inheritdoc />
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
calls.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
return 0;
}
/// <summary>Records an AddBufferedItem call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemDefinition">The item definition string to record.</param>
/// <param name="itemContext">The item context string to record.</param>
/// <inheritdoc />
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
calls.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
return 0;
}
/// <summary>Records a SetBufferedUpdateInterval call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="updateIntervalMilliseconds">The buffered update interval in milliseconds.</param>
/// <inheritdoc />
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
calls.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
public sealed class MxAccessCommandExecutorTests
{
/// <summary>Verifies that Register command calls MXAccess on the STA thread and preserves the server handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Register_CallsMxAccessOnStaAndPreservesServerHandle()
{
@@ -40,6 +41,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Unregister command calls MXAccess on the STA thread and removes the tracked server handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Unregister_CallsMxAccessOnStaAndRemovesTrackedServerHandle()
{
@@ -59,6 +61,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Unregister preserves the HResult when MXAccess throws and does not rewrite the failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_UnregisterWhenMxAccessThrows_PreservesHResultAndDoesNotRewriteFailure()
{
@@ -86,6 +89,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that AddItem command calls MXAccess on the STA thread and tracks the item handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AddItem_CallsMxAccessOnStaAndTracksItemHandle()
{
@@ -123,6 +127,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that AddItem2 command passes the context exactly and tracks the item handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AddItem2_PassesContextExactlyAndTracksItemHandle()
{
@@ -160,6 +165,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that RemoveItem command calls MXAccess on the STA thread and removes the tracked item handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_RemoveItem_CallsMxAccessOnStaAndRemovesTrackedItemHandle()
{
@@ -188,6 +194,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that RemoveItem removes tracked advice after MXAccess succeeds on an advised handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_RemoveItemWithAdvisedHandle_RemovesTrackedAdviceAfterMxAccessSucceeds()
{
@@ -213,6 +220,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that RemoveItem preserves the HResult and keeps the tracked item handle when using a cross-server handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_RemoveItemWithCrossServerHandle_PreservesHResultAndKeepsTrackedItemHandle()
{
@@ -247,6 +255,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that AddItem2 preserves the HResult when MXAccess throws and does not track the item handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AddItem2WhenMxAccessThrows_PreservesHResultAndDoesNotTrackItemHandle()
{
@@ -276,6 +285,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Advise command calls MXAccess on the STA thread and tracks plain advice.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Advise_CallsMxAccessOnStaAndTracksPlainAdvice()
{
@@ -309,6 +319,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that AdviseSupervisory calls a distinct MXAccess method and tracks supervisory advice.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AdviseSupervisory_CallsDistinctMxAccessMethodAndTracksSupervisoryAdvice()
{
@@ -341,6 +352,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that UnAdvise command calls MXAccess on the STA thread and removes the tracked advice.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_UnAdvise_CallsMxAccessOnStaAndRemovesTrackedAdvice()
{
@@ -369,6 +381,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Advise preserves the HResult when MXAccess throws and does not track the advice.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AdviseWhenMxAccessThrows_PreservesHResultAndDoesNotTrackAdvice()
{
@@ -399,6 +412,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that UnAdvise preserves the HResult when MXAccess throws and keeps the tracked advice.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_UnAdviseWhenMxAccessThrows_PreservesHResultAndKeepsTrackedAdvice()
{
@@ -433,6 +447,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that SubscribeBulk runs sequential MXAccess calls and returns per-item results.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_SubscribeBulk_RunsSequentialMxAccessCallsAndReturnsPerItemResults()
{
@@ -478,6 +493,7 @@ public sealed class MxAccessCommandExecutorTests
/// one BulkWriteResult per entry in input order, including a per-entry COM
/// failure surfaced as <c>WasSuccessful=false</c> with the underlying HRESULT.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteBulk_RunsSequentialWritesAndReturnsPerEntryResults()
{
@@ -530,6 +546,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Write2Bulk forwards value AND timestamp to each per-entry Write2.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Write2Bulk_ForwardsValueAndTimestampPerEntry()
{
@@ -556,6 +573,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that WriteSecuredBulk forwards both user ids per entry.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteSecuredBulk_ForwardsUserIdsPerEntry()
{
@@ -580,6 +598,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that WriteSecured2Bulk forwards user ids, value, and timestamp per entry.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteSecured2Bulk_ForwardsUserIdsValueAndTimestampPerEntry()
{
@@ -612,6 +631,7 @@ public sealed class MxAccessCommandExecutorTests
/// The fake COM object never fires events so the wait always times out — but
/// the lifecycle calls must still happen, in order, on the STA.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_ReadBulk_WhenTagNotCached_TakesSnapshotLifecycleAndTimesOut()
{
@@ -650,6 +670,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that ReadBulk with no payload returns an invalid request error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_ReadBulkWithoutPayload_ReturnsInvalidRequest()
{
@@ -671,6 +692,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that UnsubscribeBulk removes items after UnAdvise failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_UnsubscribeBulk_RemovesItemAfterUnAdviseFailure()
{
@@ -700,6 +722,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that ShutdownGracefullyAsync cleans up handles in advice, item, server order.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ShutdownGracefullyAsync_CleansHandlesInAdviceItemServerOrder()
{
@@ -725,6 +748,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that ShutdownGracefullyAsync records cleanup failures and continues.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ShutdownGracefullyAsync_RecordsCleanupFailuresAndContinues()
{
@@ -753,6 +777,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Register without payload returns an invalid request error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_RegisterWithoutPayload_ReturnsInvalidRequest()
{
@@ -774,6 +799,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that AddItem without payload returns an invalid request error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AddItemWithoutPayload_ReturnsInvalidRequest()
{
@@ -795,6 +821,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Advise without payload returns an invalid request error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AdviseWithoutPayload_ReturnsInvalidRequest()
{
@@ -816,6 +843,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Write dispatches the converted value to MXAccess on the STA thread.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Write_CallsMxAccessOnStaWithConvertedValue()
{
@@ -838,6 +866,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Write2 forwards the converted value and timestamp to MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Write2_ForwardsValueAndTimestamp()
{
@@ -860,6 +889,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteSecured_ForwardsUserIds()
{
@@ -881,6 +911,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteSecured2_ForwardsUserIdsValueAndTimestamp()
{
@@ -904,6 +935,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Write without a payload returns an invalid request error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteWithoutPayload_ReturnsInvalidRequest()
{
@@ -926,6 +958,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies that Write without a value returns an invalid request error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WriteWithoutValue_ReturnsInvalidRequest()
{
@@ -953,6 +986,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies Suspend calls MXAccess on the STA and maps the native status to MxStatusProxy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Suspend_CallsMxAccessOnStaAndMapsStatus()
{
@@ -977,6 +1011,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies Activate calls MXAccess on the STA and maps the native status to MxStatusProxy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_Activate_CallsMxAccessOnStaAndMapsStatus()
{
@@ -1000,6 +1035,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies AuthenticateUser passes credentials to MXAccess on the STA and returns the user id.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AuthenticateUser_CallsMxAccessOnStaAndReturnsUserId()
{
@@ -1026,6 +1062,7 @@ public sealed class MxAccessCommandExecutorTests
/// command reply or any recorded diagnostic — the password is only ever
/// handed straight to the MXAccess wrapper.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AuthenticateUser_DoesNotLeakPassword()
{
@@ -1051,6 +1088,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies ArchestrAUserToId calls MXAccess on the STA and returns the resolved user id.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_ArchestrAUserToId_CallsMxAccessOnStaAndReturnsUserId()
{
@@ -1072,6 +1110,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies AddBufferedItem calls MXAccess on the STA and tracks the buffered item handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_AddBufferedItem_CallsMxAccessOnStaAndTracksItemHandle()
{
@@ -1104,6 +1143,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// <summary>Verifies SetBufferedUpdateInterval calls MXAccess on the STA and returns a base OK reply.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_SetBufferedUpdateInterval_CallsMxAccessOnStaAndReturnsOk()
{
@@ -1131,6 +1171,7 @@ public sealed class MxAccessCommandExecutorTests
/// <c>throw</c> would propagate an unhandled exception through <c>WorkerPipeSession</c>
/// and no other test would catch it.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WithUnknownCommandKind_ReturnsInvalidRequestWithUnsupportedDiagnostic()
{
@@ -1860,9 +1901,7 @@ public sealed class MxAccessCommandExecutorTests
/// <summary>Gets the list of operations performed on this fake object.</summary>
public IReadOnlyList<string> OperationNames => operationNames.ToArray();
/// <summary>Registers a client and returns a server handle.</summary>
/// <param name="clientName">Name of the client to register.</param>
/// <returns>The server handle for the registered client.</returns>
/// <inheritdoc />
public int Register(string clientName)
{
operationNames.Add($"Register:{clientName}");
@@ -1872,8 +1911,7 @@ public sealed class MxAccessCommandExecutorTests
return registerHandle;
}
/// <summary>Unregisters a server and tracks the operation.</summary>
/// <param name="serverHandle">Server handle to unregister.</param>
/// <inheritdoc />
public void Unregister(int serverHandle)
{
operationNames.Add($"Unregister:{serverHandle}");
@@ -1886,10 +1924,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
/// <summary>Adds an item to the server and returns its item handle.</summary>
/// <param name="serverHandle">Server handle to add the item to.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <returns>The item handle for the added item.</returns>
/// <inheritdoc />
public int AddItem(
int serverHandle,
string itemDefinition)
@@ -1907,11 +1942,7 @@ public sealed class MxAccessCommandExecutorTests
return addItemHandle;
}
/// <summary>Adds an item to the server with context and returns its item handle.</summary>
/// <param name="serverHandle">Server handle to add the item to.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <param name="itemContext">Item context string.</param>
/// <returns>The item handle for the added item.</returns>
/// <inheritdoc />
public int AddItem2(
int serverHandle,
string itemDefinition,
@@ -1931,9 +1962,7 @@ public sealed class MxAccessCommandExecutorTests
return addItem2Handle;
}
/// <summary>Removes an item from the server and tracks the operation.</summary>
/// <param name="serverHandle">Server handle from which to remove the item.</param>
/// <param name="itemHandle">Item handle to remove.</param>
/// <inheritdoc />
public void RemoveItem(
int serverHandle,
int itemHandle)
@@ -1949,9 +1978,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
/// <summary>Advises on item changes and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the advisory subscription.</param>
/// <param name="itemHandle">Item handle to advise on.</param>
/// <inheritdoc />
public void Advise(
int serverHandle,
int itemHandle)
@@ -1967,9 +1994,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
/// <summary>Removes an item advice subscription and tracks the operation.</summary>
/// <param name="serverHandle">Server handle from which to remove the subscription.</param>
/// <param name="itemHandle">Item handle to remove advice from.</param>
/// <inheritdoc />
public void UnAdvise(
int serverHandle,
int itemHandle)
@@ -1985,9 +2010,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
/// <summary>Advises supervisory on item changes and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the supervisory subscription.</param>
/// <param name="itemHandle">Item handle to advise supervisory on.</param>
/// <inheritdoc />
public void AdviseSupervisory(
int serverHandle,
int itemHandle)
@@ -2027,11 +2050,7 @@ public sealed class MxAccessCommandExecutorTests
/// <summary>Gets the thread ID on which the most recent write was called.</summary>
public int? WriteThreadId { get; private set; }
/// <summary>Writes a value to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="value">Value to write.</param>
/// <param name="userId">MXAccess user id for the write.</param>
/// <inheritdoc />
public void Write(
int serverHandle,
int itemHandle,
@@ -2047,12 +2066,7 @@ public sealed class MxAccessCommandExecutorTests
ThrowIfWriteFailureConfigured(itemHandle);
}
/// <summary>Writes a timestamped value to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="value">Value to write.</param>
/// <param name="timestamp">Source timestamp for the write.</param>
/// <param name="userId">MXAccess user id for the write.</param>
/// <inheritdoc />
public void Write2(
int serverHandle,
int itemHandle,
@@ -2070,12 +2084,7 @@ public sealed class MxAccessCommandExecutorTests
ThrowIfWriteFailureConfigured(itemHandle);
}
/// <summary>Performs a secured write to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="currentUserId">Operator user id.</param>
/// <param name="verifierUserId">Verifier user id.</param>
/// <param name="value">Value to write.</param>
/// <inheritdoc />
public void WriteSecured(
int serverHandle,
int itemHandle,
@@ -2093,13 +2102,7 @@ public sealed class MxAccessCommandExecutorTests
ThrowIfWriteFailureConfigured(itemHandle);
}
/// <summary>Performs a secured timestamped write to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="currentUserId">Operator user id.</param>
/// <param name="verifierUserId">Verifier user id.</param>
/// <param name="value">Value to write.</param>
/// <param name="timestamp">Source timestamp for the write.</param>
/// <inheritdoc />
public void WriteSecured2(
int serverHandle,
int itemHandle,
@@ -2181,10 +2184,7 @@ public sealed class MxAccessCommandExecutorTests
/// <summary>Gets the interval passed to SetBufferedUpdateInterval, if called.</summary>
public int? SetBufferedUpdateIntervalValue { get; private set; }
/// <summary>Suspends an item and returns a canned status whose fields drive MxStatusProxy conversion.</summary>
/// <param name="serverHandle">Server handle for the suspend.</param>
/// <param name="itemHandle">Item handle to suspend.</param>
/// <returns>A status stand-in with all-OK fields.</returns>
/// <inheritdoc />
public object Suspend(int serverHandle, int itemHandle)
{
operationNames.Add($"Suspend:{serverHandle}:{itemHandle}");
@@ -2194,10 +2194,7 @@ public sealed class MxAccessCommandExecutorTests
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
}
/// <summary>Activates an item and returns a canned status whose fields drive MxStatusProxy conversion.</summary>
/// <param name="serverHandle">Server handle for the activate.</param>
/// <param name="itemHandle">Item handle to activate.</param>
/// <returns>A status stand-in with all-OK fields.</returns>
/// <inheritdoc />
public object Activate(int serverHandle, int itemHandle)
{
operationNames.Add($"Activate:{serverHandle}:{itemHandle}");
@@ -2207,11 +2204,7 @@ public sealed class MxAccessCommandExecutorTests
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
}
/// <summary>Authenticates a user and returns a canned user id.</summary>
/// <param name="serverHandle">Server handle for the authentication.</param>
/// <param name="verifyUser">User name to authenticate.</param>
/// <param name="verifyUserPassword">Credential; recorded only to assert it is never logged.</param>
/// <returns>The canned MXAccess user id (1).</returns>
/// <inheritdoc />
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
// Deliberately does NOT include the password in the operation log.
@@ -2223,10 +2216,7 @@ public sealed class MxAccessCommandExecutorTests
return 1;
}
/// <summary>Resolves an ArchestrA user GUID and returns a canned user id.</summary>
/// <param name="serverHandle">Server handle for the resolution.</param>
/// <param name="userIdGuid">ArchestrA user GUID to resolve.</param>
/// <returns>The canned MXAccess user id (7).</returns>
/// <inheritdoc />
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
operationNames.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
@@ -2235,11 +2225,7 @@ public sealed class MxAccessCommandExecutorTests
return 7;
}
/// <summary>Adds a buffered item and returns a canned item handle.</summary>
/// <param name="serverHandle">Server handle to add the item to.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <param name="itemContext">Item context string.</param>
/// <returns>The canned buffered item handle (1).</returns>
/// <inheritdoc />
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
operationNames.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
@@ -2249,9 +2235,7 @@ public sealed class MxAccessCommandExecutorTests
return 1;
}
/// <summary>Sets the buffered update interval and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the interval change.</param>
/// <param name="updateIntervalMilliseconds">Buffered update interval in milliseconds.</param>
/// <inheritdoc />
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
operationNames.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
@@ -2274,8 +2258,7 @@ public sealed class MxAccessCommandExecutorTests
/// <summary>Gets the fake COM object.</summary>
public FakeMxAccessComObject FakeComObject { get; }
/// <summary>Creates and returns the fake MXAccess COM object.</summary>
/// <returns>The fake COM object.</returns>
/// <inheritdoc />
public object Create()
{
return FakeComObject;
@@ -15,6 +15,7 @@ public sealed class MxAccessLiveComCreationTests
private const string DefaultLiveAddItem2Context = "TestChildObject";
/// <summary>Verifies that StartAsync creates the installed MXAccess COM object on the STA thread when opted in.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact]
public async Task StartAsync_WhenOptedIn_CreatesInstalledMxAccessComObjectOnSta()
{
@@ -24,6 +25,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// <summary>Verifies that Register and Unregister round-trip server handles with installed MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact]
public async Task RegisterAndUnregister_WhenOptedIn_RoundTripsInstalledMxAccessServerHandle()
{
@@ -61,6 +63,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// <summary>Verifies that AddItem and RemoveItem round-trip item handles with installed MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact]
public async Task AddItemAndRemoveItem_WhenOptedIn_RoundTripsInstalledMxAccessItemHandle()
{
@@ -129,6 +132,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// <summary>Verifies that AddItem2 and RemoveItem preserve item context with installed MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact]
public async Task AddItem2AndRemoveItem_WhenOptedIn_PreservesContextForInstalledMxAccess()
{
@@ -198,6 +202,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// <summary>Verifies that Advise and UnAdvise round-trip subscriptions with installed MXAccess.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact]
public async Task AdviseAndUnAdvise_WhenOptedIn_RoundTripsInstalledMxAccessSubscription()
{
@@ -18,6 +18,7 @@ public sealed class MxAccessStaSessionTests
/// <summary>
/// Verifies that StartAsync creates the MXAccess COM object and attaches the event sink on the STA thread.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StartAsync_CreatesComObjectAndAttachesEventSinkOnStaThread()
{
@@ -42,6 +43,7 @@ public sealed class MxAccessStaSessionTests
/// <summary>
/// Verifies that StartAsync maps creation exceptions with HResult when the factory fails.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StartAsync_WhenFactoryFails_MapsCreationExceptionWithHResult()
{
@@ -63,6 +65,7 @@ public sealed class MxAccessStaSessionTests
/// <summary>
/// Verifies that Dispose detaches the event sink on the STA thread.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Dispose_DetachesEventSinkOnStaThread()
{
@@ -116,9 +119,7 @@ public sealed class MxAccessStaSessionTests
/// </summary>
public ApartmentState? CreateApartmentState { get; private set; }
/// <summary>
/// Creates the COM object or throws the configured exception.
/// </summary>
/// <inheritdoc />
public object Create()
{
CreateThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -158,11 +159,7 @@ public sealed class MxAccessStaSessionTests
/// </summary>
public string? SessionId { get; private set; }
/// <summary>
/// Attaches the MXAccess COM object and records thread context.
/// </summary>
/// <param name="mxAccessComObject">MXAccess COM object to attach.</param>
/// <param name="sessionId">Identifier of the session.</param>
/// <inheritdoc />
public void Attach(
object mxAccessComObject,
string sessionId)
@@ -172,9 +169,7 @@ public sealed class MxAccessStaSessionTests
SessionId = sessionId;
}
/// <summary>
/// Detaches the MXAccess COM object and records thread context.
/// </summary>
/// <inheritdoc />
public void Detach()
{
DetachThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -188,6 +183,7 @@ public sealed class MxAccessStaSessionTests
/// This proves the fix in WorkerPipeSession (and the new internal constructor) correctly
/// wires the factory rather than leaving alarmCommandHandler null.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StartAsync_WithAlarmCommandHandlerFactory_SubscribeAlarmsCommandReachesHandler()
{
@@ -231,6 +227,7 @@ public sealed class MxAccessStaSessionTests
/// test fails if the diagnostic regresses to a misleading message that still
/// happens to contain the word "alarm".
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StartAsync_WithoutAlarmCommandHandlerFactory_SubscribeAlarmsReturnsInvalidRequest()
{
@@ -267,6 +264,7 @@ public sealed class MxAccessStaSessionTests
/// loop calls PollOnce on the handler via the STA within a reasonable timeout.
/// This proves polling is driven by the STA rather than the consumer's internal timer.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StartAsync_WithAlarmCommandHandlerFactory_PollOnceCalledViaSta()
{
@@ -304,6 +302,7 @@ public sealed class MxAccessStaSessionTests
/// immediately after Dispose and stays frozen — deterministic, with no
/// elapsed-time "no further polls" window that a slow agent could race.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Dispose_StopsAlarmPollLoop()
{
@@ -346,12 +345,13 @@ public sealed class MxAccessStaSessionTests
}
/// <summary>
/// Worker-005 regression: when the alarm poll loop's PollOnce throws a
/// When the alarm poll loop's PollOnce throws a
/// real failure (e.g. a COMException from GetXmlCurrentAlarms2), the
/// failure must be recorded as a fault on the event queue so a broken
/// alarm subscription becomes observable on the IPC fault path instead
/// of silently faulting the never-awaited poll task.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAlarmPollLoop_WhenPollOnceThrows_RecordsFaultOnEventQueue()
{
@@ -389,7 +389,7 @@ public sealed class MxAccessStaSessionTests
}
/// <summary>
/// Worker-016 regression: the alarm poll loop's catch for the graceful
/// The alarm poll loop's catch for the graceful
/// STA-runtime-shutdown signal must NOT also swallow a vanilla
/// <see cref="InvalidOperationException"/> raised from inside the marshalled
/// poll lambda — for example the STA-affinity assertion thrown by
@@ -399,6 +399,7 @@ public sealed class MxAccessStaSessionTests
/// <see cref="InvalidOperationException"/> from <c>PollOnce</c> must reach
/// the fault-recording arm and become observable on the event queue.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RunAlarmPollLoop_WhenPollOnceThrowsInvalidOperation_RecordsFaultOnEventQueue()
{
@@ -438,7 +439,7 @@ public sealed class MxAccessStaSessionTests
}
/// <summary>
/// Worker-008 regression: the STA-affinity guard throws when an
/// The STA-affinity guard throws when an
/// IMxAccessAlarmConsumer call is attempted off the thread that created
/// the consumer, mirroring the MxAccessSession.CreationThreadId invariant.
/// </summary>
@@ -455,7 +456,7 @@ public sealed class MxAccessStaSessionTests
}
/// <summary>
/// Worker-008: the STA-affinity guard is a no-op on the owning thread and
/// The STA-affinity guard is a no-op on the owning thread and
/// when no alarm consumer is configured (expected thread id null).
/// </summary>
[Fact]
@@ -495,52 +496,35 @@ public sealed class MxAccessStaSessionTests
get { lock (gate) return lastPollThreadId; }
}
/// <summary>Subscribes to alarm events.</summary>
/// <param name="command">The subscribe-alarms command.</param>
/// <param name="sessionId">The session identifier.</param>
/// <inheritdoc />
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
{
IsSubscribed = true;
LastSubscription = command.SubscriptionExpression;
}
/// <summary>Unsubscribes from alarm events.</summary>
/// <inheritdoc />
public void Unsubscribe()
{
IsSubscribed = false;
}
/// <summary>Acknowledges an alarm by guid.</summary>
/// <param name="alarmGuid">The alarm GUID.</param>
/// <param name="comment">The acknowledgment comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
/// <inheritdoc />
public int Acknowledge(Guid alarmGuid, string comment, string operatorUser,
string operatorNode, string operatorDomain, string operatorFullName)
=> 0;
/// <summary>Acknowledges an alarm by name.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The alarm group name.</param>
/// <param name="comment">The acknowledgment comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
/// <inheritdoc />
public int AcknowledgeByName(string alarmName, string providerName, string groupName,
string comment, string operatorUser, string operatorNode,
string operatorDomain, string operatorFullName)
=> 0;
/// <summary>Queries active alarms.</summary>
/// <param name="alarmFilterPrefix">Optional alarm name filter prefix.</param>
/// <inheritdoc />
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
=> Array.Empty<ActiveAlarmSnapshot>();
/// <summary>Polls for alarm events once.</summary>
/// <inheritdoc />
public void PollOnce()
{
lock (gate)
@@ -115,6 +115,7 @@ public sealed class MxAccessValueCacheTests
}
/// <summary>Verifies that TryWaitForUpdate returns true when the cache is updated after the baseline.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task TryWaitForUpdate_ReturnsTrue_WhenSetFiresAfterBaselineVersion()
{
@@ -294,16 +294,13 @@ public sealed class SubtagAlarmConsumerTests
/// <summary>Gets the most recent (address, value) pair passed to <see cref="Write"/>.</summary>
public (string Address, object? Value)? LastWrite { get; private set; }
/// <summary>Records the advised subtag addresses.</summary>
/// <param name="itemAddresses">The subtag references to advise.</param>
/// <inheritdoc />
public void Advise(IReadOnlyCollection<string> itemAddresses)
{
Advised.AddRange(itemAddresses);
}
/// <summary>Records the most recent write.</summary>
/// <param name="itemAddress">The subtag reference to write.</param>
/// <param name="value">The value to write.</param>
/// <inheritdoc />
public void Write(string itemAddress, object? value)
{
LastWrite = (itemAddress, value);
@@ -23,6 +23,7 @@ public sealed class SubtagAlarmStateMachineTests
AckCommentSubtag = "Tank01.Level.HiHi.ackmsg",
};
/// <summary>Verifies that an active transition from false to true emits an unacked raise.</summary>
[Fact]
public void ActiveFalseToTrue_EmitsRaise()
{
@@ -37,6 +38,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal("Area", e.Record.Group);
}
/// <summary>Verifies that a subtag target built from a real Galaxy area round-trips to native alarmmgr provider/group/tag-name fields.</summary>
[Fact]
public void ActiveFalseToTrue_AlarmMgrShape_EmitsNativeProviderGroupTagName()
{
@@ -59,6 +61,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal("TestMachine_001.TestAlarm001", e.Record.TagName);
}
/// <summary>Verifies that when the alarm reference has no provider "!" separator, the whole reference is used as the tag name.</summary>
[Fact]
public void ActiveFalseToTrue_NoProviderBang_UsesWholeReferenceAsTagName()
{
@@ -77,6 +80,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(string.Empty, e.Record.Group);
}
/// <summary>Verifies that an out-of-order un-ack arriving before the active-false clear still results in an AckRtn transition.</summary>
[Fact]
public void OutOfOrderAckThenClear_StillEmitsAckRtn()
{
@@ -91,6 +95,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.AckRtn, e.Record.State);
}
/// <summary>Verifies that constructing the state machine with two targets sharing an active subtag address throws.</summary>
[Fact]
public void DuplicateActiveSubtag_Throws()
{
@@ -110,7 +115,7 @@ public sealed class SubtagAlarmStateMachineTests
}
/// <summary>
/// Worker-028 regression: two watch-list entries sharing an
/// Two watch-list entries sharing an
/// <see cref="AlarmSubtagTarget.AlarmFullReference"/> (but using distinct
/// subtag addresses) must throw at construction, symmetric with the
/// duplicate-address guard, rather than silently overwriting the earlier
@@ -134,6 +139,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Throws<ArgumentException>(() => new SubtagAlarmStateMachine(new[] { first, second }));
}
/// <summary>Verifies that acking an active, unacked alarm emits an AckAlm transition from UnackAlm.</summary>
[Fact]
public void AckedTrueWhileActive_EmitsAck()
{
@@ -146,6 +152,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.UnackAlm, e.PreviousState);
}
/// <summary>Verifies that clearing an active, unacked alarm emits an UnackRtn transition.</summary>
[Fact]
public void ActiveTrueToFalse_WhileUnacked_EmitsUnackRtn()
{
@@ -157,6 +164,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.UnackRtn, e.Record.State);
}
/// <summary>Verifies that clearing an active, acked alarm emits an AckRtn transition.</summary>
[Fact]
public void ActiveTrueToFalse_WhileAcked_EmitsAckRtn()
{
@@ -169,6 +177,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.AckRtn, e.Record.State);
}
/// <summary>Verifies that <c>SnapshotActive</c> reflects the current active and acked state.</summary>
[Fact]
public void Snapshot_ReflectsActiveAndAckedState()
{
@@ -180,6 +189,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.AckAlm, snap.State);
}
/// <summary>Verifies that a value change on an address not bound to any target produces no events.</summary>
[Fact]
public void UnknownAddress_NoEvents()
{
@@ -189,7 +199,7 @@ public sealed class SubtagAlarmStateMachineTests
}
/// <summary>
/// Worker.Tests-033 regression: an ack arriving while the alarm is NOT
/// An ack arriving while the alarm is NOT
/// active must emit nothing and must NOT latch
/// <c>AckedDuringEpisode</c> — otherwise a stale ack from a prior episode
/// would mis-latch the next raise into a spurious ACK_RTN on clear. The
@@ -214,7 +224,7 @@ public sealed class SubtagAlarmStateMachineTests
}
/// <summary>
/// Worker.Tests-033 regression: a priority-subtag value change must flow
/// A priority-subtag value change must flow
/// through <c>CoerceInt</c> into the emitted record's
/// <see cref="MxAlarmSnapshotRecord.Priority"/>. A non-numeric value must
/// leave the prior priority unchanged (the CoerceInt fallback path).
@@ -35,7 +35,7 @@ public sealed class SyntheticAlarmGuidTests
Assert.NotEqual(Guid.Empty, SyntheticAlarmGuid.ForReference(string.Empty));
/// <summary>
/// Worker-027 regression: <see cref="SyntheticAlarmGuid.ForReference"/>
/// Regression test: <see cref="SyntheticAlarmGuid.ForReference"/>
/// must derive its GUID without routing through
/// <see cref="System.Security.Cryptography"/>, because on net48
/// <c>MD5.Create()</c> throws under the Windows FIPS-compliance policy.
@@ -125,7 +125,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker-001 regression: the consumer must own no internal
/// The consumer must own no internal
/// <see cref="Timer"/>. A thread-pool timer calling the
/// apartment-threaded wnwrap COM object off its owning STA can
/// deadlock on cross-apartment marshaling, so the timer field and
@@ -144,7 +144,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker-001 regression: no public constructor may accept a
/// No public constructor may accept a
/// poll-interval parameter. A non-zero poll interval was the only
/// way to arm the off-STA timer; removing the parameter makes the
/// footgun structurally unreachable.
@@ -163,7 +163,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker.Tests-022: pins the "new alarm sighting" branch of
/// Pins the "new alarm sighting" branch of
/// <see cref="WnWrapAlarmConsumer.ComputeTransitions"/>. A GUID
/// that appears in <c>next</c> but not in <c>previous</c> must
/// produce exactly one transition with
@@ -191,7 +191,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker.Tests-022: pins the "state unchanged" branch. A GUID
/// Pins the "state unchanged" branch. A GUID
/// present in both snapshots with identical
/// <see cref="MxAlarmSnapshotRecord.State"/> must produce no
/// transition — a regression that emits a transition every poll
@@ -218,7 +218,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker.Tests-022: pins the "state changed" branch. A GUID
/// Pins the "state changed" branch. A GUID
/// present in both snapshots with a different state must produce
/// one transition carrying the prior state so the proto layer
/// can distinguish e.g. <c>UnackAlm</c>→<c>AckAlm</c>
@@ -247,7 +247,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker.Tests-022: pins the "alarm cleared from the active set"
/// Pins the "alarm cleared from the active set"
/// branch. AVEVA drops cleared alarms from
/// <c>GetXmlCurrentAlarms2</c>'s active set rather than emitting a
/// transition record. A GUID present in
@@ -273,7 +273,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
/// <summary>
/// Worker.Tests-022: pins the multi-alarm fan-out. Multiple
/// Pins the multi-alarm fan-out. Multiple
/// simultaneous transitions (new + changed + unchanged + dropped)
/// in one snapshot must produce exactly the changed and new
/// entries — not the unchanged and not the dropped.
@@ -555,6 +555,13 @@ public sealed class AlarmSubtagLiveSmokeTests
internal int ptY;
}
/// <summary>Checks the thread message queue for a message, optionally removing it.</summary>
/// <param name="lpMsg">Receives the retrieved message.</param>
/// <param name="hWnd">Window handle to filter messages for, or <see cref="IntPtr.Zero"/> for any window owned by the calling thread.</param>
/// <param name="wMsgFilterMin">Lowest message value to examine.</param>
/// <param name="wMsgFilterMax">Highest message value to examine.</param>
/// <param name="wRemoveMsg">Specifies how the message is handled (e.g. <see cref="PM_REMOVE"/>).</param>
/// <returns><see langword="true"/> if a message is available; otherwise <see langword="false"/>.</returns>
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool PeekMessage(
@@ -564,10 +571,16 @@ public sealed class AlarmSubtagLiveSmokeTests
uint wMsgFilterMax,
uint wRemoveMsg);
/// <summary>Translates virtual-key messages into character messages.</summary>
/// <param name="lpMsg">The message to translate.</param>
/// <returns><see langword="true"/> if the message was translated; otherwise <see langword="false"/>.</returns>
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool TranslateMessage(ref MSG lpMsg);
/// <summary>Dispatches a message to the window procedure.</summary>
/// <param name="lpmsg">The message to dispatch.</param>
/// <returns>The value returned by the window procedure.</returns>
[DllImport("user32.dll")]
internal static extern IntPtr DispatchMessage(ref MSG lpmsg);
}
@@ -27,7 +27,7 @@ public sealed class WnWrapConsumerProbeTests
private static readonly string SubscriptionExpression =
$@"\\{MachineName}\Galaxy!DEV";
// XML query form — per WIN-911 / ArchestrA reference. NODE is the
// XML query form — per ArchestrA reference. NODE is the
// machine, PROVIDER is the literal "Galaxy", GROUP is the area.
private static readonly string XmlAlarmQuery =
"<QUERIES FROM_PRIORITY=\"1\" TO_PRIORITY=\"999\" ALARM_STATE=\"ALL\" DISPLAY_MODE=\"Summary\">" +
@@ -18,6 +18,7 @@ public sealed class StaCommandDispatcherTests
/// <summary>
/// Verifies commands execute on the STA thread in queue order.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_ExecutesCommandsOnStaInQueueOrder()
{
@@ -41,6 +42,7 @@ public sealed class StaCommandDispatcherTests
/// <summary>
/// Verifies executor exceptions are captured as HResult in the reply without exposing message details.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WhenExecutorThrows_ReturnsFailureReplyWithHResult()
{
@@ -64,6 +66,7 @@ public sealed class StaCommandDispatcherTests
/// <summary>
/// Verifies cancellation before execution prevents the command from running.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WhenCanceledBeforeExecution_ReturnsCanceledReplyWithoutExecuting()
{
@@ -96,6 +99,7 @@ public sealed class StaCommandDispatcherTests
/// observed and ignored" from "cancel never checked"; it only proves the
/// in-flight command is not aborted.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WhenCanceledWhileExecuting_DoesNotAbortInFlightCommand()
{
@@ -121,6 +125,7 @@ public sealed class StaCommandDispatcherTests
/// <summary>
/// Verifies shutdown rejects new dispatch attempts.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DispatchAsync_WhenShutdownRequested_RejectsNewCommands()
{
@@ -138,6 +143,7 @@ public sealed class StaCommandDispatcherTests
/// <summary>
/// Verifies shutdown allows the current command to complete but rejects queued commands.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RequestShutdown_RejectsQueuedCommandButLetsCurrentCommandFinish()
{
@@ -162,6 +168,7 @@ public sealed class StaCommandDispatcherTests
/// <summary>
/// Verifies heartbeat reports current command correlation ID and pending command count.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PopulateHeartbeat_ReportsCurrentCorrelationAndPendingCount()
{
@@ -37,6 +37,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that WaitForWorkOrMessages returns promptly when the wake event is already signalled.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WaitForWorkOrMessages_WakeEventAlreadySignalled_ReturnsImmediately()
{
@@ -60,6 +61,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that WaitForWorkOrMessages wakes when the wake event is signalled from another thread.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WaitForWorkOrMessages_WakeEventSignalledDuringWait_Returns()
{
@@ -89,6 +91,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that WaitForWorkOrMessages returns on timeout when the wake event is never signalled.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WaitForWorkOrMessages_WakeEventNeverSignalled_ReturnsAfterTimeout()
{
@@ -113,6 +116,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that a zero timeout (the TimeSpan.Zero conversion branch) returns without blocking.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WaitForWorkOrMessages_ZeroTimeout_ReturnsWithoutBlocking()
{
@@ -137,6 +141,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that PumpPendingMessages returns zero when the STA thread message queue is empty.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PumpPendingMessages_NoMessagesPosted_ReturnsZero()
{
@@ -155,6 +160,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that PumpPendingMessages dispatches and counts messages posted to the STA thread.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PumpPendingMessages_MessagesPostedToStaThread_ReturnsCountProcessed()
{
@@ -179,6 +185,7 @@ public sealed class StaMessagePumpTests
/// <summary>
/// Verifies that WaitForWorkOrMessages returns once a Windows message is posted to the STA thread.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WaitForWorkOrMessages_WindowsMessagePosted_ReturnsForInputAvailable()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.Sta;
public sealed class StaRuntimeTests
{
/// <summary>Verifies that InvokeAsync executes commands on the STA thread.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeAsync_ExecutesCommandOnStaThread()
{
@@ -35,6 +36,7 @@ public sealed class StaRuntimeTests
/// correct dispatch past an arbitrary millisecond budget, which would be a
/// false failure.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeAsync_WakesIdlePumpForQueuedCommand()
{
@@ -84,6 +86,7 @@ public sealed class StaRuntimeTests
}
/// <summary>Verifies that InvokeAsync faults the returned task when a command raises an exception without stopping the runtime.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeAsync_CommandException_FaultsReturnedTaskWithoutStoppingRuntime()
{
@@ -101,14 +104,15 @@ public sealed class StaRuntimeTests
/// <summary>
/// Verifies that InvokeAsync returns a faulted task when called after
/// Shutdown. Worker-016 introduced <see cref="StaRuntimeShutdownException"/>
/// (a dedicated subtype of <see cref="InvalidOperationException"/>) so
/// Shutdown. <see cref="StaRuntimeShutdownException"/> is
/// a dedicated subtype of <see cref="InvalidOperationException"/> so
/// callers — notably <c>MxAccessStaSession.RunAlarmPollLoopAsync</c> —
/// can distinguish the graceful shutdown signal from a vanilla
/// <see cref="InvalidOperationException"/> such as an STA-affinity
/// assertion. The test pins the exact type so a regression that
/// reverts to a plain <c>InvalidOperationException</c> fails here.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeAsync_AfterShutdown_ReturnsFaultedTask()
{
@@ -164,14 +168,14 @@ public sealed class StaRuntimeTests
/// <summary>The thread ID where Uninitialize was called.</summary>
public int? UninitializeThreadId { get; private set; }
/// <summary>Initializes the COM apartment and records the calling thread.</summary>
/// <inheritdoc />
public void Initialize()
{
InitializeCount++;
InitializeThreadId = Thread.CurrentThread.ManagedThreadId;
}
/// <summary>Uninitializes the COM apartment and records the calling thread.</summary>
/// <inheritdoc />
public void Uninitialize()
{
UninitializeCount++;
@@ -46,11 +46,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// <summary>Gets a value indicating whether Dispose was called.</summary>
public bool Disposed { get; private set; }
/// <summary>Starts the worker session with the given session ID and process ID.</summary>
/// <param name="sessionId">The session identifier.</param>
/// <param name="workerProcessId">The worker process ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Worker ready response.</returns>
/// <inheritdoc />
public Task<WorkerReady> StartAsync(
string sessionId,
int workerProcessId,
@@ -65,9 +61,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
});
}
/// <summary>Dispatches a command to the STA thread.</summary>
/// <param name="command">The command to dispatch.</param>
/// <returns>The command reply.</returns>
/// <inheritdoc />
public Task<MxCommandReply> DispatchAsync(StaCommand command)
{
return Task.Run(
@@ -112,8 +106,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
});
}
/// <summary>Captures current heartbeat snapshot.</summary>
/// <returns>Current runtime heartbeat snapshot.</returns>
/// <inheritdoc />
public WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat()
{
lock (gate)
@@ -132,9 +125,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// </summary>
public uint? SuppressDrainForBatchSize { get; set; }
/// <summary>Drains queued events up to the specified limit.</summary>
/// <param name="maxEvents">Maximum events to drain; 0 drains all.</param>
/// <returns>The drained events.</returns>
/// <inheritdoc />
public IReadOnlyList<WorkerEvent> DrainEvents(uint maxEvents)
{
if (SuppressDrainForBatchSize is uint suppressed && maxEvents == suppressed)
@@ -157,8 +148,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
/// <summary>Drains a pending fault if any.</summary>
/// <returns>Pending fault or null.</returns>
/// <inheritdoc />
public WorkerFault? DrainFault()
{
return null;
@@ -168,7 +158,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// Gets a snapshot of every correlation id passed to
/// <see cref="CancelCommand"/>. Recording lets the IPC tests
/// assert that a <c>WorkerCancel</c> envelope dispatched on the
/// gateway side reaches the runtime session — see Worker.Tests-017.
/// gateway side reaches the runtime session.
/// </summary>
public IReadOnlyList<string> CancelledCorrelationIds
{
@@ -189,7 +179,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// command), matching the previous test-double behaviour. Mutated
/// and read under <c>lock(gate)</c> to match the locking convention
/// the rest of this fake uses for <c>cancelledCorrelationIds</c>,
/// <c>snapshot</c>, and <c>events</c> (Worker.Tests-027).
/// <c>snapshot</c>, and <c>events</c>.
/// </summary>
public bool CancelCommandReturnValue
{
@@ -210,9 +200,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
/// <summary>Cancels command by correlation ID.</summary>
/// <param name="correlationId">The command correlation ID.</param>
/// <returns>True if cancelled; false otherwise.</returns>
/// <inheritdoc />
public bool CancelCommand(string correlationId)
{
lock (gate)
@@ -222,16 +210,13 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
/// <summary>Requests graceful shutdown.</summary>
/// <inheritdoc />
public void RequestShutdown()
{
releaseDispatch.Set();
}
/// <summary>Shuts down gracefully within the specified timeout.</summary>
/// <param name="timeout">Shutdown timeout period.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Shutdown result.</returns>
/// <inheritdoc />
public Task<MxAccessShutdownResult> ShutdownGracefullyAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default)
@@ -272,7 +257,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
/// <summary>Disposes resources.</summary>
/// <inheritdoc />
public void Dispose()
{
Disposed = true;
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
/// otherwise report as <c>Passed</c>). Mirrors
/// <c>ZB.MOM.WW.MxGateway.IntegrationTests.LiveMxAccessFactAttribute</c>; both
/// copies bind to the same <c>GatewayContractInfo</c> constant so the
/// env-var name has a single literal source of truth (Worker.Tests-025).
/// env-var name has a single literal source of truth.
/// </summary>
public sealed class LiveMxAccessFactAttribute : FactAttribute
{
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
/// <see cref="MxAccessSession.CreateForTesting"/> but do not exercise any
/// MXAccess COM call. Replaces the per-file <c>NullMxAccessServer</c> copy
/// that previously lived inside <c>AlarmCommandExecutorTests</c> and was
/// constructed via reflection — see Worker.Tests-016 for the rationale.
/// constructed via reflection.
/// </summary>
internal sealed class NoopMxAccessServer : IMxAccessServer
{
@@ -96,7 +96,7 @@ internal sealed class NoopMxAccessServer : IMxAccessServer
/// </summary>
/// <remarks>
/// Previously duplicated as a nested class in <c>MxAccessCommandExecutorTests.FakeMxAccessComObject</c>.
/// Consolidated here per Worker.Tests-034 so a future field change to the real COM struct only
/// Consolidated here so a future field change to the real COM struct only
/// requires updating one place.
/// </remarks>
internal sealed class FakeMxStatus
@@ -12,6 +12,7 @@ internal static class WorkerFrameTestHelpers
{
/// <summary>Builds a length-prefixed frame from a protobuf message.</summary>
/// <param name="message">Message to serialize into the frame payload.</param>
/// <returns>The length-prefixed frame bytes.</returns>
public static byte[] CreateFrame(IMessage message)
{
return CreateFrame(message.ToByteArray());
@@ -19,6 +20,7 @@ internal static class WorkerFrameTestHelpers
/// <summary>Builds a length-prefixed frame from a raw payload.</summary>
/// <param name="payload">Payload bytes to wrap in a frame.</param>
/// <returns>The length-prefixed frame bytes.</returns>
public static byte[] CreateFrame(byte[] payload)
{
byte[] frame = new byte[sizeof(uint) + payload.Length];