using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; internal class FakeFocasClient : IFocasClient { public bool IsConnected { get; private set; } public int ConnectCount { get; private set; } public int DisposeCount { get; private set; } public bool ThrowOnConnect { get; set; } public bool ThrowOnRead { get; set; } public bool ThrowOnWrite { get; set; } public bool ProbeResult { get; set; } = true; public Exception? Exception { get; set; } public Dictionary Values { get; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary ReadStatuses { get; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary WriteStatuses { get; } = new(StringComparer.OrdinalIgnoreCase); public List<(FocasAddress addr, FocasDataType type, object? value)> WriteLog { get; } = new(); public virtual Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken ct) { ConnectCount++; if (ThrowOnConnect) throw Exception ?? new InvalidOperationException(); IsConnected = true; return Task.CompletedTask; } public virtual Task<(object? value, uint status)> ReadAsync( FocasAddress address, FocasDataType type, CancellationToken ct) { if (ThrowOnRead) throw Exception ?? new InvalidOperationException(); var key = address.Canonical; var status = ReadStatuses.TryGetValue(key, out var s) ? s : FocasStatusMapper.Good; var value = Values.TryGetValue(key, out var v) ? v : null; return Task.FromResult((value, status)); } public virtual Task WriteAsync( FocasAddress address, FocasDataType type, object? value, CancellationToken ct) { if (ThrowOnWrite) throw Exception ?? new InvalidOperationException(); WriteLog.Add((address, type, value)); Values[address.Canonical] = value; var status = WriteStatuses.TryGetValue(address.Canonical, out var s) ? s : FocasStatusMapper.Good; return Task.FromResult(status); } public List<(int number, int axis, FocasDataType type)> DiagnosticReads { get; } = new(); public virtual Task<(object? value, uint status)> ReadDiagnosticAsync( int diagNumber, int axisOrZero, FocasDataType type, CancellationToken ct) { if (ThrowOnRead) throw Exception ?? new InvalidOperationException(); DiagnosticReads.Add((diagNumber, axisOrZero, type)); var key = axisOrZero == 0 ? $"DIAG:{diagNumber}" : $"DIAG:{diagNumber}/{axisOrZero}"; var status = ReadStatuses.TryGetValue(key, out var s) ? s : FocasStatusMapper.Good; var value = Values.TryGetValue(key, out var v) ? v : null; return Task.FromResult((value, status)); } public virtual Task ProbeAsync(CancellationToken ct) => Task.FromResult(ProbeResult); /// /// Configurable path count surfaced via — defaults to /// 1 (single-path controller). Tests asserting multi-path behaviour set this to 2..N /// so the driver's PathId validation + cnc_setpath dispatch can be exercised /// without a live CNC (issue #264). /// public int PathCount { get; set; } = 1; /// Ordered log of cnc_setpath calls observed on this fake session. public List SetPathLog { get; } = new(); public virtual Task GetPathCountAsync(CancellationToken ct) => Task.FromResult(PathCount); public virtual Task SetPathAsync(int pathId, CancellationToken ct) { SetPathLog.Add(pathId); return Task.CompletedTask; } /// /// Per-letter / per-path byte storage the coalesced range path reads from. Tests /// populate PmcByteRanges[("R", 1)] = new byte[size] + the corresponding values to /// drive both the per-tag + the coalesced /// path against the same source of truth (issue #266). /// public Dictionary<(string Letter, int PathId), byte[]> PmcByteRanges { get; } = new(); /// /// Ordered log of pmc_rdpmcrng-shaped range calls observed on this fake /// session — one entry per coalesced wire call. Tests assert this count to verify /// coalescing actually collapsed N per-byte reads into one range read (issue #266). /// public List<(string Letter, int PathId, int StartByte, int ByteCount)> RangeReadLog { get; } = new(); public virtual Task<(byte[]? buffer, uint status)> ReadPmcRangeAsync( string letter, int pathId, int startByte, int byteCount, CancellationToken ct) { RangeReadLog.Add((letter, pathId, startByte, byteCount)); if (!PmcByteRanges.TryGetValue((letter.ToUpperInvariant(), pathId), out var src)) return Task.FromResult<(byte[]?, uint)>((new byte[byteCount], FocasStatusMapper.Good)); var buf = new byte[byteCount]; var copy = Math.Min(byteCount, Math.Max(0, src.Length - startByte)); if (copy > 0) Array.Copy(src, startByte, buf, 0, copy); return Task.FromResult<(byte[]?, uint)>((buf, FocasStatusMapper.Good)); } public virtual void Dispose() { DisposeCount++; IsConnected = false; } } internal sealed class FakeFocasClientFactory : IFocasClientFactory { public List Clients { get; } = new(); public Func? Customise { get; set; } public IFocasClient Create() { var c = Customise?.Invoke() ?? new FakeFocasClient(); Clients.Add(c); return c; } }