using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; internal class FakeAbLegacyTag : IAbLegacyTagRuntime { public AbLegacyTagCreateParams CreationParams { get; } public object? Value { get; set; } public int Status { get; set; } public bool ThrowOnInitialize { get; set; } public bool ThrowOnRead { get; set; } public bool ThrowOnWrite { get; set; } public Exception? Exception { get; set; } public int InitializeCount { get; private set; } public int ReadCount { get; private set; } public int WriteCount { get; private set; } public bool Disposed { get; private set; } public FakeAbLegacyTag(AbLegacyTagCreateParams p) => CreationParams = p; public virtual Task InitializeAsync(CancellationToken ct) { InitializeCount++; if (ThrowOnInitialize) throw Exception ?? new InvalidOperationException(); return Task.CompletedTask; } public virtual Task ReadAsync(CancellationToken ct) { ReadCount++; if (ThrowOnRead) throw Exception ?? new InvalidOperationException(); return Task.CompletedTask; } public virtual Task WriteAsync(CancellationToken ct) { WriteCount++; if (ThrowOnWrite) throw Exception ?? new InvalidOperationException(); return Task.CompletedTask; } public virtual int GetStatus() => Status; public int? LastDecodeBitIndex { get; private set; } public AbLegacyDataType? LastDecodeType { get; private set; } public virtual object? DecodeValue(AbLegacyDataType type, int? bitIndex) { LastDecodeType = type; LastDecodeBitIndex = bitIndex; // If the test seeded a parent-word value (ushort/short/int) and the driver asked for a // specific status bit, mask it out so we can assert the correct bit reaches the client. if (bitIndex is int bit && Value is not null and not bool) { try { var word = Convert.ToInt32(Value); return ((word >> bit) & 1) != 0; } catch (Exception ex) when (ex is FormatException or InvalidCastException) { } } return Value; } public virtual void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) => Value = value; /// /// PR 7 — array contiguous-block element decoder. Tests seed /// with the per-index payload; the fake then returns each element in order. Falls back /// to when the test only seeded a scalar (Convert.ChangeType handles /// the cast back to the requested element type in the driver's BuildArray helper). /// public IReadOnlyList? ArrayValues { get; set; } public AbLegacyDataType? LastArrayDecodeType { get; private set; } public int LastArrayDecodeMaxIndex { get; private set; } = -1; public virtual object? DecodeArrayElement(AbLegacyDataType type, int elementIndex) { LastArrayDecodeType = type; if (elementIndex > LastArrayDecodeMaxIndex) LastArrayDecodeMaxIndex = elementIndex; if (ArrayValues is not null && elementIndex < ArrayValues.Count) return ArrayValues[elementIndex]; return Value; } public virtual void Dispose() => Disposed = true; } internal sealed class FakeAbLegacyTagFactory : IAbLegacyTagFactory { // PR ablegacy-12 / #255 — switched from plain Dictionary to ConcurrentDictionary so // the read path (test thread) and the probe loop (background Task) can both call // Create without corrupting the dict. Pre-PR-12 the race existed but only tipped // a few percent of test runs into KeyNotFoundException; PR-12's added // Interlocked.Exchange writes shifted timing enough to make it deterministic-flaky // (~60%). public System.Collections.Concurrent.ConcurrentDictionary Tags { get; } = new(StringComparer.OrdinalIgnoreCase); public Func? Customise { get; set; } public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams p) { var fake = Customise?.Invoke(p) ?? new FakeAbLegacyTag(p); Tags[p.TagName] = fake; return fake; } }