Add IDriverControl capability interface in Core.Abstractions with a RebrowseAsync(IAddressSpaceBuilder, CancellationToken) hook so operators can force a controller-side @tags re-walk without restarting the driver. AbCipDriver now implements IDriverControl. RebrowseAsync clears the UDT template cache (so stale shapes from a pre-download program don't survive) then runs the same enumerator + builder fan-out as DiscoverAsync, serialised against concurrent discovery / rebrowse via a new SemaphoreSlim. Driver.AbCip.Cli ships a `rebrowse` subcommand mirroring the existing probe / read shape: connects to a single gateway, runs RebrowseAsync against an in-memory builder, and prints discovered tag names so operators can sanity-check the controller's symbol table from a shell. Tests cover: two consecutive RebrowseAsync calls bump the enumerator's Create / Enumerate counters once each, discovered tags reach the supplied builder, the template cache is dropped on rebrowse, and the driver exposes IDriverControl. 313 AbCip unit tests + 17 CLI tests + 37 Core.Abstractions tests pass. Closes #233
142 lines
5.4 KiB
C#
142 lines
5.4 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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<IDriverControl>();
|
|
}
|
|
|
|
// ---- 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) { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tracks both <see cref="Create"/> calls (one per discovery / rebrowse pass) and
|
|
/// <see cref="EnumerationCount"/> (incremented when the resulting enumerator is
|
|
/// actually iterated). Two consecutive RebrowseAsync calls must bump both counters.
|
|
/// </summary>
|
|
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<AbCipDiscoveredTag> EnumerateAsync(
|
|
AbCipTagCreateParams deviceParams,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
outer.EnumerationCount++;
|
|
await Task.CompletedTask;
|
|
foreach (var t in outer._tags) yield return t;
|
|
}
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|