Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs
2026-04-26 04:54:28 -04:00

197 lines
9.0 KiB
C#

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<string, object?> Values { get; } = new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, uint> ReadStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, uint> WriteStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
public List<(FocasAddress addr, FocasDataType type, object? value)> WriteLog { get; } = new();
/// <summary>
/// Plan PR F4-b (issue #269) — separate log of <c>cnc_wrparam</c>-shaped calls
/// observed via <see cref="WriteParameterAsync"/>. Tests assert this list to
/// verify the driver routed PARAM writes through the typed entry point rather
/// than the generic <see cref="WriteAsync"/> dispatch.
/// </summary>
public List<(FocasAddress addr, FocasDataType type, object? value)> ParameterWriteLog { get; } = new();
/// <summary>
/// Plan PR F4-b (issue #269) — separate log of <c>cnc_wrmacro</c>-shaped calls
/// observed via <see cref="WriteMacroAsync"/>.
/// </summary>
public List<(FocasAddress addr, object? value)> MacroWriteLog { 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<uint> 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);
}
/// <summary>
/// Plan PR F4-b (issue #269) — typed parameter-write entry point. Records the
/// call in <see cref="ParameterWriteLog"/>, persists the value into
/// <see cref="Values"/> at the canonical address (so a subsequent read returns
/// the written value), and resolves to <see cref="WriteStatuses"/> if seeded
/// (lets a test simulate <c>EW_PASSWD</c> -> <see cref="FocasStatusMapper.BadUserAccessDenied"/>).
/// </summary>
public virtual Task<uint> WriteParameterAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken ct)
{
if (ThrowOnWrite) throw Exception ?? new InvalidOperationException();
ParameterWriteLog.Add((address, type, value));
Values[address.Canonical] = value;
var status = WriteStatuses.TryGetValue(address.Canonical, out var s) ? s : FocasStatusMapper.Good;
return Task.FromResult(status);
}
/// <summary>
/// Plan PR F4-b (issue #269) — typed macro-write entry point. See
/// <see cref="WriteParameterAsync"/> for the per-canonical-address store / log shape.
/// </summary>
public virtual Task<uint> WriteMacroAsync(
FocasAddress address, object? value, CancellationToken ct)
{
if (ThrowOnWrite) throw Exception ?? new InvalidOperationException();
MacroWriteLog.Add((address, 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<bool> ProbeAsync(CancellationToken ct) => Task.FromResult(ProbeResult);
/// <summary>
/// Configurable path count surfaced via <see cref="GetPathCountAsync"/> — 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).
/// </summary>
public int PathCount { get; set; } = 1;
/// <summary>Ordered log of <c>cnc_setpath</c> calls observed on this fake session.</summary>
public List<int> SetPathLog { get; } = new();
public virtual Task<int> GetPathCountAsync(CancellationToken ct) => Task.FromResult(PathCount);
public virtual Task SetPathAsync(int pathId, CancellationToken ct)
{
SetPathLog.Add(pathId);
return Task.CompletedTask;
}
/// <summary>
/// Per-letter / per-path byte storage the coalesced range path reads from. Tests
/// populate <c>PmcByteRanges[("R", 1)] = new byte[size]</c> + the corresponding values to
/// drive both the per-tag <see cref="ReadAsync"/> + the coalesced
/// <see cref="ReadPmcRangeAsync"/> path against the same source of truth (issue #266).
/// </summary>
public Dictionary<(string Letter, int PathId), byte[]> PmcByteRanges { get; } = new();
/// <summary>
/// Ordered log of <c>pmc_rdpmcrng</c>-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).
/// </summary>
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));
}
/// <summary>
/// Canned alarm-history payload returned to <see cref="ReadAlarmHistoryAsync"/>.
/// Defaults to empty so tests that don't care about history get the back-compat
/// no-op behaviour. Tests asserting <c>cnc_rdalmhistry</c> behaviour seed entries
/// here (issue #267, plan PR F3-a).
/// </summary>
public List<FocasAlarmHistoryEntry> AlarmHistory { get; } = new();
/// <summary>
/// Ordered log of <c>cnc_rdalmhistry</c>-shaped calls observed on this fake session
/// (depth-per-call). Tests assert this length to verify the projection's poll
/// cadence + that <c>HistoryDepth</c> got clamped to the wire correctly.
/// </summary>
public List<int> AlarmHistoryReadLog { get; } = new();
public virtual Task<IReadOnlyList<FocasAlarmHistoryEntry>> ReadAlarmHistoryAsync(
int depth, CancellationToken ct)
{
AlarmHistoryReadLog.Add(depth);
IReadOnlyList<FocasAlarmHistoryEntry> snap = AlarmHistory.ToList();
return Task.FromResult(snap);
}
public virtual void Dispose()
{
DisposeCount++;
IsConnected = false;
}
}
internal sealed class FakeFocasClientFactory : IFocasClientFactory
{
public List<FakeFocasClient> Clients { get; } = new();
public Func<FakeFocasClient>? Customise { get; set; }
public IFocasClient Create()
{
var c = Customise?.Invoke() ?? new FakeFocasClient();
Clients.Add(c);
return c;
}
}