Implement LmxOpcUa server — all 6 phases complete
Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as OPC UA address space, translating contained-name browse paths to tag-name runtime references. Components implemented: - Configuration: AppConfiguration with 4 sections, validator - Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes - MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter using strongly-typed ArchestrA.MxAccess COM interop - Galaxy Repository: SQL queries (hierarchy, attributes, change detection), ChangeDetectionService with auto-rebuild on deploy - OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer, OpcUaServerHost with programmatic config, SecurityPolicy None - Status Dashboard: HTTP server with HTML/JSON/health endpoints - Integration: Full 14-step startup, graceful shutdown, component wiring 175 tests (174 unit + 1 integration), all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
119
tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxProxy.cs
Normal file
119
tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/FakeMxProxy.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ArchestrA.MxAccess;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.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 _nextHandle = 1;
|
||||
private int _connectionHandle;
|
||||
private bool _registered;
|
||||
|
||||
public event MxDataChangeHandler? OnDataChange;
|
||||
public event MxWriteCompleteHandler? OnWriteComplete;
|
||||
|
||||
public ConcurrentDictionary<int, string> Items { get; } = new ConcurrentDictionary<int, string>();
|
||||
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new ConcurrentDictionary<int, bool>();
|
||||
public List<(string Address, object Value)> WrittenValues { get; } = new List<(string, object)>();
|
||||
|
||||
public bool IsRegistered => _registered;
|
||||
public int RegisterCallCount { get; private set; }
|
||||
public int UnregisterCallCount { get; private set; }
|
||||
public bool ShouldFailRegister { get; set; }
|
||||
public bool ShouldFailWrite { get; set; }
|
||||
public int WriteCompleteStatus { get; set; } = 0; // 0 = success
|
||||
|
||||
public int Register(string clientName)
|
||||
{
|
||||
RegisterCallCount++;
|
||||
if (ShouldFailRegister) throw new InvalidOperationException("Register failed (simulated)");
|
||||
_registered = true;
|
||||
_connectionHandle = Interlocked.Increment(ref _nextHandle);
|
||||
return _connectionHandle;
|
||||
}
|
||||
|
||||
public void Unregister(int handle)
|
||||
{
|
||||
UnregisterCallCount++;
|
||||
_registered = false;
|
||||
_connectionHandle = 0;
|
||||
}
|
||||
|
||||
public int AddItem(int handle, string address)
|
||||
{
|
||||
var itemHandle = Interlocked.Increment(ref _nextHandle);
|
||||
Items[itemHandle] = address;
|
||||
return itemHandle;
|
||||
}
|
||||
|
||||
public void RemoveItem(int handle, int itemHandle)
|
||||
{
|
||||
Items.TryRemove(itemHandle, out _);
|
||||
}
|
||||
|
||||
public void AdviseSupervisory(int handle, int itemHandle)
|
||||
{
|
||||
AdvisedItems[itemHandle] = true;
|
||||
}
|
||||
|
||||
public void UnAdviseSupervisory(int handle, int itemHandle)
|
||||
{
|
||||
AdvisedItems.TryRemove(itemHandle, out _);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
OnWriteComplete?.Invoke(_connectionHandle, itemHandle, ref status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates an MXAccess data change event for a specific item handle.
|
||||
/// </summary>
|
||||
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,
|
||||
(object)(timestamp ?? DateTime.UtcNow), ref status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates data change for a specific address (finds handle by address).
|
||||
/// </summary>
|
||||
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