283 lines
11 KiB
C#
283 lines
11 KiB
C#
using System.Runtime.CompilerServices;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipDriverDiscoveryTests
|
|
{
|
|
[Fact]
|
|
public async Task PreDeclared_tags_emit_as_variables_under_device_folder()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", DeviceName: "Line1-PLC")],
|
|
Tags =
|
|
[
|
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt),
|
|
new AbCipTagDefinition("Temperature", "ab://10.0.0.5/1,0", "T", AbCipDataType.Real, Writable: false),
|
|
],
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Folders.ShouldContain(f => f.BrowseName == "AbCip");
|
|
builder.Folders.ShouldContain(f => f.BrowseName == "ab://10.0.0.5/1,0" && f.DisplayName == "Line1-PLC");
|
|
builder.Variables.Count.ShouldBe(2);
|
|
builder.Variables.Single(v => v.BrowseName == "Speed").Info.SecurityClass.ShouldBe(SecurityClassification.Operate);
|
|
builder.Variables.Single(v => v.BrowseName == "Temperature").Info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Device_folder_displayname_falls_back_to_host_address()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], // no DeviceName
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Folders.ShouldContain(f => f.BrowseName == "ab://10.0.0.5/1,0"
|
|
&& f.DisplayName == "ab://10.0.0.5/1,0");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PreDeclared_system_tags_are_filtered_out()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbCipTagDefinition("__DEFVAL_X", "ab://10.0.0.5/1,0", "__DEFVAL_X", AbCipDataType.DInt),
|
|
new AbCipTagDefinition("Routine:SomeRoutine", "ab://10.0.0.5/1,0", "R", AbCipDataType.DInt),
|
|
new AbCipTagDefinition("UserTag", "ab://10.0.0.5/1,0", "U", AbCipDataType.DInt),
|
|
],
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Variables.Select(v => v.BrowseName).ShouldBe(["UserTag"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Tags_for_mismatched_device_are_ignored()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "O", AbCipDataType.DInt)],
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Variables.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Controller_enumeration_adds_tags_under_Discovered_folder()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var enumeratorFactory = new FakeEnumeratorFactory(
|
|
new AbCipDiscoveredTag("Pressure", null, AbCipDataType.Real, ReadOnly: false),
|
|
new AbCipDiscoveredTag("StepIndex", ProgramScope: "MainProgram", AbCipDataType.DInt, ReadOnly: false));
|
|
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
EnableControllerBrowse = true,
|
|
}, "drv-1", enumeratorFactory: enumeratorFactory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Folders.ShouldContain(f => f.BrowseName == "Discovered");
|
|
builder.Variables.Select(v => v.Info.FullName).ShouldContain("Pressure");
|
|
builder.Variables.Select(v => v.Info.FullName).ShouldContain("Program:MainProgram.StepIndex");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Controller_enumeration_honours_system_tag_hint_and_filter()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var factory = new FakeEnumeratorFactory(
|
|
new AbCipDiscoveredTag("HiddenByHint", null, AbCipDataType.DInt, ReadOnly: false, IsSystemTag: true),
|
|
new AbCipDiscoveredTag("Routine:Foo", null, AbCipDataType.DInt, ReadOnly: false, IsSystemTag: false),
|
|
new AbCipDiscoveredTag("KeepMe", null, AbCipDataType.DInt, ReadOnly: false));
|
|
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
EnableControllerBrowse = true,
|
|
}, "drv-1", enumeratorFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Variables.Select(v => v.Info.FullName).ShouldBe(["KeepMe"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Controller_enumeration_ReadOnly_surfaces_ViewOnly_classification()
|
|
{
|
|
var builder = new RecordingBuilder();
|
|
var factory = new FakeEnumeratorFactory(
|
|
new AbCipDiscoveredTag("SafetyTag", null, AbCipDataType.DInt, ReadOnly: true));
|
|
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
EnableControllerBrowse = true,
|
|
}, "drv-1", enumeratorFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(builder, CancellationToken.None);
|
|
|
|
builder.Variables.Single().Info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Controller_enumeration_receives_correct_device_params()
|
|
{
|
|
var factory = new FakeEnumeratorFactory();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5:44818/1,2,3", AbCipPlcFamily.ControlLogix)],
|
|
Timeout = TimeSpan.FromSeconds(7),
|
|
EnableControllerBrowse = true,
|
|
}, "drv-1", enumeratorFactory: factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.DiscoverAsync(new RecordingBuilder(), CancellationToken.None);
|
|
|
|
var capturedParams = factory.LastDeviceParams.ShouldNotBeNull();
|
|
capturedParams.Gateway.ShouldBe("10.0.0.5");
|
|
capturedParams.Port.ShouldBe(44818);
|
|
capturedParams.CipPath.ShouldBe("1,2,3");
|
|
capturedParams.LibplctagPlcAttribute.ShouldBe("controllogix");
|
|
capturedParams.TagName.ShouldBe("@tags");
|
|
capturedParams.Timeout.ShouldBe(TimeSpan.FromSeconds(7));
|
|
}
|
|
|
|
[Fact]
|
|
public void Default_enumerator_factory_is_used_when_not_injected()
|
|
{
|
|
// Sanity — absent enumerator factory does not crash discovery + uses EmptyAbCipTagEnumerator
|
|
// (covered by the other tests which instantiate without injecting a factory).
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
}, "drv-1");
|
|
drv.ShouldNotBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("__DEFVAL_X", true)]
|
|
[InlineData("__DEFAULT_Y", true)]
|
|
[InlineData("Routine:Main", true)]
|
|
[InlineData("Task:MainTask", true)]
|
|
[InlineData("Local:1:I", true)]
|
|
[InlineData("Map:Alias", true)]
|
|
[InlineData("Axis:MoveX", true)]
|
|
[InlineData("Cam:Profile1", true)]
|
|
[InlineData("MotionGroup:MG0", true)]
|
|
[InlineData("Motor1", false)]
|
|
[InlineData("Program:Main.Step", false)]
|
|
[InlineData("Recipe_2", false)]
|
|
[InlineData("", true)]
|
|
[InlineData(" ", true)]
|
|
public void SystemTagFilter_rejects_infrastructure_names(string name, bool expected)
|
|
{
|
|
AbCipSystemTagFilter.IsSystemTag(name).ShouldBe(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void TemplateCache_roundtrip_put_get()
|
|
{
|
|
var cache = new AbCipTemplateCache();
|
|
var shape = new AbCipUdtShape("MyUdt", 32,
|
|
[
|
|
new AbCipUdtMember("A", 0, AbCipDataType.DInt, ArrayLength: 1),
|
|
new AbCipUdtMember("B", 4, AbCipDataType.Real, ArrayLength: 1),
|
|
]);
|
|
cache.Put("ab://10.0.0.5/1,0", 42, shape);
|
|
|
|
cache.TryGet("ab://10.0.0.5/1,0", 42).ShouldBe(shape);
|
|
cache.TryGet("ab://10.0.0.5/1,0", 99).ShouldBeNull();
|
|
cache.Count.ShouldBe(1);
|
|
|
|
cache.Clear();
|
|
cache.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FlushOptionalCachesAsync_clears_template_cache()
|
|
{
|
|
var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1");
|
|
drv.TemplateCache.Put("dev", 1, new AbCipUdtShape("T", 4, []));
|
|
drv.TemplateCache.Count.ShouldBe(1);
|
|
|
|
await drv.FlushOptionalCachesAsync(CancellationToken.None);
|
|
drv.TemplateCache.Count.ShouldBe(0);
|
|
}
|
|
|
|
// ---- helpers ----
|
|
|
|
private sealed class RecordingBuilder : IAddressSpaceBuilder
|
|
{
|
|
public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
|
|
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
|
|
|
|
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 _, DriverDataType __, object? ___) { }
|
|
|
|
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) { }
|
|
}
|
|
}
|
|
|
|
private sealed class FakeEnumeratorFactory : IAbCipTagEnumeratorFactory
|
|
{
|
|
private readonly AbCipDiscoveredTag[] _tags;
|
|
public AbCipTagCreateParams? LastDeviceParams { get; private set; }
|
|
public FakeEnumeratorFactory(params AbCipDiscoveredTag[] tags) => _tags = tags;
|
|
public IAbCipTagEnumerator Create() => new FakeEnumerator(this);
|
|
|
|
private sealed class FakeEnumerator(FakeEnumeratorFactory outer) : IAbCipTagEnumerator
|
|
{
|
|
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
|
AbCipTagCreateParams deviceParams,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
outer.LastDeviceParams = deviceParams;
|
|
await Task.CompletedTask;
|
|
foreach (var t in outer._tags) yield return t;
|
|
}
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|