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);