using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
///
/// An that records the tree a driver streams into it, so
/// DiscoverAsync can be asserted on shape (which node landed under which folder)
/// and not merely on a flat list of leaves.
///
///
///
/// Why this exists rather than a reference to the shipped capturing builder. Two
/// production capturing builders exist — Commons.Browsing.CapturingAddressSpaceBuilder
/// (universal browser) and Runtime.Drivers.CapturingAddressSpaceBuilder (deploy-time
/// discovery) — but this suite deliberately references only the driver and its
/// .Contracts, 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
/// FullName, which is exactly the conflation a "did the device-level data item land in
/// the device folder?" assertion needs to see through.
///
///
/// Deliberately not thread-safe. A driver streams discovery on one caller; recording
/// behind a lock would hide a driver that fanned out onto other threads.
///
///
internal sealed class CapturingBuilder : IAddressSpaceBuilder
{
private readonly State _state;
private readonly string _path;
/// Creates a root builder — the scope a driver's DiscoverAsync is handed.
public CapturingBuilder()
{
_state = new State();
_path = string.Empty;
}
private CapturingBuilder(State state, string path)
{
_state = state;
_path = path;
}
/// Every folder streamed, in call order, with the slash-joined path it landed at.
public IReadOnlyList Folders => _state.Folders;
/// Every variable streamed, in call order, with the folder path it landed under.
public IReadOnlyList Variables => _state.Variables;
/// Every property streamed, with the scope path it was added to.
public IReadOnlyList Properties => _state.Properties;
///
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);
}
///
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);
}
///
public void AddProperty(string browseName, DriverDataType dataType, object? value) =>
_state.Properties.Add(new CapturedProperty(_path, browseName, dataType, value));
private sealed class State
{
public List Folders { get; } = [];
public List Variables { get; } = [];
public List 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.
}
}
}
}
/// One folder captured from a discovery stream.
/// Slash-joined path of the folder itself.
/// Slash-joined path of the scope it was added to (empty at the root).
/// The browse name the driver supplied.
/// The display name the driver supplied.
internal sealed record CapturedFolder(string Path, string ParentPath, string BrowseName, string DisplayName);
/// One variable captured from a discovery stream.
/// Slash-joined path of the folder it landed under (empty at the root).
/// The browse name the driver supplied.
/// The display name the driver supplied.
/// The driver-side attribute metadata the driver stamped on it.
internal sealed record CapturedVariable(
string ParentPath, string BrowseName, string DisplayName, DriverAttributeInfo Attr)
{
/// Alarm conditions the driver marked this variable with, if any.
public List AlarmConditions { get; } = [];
}
/// One property captured from a discovery stream.
/// Slash-joined path of the node it was added to.
/// The property browse name.
/// The property data type.
/// The property value.
internal sealed record CapturedProperty(string ScopePath, string BrowseName, DriverDataType DataType, object? Value);