worker(alarms): COM-backed LmxSubtagAlarmSource advising alarm subtags
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Unit-test coverage for <see cref="LmxSubtagAlarmSource"/>'s advise/write
|
||||
/// sequencing and its <c>OnDataChange</c> normalization. The actual
|
||||
/// <c>LMXProxyServerClass</c> COM event subscription cannot be exercised
|
||||
/// without a live MXAccess install, so these tests drive the source through
|
||||
/// its internal <see cref="IMxAccessServer"/> seam and call
|
||||
/// <c>HandleDataChange</c> directly to simulate a COM callback — exactly the
|
||||
/// boundary <c>MxAccessBaseEventSink.OnDataChange</c> uses for the
|
||||
/// per-session pipeline. End-to-end COM delivery is covered by the
|
||||
/// Skip-gated alarm live smoke tests.
|
||||
/// </summary>
|
||||
public sealed class LmxSubtagAlarmSourceTests
|
||||
{
|
||||
private const int FakeServerHandle = 7;
|
||||
|
||||
/// <summary>Verifies the production constructor rejects a null factory.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullFactory_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new LmxSubtagAlarmSource(factory: null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Advise"/> calls AddItem then
|
||||
/// Advise once per distinct address, and is idempotent on a repeated
|
||||
/// address.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Advise_AddsAndAdvisesEachAddressOnce()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Advise(new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" });
|
||||
// Re-advising an already-advised address is a no-op.
|
||||
source.Advise(new[] { "Tank1.Alarm.Subtag" });
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" },
|
||||
server.AddedItems);
|
||||
Assert.Equal(2, server.AdviseCount);
|
||||
// Every advise targeted the supplied server handle.
|
||||
Assert.All(server.AdvisedServerHandles, h => Assert.Equal(FakeServerHandle, h));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a simulated <c>OnDataChange</c> for an advised item handle
|
||||
/// raises <see cref="LmxSubtagAlarmSource.ValueChanged"/> with the
|
||||
/// address that was advised and the delivered value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HandleDataChange_RaisesValueChangedWithAdvisedAddress()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Advise(new[] { "Tank1.Alarm.Subtag" });
|
||||
int itemHandle = server.LastItemHandleFor("Tank1.Alarm.Subtag");
|
||||
|
||||
SubtagValueChange? received = null;
|
||||
source.ValueChanged += (_, change) => received = change;
|
||||
|
||||
source.HandleDataChange(itemHandle, pvItemValue: 42, pftItemTimeStamp: null);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal("Tank1.Alarm.Subtag", received!.ItemAddress);
|
||||
Assert.Equal(42, received.Value);
|
||||
Assert.Equal(DateTimeKind.Utc, received.TimestampUtc.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <c>OnDataChange</c> for an unknown item handle is ignored
|
||||
/// (no <see cref="LmxSubtagAlarmSource.ValueChanged"/> raised).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HandleDataChange_UnknownHandle_DoesNotRaise()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
bool raised = false;
|
||||
source.ValueChanged += (_, _) => raised = true;
|
||||
|
||||
source.HandleDataChange(phItemHandle: 999, pvItemValue: 1, pftItemTimeStamp: null);
|
||||
|
||||
Assert.False(raised);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Write"/> adds the item when
|
||||
/// it was not previously advised and writes with user id 0.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Write_AddsItemWhenUnknownAndWrites()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Write("Tank1.Alarm.AckComment", "acknowledged");
|
||||
|
||||
Assert.Contains("Tank1.Alarm.AckComment", server.AddedItems);
|
||||
Assert.Single(server.Writes);
|
||||
RecordingMxAccessServer.WriteRecord write = server.Writes[0];
|
||||
Assert.Equal(FakeServerHandle, write.ServerHandle);
|
||||
Assert.Equal("acknowledged", write.Value);
|
||||
Assert.Equal(0, write.UserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Write"/> reuses an existing
|
||||
/// item handle (no duplicate AddItem) when the address was already
|
||||
/// advised.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Write_ReusesHandleForAdvisedAddress()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Advise(new[] { "Tank1.Alarm.Subtag" });
|
||||
int adviseAddCount = server.AddedItems.Count;
|
||||
|
||||
source.Write("Tank1.Alarm.Subtag", true);
|
||||
|
||||
// No second AddItem for the already-advised address.
|
||||
Assert.Equal(adviseAddCount, server.AddedItems.Count);
|
||||
Assert.Single(server.Writes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recording <see cref="IMxAccessServer"/> test double that captures the
|
||||
/// AddItem/Advise/Write/UnAdvise/RemoveItem/Unregister calls
|
||||
/// <see cref="LmxSubtagAlarmSource"/> makes and hands out monotonically
|
||||
/// increasing item handles.
|
||||
/// </summary>
|
||||
private sealed class RecordingMxAccessServer : IMxAccessServer
|
||||
{
|
||||
private readonly Dictionary<string, int> handlesByAddress = new(StringComparer.Ordinal);
|
||||
private int nextItemHandle = 100;
|
||||
|
||||
public List<string> AddedItems { get; } = new();
|
||||
|
||||
public int AdviseCount { get; private set; }
|
||||
|
||||
public List<int> AdvisedServerHandles { get; } = new();
|
||||
|
||||
public List<WriteRecord> Writes { get; } = new();
|
||||
|
||||
public int LastItemHandleFor(string itemAddress) => handlesByAddress[itemAddress];
|
||||
|
||||
public int Register(string clientName) => FakeServerHandle;
|
||||
|
||||
public void Unregister(int serverHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public int AddItem(int serverHandle, string itemDefinition)
|
||||
{
|
||||
AddedItems.Add(itemDefinition);
|
||||
int handle = nextItemHandle++;
|
||||
handlesByAddress[itemDefinition] = handle;
|
||||
return handle;
|
||||
}
|
||||
|
||||
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
|
||||
=> AddItem(serverHandle, itemDefinition);
|
||||
|
||||
public void RemoveItem(int serverHandle, int itemHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void Advise(int serverHandle, int itemHandle)
|
||||
{
|
||||
AdviseCount++;
|
||||
AdvisedServerHandles.Add(serverHandle);
|
||||
}
|
||||
|
||||
public void UnAdvise(int serverHandle, int itemHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void AdviseSupervisory(int serverHandle, int itemHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void Write(int serverHandle, int itemHandle, object? value, int userId)
|
||||
=> Writes.Add(new WriteRecord(serverHandle, itemHandle, value, userId));
|
||||
|
||||
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
|
||||
{
|
||||
}
|
||||
|
||||
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
|
||||
{
|
||||
}
|
||||
|
||||
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
|
||||
{
|
||||
}
|
||||
|
||||
internal sealed record WriteRecord(int ServerHandle, int ItemHandle, object? Value, int UserId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user