133 lines
5.8 KiB
C#
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);
|