using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using static ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.ServerEventRig;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
///
/// Gitea #626 over the wire — an MTConnect CONDITION transition, streamed by a scripted Agent through the real
/// , projected by the production and written
/// through the production sink, reaches a client's event subscription on the RAW DEVICE FOLDER as a Part 9
/// condition event carrying the driver's message — then its return to normal does too.
///
///
/// The one seam not exercised is DriverHostActor's routing (ConditionId → condition NodeId), which
/// DriverHostActorNativeAlarmTests covers; here the driver's ConditionId is asserted to BE the
/// materialised condition's RawPath, which is the whole contract that routing relies on.
///
public sealed class MTConnectConditionAlarmWireTests
{
private const string DataItemId = "dev1_system_cond";
[Fact]
public async Task A_condition_fault_and_its_clear_reach_the_raw_device_folder_as_part9_events()
{
var ct = TestContext.Current.CancellationToken;
var rig = await StartAsync("MTConnectConditionWire", encrypted: false, ct);
try
{
rig.MaterialiseNative();
using var session = await rig.ConnectAsync(anonymous: true, ct);
var events = new RecordedEvents();
var subscription = await RecordedEvents.SubscribeAsync(session, events, ct);
var rawNs = (ushort)session.NamespaceUris.GetIndex(V3NodeIds.RawNamespaceUri);
var folderItem = RecordedEvents.AddEventItem(subscription, new NodeId(RawDeviceFolder, rawNs));
await subscription.ApplyChangesAsync(ct);
folderItem.Status.Error.ShouldBeNull();
var agent = new ScriptedAgent();
var driver = new MTConnectDriver(
new ZB.MOM.WW.OtOpcUa.Driver.MTConnect.MTConnectDriverOptions
{
AgentUri = "http://scripted-agent:5000",
RawTags = [new RawTagEntry(RawAlarmPath, $$"""{"address":"{{DataItemId}}"}""", false, "dev1")],
},
"mt-wire",
_ => agent);
// What DriverHostActor.ForwardNativeAlarm does for a routed transition, minus the routing.
var projector = new NativeAlarmProjector();
var routed = new List();
driver.OnAlarmEvent += (_, e) =>
{
lock (routed) routed.Add(e.ConditionId);
rig.Sink.WriteAlarmCondition(e.ConditionId, projector.Project(e.ConditionId, e), e.SourceTimestampUtc, AddressSpaceRealm.Raw);
};
await driver.InitializeAsync("{}", ct);
await driver.SubscribeAlarmsAsync([RawAlarmPath], ct);
agent.Push(Condition("Fault", "E17", "Spindle overtemperature", 108));
const string FaultMessage = "Spindle overtemperature [nativeCode=E17]";
await RecordedEvents.WaitUntilAsync(() => events.Has(folderItem, FaultMessage));
agent.Push(Condition("Normal", null, null, 109));
await RecordedEvents.WaitUntilAsync(() => events.Has(folderItem, "SYSTEM Normal"));
lock (routed) routed.ShouldAllBe(id => id == RawAlarmPath, "ConditionId must be the alarm tag's RawPath");
var fault = events.ConditionEvents(folderItem, FaultMessage).ShouldHaveSingleItem();
fault[RecordedEvents.AckedField].Value.ShouldBe(false, "a fresh fault is unacknowledged");
await driver.ShutdownAsync(ct);
}
finally
{
await rig.DisposeAsync();
}
}
private static MTConnectStreamsResult Condition(string state, string? nativeCode, string? text, long sequence) =>
new(1, sequence + 1, sequence,
[
new MTConnectObservation(DataItemId, state, DateTime.UtcNow,
Condition: new MTConnectConditionDetail(text, nativeCode, null, null, "SYSTEM")),
]);
/// An Agent whose /current reports the condition Normal and whose /sample a test drives.
private sealed class ScriptedAgent : IMTConnectAgentClient
{
private readonly Channel _chunks = Channel.CreateUnbounded();
public void Push(MTConnectStreamsResult chunk) => _chunks.Writer.TryWrite(chunk);
public Task ProbeAsync(CancellationToken ct) =>
Task.FromResult(new MTConnectProbeModel(
[
new MTConnectDevice("dev1", "dev1", [],
[new MTConnectDataItem(DataItemId, "system", "CONDITION", "SYSTEM", null, null, null, null)]),
]));
public Task CurrentAsync(CancellationToken ct) =>
Task.FromResult(new MTConnectStreamsResult(1, 108, 1,
[
new MTConnectObservation(DataItemId, "Normal", DateTime.UtcNow,
Condition: new MTConnectConditionDetail(null, null, null, null, "SYSTEM")),
]));
public async IAsyncEnumerable SampleAsync(
long from, [EnumeratorCancellation] CancellationToken ct)
{
while (true)
{
yield return await _chunks.Reader.ReadAsync(ct);
}
}
public Task WriteObservationAsync(
string deviceName, string dataItemId, string value, CancellationToken cancellationToken) =>
throw new NotSupportedException();
public void Dispose() => _chunks.Writer.TryComplete();
}
}