rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Sta;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MxAccessStaSession"/>.
|
||||
/// </summary>
|
||||
public sealed class MxAccessStaSessionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that StartAsync creates the MXAccess COM object and attaches the event sink on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StartAsync_CreatesComObjectAndAttachesEventSinkOnStaThread()
|
||||
{
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||
|
||||
WorkerReady ready = await session.StartAsync("session-1", workerProcessId: 1234);
|
||||
|
||||
Assert.Equal(1234, ready.WorkerProcessId);
|
||||
Assert.Equal(MxAccessInteropInfo.ProgId, ready.MxaccessProgid);
|
||||
Assert.Equal(MxAccessInteropInfo.Clsid, ready.MxaccessClsid);
|
||||
Assert.NotNull(ready.ReadyTimestamp);
|
||||
Assert.Equal(runtime.StaThreadId, factory.CreateThreadId);
|
||||
Assert.Equal(runtime.StaThreadId, eventSink.AttachThreadId);
|
||||
Assert.Equal(ApartmentState.STA, factory.CreateApartmentState);
|
||||
Assert.Same(factory.CreatedObject, eventSink.AttachedObject);
|
||||
Assert.Equal("session-1", eventSink.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that StartAsync maps creation exceptions with HResult when the factory fails.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StartAsync_WhenFactoryFails_MapsCreationExceptionWithHResult()
|
||||
{
|
||||
const int hresult = unchecked((int)0x80040154);
|
||||
FakeMxAccessComObjectFactory factory = new(new COMException("Class not registered.", hresult));
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||
|
||||
MxAccessCreationException exception = await Assert.ThrowsAsync<MxAccessCreationException>(
|
||||
() => session.StartAsync(workerProcessId: 1234));
|
||||
|
||||
Assert.Equal(hresult, exception.CapturedHResult);
|
||||
Assert.Equal(MxAccessInteropInfo.ProgId, exception.AttemptedProgId);
|
||||
Assert.Equal(MxAccessInteropInfo.Clsid, exception.AttemptedClsid);
|
||||
Assert.Null(eventSink.AttachedObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Dispose detaches the event sink on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Dispose_DetachesEventSinkOnStaThread()
|
||||
{
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||
await session.StartAsync(workerProcessId: 1234);
|
||||
|
||||
session.Dispose();
|
||||
|
||||
Assert.Equal(runtime.StaThreadId, eventSink.DetachThreadId);
|
||||
}
|
||||
|
||||
private static StaRuntime CreateRuntime()
|
||||
{
|
||||
return new StaRuntime(
|
||||
new NoopComApartmentInitializer(),
|
||||
new StaMessagePump(),
|
||||
TimeSpan.FromMilliseconds(25));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake MXAccess COM object factory for testing.
|
||||
/// </summary>
|
||||
private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory
|
||||
{
|
||||
private readonly Exception? exception;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a fake factory that optionally throws an exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">Exception to throw when Create is called; null to succeed.</param>
|
||||
public FakeMxAccessComObjectFactory(Exception? exception = null)
|
||||
{
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the COM object created by this factory.
|
||||
/// </summary>
|
||||
public object CreatedObject { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the managed thread ID when Create was called.
|
||||
/// </summary>
|
||||
public int? CreateThreadId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the apartment state when Create was called.
|
||||
/// </summary>
|
||||
public ApartmentState? CreateApartmentState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the COM object or throws the configured exception.
|
||||
/// </summary>
|
||||
public object Create()
|
||||
{
|
||||
CreateThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
CreateApartmentState = Thread.CurrentThread.GetApartmentState();
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return CreatedObject;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake MXAccess event sink for testing.
|
||||
/// </summary>
|
||||
private sealed class FakeMxAccessEventSink : IMxAccessEventSink
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the attached MXAccess COM object.
|
||||
/// </summary>
|
||||
public object? AttachedObject { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the managed thread ID when Attach was called.
|
||||
/// </summary>
|
||||
public int? AttachThreadId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the managed thread ID when Detach was called.
|
||||
/// </summary>
|
||||
public int? DetachThreadId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session identifier.
|
||||
/// </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>
|
||||
public void Attach(
|
||||
object mxAccessComObject,
|
||||
string sessionId)
|
||||
{
|
||||
AttachedObject = mxAccessComObject;
|
||||
AttachThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
SessionId = sessionId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches the MXAccess COM object and records thread context.
|
||||
/// </summary>
|
||||
public void Detach()
|
||||
{
|
||||
DetachThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
AttachedObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gap 1: Verifies that when MxAccessStaSession is created with an alarm handler factory,
|
||||
/// a SubscribeAlarms command dispatched through the session reaches the handler.
|
||||
/// This proves the fix in WorkerPipeSession (and the new internal constructor) correctly
|
||||
/// wires the factory rather than leaving alarmCommandHandler null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StartAsync_WithAlarmCommandHandlerFactory_SubscribeAlarmsCommandReachesHandler()
|
||||
{
|
||||
FakeAlarmCommandHandler handler = new();
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(
|
||||
runtime,
|
||||
factory,
|
||||
eventSink,
|
||||
new MxAccessEventQueue(),
|
||||
(_eq, _affinity) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
StaCommand subscribeCommand = new StaCommand(
|
||||
"session-1",
|
||||
"corr-1",
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.SubscribeAlarms,
|
||||
SubscribeAlarms = new SubscribeAlarmsCommand
|
||||
{
|
||||
SubscriptionExpression = @"\\HOST\Galaxy!Area",
|
||||
},
|
||||
});
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(subscribeCommand);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.True(handler.IsSubscribed);
|
||||
Assert.Equal(@"\\HOST\Galaxy!Area", handler.LastSubscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gap 1: Verifies that when MxAccessStaSession is created without an alarm
|
||||
/// command handler factory, SubscribeAlarms returns InvalidRequest with the
|
||||
/// exact "SubscribeAlarms requires an alarm command handler; the worker was
|
||||
/// constructed without one." diagnostic. The full phrase is asserted so the
|
||||
/// test fails if the diagnostic regresses to a misleading message that still
|
||||
/// happens to contain the word "alarm".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StartAsync_WithoutAlarmCommandHandlerFactory_SubscribeAlarmsReturnsInvalidRequest()
|
||||
{
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
// Use the 4-arg (no factory) constructor — equivalent to the old MxAccessStaSession()
|
||||
using MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
StaCommand subscribeCommand = new StaCommand(
|
||||
"session-1",
|
||||
"corr-1",
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.SubscribeAlarms,
|
||||
SubscribeAlarms = new SubscribeAlarmsCommand
|
||||
{
|
||||
SubscriptionExpression = @"\\HOST\Galaxy!Area",
|
||||
},
|
||||
});
|
||||
|
||||
MxCommandReply reply = await session.DispatchAsync(subscribeCommand);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||
Assert.Equal(
|
||||
"SubscribeAlarms requires an alarm command handler; the worker was constructed without one.",
|
||||
reply.DiagnosticMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gap 2: Verifies that after StartAsync with an alarm handler factory, the STA poll
|
||||
/// 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>
|
||||
[Fact]
|
||||
public async Task StartAsync_WithAlarmCommandHandlerFactory_PollOnceCalledViaSta()
|
||||
{
|
||||
FakeAlarmCommandHandler handler = new();
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
using MxAccessStaSession session = new(
|
||||
runtime,
|
||||
factory,
|
||||
eventSink,
|
||||
new MxAccessEventQueue(),
|
||||
(_eq, _affinity) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
// Wait up to 3s for at least one PollOnce call from the STA poll loop.
|
||||
using CancellationTokenSource timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
while (handler.PollCount == 0 && !timeout.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(50, CancellationToken.None);
|
||||
}
|
||||
|
||||
Assert.True(handler.PollCount > 0,
|
||||
"Expected PollOnce to be called at least once by the STA poll loop within 3 seconds.");
|
||||
Assert.NotNull(handler.LastPollThreadId);
|
||||
Assert.Equal(runtime.StaThreadId, handler.LastPollThreadId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gap 2: Verifies that the STA poll loop stops when the session is disposed —
|
||||
/// no further PollOnce calls after disposal. <see cref="MxAccessStaSession.Dispose"/>
|
||||
/// joins the poll task before returning, so once Dispose returns no PollOnce
|
||||
/// call can still be in flight. The test asserts the poll count is frozen
|
||||
/// immediately after Dispose and stays frozen — deterministic, with no
|
||||
/// elapsed-time "no further polls" window that a slow agent could race.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Dispose_StopsAlarmPollLoop()
|
||||
{
|
||||
FakeAlarmCommandHandler handler = new();
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
// using declaration: if an assertion below throws before the explicit
|
||||
// Dispose, the session (its STA poll loop and alarm handler) is still
|
||||
// torn down. Dispose is idempotent, so the explicit call mid-test and
|
||||
// the using-scope call do not conflict.
|
||||
using MxAccessStaSession session = new(
|
||||
runtime,
|
||||
factory,
|
||||
eventSink,
|
||||
new MxAccessEventQueue(),
|
||||
(_eq, _affinity) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
// Wait for at least one poll to occur, then dispose.
|
||||
using CancellationTokenSource initTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
while (handler.PollCount == 0 && !initTimeout.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(50, CancellationToken.None);
|
||||
}
|
||||
|
||||
Assert.True(handler.PollCount > 0, "Prerequisite: poll loop must have fired before dispose.");
|
||||
|
||||
// Dispose joins the poll task; when it returns the loop has stopped
|
||||
// and no PollOnce call is still running.
|
||||
session.Dispose();
|
||||
int pollCountAtDispose = handler.PollCount;
|
||||
|
||||
// The count is already frozen — re-reading after a yield must not
|
||||
// observe any further poll. This is a deterministic check, not a
|
||||
// timing window: a poll cannot start once the joined loop has exited.
|
||||
await Task.Yield();
|
||||
Assert.Equal(pollCountAtDispose, handler.PollCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-005 regression: 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>
|
||||
[Fact]
|
||||
public async Task RunAlarmPollLoop_WhenPollOnceThrows_RecordsFaultOnEventQueue()
|
||||
{
|
||||
FakeAlarmCommandHandler handler = new()
|
||||
{
|
||||
PollException = new System.Runtime.InteropServices.COMException(
|
||||
"GetXmlCurrentAlarms2 failed.", unchecked((int)0x80004005)),
|
||||
};
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
MxAccessEventQueue eventQueue = new();
|
||||
using MxAccessStaSession session = new(
|
||||
runtime,
|
||||
factory,
|
||||
eventSink,
|
||||
eventQueue,
|
||||
(_eq, _affinity) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
// Wait up to 5s for the poll loop to fault the queue.
|
||||
using CancellationTokenSource timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
while (!eventQueue.IsFaulted && !timeout.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(50, CancellationToken.None);
|
||||
}
|
||||
|
||||
Assert.True(eventQueue.IsFaulted, "Expected the alarm poll failure to fault the event queue.");
|
||||
WorkerFault? fault = session.DrainFault();
|
||||
Assert.NotNull(fault);
|
||||
Assert.Equal(WorkerFaultCategory.MxaccessEventConversionFailed, fault!.Category);
|
||||
Assert.Contains("alarm poll failed", fault.DiagnosticMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(typeof(System.Runtime.InteropServices.COMException).FullName, fault.ExceptionType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-016 regression: 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
|
||||
/// <c>EnsureOnAlarmConsumerThread</c> if a regression ever caused the poll
|
||||
/// to run off the alarm-consumer thread. The runtime-shutdown signal is now
|
||||
/// the dedicated <see cref="StaRuntimeShutdownException"/>; a plain
|
||||
/// <see cref="InvalidOperationException"/> from <c>PollOnce</c> must reach
|
||||
/// the fault-recording arm and become observable on the event queue.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAlarmPollLoop_WhenPollOnceThrowsInvalidOperation_RecordsFaultOnEventQueue()
|
||||
{
|
||||
FakeAlarmCommandHandler handler = new()
|
||||
{
|
||||
PollException = new InvalidOperationException(
|
||||
"Alarm consumer accessed off its owning STA thread."),
|
||||
};
|
||||
FakeMxAccessComObjectFactory factory = new();
|
||||
FakeMxAccessEventSink eventSink = new();
|
||||
using StaRuntime runtime = CreateRuntime();
|
||||
MxAccessEventQueue eventQueue = new();
|
||||
using MxAccessStaSession session = new(
|
||||
runtime,
|
||||
factory,
|
||||
eventSink,
|
||||
eventQueue,
|
||||
(_eq, _affinity) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
using CancellationTokenSource timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
while (!eventQueue.IsFaulted && !timeout.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(50, CancellationToken.None);
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
eventQueue.IsFaulted,
|
||||
"Expected the alarm poll InvalidOperationException to fault the event queue, "
|
||||
+ "not be silently swallowed as a shutdown signal.");
|
||||
WorkerFault? fault = session.DrainFault();
|
||||
Assert.NotNull(fault);
|
||||
Assert.Equal(WorkerFaultCategory.MxaccessEventConversionFailed, fault!.Category);
|
||||
Assert.Equal(typeof(InvalidOperationException).FullName, fault.ExceptionType);
|
||||
Assert.Contains("alarm poll failed", fault.DiagnosticMessage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-008 regression: the STA-affinity guard throws when an
|
||||
/// IMxAccessAlarmConsumer call is attempted off the thread that created
|
||||
/// the consumer, mirroring the MxAccessSession.CreationThreadId invariant.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AssertOnAlarmConsumerThread_WhenOffOwningThread_Throws()
|
||||
{
|
||||
const int owningThread = 7;
|
||||
const int otherThread = 99;
|
||||
|
||||
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(
|
||||
() => MxAccessStaSession.AssertOnAlarmConsumerThread(owningThread, otherThread));
|
||||
|
||||
Assert.Contains("off its owning STA thread", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-008: 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]
|
||||
public void AssertOnAlarmConsumerThread_OnOwningThreadOrUnset_DoesNotThrow()
|
||||
{
|
||||
MxAccessStaSession.AssertOnAlarmConsumerThread(expectedThreadId: 42, actualThreadId: 42);
|
||||
MxAccessStaSession.AssertOnAlarmConsumerThread(expectedThreadId: null, actualThreadId: 123);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake alarm command handler that records calls and tracks poll thread.
|
||||
/// </summary>
|
||||
private sealed class FakeAlarmCommandHandler : IAlarmCommandHandler
|
||||
{
|
||||
private readonly object gate = new object();
|
||||
private int pollCount;
|
||||
private int? lastPollThreadId;
|
||||
|
||||
public bool IsSubscribed { get; private set; }
|
||||
public string? LastSubscription { get; private set; }
|
||||
|
||||
/// <summary>Exception thrown by PollOnce; null to succeed.</summary>
|
||||
public Exception? PollException { get; set; }
|
||||
|
||||
public int PollCount
|
||||
{
|
||||
get { lock (gate) return pollCount; }
|
||||
}
|
||||
|
||||
public int? LastPollThreadId
|
||||
{
|
||||
get { lock (gate) return lastPollThreadId; }
|
||||
}
|
||||
|
||||
public void Subscribe(string subscription, string sessionId)
|
||||
{
|
||||
IsSubscribed = true;
|
||||
LastSubscription = subscription;
|
||||
}
|
||||
|
||||
public void Unsubscribe()
|
||||
{
|
||||
IsSubscribed = false;
|
||||
}
|
||||
|
||||
public int Acknowledge(Guid alarmGuid, string comment, string operatorUser,
|
||||
string operatorNode, string operatorDomain, string operatorFullName)
|
||||
=> 0;
|
||||
|
||||
public int AcknowledgeByName(string alarmName, string providerName, string groupName,
|
||||
string comment, string operatorUser, string operatorNode,
|
||||
string operatorDomain, string operatorFullName)
|
||||
=> 0;
|
||||
|
||||
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
||||
=> Array.Empty<ActiveAlarmSnapshot>();
|
||||
|
||||
public void PollOnce()
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
pollCount++;
|
||||
lastPollThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
}
|
||||
|
||||
if (PollException is not null)
|
||||
{
|
||||
throw PollException;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user