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; /// /// Issue #233 — RebrowseAsync forces a re-walk of the controller symbol table without /// restarting the driver. Tests cover the call-counting contract (each invocation issues /// a fresh enumeration pass), the IDriverControl interface implementation, and that the /// UDT template cache is dropped so stale shapes don't survive a program-download. /// [Trait("Category", "Unit")] public sealed class AbCipRebrowseTests { [Fact] public async Task RebrowseAsync_runs_enumerator_once_per_call() { var factory = new CountingEnumeratorFactory( new AbCipDiscoveredTag("Pressure", null, AbCipDataType.Real, ReadOnly: false)); await using 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.RebrowseAsync(new RecordingBuilder(), CancellationToken.None); factory.CreateCount.ShouldBe(1); factory.EnumerationCount.ShouldBe(1); await drv.RebrowseAsync(new RecordingBuilder(), CancellationToken.None); factory.CreateCount.ShouldBe(2); factory.EnumerationCount.ShouldBe(2); } [Fact] public async Task RebrowseAsync_emits_discovered_tags_through_supplied_builder() { var factory = new CountingEnumeratorFactory( new AbCipDiscoveredTag("NewTag", null, AbCipDataType.DInt, ReadOnly: false)); await using 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); var builder = new RecordingBuilder(); await drv.RebrowseAsync(builder, CancellationToken.None); builder.Variables.Select(v => v.Info.FullName).ShouldContain("NewTag"); } [Fact] public async Task RebrowseAsync_clears_template_cache() { await using var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.TemplateCache.Put("ab://10.0.0.5/1,0", 42, new AbCipUdtShape("T", 4, [])); drv.TemplateCache.Count.ShouldBe(1); await drv.RebrowseAsync(new RecordingBuilder(), CancellationToken.None); drv.TemplateCache.Count.ShouldBe(0); } [Fact] public async Task AbCipDriver_implements_IDriverControl() { await using var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1"); drv.ShouldBeAssignableTo(); } // ---- 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) { } } } /// /// Tracks both calls (one per discovery / rebrowse pass) and /// (incremented when the resulting enumerator is /// actually iterated). Two consecutive RebrowseAsync calls must bump both counters. /// private sealed class CountingEnumeratorFactory : IAbCipTagEnumeratorFactory { private readonly AbCipDiscoveredTag[] _tags; public int CreateCount { get; private set; } public int EnumerationCount { get; private set; } public CountingEnumeratorFactory(params AbCipDiscoveredTag[] tags) => _tags = tags; public IAbCipTagEnumerator Create() { CreateCount++; return new CountingEnumerator(this); } private sealed class CountingEnumerator(CountingEnumeratorFactory outer) : IAbCipTagEnumerator { public async IAsyncEnumerable EnumerateAsync( AbCipTagCreateParams deviceParams, [EnumeratorCancellation] CancellationToken cancellationToken) { outer.EnumerationCount++; await Task.CompletedTask; foreach (var t in outer._tags) yield return t; } public void Dispose() { } } } }