Phase 2 Stream D Option B — archive v1 surface + new Driver.Galaxy.E2E parity suite. Non-destructive intermediate state: the v1 OtOpcUa.Host + Historian.Aveva + Tests + IntegrationTests projects all still build (494 v1 unit + 6 v1 integration tests still pass when run explicitly), but solution-level dotnet test ZB.MOM.WW.OtOpcUa.slnx now skips them via IsTestProject=false on the test projects + archive-status PropertyGroup comments on the src projects. The destructive deletion is reserved for Phase 2 PR 3 with explicit operator review per CLAUDE.md "only use destructive operations when truly the best approach". tests/ZB.MOM.WW.OtOpcUa.Tests/ renamed via git mv to tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive/; csproj <AssemblyName> kept as the original ZB.MOM.WW.OtOpcUa.Tests so v1 OtOpcUa.Host's [InternalsVisibleTo("ZB.MOM.WW.OtOpcUa.Tests")] still matches and the project rebuilds clean. tests/ZB.MOM.WW.OtOpcUa.IntegrationTests gets <IsTestProject>false</IsTestProject>. src/ZB.MOM.WW.OtOpcUa.Host + src/ZB.MOM.WW.OtOpcUa.Historian.Aveva get PropertyGroup archive-status comments documenting they're functionally superseded but kept in-build because cascading dependencies (Historian.Aveva → Host; IntegrationTests → Host) make a single-PR deletion high blast-radius. New tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ project (.NET 10) with ParityFixture that spawns OtOpcUa.Driver.Galaxy.Host.exe (net48 x86) as a Process.Start subprocess with OTOPCUA_GALAXY_BACKEND=db env vars, awaits 2s for the PipeServer to bind, then exposes a connected GalaxyProxyDriver; skips on non-Windows / Administrator shells (PipeAcl denies admins per decision #76) / ZB unreachable / Host EXE not built — each skip carries a SkipReason string the test method reads via Assert.Skip(SkipReason). RecordingAddressSpaceBuilder captures every Folder/Variable/AddProperty registration so parity tests can assert on the same shape v1 LmxNodeManager produced. HierarchyParityTests (3) — Discover returns gobjects with attributes; attribute full references match the tag.attribute Galaxy reference grammar; HistoryExtension flag flows through correctly. StabilityFindingsRegressionTests (4) — one test per 2026-04-13 stability finding from commits c76ab8f and 7310925: phantom probe subscription doesn't corrupt unrelated host status; HostStatusChangedEventArgs structurally carries a specific HostName + OldState + NewState (event signature mathematically prevents the v1 cross-host quality-clear bug); all GalaxyProxyDriver capability methods return Task or Task<T> (sync-over-async would deadlock OPC UA stack thread); AcknowledgeAsync completes before returning (no fire-and-forget background work that could race shutdown). Solution test count: 470 pass / 7 skip (E2E on admin shell) / 1 pre-existing Phase 0 baseline. Run archived suites explicitly: dotnet test tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive (494 pass) + dotnet test tests/ZB.MOM.WW.OtOpcUa.IntegrationTests (6 pass). docs/v2/V1_ARCHIVE_STATUS.md inventories every archived surface with run-it-explicitly instructions + a 10-step deletion plan for PR 3 + rollback procedure (git revert restores all four projects). docs/v2/implementation/exit-gate-phase-2-final.md supersedes the two partial-exit docs with the per-stream status table (A/B/C/D/E all addressed, D split across PR 2/3 per safety protocol), the test count breakdown, fresh adversarial review of PR 2 deltas (4 new findings: medium IsTestProject=false safety net loss, medium structural-vs-behavioral stability tests, low backend=db default, low Process.Start env inheritance), the 8 carried-forward findings from exit-gate-phase-2.md, the recommended PR order (1 → 2 → 3 → 4). docs/v2/implementation/pr-2-body.md is the Gitea web-UI paste-in for opening PR 2 once pushed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
210
tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive/Helpers/FakeMxProxy.cs
Normal file
210
tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive/Helpers/FakeMxProxy.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ArchestrA.MxAccess;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Fake IMxProxy for testing without the MxAccess COM runtime.
|
||||
/// Simulates connections, subscriptions, data changes, and writes.
|
||||
/// </summary>
|
||||
public class FakeMxProxy : IMxProxy
|
||||
{
|
||||
private int _connectionHandle;
|
||||
private int _nextHandle = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, string> Items { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values written through the fake runtime so write scenarios can assert the final payload.
|
||||
/// </summary>
|
||||
public List<(string Address, object Value)> WrittenValues { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the fake runtime is currently considered registered.
|
||||
/// </summary>
|
||||
public bool IsRegistered { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times the system under test attempted to register with the fake runtime.
|
||||
/// </summary>
|
||||
public int RegisterCallCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times the system under test attempted to unregister from the fake runtime.
|
||||
/// </summary>
|
||||
public int UnregisterCallCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether registration should fail to exercise connection-error paths.
|
||||
/// </summary>
|
||||
public bool ShouldFailRegister { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether writes should fail to exercise runtime write-error paths.
|
||||
/// </summary>
|
||||
public bool ShouldFailWrite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the fake should suppress the write-complete callback for timeout scenarios.
|
||||
/// </summary>
|
||||
public bool SkipWriteCompleteCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status code returned in the simulated write-complete callback.
|
||||
/// </summary>
|
||||
public int WriteCompleteStatus { get; set; } = 0; // 0 = success
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test.
|
||||
/// </summary>
|
||||
public event MxDataChangeHandler? OnDataChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test.
|
||||
/// </summary>
|
||||
public event MxWriteCompleteHandler? OnWriteComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the MXAccess registration handshake and returns a synthetic connection handle.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client name supplied by the code under test.</param>
|
||||
/// <returns>A synthetic connection handle for subsequent fake operations.</returns>
|
||||
public int Register(string clientName)
|
||||
{
|
||||
RegisterCallCount++;
|
||||
if (ShouldFailRegister) throw new InvalidOperationException("Register failed (simulated)");
|
||||
IsRegistered = true;
|
||||
_connectionHandle = Interlocked.Increment(ref _nextHandle);
|
||||
return _connectionHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates tearing down the fake MXAccess connection.
|
||||
/// </summary>
|
||||
/// <param name="handle">The connection handle supplied by the code under test.</param>
|
||||
public void Unregister(int handle)
|
||||
{
|
||||
UnregisterCallCount++;
|
||||
IsRegistered = false;
|
||||
_connectionHandle = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates resolving a tag reference into a fake runtime item handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="address">The Galaxy attribute reference being registered.</param>
|
||||
/// <returns>A synthetic item handle.</returns>
|
||||
public int AddItem(int handle, string address)
|
||||
{
|
||||
var itemHandle = Interlocked.Increment(ref _nextHandle);
|
||||
Items[itemHandle] = address;
|
||||
return itemHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates removing an item from the fake runtime session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle to remove.</param>
|
||||
public void RemoveItem(int handle, int itemHandle)
|
||||
{
|
||||
Items.TryRemove(itemHandle, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item as actively advised so tests can assert subscription activation.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle being monitored.</param>
|
||||
public void AdviseSupervisory(int handle, int itemHandle)
|
||||
{
|
||||
AdvisedItems[itemHandle] = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item as no longer advised so tests can assert subscription teardown.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle no longer being monitored.</param>
|
||||
public void UnAdviseSupervisory(int handle, int itemHandle)
|
||||
{
|
||||
AdvisedItems.TryRemove(itemHandle, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a runtime write, records the written value, and optionally raises the write-complete callback.
|
||||
/// </summary>
|
||||
/// <param name="handle">The synthetic connection handle.</param>
|
||||
/// <param name="itemHandle">The synthetic item handle to write.</param>
|
||||
/// <param name="value">The value supplied by the system under test.</param>
|
||||
/// <param name="securityClassification">The security classification supplied with the write request.</param>
|
||||
public void Write(int handle, int itemHandle, object value, int securityClassification)
|
||||
{
|
||||
if (ShouldFailWrite) throw new InvalidOperationException("Write failed (simulated)");
|
||||
|
||||
if (Items.TryGetValue(itemHandle, out var address))
|
||||
WrittenValues.Add((address, value));
|
||||
|
||||
// Simulate async write complete callback
|
||||
var status = new MXSTATUS_PROXY[1];
|
||||
if (WriteCompleteStatus == 0)
|
||||
{
|
||||
status[0].success = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
status[0].success = 0;
|
||||
status[0].detail = (short)WriteCompleteStatus;
|
||||
}
|
||||
|
||||
if (!SkipWriteCompleteCallback)
|
||||
OnWriteComplete?.Invoke(_connectionHandle, itemHandle, ref status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates an MXAccess data change event for a specific item handle.
|
||||
/// </summary>
|
||||
/// <param name="itemHandle">The synthetic item handle that should receive the new value.</param>
|
||||
/// <param name="value">The value to publish to the system under test.</param>
|
||||
/// <param name="quality">The runtime quality code to send with the value.</param>
|
||||
/// <param name="timestamp">The optional timestamp to send with the value; defaults to the current UTC time.</param>
|
||||
public void SimulateDataChange(int itemHandle, object value, int quality = 192, DateTime? timestamp = null)
|
||||
{
|
||||
var status = new MXSTATUS_PROXY[1];
|
||||
status[0].success = 1;
|
||||
OnDataChange?.Invoke(_connectionHandle, itemHandle, value, quality,
|
||||
timestamp ?? DateTime.UtcNow, ref status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates data change for a specific address (finds handle by address).
|
||||
/// </summary>
|
||||
/// <param name="address">The Galaxy attribute reference whose registered handle should receive the new value.</param>
|
||||
/// <param name="value">The value to publish to the system under test.</param>
|
||||
/// <param name="quality">The runtime quality code to send with the value.</param>
|
||||
/// <param name="timestamp">The optional timestamp to send with the value; defaults to the current UTC time.</param>
|
||||
public void SimulateDataChangeByAddress(string address, object value, int quality = 192,
|
||||
DateTime? timestamp = null)
|
||||
{
|
||||
foreach (var kvp in Items)
|
||||
if (string.Equals(kvp.Value, address, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SimulateDataChange(kvp.Key, value, quality, timestamp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user