Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Tests/Helpers/FakeMxAccessClient.cs
Joseph Doherty 3b2defd94f Phase 0 — mechanical rename ZB.MOM.WW.LmxOpcUa.* → ZB.MOM.WW.OtOpcUa.*
Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.

Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.

Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:57:47 -04:00

185 lines
7.8 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Host.Domain;
namespace ZB.MOM.WW.OtOpcUa.Tests.Helpers
{
/// <summary>
/// In-memory IMxAccessClient used by tests to drive connection, read, write, and subscription scenarios without COM
/// runtime dependencies.
/// </summary>
public class FakeMxAccessClient : IMxAccessClient
{
private readonly ConcurrentDictionary<string, Action<string, Vtq>> _subscriptions =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the in-memory tag-value table returned by fake reads.
/// </summary>
public ConcurrentDictionary<string, Vtq> TagValues { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the values written through the fake client so tests can assert write behavior.
/// </summary>
public List<(string Tag, object Value)> WrittenValues { get; } = new();
/// <summary>
/// Gets or sets the result returned by fake writes to simulate success or failure.
/// </summary>
public bool WriteResult { get; set; } = true;
/// <summary>
/// Gets or sets the connection state returned to the system under test.
/// </summary>
public ConnectionState State { get; set; } = ConnectionState.Connected;
/// <summary>
/// Gets the number of active subscriptions currently stored by the fake client.
/// </summary>
public int ActiveSubscriptionCount => _subscriptions.Count;
/// <summary>
/// Gets or sets the reconnect count exposed to health and dashboard tests.
/// </summary>
public int ReconnectCount { get; set; }
/// <summary>
/// When set, <see cref="SubscribeAsync"/> returns a faulted task with this exception.
/// </summary>
public Exception? SubscribeException { get; set; }
/// <summary>
/// When set, <see cref="UnsubscribeAsync"/> returns a faulted task with this exception.
/// </summary>
public Exception? UnsubscribeException { get; set; }
/// <summary>
/// When set, <see cref="ReadAsync"/> returns a faulted task with this exception.
/// </summary>
public Exception? ReadException { get; set; }
/// <summary>
/// When set, <see cref="WriteAsync"/> returns a faulted task with this exception.
/// </summary>
public Exception? WriteException { get; set; }
/// <summary>
/// Occurs when tests explicitly simulate a connection-state transition.
/// </summary>
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
/// <summary>
/// Occurs when tests publish a simulated runtime value change.
/// </summary>
public event Action<string, Vtq>? OnTagValueChanged;
/// <summary>
/// Simulates establishing a healthy runtime connection.
/// </summary>
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
public Task ConnectAsync(CancellationToken ct = default)
{
State = ConnectionState.Connected;
return Task.CompletedTask;
}
/// <summary>
/// Simulates disconnecting from the runtime.
/// </summary>
public Task DisconnectAsync()
{
State = ConnectionState.Disconnected;
return Task.CompletedTask;
}
/// <summary>
/// Stores a subscription callback so later simulated data changes can target it.
/// </summary>
/// <param name="fullTagReference">The Galaxy attribute reference to monitor.</param>
/// <param name="callback">The callback that should receive simulated value changes.</param>
public Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback)
{
if (SubscribeException != null)
return Task.FromException(SubscribeException);
_subscriptions[fullTagReference] = callback;
return Task.CompletedTask;
}
/// <summary>
/// Removes a stored subscription callback for the specified tag reference.
/// </summary>
/// <param name="fullTagReference">The Galaxy attribute reference to stop monitoring.</param>
public Task UnsubscribeAsync(string fullTagReference)
{
if (UnsubscribeException != null)
return Task.FromException(UnsubscribeException);
_subscriptions.TryRemove(fullTagReference, out _);
return Task.CompletedTask;
}
/// <summary>
/// Returns the current in-memory VTQ for a tag reference or a bad-quality placeholder when none has been seeded.
/// </summary>
/// <param name="fullTagReference">The Galaxy attribute reference to read.</param>
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
/// <returns>The seeded VTQ value or a bad not-connected VTQ when the tag was not populated.</returns>
public Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default)
{
if (ReadException != null)
return Task.FromException<Vtq>(ReadException);
if (TagValues.TryGetValue(fullTagReference, out var vtq))
return Task.FromResult(vtq);
return Task.FromResult(Vtq.Bad(Quality.BadNotConnected));
}
/// <summary>
/// Records a write request, optionally updates the in-memory tag table, and returns the configured write result.
/// </summary>
/// <param name="fullTagReference">The Galaxy attribute reference being written.</param>
/// <param name="value">The value supplied by the code under test.</param>
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
/// <returns>A completed task returning the configured write outcome.</returns>
public Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
{
if (WriteException != null)
return Task.FromException<bool>(WriteException);
WrittenValues.Add((fullTagReference, value));
if (WriteResult)
TagValues[fullTagReference] = Vtq.Good(value);
return Task.FromResult(WriteResult);
}
/// <summary>
/// Releases the fake client. No unmanaged resources are held.
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Publishes a simulated tag-value change to both the event stream and any stored subscription callback.
/// </summary>
/// <param name="address">The Galaxy attribute reference whose value changed.</param>
/// <param name="vtq">The value, timestamp, and quality payload to publish.</param>
public void SimulateDataChange(string address, Vtq vtq)
{
OnTagValueChanged?.Invoke(address, vtq);
if (_subscriptions.TryGetValue(address, out var callback))
callback(address, vtq);
}
/// <summary>
/// Raises a simulated connection-state transition for health and reconnect tests.
/// </summary>
/// <param name="prev">The previous connection state.</param>
/// <param name="curr">The new connection state.</param>
public void RaiseConnectionStateChanged(ConnectionState prev, ConnectionState curr)
{
State = curr;
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(prev, curr));
}
}
}