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.IntegrationTests; /// /// PR abcip-4.4 — end-to-end coverage that writing _RefreshTagDb on the /// synthetic system folder dispatches to /// against a live ab_server. Mirrors /// but exercises the write entry point so the same outcome (template cache cleared, /// enumerator re-walked) is observable through the OPC UA write surface. /// [Trait("Category", "Integration")] [Trait("Requires", "AbServer")] public sealed class AbCipRefreshTagDbTests { [AbServerFact] public async Task RefreshTagDb_write_invokes_rebrowse_and_bumps_counter() { var profile = KnownProfiles.ControlLogix; var fixture = new AbServerFixture(profile); await fixture.InitializeAsync(); try { var deviceUri = $"ab://127.0.0.1:{fixture.Port}/1,0"; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)], Tags = [new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)], EnableControllerBrowse = true, Timeout = TimeSpan.FromSeconds(5), }, "drv-refresh-tagdb"); await drv.InitializeAsync("{}", CancellationToken.None); // Discovery primes the cached builder so the subsequent _RefreshTagDb write // has a target to dispatch to. The same fixture pattern from AbCipRebrowseTests // is exercised here through the write surface instead of a direct // RebrowseAsync call. var builder = new RecordingBuilder(); await drv.DiscoverAsync(builder, CancellationToken.None); // Seed the template cache so we can assert RebrowseAsync clears it — same // behavioural contract as the unit test, validated against a live walker. drv.TemplateCache.Put(deviceUri, 42, new AbCipUdtShape("T", 4, [])); drv.TemplateCache.Count.ShouldBe(1); var refreshRef = $"_System/{deviceUri}/{AbCipSystemTagSource.RefreshTagDbName}"; var results = await drv.WriteAsync( [new WriteRequest(refreshRef, true)], CancellationToken.None); results[0].StatusCode.ShouldBe(AbCipStatusMapper.Good); // RebrowseAsync drops the template cache + the diagnostics counter advances. drv.TemplateCache.Count.ShouldBe(0); drv.SystemTagSource.GetRefreshTriggerCount(deviceUri).ShouldBe(1); drv.GetHealth().DiagnosticsOrEmpty["AbCip.RefreshTriggers"].ShouldBe(1); await drv.ShutdownAsync(CancellationToken.None); } finally { await fixture.DisposeAsync(); } } 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) { } } } }