Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasStatusFlagsTests.cs
T

174 lines
7.3 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
/// <summary>
/// Gitea #257 — <c>cnc_statinfo</c> (Auto / Run / Motion / Mstb / Emergency / Alarm / Edit / TmMode)
/// surfaces as a read-only <c>Status/</c> fixed-tree subtree, authorable as <c>FIXED:Status/Run</c>.
/// The wire client already decoded the call (<c>FocasWireClient.ReadStatusAsync</c>) but nothing consumed it.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasStatusFlagsTests
{
private const string Host = "focas://10.0.0.5:8193";
private static FocasDriver NewDriver(FakeFocasClient client, params FocasTagDefinition[] tags) =>
new(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions(Host)],
Probe = new FocasProbeOptions { Enabled = false },
RawTags = FocasRawTags.Entries(tags),
FixedTree = new FocasFixedTreeOptions
{
Enabled = true,
PollInterval = TimeSpan.FromMilliseconds(10),
ProgramPollInterval = TimeSpan.FromMilliseconds(10),
},
}, "drv-1", new FakeFocasClientFactory { Customise = () => client });
private static async Task WaitAsync(Func<bool> predicate)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
while (DateTime.UtcNow < deadline && !predicate()) await Task.Delay(15);
predicate().ShouldBeTrue("condition never became true");
}
[Fact]
public void The_wire_status_block_maps_field_for_field()
{
var s = Wire.WireFocasClient.MapStatus(new Wire.WireStatus(Auto: 1, Run: 2, Motion: 3, Mstb: 4, Emergency: 5, Alarm: 6, Edit: 7, TmMode: 8));
s.ShouldBe(new FocasStatus(1, 2, 3, 4, 5, 6, 7, 8));
}
[Theory]
[InlineData(0, "RESET")]
[InlineData(1, "STOP")]
[InlineData(2, "HOLD")]
[InlineData(3, "START")]
[InlineData(4, "MSTR")]
[InlineData(9, "Run9")]
public void Run_state_text(int run, string text) => FocasRunState.ToText(run).ShouldBe(text);
[Fact]
public async Task Status_flags_read_good_through_the_fixed_tree_references()
{
var client = new FakeFocasClient { Status = new FocasStatus(Auto: 1, Run: 3, Motion: 1, Mstb: 2, Emergency: 0, Alarm: 1, Edit: 0, TmMode: 4) };
var drv = NewDriver(client);
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus is not null);
string[] fields = ["Auto", "Run", "Motion", "Mstb", "Emergency", "Alarm", "Edit", "TmMode"];
int[] expected = [1, 3, 1, 2, 0, 1, 0, 4];
var read = await drv.ReadAsync([.. fields.Select(f => $"{Host}/Status/{f}")], CancellationToken.None);
for (var i = 0; i < fields.Length; i++)
{
read[i].StatusCode.ShouldBe(FocasStatusMapper.Good, fields[i]);
read[i].Value.ShouldBe(expected[i], fields[i]);
}
var text = await drv.ReadAsync([$"{Host}/Status/RunText"], CancellationToken.None);
text[0].Value.ShouldBe("START");
await drv.ShutdownAsync(CancellationToken.None);
}
[Fact]
public async Task An_authored_FIXED_Status_tag_reads_the_flag()
{
var client = new FakeFocasClient { Status = new FocasStatus(1, 3, 1, 0, 1, 0, 0, 0) };
var drv = NewDriver(client,
new FocasTagDefinition("EStop", Host, "FIXED:Status/Emergency", FocasDataType.Int32, Writable: false));
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus is not null);
var snap = (await drv.ReadAsync(["EStop"], CancellationToken.None))[0];
snap.StatusCode.ShouldBe(FocasStatusMapper.Good);
snap.Value.ShouldBe(1);
await drv.ShutdownAsync(CancellationToken.None);
}
[Fact]
public async Task Status_updates_follow_the_machine()
{
var client = new FakeFocasClient { Status = new FocasStatus(1, 0, 0, 0, 0, 0, 0, 0) };
var drv = NewDriver(client);
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus is not null);
client.Status = new FocasStatus(1, 3, 1, 0, 0, 0, 0, 0);
await WaitAsync(() => drv.GetDeviceState(Host)?.LastStatus?.Run == 3);
(await drv.ReadAsync([$"{Host}/Status/Run"], CancellationToken.None))[0].Value.ShouldBe(3);
await drv.ShutdownAsync(CancellationToken.None);
}
[Fact]
public async Task A_cnc_that_refuses_statinfo_gets_no_Status_node_and_the_rest_of_the_tree_survives()
{
var client = new FakeFocasClient { ThrowOnStatus = true };
var drv = NewDriver(client);
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitAsync(() => drv.GetDeviceState(Host)?.FixedTreeCache is not null);
var builder = new RecordingBuilder();
await drv.DiscoverAsync(builder, CancellationToken.None);
builder.Folders.ShouldNotContain(f => f.BrowseName == "Status");
builder.Variables.ShouldContain(v => v.Info.FullName == "FIXED:Identity/SeriesNumber");
await drv.ShutdownAsync(CancellationToken.None);
}
[Fact]
public async Task Discovery_emits_every_status_leaf_with_a_parseable_read_only_address()
{
var drv = NewDriver(new FakeFocasClient());
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitAsync(() => drv.GetDeviceState(Host)?.FixedTreeCache is not null);
var builder = new RecordingBuilder();
await drv.DiscoverAsync(builder, CancellationToken.None);
builder.Folders.ShouldContain(f => f.BrowseName == "Status");
var leaves = builder.Variables.Where(v => v.Info.FullName.StartsWith("FIXED:Status/", StringComparison.Ordinal)).ToList();
leaves.Select(l => l.BrowseName).ShouldBe(
["Auto", "Run", "Motion", "Mstb", "Emergency", "Alarm", "Edit", "TmMode", "RunText"], ignoreOrder: true);
foreach (var (_, info) in leaves)
{
FocasAddress.TryParse(info.FullName).ShouldNotBeNull();
info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly);
}
await drv.ShutdownAsync(CancellationToken.None);
}
private sealed class RecordingBuilder : IAddressSpaceBuilder
{
public List<(string BrowseName, string DisplayName)> Folders { get; } = [];
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = [];
public IAddressSpaceBuilder Folder(string browseName, string displayName)
{ Folders.Add((browseName, displayName)); return this; }
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
public void AddProperty(string name, DriverDataType type, object? value) { }
private sealed class Handle(string fullRef) : IVariableHandle
{
public string FullReference => fullRef;
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
}
private sealed class NullSink : IAlarmConditionSink
{
public void OnTransition(AlarmEventArgs args) { }
}
}
}