Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/CapturingBuilder.cs
T

133 lines
5.8 KiB
C#

using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
/// <summary>
/// An <see cref="IAddressSpaceBuilder"/> that records the tree a driver streams into it, so
/// <c>DiscoverAsync</c> can be asserted on <b>shape</b> (which node landed under which folder)
/// and not merely on a flat list of leaves.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why this exists rather than a reference to the shipped capturing builder.</b> Two
/// production capturing builders exist — <c>Commons.Browsing.CapturingAddressSpaceBuilder</c>
/// (universal browser) and <c>Runtime.Drivers.CapturingAddressSpaceBuilder</c> (deploy-time
/// discovery) — but this suite deliberately references only the driver and its
/// <c>.Contracts</c>, so neither is reachable without pulling the whole server stack into a
/// driver unit-test project. Both also flatten differently from what these tests must prove:
/// the browser's node id for a folder is a synthetic path while a leaf's id is its
/// <c>FullName</c>, which is exactly the conflation a "did the device-level data item land in
/// the device folder?" assertion needs to see through.
/// </para>
/// <para>
/// <b>Deliberately not thread-safe.</b> A driver streams discovery on one caller; recording
/// behind a lock would hide a driver that fanned out onto other threads.
/// </para>
/// </remarks>
internal sealed class CapturingBuilder : IAddressSpaceBuilder
{
private readonly State _state;
private readonly string _path;
/// <summary>Creates a root builder — the scope a driver's <c>DiscoverAsync</c> is handed.</summary>
public CapturingBuilder()
{
_state = new State();
_path = string.Empty;
}
private CapturingBuilder(State state, string path)
{
_state = state;
_path = path;
}
/// <summary>Every folder streamed, in call order, with the slash-joined path it landed at.</summary>
public IReadOnlyList<CapturedFolder> Folders => _state.Folders;
/// <summary>Every variable streamed, in call order, with the folder path it landed under.</summary>
public IReadOnlyList<CapturedVariable> Variables => _state.Variables;
/// <summary>Every property streamed, with the scope path it was added to.</summary>
public IReadOnlyList<CapturedProperty> Properties => _state.Properties;
/// <inheritdoc/>
public IAddressSpaceBuilder Folder(string browseName, string displayName)
{
var path = _path.Length == 0 ? browseName : $"{_path}/{browseName}";
_state.Folders.Add(new CapturedFolder(path, _path, browseName, displayName));
return new CapturingBuilder(_state, path);
}
/// <inheritdoc/>
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
{
var captured = new CapturedVariable(_path, browseName, displayName, attributeInfo);
_state.Variables.Add(captured);
return new CapturedVariableHandle(captured);
}
/// <inheritdoc/>
public void AddProperty(string browseName, DriverDataType dataType, object? value) =>
_state.Properties.Add(new CapturedProperty(_path, browseName, dataType, value));
private sealed class State
{
public List<CapturedFolder> Folders { get; } = [];
public List<CapturedVariable> Variables { get; } = [];
public List<CapturedProperty> Properties { get; } = [];
}
private sealed class CapturedVariableHandle(CapturedVariable variable) : IVariableHandle
{
public string FullReference => variable.Attr.FullName;
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
{
variable.AlarmConditions.Add(info);
return new NullSink();
}
private sealed class NullSink : IAlarmConditionSink
{
public void OnTransition(AlarmEventArgs args)
{
// Nothing to record: no test in this suite drives an alarm transition through
// discovery, and a sink that threw would fail a driver that legitimately never
// pushes one.
}
}
}
}
/// <summary>One folder captured from a discovery stream.</summary>
/// <param name="Path">Slash-joined path of the folder itself.</param>
/// <param name="ParentPath">Slash-joined path of the scope it was added to (empty at the root).</param>
/// <param name="BrowseName">The browse name the driver supplied.</param>
/// <param name="DisplayName">The display name the driver supplied.</param>
internal sealed record CapturedFolder(string Path, string ParentPath, string BrowseName, string DisplayName);
/// <summary>One variable captured from a discovery stream.</summary>
/// <param name="ParentPath">Slash-joined path of the folder it landed under (empty at the root).</param>
/// <param name="BrowseName">The browse name the driver supplied.</param>
/// <param name="DisplayName">The display name the driver supplied.</param>
/// <param name="Attr">The driver-side attribute metadata the driver stamped on it.</param>
internal sealed record CapturedVariable(
string ParentPath, string BrowseName, string DisplayName, DriverAttributeInfo Attr)
{
/// <summary>Alarm conditions the driver marked this variable with, if any.</summary>
public List<AlarmConditionInfo> AlarmConditions { get; } = [];
}
/// <summary>One property captured from a discovery stream.</summary>
/// <param name="ScopePath">Slash-joined path of the node it was added to.</param>
/// <param name="BrowseName">The property browse name.</param>
/// <param name="DataType">The property data type.</param>
/// <param name="Value">The property value.</param>
internal sealed record CapturedProperty(string ScopePath, string BrowseName, DriverDataType DataType, object? Value);