Expand XML docs across bridge and test code
This commit is contained in:
@@ -5,9 +5,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// </summary>
|
||||
public class AppConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the OPC UA endpoint settings exposed to downstream clients that browse the LMX address space.
|
||||
/// </summary>
|
||||
public OpcUaConfiguration OpcUa { get; set; } = new OpcUaConfiguration();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MXAccess runtime connection settings used to read and write live Galaxy attributes.
|
||||
/// </summary>
|
||||
public MxAccessConfiguration MxAccess { get; set; } = new MxAccessConfiguration();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the repository settings used to query Galaxy metadata for address-space construction.
|
||||
/// </summary>
|
||||
public GalaxyRepositoryConfiguration GalaxyRepository { get; set; } = new GalaxyRepositoryConfiguration();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the embedded dashboard settings used to surface service health to operators.
|
||||
/// </summary>
|
||||
public DashboardConfiguration Dashboard { get; set; } = new DashboardConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext(typeof(ConfigurationValidator));
|
||||
|
||||
/// <summary>
|
||||
/// Validates the effective host configuration and writes the resolved values to the startup log before service initialization continues.
|
||||
/// </summary>
|
||||
/// <param name="config">The bound service configuration that drives OPC UA hosting, MXAccess connectivity, Galaxy queries, and dashboard behavior.</param>
|
||||
/// <returns><see langword="true"/> when the required settings are present and within supported bounds; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool ValidateAndLog(AppConfiguration config)
|
||||
{
|
||||
bool valid = true;
|
||||
|
||||
@@ -5,8 +5,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// </summary>
|
||||
public class DashboardConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the operator dashboard is hosted alongside the OPC UA service.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP port used by the dashboard endpoint that exposes service health and rebuild state.
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 8081;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refresh interval, in seconds, for recalculating the dashboard status snapshot.
|
||||
/// </summary>
|
||||
public int RefreshIntervalSeconds { get; set; } = 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// </summary>
|
||||
public class GalaxyRepositoryConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the database connection string used to read Galaxy hierarchy and attribute metadata.
|
||||
/// </summary>
|
||||
public string ConnectionString { get; set; } = "Server=localhost;Database=ZB;Integrated Security=true;";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how often, in seconds, the service polls for Galaxy deploy changes that require an address-space rebuild.
|
||||
/// </summary>
|
||||
public int ChangeDetectionIntervalSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL command timeout, in seconds, for repository queries against the Galaxy catalog.
|
||||
/// </summary>
|
||||
public int CommandTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether extended Galaxy attribute metadata should be loaded into the OPC UA model.
|
||||
/// </summary>
|
||||
public bool ExtendedAttributes { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,54 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// </summary>
|
||||
public class MxAccessConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the client name registered with the MXAccess runtime for this bridge instance.
|
||||
/// </summary>
|
||||
public string ClientName { get; set; } = "LmxOpcUa";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy node name to target when the service connects to a specific runtime node.
|
||||
/// </summary>
|
||||
public string? NodeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy name used when resolving MXAccess references and diagnostics.
|
||||
/// </summary>
|
||||
public string? GalaxyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time, in seconds, to wait for a live tag read to complete.
|
||||
/// </summary>
|
||||
public int ReadTimeoutSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time, in seconds, to wait for a tag write acknowledgment from the runtime.
|
||||
/// </summary>
|
||||
public int WriteTimeoutSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cap on concurrent MXAccess operations so the bridge does not overload the runtime.
|
||||
/// </summary>
|
||||
public int MaxConcurrentOperations { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how often, in seconds, the connectivity monitor probes the runtime connection.
|
||||
/// </summary>
|
||||
public int MonitorIntervalSeconds { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the bridge should automatically attempt to re-establish a dropped MXAccess session.
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional probe tag used to verify that the MXAccess runtime is still returning fresh data.
|
||||
/// </summary>
|
||||
public string? ProbeTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of seconds a probe value may remain unchanged before the connection is considered stale.
|
||||
/// </summary>
|
||||
public int ProbeStaleThresholdSeconds { get; set; } = 60;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
/// </summary>
|
||||
public class OpcUaConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP port on which the OPC UA server listens for client sessions.
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 4840;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint path appended to the host URI for the LMX OPC UA server.
|
||||
/// </summary>
|
||||
public string EndpointPath { get; set; } = "/LmxOpcUa";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server name presented to OPC UA clients and used in diagnostics.
|
||||
/// </summary>
|
||||
public string ServerName { get; set; } = "LmxOpcUa";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy name represented by the published OPC UA namespace.
|
||||
/// </summary>
|
||||
public string GalaxyName { get; set; } = "ZB";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of simultaneous OPC UA sessions accepted by the host.
|
||||
/// </summary>
|
||||
public int MaxSessions { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the session timeout, in minutes, before idle client sessions are closed.
|
||||
/// </summary>
|
||||
public int SessionTimeoutMinutes { get; set; } = 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public enum ConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// No active session exists to the Galaxy runtime.
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is opening a new MXAccess session to the runtime.
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge has an active MXAccess session and can service reads, writes, and subscriptions.
|
||||
/// </summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is closing the current MXAccess session and draining runtime resources.
|
||||
/// </summary>
|
||||
Disconnecting,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge detected a connection fault that requires operator attention or recovery logic.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is attempting to restore service after a runtime communication failure.
|
||||
/// </summary>
|
||||
Reconnecting
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the previous MXAccess connection state before the transition was raised.
|
||||
/// </summary>
|
||||
public ConnectionState PreviousState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new MXAccess connection state that the bridge moved into.
|
||||
/// </summary>
|
||||
public ConnectionState CurrentState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an operator-facing message that explains why the connection state changed.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionStateChangedEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="previous">The connection state being exited.</param>
|
||||
/// <param name="current">The connection state being entered.</param>
|
||||
/// <param name="message">Additional context about the transition, such as a connection fault or reconnect attempt.</param>
|
||||
public ConnectionStateChangedEventArgs(ConnectionState previous, ConnectionState current, string message = "")
|
||||
{
|
||||
PreviousState = previous;
|
||||
|
||||
@@ -5,15 +5,54 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public class GalaxyAttributeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy object identifier that owns the attribute.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Wonderware tag name used to associate the attribute with its runtime object.
|
||||
/// </summary>
|
||||
public string TagName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attribute name as defined on the Galaxy template or instance.
|
||||
/// </summary>
|
||||
public string AttributeName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fully qualified MXAccess reference used for runtime reads and writes.
|
||||
/// </summary>
|
||||
public string FullTagReference { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric Galaxy data type code used to map the attribute into OPC UA.
|
||||
/// </summary>
|
||||
public int MxDataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable Galaxy data type name returned by the repository query.
|
||||
/// </summary>
|
||||
public string DataTypeName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the attribute is an array and should be exposed as a collection node.
|
||||
/// </summary>
|
||||
public bool IsArray { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the array length when the Galaxy attribute is modeled as a fixed-size array.
|
||||
/// </summary>
|
||||
public int? ArrayDimension { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primitive data type name used when flattening the attribute for OPC UA clients.
|
||||
/// </summary>
|
||||
public string PrimitiveName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source classification that explains whether the attribute comes from configuration, calculation, or runtime data.
|
||||
/// </summary>
|
||||
public string AttributeSource { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public class GalaxyObjectInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy object identifier used to connect hierarchy rows to attribute rows.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime tag name for the Galaxy object represented in the OPC UA tree.
|
||||
/// </summary>
|
||||
public string TagName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the contained name shown for the object inside its parent area or object.
|
||||
/// </summary>
|
||||
public string ContainedName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the browse name emitted into OPC UA so clients can navigate the Galaxy hierarchy.
|
||||
/// </summary>
|
||||
public string BrowseName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent Galaxy object identifier that establishes the hierarchy relationship.
|
||||
/// </summary>
|
||||
public int ParentGobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the row represents a Galaxy area rather than a contained object.
|
||||
/// </summary>
|
||||
public bool IsArea { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,37 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public interface IGalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the Galaxy object hierarchy used to construct the OPC UA browse tree.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the repository query.</param>
|
||||
/// <returns>A list of Galaxy objects ordered for address-space construction.</returns>
|
||||
Task<List<GalaxyObjectInfo>> GetHierarchyAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Galaxy attributes that become OPC UA variables under the object hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the repository query.</param>
|
||||
/// <returns>A list of attribute definitions with MXAccess references and type metadata.</returns>
|
||||
Task<List<GalaxyAttributeInfo>> GetAttributesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last Galaxy deploy timestamp used to detect metadata changes that require an address-space rebuild.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the repository query.</param>
|
||||
/// <returns>The latest deploy timestamp, or <see langword="null"/> when it cannot be determined.</returns>
|
||||
Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the service can reach the Galaxy repository before it attempts to build the address space.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the connectivity check.</param>
|
||||
/// <returns><see langword="true"/> when repository access succeeds; otherwise, <see langword="false"/>.</returns>
|
||||
Task<bool> TestConnectionAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the repository detects a Galaxy deployment change that should trigger an OPC UA rebuild.
|
||||
/// </summary>
|
||||
event Action? OnGalaxyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,20 +10,70 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public interface IMxAccessClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current runtime connectivity state for the bridge.
|
||||
/// </summary>
|
||||
ConnectionState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the MXAccess session changes state so the host can update diagnostics and retry logic.
|
||||
/// </summary>
|
||||
event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a subscribed Galaxy attribute publishes a new runtime value.
|
||||
/// </summary>
|
||||
event Action<string, Vtq>? OnTagValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the MXAccess session required for runtime reads, writes, and subscriptions.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the connection attempt.</param>
|
||||
Task ConnectAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the MXAccess session and releases runtime resources.
|
||||
/// </summary>
|
||||
Task DisconnectAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Starts monitoring a Galaxy attribute so value changes can be pushed to OPC UA subscribers.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
/// <param name="callback">The callback to invoke when the runtime publishes a new value for the attribute.</param>
|
||||
Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Stops monitoring a Galaxy attribute when it is no longer needed by the OPC UA layer.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
Task UnsubscribeAsync(string fullTagReference);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current runtime value for a Galaxy attribute.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
/// <param name="ct">A token that cancels the read.</param>
|
||||
/// <returns>The value, timestamp, and quality returned by the runtime.</returns>
|
||||
Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a new runtime value to a writable Galaxy attribute.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
/// <param name="value">The value to write to the runtime.</param>
|
||||
/// <param name="ct">A token that cancels the write.</param>
|
||||
/// <returns><see langword="true"/> when the write is accepted by the runtime; otherwise, <see langword="false"/>.</returns>
|
||||
Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of active runtime subscriptions currently being mirrored into OPC UA.
|
||||
/// </summary>
|
||||
int ActiveSubscriptionCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of reconnect cycles attempted since the client was created.
|
||||
/// </summary>
|
||||
int ReconnectCount { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// <summary>
|
||||
/// Delegate matching LMXProxyServer.OnDataChange COM event signature.
|
||||
/// </summary>
|
||||
/// <param name="hLMXServerHandle">The runtime connection handle that raised the change.</param>
|
||||
/// <param name="phItemHandle">The runtime item handle for the attribute that changed.</param>
|
||||
/// <param name="pvItemValue">The new raw runtime value for the attribute.</param>
|
||||
/// <param name="pwItemQuality">The OPC DA quality code supplied by the runtime.</param>
|
||||
/// <param name="pftItemTimeStamp">The timestamp object supplied by the runtime for the value.</param>
|
||||
/// <param name="ItemStatus">The MXAccess status payload associated with the callback.</param>
|
||||
public delegate void MxDataChangeHandler(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
@@ -17,6 +23,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// <summary>
|
||||
/// Delegate matching LMXProxyServer.OnWriteComplete COM event signature.
|
||||
/// </summary>
|
||||
/// <param name="hLMXServerHandle">The runtime connection handle that processed the write.</param>
|
||||
/// <param name="phItemHandle">The runtime item handle that was written.</param>
|
||||
/// <param name="ItemStatus">The MXAccess status payload describing the write outcome.</param>
|
||||
public delegate void MxWriteCompleteHandler(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
@@ -27,15 +36,65 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public interface IMxProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the bridge as an MXAccess client with the runtime proxy.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client identity reported to the runtime for diagnostics and session tracking.</param>
|
||||
/// <returns>The runtime connection handle assigned to the client session.</returns>
|
||||
int Register(string clientName);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the bridge from the runtime proxy and releases the connection handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The connection handle returned by <see cref="Register(string)"/>.</param>
|
||||
void Unregister(int handle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a Galaxy attribute reference to the active runtime session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="address">The fully qualified attribute reference to resolve.</param>
|
||||
/// <returns>The runtime item handle assigned to the attribute.</returns>
|
||||
int AddItem(int handle, string address);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered attribute from the runtime session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle returned by <see cref="AddItem(int, string)"/>.</param>
|
||||
void RemoveItem(int handle, int itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Starts supervisory updates for an attribute so runtime changes are pushed to the bridge.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to monitor.</param>
|
||||
void AdviseSupervisory(int handle, int itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Stops supervisory updates for an attribute.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to stop monitoring.</param>
|
||||
void UnAdviseSupervisory(int handle, int itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a new value to a runtime attribute through the COM proxy.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to write.</param>
|
||||
/// <param name="value">The new value to push into the runtime.</param>
|
||||
/// <param name="securityClassification">The Wonderware security classification applied to the write.</param>
|
||||
void Write(int handle, int itemHandle, object value, int securityClassification);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the runtime pushes a data-change callback for a subscribed attribute.
|
||||
/// </summary>
|
||||
event MxDataChangeHandler? OnDataChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the runtime acknowledges completion of a write request.
|
||||
/// </summary>
|
||||
event MxWriteCompleteHandler? OnWriteComplete;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// Maps mx_data_type to OPC UA DataType NodeId numeric identifier.
|
||||
/// Unknown types default to String (i=12).
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <returns>The OPC UA built-in data type node identifier.</returns>
|
||||
public static uint MapToOpcUaDataType(int mxDataType)
|
||||
{
|
||||
return mxDataType switch
|
||||
@@ -35,6 +37,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// <summary>
|
||||
/// Maps mx_data_type to the corresponding CLR type.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <returns>The CLR type used to represent runtime values for the MX type.</returns>
|
||||
public static Type MapToClrType(int mxDataType)
|
||||
{
|
||||
return mxDataType switch
|
||||
@@ -58,6 +62,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// <summary>
|
||||
/// Returns the OPC UA type name for a given mx_data_type.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <returns>The OPC UA type name used in diagnostics.</returns>
|
||||
public static string GetOpcUaTypeName(int mxDataType)
|
||||
{
|
||||
return mxDataType switch
|
||||
|
||||
@@ -5,13 +5,41 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public static class MxErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// The requested Galaxy attribute reference does not resolve in the runtime.
|
||||
/// </summary>
|
||||
public const int MX_E_InvalidReference = 1008;
|
||||
|
||||
/// <summary>
|
||||
/// The supplied value does not match the attribute's configured data type.
|
||||
/// </summary>
|
||||
public const int MX_E_WrongDataType = 1012;
|
||||
|
||||
/// <summary>
|
||||
/// The target attribute cannot be written because it is read-only or protected.
|
||||
/// </summary>
|
||||
public const int MX_E_NotWritable = 1013;
|
||||
|
||||
/// <summary>
|
||||
/// The runtime did not complete the operation within the configured timeout.
|
||||
/// </summary>
|
||||
public const int MX_E_RequestTimedOut = 1014;
|
||||
|
||||
/// <summary>
|
||||
/// Communication with the MXAccess runtime failed during the operation.
|
||||
/// </summary>
|
||||
public const int MX_E_CommFailure = 1015;
|
||||
|
||||
/// <summary>
|
||||
/// The operation was attempted without an active MXAccess session.
|
||||
/// </summary>
|
||||
public const int MX_E_NotConnected = 1016;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a numeric MXAccess error code into an operator-facing message.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">The MXAccess error code returned by the runtime.</param>
|
||||
/// <returns>A human-readable description of the runtime failure.</returns>
|
||||
public static string GetMessage(int errorCode)
|
||||
{
|
||||
return errorCode switch
|
||||
@@ -26,6 +54,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an MXAccess error code to the OPC quality state that should be exposed to clients.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">The MXAccess error code returned by the runtime.</param>
|
||||
/// <returns>The quality classification that best represents the runtime failure.</returns>
|
||||
public static Quality MapToQuality(int errorCode)
|
||||
{
|
||||
return errorCode switch
|
||||
|
||||
@@ -6,31 +6,96 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
public enum Quality : byte
|
||||
{
|
||||
// Bad family (0-63)
|
||||
/// <summary>
|
||||
/// No valid process value is available.
|
||||
/// </summary>
|
||||
Bad = 0,
|
||||
/// <summary>
|
||||
/// The value is invalid because the Galaxy attribute definition or mapping is wrong.
|
||||
/// </summary>
|
||||
BadConfigError = 4,
|
||||
/// <summary>
|
||||
/// The bridge is not currently connected to the Galaxy runtime.
|
||||
/// </summary>
|
||||
BadNotConnected = 8,
|
||||
/// <summary>
|
||||
/// The runtime device or adapter failed while obtaining the value.
|
||||
/// </summary>
|
||||
BadDeviceFailure = 12,
|
||||
/// <summary>
|
||||
/// The underlying field source reported a bad sensor condition.
|
||||
/// </summary>
|
||||
BadSensorFailure = 16,
|
||||
/// <summary>
|
||||
/// Communication with the runtime failed while retrieving the value.
|
||||
/// </summary>
|
||||
BadCommFailure = 20,
|
||||
/// <summary>
|
||||
/// The attribute is intentionally unavailable for service, such as a locked or unwritable value.
|
||||
/// </summary>
|
||||
BadOutOfService = 24,
|
||||
/// <summary>
|
||||
/// The bridge is still waiting for the first usable value after startup or resubscription.
|
||||
/// </summary>
|
||||
BadWaitingForInitialData = 32,
|
||||
|
||||
// Uncertain family (64-191)
|
||||
/// <summary>
|
||||
/// A value is available, but it should be treated cautiously.
|
||||
/// </summary>
|
||||
Uncertain = 64,
|
||||
/// <summary>
|
||||
/// The last usable value is being repeated because a newer one is unavailable.
|
||||
/// </summary>
|
||||
UncertainLastUsable = 68,
|
||||
/// <summary>
|
||||
/// The sensor or source is providing a value with reduced accuracy.
|
||||
/// </summary>
|
||||
UncertainSensorNotAccurate = 80,
|
||||
/// <summary>
|
||||
/// The value exceeds its engineered limits.
|
||||
/// </summary>
|
||||
UncertainEuExceeded = 84,
|
||||
/// <summary>
|
||||
/// The source is operating in a degraded or subnormal state.
|
||||
/// </summary>
|
||||
UncertainSubNormal = 88,
|
||||
|
||||
// Good family (192+)
|
||||
/// <summary>
|
||||
/// The value is current and suitable for normal client use.
|
||||
/// </summary>
|
||||
Good = 192,
|
||||
/// <summary>
|
||||
/// The value is good but currently overridden locally rather than flowing from the live source.
|
||||
/// </summary>
|
||||
GoodLocalOverride = 216
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for reasoning about OPC quality families used by the bridge.
|
||||
/// </summary>
|
||||
public static class QualityExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the quality represents a good runtime value that can be trusted by OPC UA clients.
|
||||
/// </summary>
|
||||
/// <param name="q">The quality code to inspect.</param>
|
||||
/// <returns><see langword="true"/> when the value is in the good quality range; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsGood(this Quality q) => (byte)q >= 192;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the quality represents an uncertain runtime value that should be treated cautiously.
|
||||
/// </summary>
|
||||
/// <param name="q">The quality code to inspect.</param>
|
||||
/// <returns><see langword="true"/> when the value is in the uncertain range; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsUncertain(this Quality q) => (byte)q >= 64 && (byte)q < 192;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the quality represents a bad runtime value that should not be used as valid process data.
|
||||
/// </summary>
|
||||
/// <param name="q">The quality code to inspect.</param>
|
||||
/// <returns><see langword="true"/> when the value is in the bad range; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsBad(this Quality q) => (byte)q < 64;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// Maps an MXAccess quality integer (OPC DA quality byte) to domain Quality.
|
||||
/// Uses category bits: 192+ = Good, 64-191 = Uncertain, 0-63 = Bad.
|
||||
/// </summary>
|
||||
/// <param name="mxQuality">The raw MXAccess quality integer.</param>
|
||||
/// <returns>The mapped bridge quality value.</returns>
|
||||
public static Quality MapFromMxAccessQuality(int mxQuality)
|
||||
{
|
||||
var b = (byte)(mxQuality & 0xFF);
|
||||
@@ -26,6 +28,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// <summary>
|
||||
/// Maps domain Quality to OPC UA StatusCode uint32.
|
||||
/// </summary>
|
||||
/// <param name="quality">The bridge quality value.</param>
|
||||
/// <returns>The OPC UA status code represented as a 32-bit unsigned integer.</returns>
|
||||
public static uint MapToOpcUaStatusCode(Quality quality)
|
||||
{
|
||||
return quality switch
|
||||
|
||||
@@ -7,10 +7,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
/// </summary>
|
||||
public readonly struct Vtq : IEquatable<Vtq>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the runtime value returned for the Galaxy attribute.
|
||||
/// </summary>
|
||||
public object? Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp associated with the runtime value.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quality classification that tells OPC UA clients whether the value is usable.
|
||||
/// </summary>
|
||||
public Quality Quality { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Vtq"/> struct for a Galaxy attribute value.
|
||||
/// </summary>
|
||||
/// <param name="value">The runtime value returned by MXAccess.</param>
|
||||
/// <param name="timestamp">The timestamp assigned to the runtime value.</param>
|
||||
/// <param name="quality">The quality classification for the runtime value.</param>
|
||||
public Vtq(object? value, DateTime timestamp, Quality quality)
|
||||
{
|
||||
Value = value;
|
||||
@@ -18,15 +35,42 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
Quality = quality;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a good-quality VTQ snapshot for a successfully read or subscribed attribute value.
|
||||
/// </summary>
|
||||
/// <param name="value">The runtime value to wrap.</param>
|
||||
/// <returns>A VTQ carrying the provided value with the current UTC timestamp and good quality.</returns>
|
||||
public static Vtq Good(object? value) => new Vtq(value, DateTime.UtcNow, Quality.Good);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bad-quality VTQ snapshot when no usable runtime value is available.
|
||||
/// </summary>
|
||||
/// <param name="quality">The specific bad quality reason to expose to clients.</param>
|
||||
/// <returns>A VTQ with no value, the current UTC timestamp, and the requested bad quality.</returns>
|
||||
public static Vtq Bad(Quality quality = Quality.Bad) => new Vtq(null, DateTime.UtcNow, quality);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an uncertain VTQ snapshot when the runtime value exists but should be treated cautiously.
|
||||
/// </summary>
|
||||
/// <param name="value">The runtime value to wrap.</param>
|
||||
/// <returns>A VTQ carrying the provided value with the current UTC timestamp and uncertain quality.</returns>
|
||||
public static Vtq Uncertain(object? value) => new Vtq(value, DateTime.UtcNow, Quality.Uncertain);
|
||||
|
||||
/// <summary>
|
||||
/// Compares two VTQ snapshots for exact value, timestamp, and quality equality.
|
||||
/// </summary>
|
||||
/// <param name="other">The other VTQ snapshot to compare.</param>
|
||||
/// <returns><see langword="true"/> when all fields match; otherwise, <see langword="false"/>.</returns>
|
||||
public bool Equals(Vtq other) =>
|
||||
Equals(Value, other.Value) && Timestamp == other.Timestamp && Quality == other.Quality;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is Vtq other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => HashCode.Combine(Value, Timestamp, Quality);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"Vtq({Value}, {Timestamp:O}, {Quality})";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,22 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
private CancellationTokenSource? _cts;
|
||||
private DateTime? _lastKnownDeployTime;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a new Galaxy deploy timestamp indicates the OPC UA address space should be rebuilt.
|
||||
/// </summary>
|
||||
public event Action? OnGalaxyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last deploy timestamp observed by the polling loop.
|
||||
/// </summary>
|
||||
public DateTime? LastKnownDeployTime => _lastKnownDeployTime;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new change detector for Galaxy deploy timestamps.
|
||||
/// </summary>
|
||||
/// <param name="repository">The repository used to query the latest deploy timestamp.</param>
|
||||
/// <param name="intervalSeconds">The polling interval, in seconds, between deploy checks.</param>
|
||||
/// <param name="initialDeployTime">An optional deploy timestamp already known at service startup.</param>
|
||||
public ChangeDetectionService(IGalaxyRepository repository, int intervalSeconds, DateTime? initialDeployTime = null)
|
||||
{
|
||||
_repository = repository;
|
||||
@@ -29,6 +42,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
_lastKnownDeployTime = initialDeployTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the background polling loop that watches for Galaxy deploy changes.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
@@ -36,6 +52,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
Log.Information("Change detection started (interval={Interval}s)", _intervalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the background polling loop.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
@@ -88,6 +107,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the polling loop and disposes the underlying cancellation resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
|
||||
private readonly GalaxyRepositoryConfiguration _config;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the repository detects a Galaxy deploy change that should trigger an address-space rebuild.
|
||||
/// </summary>
|
||||
public event Action? OnGalaxyChanged;
|
||||
|
||||
#region SQL Queries (GR-006: const string, no dynamic SQL)
|
||||
@@ -204,11 +207,20 @@ ORDER BY tag_name, primitive_name, attribute_name";
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new repository service that reads Galaxy metadata from the configured SQL database.
|
||||
/// </summary>
|
||||
/// <param name="config">The repository connection, timeout, and attribute-selection settings.</param>
|
||||
public GalaxyRepositoryService(GalaxyRepositoryConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the Galaxy repository for the deployed object hierarchy that becomes the OPC UA browse tree.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the database query.</param>
|
||||
/// <returns>The deployed Galaxy objects that should appear in the namespace.</returns>
|
||||
public async Task<List<GalaxyObjectInfo>> GetHierarchyAsync(CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<GalaxyObjectInfo>();
|
||||
@@ -240,6 +252,11 @@ ORDER BY tag_name, primitive_name, attribute_name";
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the Galaxy repository for attribute metadata that becomes OPC UA variable nodes.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the database query.</param>
|
||||
/// <returns>The attribute rows required to build runtime tag mappings and variable metadata.</returns>
|
||||
public async Task<List<GalaxyAttributeInfo>> GetAttributesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<GalaxyAttributeInfo>();
|
||||
@@ -304,6 +321,11 @@ ORDER BY tag_name, primitive_name, attribute_name";
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the latest Galaxy deploy timestamp so change detection can decide whether the address space is stale.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the database query.</param>
|
||||
/// <returns>The most recent deploy timestamp, or <see langword="null"/> when none is available.</returns>
|
||||
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
||||
{
|
||||
using var conn = new SqlConnection(_config.ConnectionString);
|
||||
@@ -315,6 +337,11 @@ ORDER BY tag_name, primitive_name, attribute_name";
|
||||
return result is DateTime dt ? dt : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a lightweight query to confirm that the repository database is reachable.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the connectivity check.</param>
|
||||
/// <returns><see langword="true"/> when the query succeeds; otherwise, <see langword="false"/>.</returns>
|
||||
public async Task<bool> TestConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
@@ -335,6 +362,9 @@ ORDER BY tag_name, primitive_name, attribute_name";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the change event used by tests and monitoring components to simulate or announce a Galaxy deploy.
|
||||
/// </summary>
|
||||
public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
|
||||
/// </summary>
|
||||
public class GalaxyRepositoryStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy name currently being represented by the bridge.
|
||||
/// </summary>
|
||||
public string GalaxyName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Galaxy repository database is reachable.
|
||||
/// </summary>
|
||||
public bool DbConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the latest deploy timestamp read from the Galaxy repository.
|
||||
/// </summary>
|
||||
public DateTime? LastDeployTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of Galaxy objects currently published into the OPC UA address space.
|
||||
/// </summary>
|
||||
public int ObjectCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of Galaxy attributes currently published into the OPC UA address space.
|
||||
/// </summary>
|
||||
public int AttributeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC time when the address space was last rebuilt from repository data.
|
||||
/// </summary>
|
||||
public DateTime? LastRebuildTime { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
/// </summary>
|
||||
public interface ITimingScope : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks whether the timed bridge operation completed successfully.
|
||||
/// </summary>
|
||||
/// <param name="success">A value indicating whether the measured operation succeeded.</param>
|
||||
void SetSuccess(bool success);
|
||||
}
|
||||
|
||||
@@ -21,12 +25,39 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
/// </summary>
|
||||
public class MetricsStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of recorded executions for the operation.
|
||||
/// </summary>
|
||||
public long TotalCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of recorded executions that completed successfully.
|
||||
/// </summary>
|
||||
public long SuccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ratio of successful executions to total executions.
|
||||
/// </summary>
|
||||
public double SuccessRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mean execution time in milliseconds across the recorded sample.
|
||||
/// </summary>
|
||||
public double AverageMilliseconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fastest recorded execution time in milliseconds.
|
||||
/// </summary>
|
||||
public double MinMilliseconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the slowest recorded execution time in milliseconds.
|
||||
/// </summary>
|
||||
public double MaxMilliseconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the 95th percentile execution time in milliseconds.
|
||||
/// </summary>
|
||||
public double Percentile95Milliseconds { get; set; }
|
||||
}
|
||||
|
||||
@@ -43,6 +74,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
private double _minMilliseconds = double.MaxValue;
|
||||
private double _maxMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Records the outcome and duration of a single bridge operation invocation.
|
||||
/// </summary>
|
||||
/// <param name="duration">The elapsed time for the operation.</param>
|
||||
/// <param name="success">A value indicating whether the operation completed successfully.</param>
|
||||
public void Record(TimeSpan duration, bool success)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -61,6 +97,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a snapshot of the current statistics for this operation type.
|
||||
/// </summary>
|
||||
/// <returns>A statistics snapshot suitable for logs, status reporting, and tests.</returns>
|
||||
public MetricsStatistics GetStatistics()
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -98,28 +138,51 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
private readonly Timer _reportingTimer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new metrics collector and starts periodic performance reporting.
|
||||
/// </summary>
|
||||
public PerformanceMetrics()
|
||||
{
|
||||
_reportingTimer = new Timer(ReportMetrics, null,
|
||||
TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a completed bridge operation under the specified metrics bucket.
|
||||
/// </summary>
|
||||
/// <param name="operationName">The logical operation name, such as read, write, or subscribe.</param>
|
||||
/// <param name="duration">The elapsed time for the operation.</param>
|
||||
/// <param name="success">A value indicating whether the operation completed successfully.</param>
|
||||
public void RecordOperation(string operationName, TimeSpan duration, bool success = true)
|
||||
{
|
||||
var metrics = _metrics.GetOrAdd(operationName, _ => new OperationMetrics());
|
||||
metrics.Record(duration, success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts timing a bridge operation and returns a disposable scope that records the result when disposed.
|
||||
/// </summary>
|
||||
/// <param name="operationName">The logical operation name to record.</param>
|
||||
/// <returns>A timing scope that reports elapsed time back into this collector.</returns>
|
||||
public ITimingScope BeginOperation(string operationName)
|
||||
{
|
||||
return new TimingScope(this, operationName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the raw metrics bucket for a named operation.
|
||||
/// </summary>
|
||||
/// <param name="operationName">The logical operation name to look up.</param>
|
||||
/// <returns>The metrics bucket when present; otherwise, <see langword="null"/>.</returns>
|
||||
public OperationMetrics? GetMetrics(string operationName)
|
||||
{
|
||||
return _metrics.TryGetValue(operationName, out var metrics) ? metrics : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produces a statistics snapshot for all recorded bridge operations.
|
||||
/// </summary>
|
||||
/// <returns>A dictionary keyed by operation name containing current metrics statistics.</returns>
|
||||
public Dictionary<string, MetricsStatistics> GetStatistics()
|
||||
{
|
||||
var result = new Dictionary<string, MetricsStatistics>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -144,6 +207,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops periodic reporting and emits a final metrics snapshot.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
@@ -152,6 +218,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
ReportMetrics(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timing scope that records one operation result into the owning metrics collector.
|
||||
/// </summary>
|
||||
private class TimingScope : ITimingScope
|
||||
{
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
@@ -160,6 +229,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
private bool _success = true;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a timing scope for a named bridge operation.
|
||||
/// </summary>
|
||||
/// <param name="metrics">The metrics collector that should receive the result.</param>
|
||||
/// <param name="operationName">The logical operation name being timed.</param>
|
||||
public TimingScope(PerformanceMetrics metrics, string operationName)
|
||||
{
|
||||
_metrics = metrics;
|
||||
@@ -167,8 +241,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Metrics
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks whether the timed operation should be recorded as successful.
|
||||
/// </summary>
|
||||
/// <param name="success">A value indicating whether the operation succeeded.</param>
|
||||
public void SetSuccess(bool success) => _success = success;
|
||||
|
||||
/// <summary>
|
||||
/// Stops timing and records the operation result once.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
@@ -9,6 +9,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
{
|
||||
public sealed partial class MxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens the MXAccess runtime connection, replays stored subscriptions, and starts the optional probe subscription.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the connection attempt.</param>
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_state == ConnectionState.Connected) return;
|
||||
@@ -54,6 +58,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from the runtime and cleans up active handles, callbacks, and pending operations.
|
||||
/// </summary>
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_state == ConnectionState.Disconnected) return;
|
||||
@@ -100,6 +107,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to recover from a runtime fault by disconnecting and reconnecting the client.
|
||||
/// </summary>
|
||||
public async Task ReconnectAsync()
|
||||
{
|
||||
SetState(ConnectionState.Reconnecting);
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
{
|
||||
public sealed partial class MxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the background monitor that reconnects dropped sessions and watches the probe tag for staleness.
|
||||
/// </summary>
|
||||
public void StartMonitor()
|
||||
{
|
||||
_monitorCts = new CancellationTokenSource();
|
||||
@@ -15,6 +18,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
Log.Information("MxAccess monitor started (interval={Interval}s)", _config.MonitorIntervalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the background monitor loop.
|
||||
/// </summary>
|
||||
public void StopMonitor()
|
||||
{
|
||||
_monitorCts?.Cancel();
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
{
|
||||
public sealed partial class MxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs a one-shot read of a Galaxy tag by waiting for the next runtime data-change callback.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified Galaxy tag reference to read.</param>
|
||||
/// <param name="ct">A token that cancels the read.</param>
|
||||
/// <returns>The resulting VTQ value or a bad-quality fallback on timeout or failure.</returns>
|
||||
public async Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default)
|
||||
{
|
||||
if (_state != ConnectionState.Connected)
|
||||
@@ -79,6 +85,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to a Galaxy tag and waits for the runtime write-complete callback.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified Galaxy tag reference to write.</param>
|
||||
/// <param name="value">The value to send to the runtime.</param>
|
||||
/// <param name="ct">A token that cancels the write.</param>
|
||||
/// <returns><see langword="true"/> when the runtime acknowledges success; otherwise, <see langword="false"/>.</returns>
|
||||
public async Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
|
||||
{
|
||||
if (_state != ConnectionState.Connected) return false;
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
{
|
||||
public sealed partial class MxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a persistent subscription callback for a Galaxy tag and activates it immediately when connected.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified Galaxy tag reference to monitor.</param>
|
||||
/// <param name="callback">The callback that should receive runtime value changes.</param>
|
||||
public async Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback)
|
||||
{
|
||||
_storedSubscriptions[fullTagReference] = callback;
|
||||
@@ -15,6 +20,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
await SubscribeInternalAsync(fullTagReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a persistent subscription callback and tears down the runtime item when appropriate.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified Galaxy tag reference to stop monitoring.</param>
|
||||
public async Task UnsubscribeAsync(string fullTagReference)
|
||||
{
|
||||
_storedSubscriptions.TryRemove(fullTagReference, out _);
|
||||
|
||||
@@ -49,13 +49,38 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
private DateTime _lastProbeValueTime = DateTime.UtcNow;
|
||||
private int _reconnectCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current runtime connection state for the MXAccess client.
|
||||
/// </summary>
|
||||
public ConnectionState State => _state;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of active tag subscriptions currently maintained against the runtime.
|
||||
/// </summary>
|
||||
public int ActiveSubscriptionCount => _storedSubscriptions.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of reconnect attempts performed since the client was created.
|
||||
/// </summary>
|
||||
public int ReconnectCount => _reconnectCount;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the MXAccess connection state changes.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a subscribed runtime tag publishes a new value.
|
||||
/// </summary>
|
||||
public event Action<string, Vtq>? OnTagValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new MXAccess client around the STA thread, COM proxy abstraction, and runtime throttling settings.
|
||||
/// </summary>
|
||||
/// <param name="staThread">The STA thread used to marshal COM interactions.</param>
|
||||
/// <param name="proxy">The COM proxy abstraction used to talk to the runtime.</param>
|
||||
/// <param name="config">The runtime timeout, throttling, and reconnect settings.</param>
|
||||
/// <param name="metrics">The metrics collector used to time MXAccess operations.</param>
|
||||
public MxAccessClient(StaComThread staThread, IMxProxy proxy, MxAccessConfiguration config, PerformanceMetrics metrics)
|
||||
{
|
||||
_staThread = staThread;
|
||||
@@ -74,6 +99,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(previous, newState, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels monitoring and disconnects the runtime session before releasing local resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -13,9 +13,21 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
{
|
||||
private LMXProxyServer? _lmxProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the COM proxy publishes a live data-change callback for a subscribed Galaxy attribute.
|
||||
/// </summary>
|
||||
public event MxDataChangeHandler? OnDataChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the COM proxy confirms completion of a write request.
|
||||
/// </summary>
|
||||
public event MxWriteCompleteHandler? OnWriteComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and registers the COM proxy session that backs live MXAccess operations.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client name reported to the Wonderware runtime.</param>
|
||||
/// <returns>The runtime connection handle assigned by the COM server.</returns>
|
||||
public int Register(string clientName)
|
||||
{
|
||||
_lmxProxy = new LMXProxyServer();
|
||||
@@ -30,6 +42,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the COM proxy session and releases the underlying COM object.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle returned by <see cref="Register(string)"/>.</param>
|
||||
public void Unregister(int handle)
|
||||
{
|
||||
if (_lmxProxy != null)
|
||||
@@ -48,10 +64,42 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a Galaxy attribute reference into a runtime item handle through the COM proxy.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="address">The fully qualified Galaxy attribute reference.</param>
|
||||
/// <returns>The item handle assigned by the COM proxy.</returns>
|
||||
public int AddItem(int handle, string address) => _lmxProxy!.AddItem(handle, address);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an item handle from the active COM proxy session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to remove.</param>
|
||||
public void RemoveItem(int handle, int itemHandle) => _lmxProxy!.RemoveItem(handle, itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Enables supervisory callbacks for the specified runtime item.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to monitor.</param>
|
||||
public void AdviseSupervisory(int handle, int itemHandle) => _lmxProxy!.AdviseSupervisory(handle, itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Disables supervisory callbacks for the specified runtime item.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to stop monitoring.</param>
|
||||
public void UnAdviseSupervisory(int handle, int itemHandle) => _lmxProxy!.UnAdvise(handle, itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to the specified runtime item through the COM proxy.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to write.</param>
|
||||
/// <param name="value">The value to send to the runtime.</param>
|
||||
/// <param name="securityClassification">The Wonderware security classification applied to the write.</param>
|
||||
public void Write(int handle, int itemHandle, object value, int securityClassification)
|
||||
=> _lmxProxy!.Write(handle, itemHandle, value, securityClassification);
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
private long _workItemsExecuted;
|
||||
private DateTime _lastLogTime;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a dedicated STA thread wrapper for Wonderware COM interop.
|
||||
/// </summary>
|
||||
public StaComThread()
|
||||
{
|
||||
_thread = new Thread(ThreadEntry)
|
||||
@@ -41,8 +44,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
_thread.SetApartmentState(ApartmentState.STA);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the STA thread is running and able to accept work.
|
||||
/// </summary>
|
||||
public bool IsRunning => _nativeThreadId != 0 && !_disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the STA thread and waits until its message pump is ready for COM work.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_thread.Start();
|
||||
@@ -50,6 +59,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
Log.Information("STA COM thread started (ThreadId={ThreadId})", _thread.ManagedThreadId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues an action to execute on the STA thread.
|
||||
/// </summary>
|
||||
/// <param name="action">The work item to execute on the STA thread.</param>
|
||||
/// <returns>A task that completes when the action has finished executing.</returns>
|
||||
public Task RunAsync(Action action)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
|
||||
@@ -71,6 +85,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a function to execute on the STA thread and returns its result.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The result type produced by the function.</typeparam>
|
||||
/// <param name="func">The work item to execute on the STA thread.</param>
|
||||
/// <returns>A task that completes with the function result.</returns>
|
||||
public Task<T> RunAsync<T>(Func<T> func)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
|
||||
@@ -91,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the STA thread and releases the message-pump resources used for COM interop.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
@@ -19,21 +19,70 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// </summary>
|
||||
public class NodeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy object identifier represented by this address-space node.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime tag name used to tie the node back to Galaxy metadata.
|
||||
/// </summary>
|
||||
public string TagName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the browse name exposed to OPC UA clients for this hierarchy node.
|
||||
/// </summary>
|
||||
public string BrowseName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent Galaxy object identifier used to assemble the tree.
|
||||
/// </summary>
|
||||
public int ParentGobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the node represents a Galaxy area folder.
|
||||
/// </summary>
|
||||
public bool IsArea { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attribute nodes published beneath this object.
|
||||
/// </summary>
|
||||
public List<AttributeNodeInfo> Attributes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the child nodes that appear under this branch of the Galaxy hierarchy.
|
||||
/// </summary>
|
||||
public List<NodeInfo> Children { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight description of an attribute node that will become an OPC UA variable.
|
||||
/// </summary>
|
||||
public class AttributeNodeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy attribute name published under the object.
|
||||
/// </summary>
|
||||
public string AttributeName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fully qualified runtime reference used for reads, writes, and subscriptions.
|
||||
/// </summary>
|
||||
public string FullTagReference { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy data type code used to pick the OPC UA variable type.
|
||||
/// </summary>
|
||||
public int MxDataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the attribute is modeled as an array.
|
||||
/// </summary>
|
||||
public bool IsArray { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the declared array length when the attribute is a fixed-size array.
|
||||
/// </summary>
|
||||
public int? ArrayDimension { get; set; }
|
||||
}
|
||||
|
||||
@@ -42,12 +91,33 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// </summary>
|
||||
public class AddressSpaceModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the root nodes that become the top-level browse entries in the Galaxy namespace.
|
||||
/// </summary>
|
||||
public List<NodeInfo> RootNodes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mapping from OPC UA node identifiers to runtime tag references.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> NodeIdToTagReference { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of non-area Galaxy objects included in the model.
|
||||
/// </summary>
|
||||
public int ObjectCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of variable nodes created from Galaxy attributes.
|
||||
/// </summary>
|
||||
public int VariableCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an in-memory model of the Galaxy hierarchy and attribute mappings before the OPC UA server materializes nodes.
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">The Galaxy object hierarchy returned by the repository.</param>
|
||||
/// <param name="attributes">The Galaxy attribute rows associated with the hierarchy.</param>
|
||||
/// <returns>An address-space model containing roots, variables, and tag-reference mappings.</returns>
|
||||
public static AddressSpaceModel Build(List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes)
|
||||
{
|
||||
var model = new AddressSpaceModel();
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// </summary>
|
||||
public static class DataValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a bridge VTQ snapshot into an OPC UA data value.
|
||||
/// </summary>
|
||||
/// <param name="vtq">The VTQ snapshot to convert.</param>
|
||||
/// <returns>An OPC UA data value suitable for reads and subscriptions.</returns>
|
||||
public static DataValue FromVtq(Vtq vtq)
|
||||
{
|
||||
var statusCode = new StatusCode(QualityMapper.MapToOpcUaStatusCode(vtq.Quality));
|
||||
@@ -24,6 +29,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
return dataValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an OPC UA data value back into a bridge VTQ snapshot.
|
||||
/// </summary>
|
||||
/// <param name="dataValue">The OPC UA data value to convert.</param>
|
||||
/// <returns>A VTQ snapshot containing the converted value, timestamp, and derived quality.</returns>
|
||||
public static Vtq ToVtq(DataValue dataValue)
|
||||
{
|
||||
var quality = MapStatusCodeToQuality(dataValue.StatusCode);
|
||||
|
||||
@@ -31,10 +31,29 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
private readonly object _lock = new object();
|
||||
private IDictionary<NodeId, IList<IReference>>? _externalReferences;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mapping from OPC UA node identifiers to the Galaxy tag references used for runtime I/O.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> NodeIdToTagReference => _nodeIdToTagReference;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of variable nodes currently published from Galaxy attributes.
|
||||
/// </summary>
|
||||
public int VariableNodeCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of non-area object nodes currently published from the Galaxy hierarchy.
|
||||
/// </summary>
|
||||
public int ObjectNodeCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new node manager for the Galaxy-backed OPC UA namespace.
|
||||
/// </summary>
|
||||
/// <param name="server">The hosting OPC UA server internals.</param>
|
||||
/// <param name="configuration">The OPC UA application configuration for the host.</param>
|
||||
/// <param name="namespaceUri">The namespace URI that identifies the Galaxy model to clients.</param>
|
||||
/// <param name="mxAccessClient">The runtime client used to service reads, writes, and subscriptions.</param>
|
||||
/// <param name="metrics">The metrics collector used to track node manager activity.</param>
|
||||
public LmxNodeManager(
|
||||
IServerInternal server,
|
||||
ApplicationConfiguration configuration,
|
||||
@@ -51,6 +70,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_mxAccessClient.OnTagValueChanged += OnMxAccessDataChange;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
|
||||
{
|
||||
lock (Lock)
|
||||
@@ -63,6 +83,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// <summary>
|
||||
/// Builds the address space from Galaxy hierarchy and attributes data. (OPC-002, OPC-003)
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">The Galaxy object hierarchy that defines folders and objects in the namespace.</param>
|
||||
/// <param name="attributes">The Galaxy attributes that become OPC UA variable nodes.</param>
|
||||
public void BuildAddressSpace(List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes)
|
||||
{
|
||||
lock (Lock)
|
||||
@@ -145,6 +167,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// <summary>
|
||||
/// Rebuilds the address space, removing old nodes and creating new ones. (OPC-010)
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">The latest Galaxy object hierarchy to publish.</param>
|
||||
/// <param name="attributes">The latest Galaxy attributes to publish.</param>
|
||||
public void RebuildAddressSpace(List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes)
|
||||
{
|
||||
lock (Lock)
|
||||
@@ -316,6 +340,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
|
||||
#region Read/Write Handlers
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Read(OperationContext context, double maxAge, IList<ReadValueId> nodesToRead,
|
||||
IList<DataValue> results, IList<ServiceResult> errors)
|
||||
{
|
||||
@@ -346,6 +371,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(OperationContext context, IList<WriteValue> nodesToWrite,
|
||||
IList<ServiceResult> errors)
|
||||
{
|
||||
@@ -444,6 +470,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// Called by the OPC UA framework after monitored items are created on nodes in our namespace.
|
||||
/// Triggers ref-counted MXAccess subscriptions for the underlying tags.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void OnCreateMonitoredItemsComplete(ServerSystemContext context, IList<IMonitoredItem> monitoredItems)
|
||||
{
|
||||
foreach (var item in monitoredItems)
|
||||
@@ -458,6 +485,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// Called by the OPC UA framework after monitored items are deleted.
|
||||
/// Decrements ref-counted MXAccess subscriptions.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void OnDeleteMonitoredItemsComplete(ServerSystemContext context, IList<IMonitoredItem> monitoredItems)
|
||||
{
|
||||
foreach (var item in monitoredItems)
|
||||
@@ -475,6 +503,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the subscription reference count for a Galaxy tag and opens the runtime subscription when the first OPC UA monitored item appears.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified Galaxy tag reference to subscribe.</param>
|
||||
internal void SubscribeTag(string fullTagReference)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -491,6 +523,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrements the subscription reference count for a Galaxy tag and closes the runtime subscription when no OPC UA monitored items remain.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified Galaxy tag reference to unsubscribe.</param>
|
||||
internal void UnsubscribeTag(string fullTagReference)
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
@@ -16,7 +16,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private LmxNodeManager? _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the custom node manager that publishes the Galaxy-backed namespace.
|
||||
/// </summary>
|
||||
public LmxNodeManager? NodeManager => _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of active OPC UA sessions currently connected to the server.
|
||||
/// </summary>
|
||||
public int ActiveSessionCount
|
||||
{
|
||||
get
|
||||
@@ -26,6 +33,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a custom OPC UA server for the specified Galaxy namespace.
|
||||
/// </summary>
|
||||
/// <param name="galaxyName">The Galaxy name used to construct the namespace URI and product URI.</param>
|
||||
/// <param name="mxAccessClient">The runtime client used by the node manager for live data access.</param>
|
||||
/// <param name="metrics">The metrics collector shared with the node manager.</param>
|
||||
public LmxOpcUaServer(string galaxyName, IMxAccessClient mxAccessClient, PerformanceMetrics metrics)
|
||||
{
|
||||
_galaxyName = galaxyName;
|
||||
@@ -33,6 +46,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
|
||||
{
|
||||
var namespaceUri = $"urn:{_galaxyName}:LmxOpcUa";
|
||||
@@ -42,6 +56,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ServerProperties LoadServerProperties()
|
||||
{
|
||||
var properties = new ServerProperties
|
||||
|
||||
@@ -8,11 +8,21 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// </summary>
|
||||
public static class OpcUaQualityMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts bridge quality values into OPC UA status codes.
|
||||
/// </summary>
|
||||
/// <param name="quality">The bridge quality value.</param>
|
||||
/// <returns>The OPC UA status code to publish.</returns>
|
||||
public static StatusCode ToStatusCode(Quality quality)
|
||||
{
|
||||
return new StatusCode(QualityMapper.MapToOpcUaStatusCode(quality));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an OPC UA status code back into a bridge quality category.
|
||||
/// </summary>
|
||||
/// <param name="statusCode">The OPC UA status code to interpret.</param>
|
||||
/// <returns>The bridge quality category represented by the status code.</returns>
|
||||
public static Quality FromStatusCode(StatusCode statusCode)
|
||||
{
|
||||
if (StatusCode.IsGood(statusCode)) return Quality.Good;
|
||||
|
||||
@@ -23,10 +23,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
private ApplicationInstance? _application;
|
||||
private LmxOpcUaServer? _server;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active node manager that holds the published Galaxy namespace.
|
||||
/// </summary>
|
||||
public LmxNodeManager? NodeManager => _server?.NodeManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of currently connected OPC UA client sessions.
|
||||
/// </summary>
|
||||
public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the OPC UA server has been started and not yet stopped.
|
||||
/// </summary>
|
||||
public bool IsRunning => _server != null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new host for the Galaxy-backed OPC UA server instance.
|
||||
/// </summary>
|
||||
/// <param name="config">The endpoint and session settings for the OPC UA host.</param>
|
||||
/// <param name="mxAccessClient">The runtime client used by the node manager for live reads, writes, and subscriptions.</param>
|
||||
/// <param name="metrics">The metrics collector shared with the node manager and runtime bridge.</param>
|
||||
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics)
|
||||
{
|
||||
_config = config;
|
||||
@@ -34,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the OPC UA application instance, prepares certificates, and binds the Galaxy namespace to the configured endpoint.
|
||||
/// </summary>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa";
|
||||
@@ -142,6 +162,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_config.Port, _config.EndpointPath, namespaceUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the OPC UA application instance and releases its in-memory server objects.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
@@ -160,6 +183,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the host and releases server resources.
|
||||
/// </summary>
|
||||
public void Dispose() => Stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
/// <summary>
|
||||
/// Test constructor. Accepts injected dependencies.
|
||||
/// </summary>
|
||||
/// <param name="config">The service configuration used to shape OPC UA hosting, MXAccess connectivity, and dashboard behavior during the test run.</param>
|
||||
/// <param name="mxProxy">The MXAccess proxy substitute used when a test wants to exercise COM-style wiring.</param>
|
||||
/// <param name="galaxyRepository">The repository substitute that supplies Galaxy hierarchy and deploy metadata for address-space builds.</param>
|
||||
/// <param name="mxAccessClientOverride">An optional direct MXAccess client substitute that bypasses STA thread setup and COM interop.</param>
|
||||
/// <param name="hasMxAccessClientOverride">A value indicating whether the override client should be used instead of creating a client from <paramref name="mxProxy"/>.</param>
|
||||
internal OpcUaService(AppConfiguration config, IMxProxy? mxProxy, IGalaxyRepository? galaxyRepository,
|
||||
IMxAccessClient? mxAccessClientOverride = null, bool hasMxAccessClientOverride = false)
|
||||
{
|
||||
@@ -72,6 +77,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
_hasMxAccessClientOverride = hasMxAccessClientOverride;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the bridge by validating configuration, connecting runtime dependencies, building the Galaxy-backed OPC UA address space, and optionally hosting the status dashboard.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
Log.Information("LmxOpcUa service starting");
|
||||
@@ -212,6 +220,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the bridge, cancels monitoring loops, disconnects runtime integrations, and releases hosted resources in shutdown order.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
Log.Information("LmxOpcUa service stopping");
|
||||
@@ -282,13 +293,44 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
internal void TriggerRebuild() => OnGalaxyChanged();
|
||||
|
||||
// Accessors for testing
|
||||
/// <summary>
|
||||
/// Gets the MXAccess client instance currently wired into the service for test inspection.
|
||||
/// </summary>
|
||||
internal IMxAccessClient? MxClient => (IMxAccessClient?)_mxAccessClient ?? _mxAccessClientForWiring;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metrics collector that tracks bridge operation timings during the service lifetime.
|
||||
/// </summary>
|
||||
internal PerformanceMetrics? Metrics => _metrics;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA server host that owns the runtime endpoint.
|
||||
/// </summary>
|
||||
internal OpcUaServerHost? ServerHost => _serverHost;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node manager instance that holds the current Galaxy-derived address space.
|
||||
/// </summary>
|
||||
internal LmxNodeManager? NodeManagerInstance => _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the change-detection service that watches for Galaxy deploys requiring a rebuild.
|
||||
/// </summary>
|
||||
internal ChangeDetectionService? ChangeDetectionInstance => _changeDetection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hosted status web server when the dashboard is enabled.
|
||||
/// </summary>
|
||||
internal StatusWebServer? StatusWeb => _statusWebServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dashboard report generator used to assemble operator-facing status snapshots.
|
||||
/// </summary>
|
||||
internal StatusReportService? StatusReportInstance => _statusReport;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Galaxy statistics snapshot populated during repository reads and rebuilds.
|
||||
/// </summary>
|
||||
internal GalaxyRepositoryStats? GalaxyStatsInstance => _galaxyStats;
|
||||
}
|
||||
|
||||
@@ -297,18 +339,75 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
/// </summary>
|
||||
internal sealed class NullMxAccessClient : IMxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the disconnected state reported when the bridge is running without live MXAccess connectivity.
|
||||
/// </summary>
|
||||
public ConnectionState State => ConnectionState.Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active subscription count, which is always zero for the null runtime client.
|
||||
/// </summary>
|
||||
public int ActiveSubscriptionCount => 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reconnect count, which is always zero because the null client never establishes a session.
|
||||
/// </summary>
|
||||
public int ReconnectCount => 0;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the runtime connection state changes. The null client never raises this event.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a subscribed tag value changes. The null client never raises this event.
|
||||
/// </summary>
|
||||
public event Action<string, Vtq>? OnTagValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Completes immediately because no live runtime connection is available or required.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token that is ignored by the null implementation.</param>
|
||||
public System.Threading.Tasks.Task ConnectAsync(CancellationToken ct = default) => System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Completes immediately because there is no live runtime session to close.
|
||||
/// </summary>
|
||||
public System.Threading.Tasks.Task DisconnectAsync() => System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Completes immediately because the null client does not subscribe to live Galaxy attributes.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The tag reference that would have been subscribed.</param>
|
||||
/// <param name="callback">The callback that would have received runtime value changes.</param>
|
||||
public System.Threading.Tasks.Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback) => System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Completes immediately because the null client does not maintain runtime subscriptions.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The tag reference that would have been unsubscribed.</param>
|
||||
public System.Threading.Tasks.Task UnsubscribeAsync(string fullTagReference) => System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a bad-quality value because no live runtime source exists.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The tag reference that would have been read from the runtime.</param>
|
||||
/// <param name="ct">A cancellation token that is ignored by the null implementation.</param>
|
||||
/// <returns>A bad-quality VTQ indicating that runtime data is unavailable.</returns>
|
||||
public System.Threading.Tasks.Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default) => System.Threading.Tasks.Task.FromResult(Vtq.Bad());
|
||||
|
||||
/// <summary>
|
||||
/// Rejects writes because there is no live runtime endpoint behind the null client.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The tag reference that would have been written.</param>
|
||||
/// <param name="value">The value that would have been sent to the runtime.</param>
|
||||
/// <param name="ct">A cancellation token that is ignored by the null implementation.</param>
|
||||
/// <returns>A completed task returning <see langword="false"/>.</returns>
|
||||
public System.Threading.Tasks.Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default) => System.Threading.Tasks.Task.FromResult(false);
|
||||
|
||||
/// <summary>
|
||||
/// Releases the null client. No unmanaged runtime resources exist.
|
||||
/// </summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,24 +18,44 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
private bool _galaxyRepositorySet;
|
||||
private bool _mxAccessClientSet;
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the default service configuration used by the test host.
|
||||
/// </summary>
|
||||
/// <param name="config">The full configuration snapshot to inject into the service under test.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithConfig(AppConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the OPC UA port used by the test host so multiple integration runs can coexist.
|
||||
/// </summary>
|
||||
/// <param name="port">The TCP port to expose for the test server.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithOpcUaPort(int port)
|
||||
{
|
||||
_config.OpcUa.Port = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Galaxy name represented by the test address space.
|
||||
/// </summary>
|
||||
/// <param name="name">The Galaxy name to expose through OPC UA and diagnostics.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithGalaxyName(string name)
|
||||
{
|
||||
_config.OpcUa.GalaxyName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects an MXAccess proxy substitute for tests that exercise the proxy-driven runtime path.
|
||||
/// </summary>
|
||||
/// <param name="proxy">The proxy fake or stub to supply to the service.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithMxProxy(IMxProxy? proxy)
|
||||
{
|
||||
_mxProxy = proxy;
|
||||
@@ -43,6 +63,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects a repository substitute for tests that control Galaxy hierarchy and deploy metadata.
|
||||
/// </summary>
|
||||
/// <param name="repository">The repository fake or stub to supply to the service.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithGalaxyRepository(IGalaxyRepository? repository)
|
||||
{
|
||||
_galaxyRepository = repository;
|
||||
@@ -54,6 +79,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
/// Override the MxAccessClient directly, skipping STA thread and COM interop entirely.
|
||||
/// When set, the service will use this client instead of creating one from IMxProxy.
|
||||
/// </summary>
|
||||
/// <param name="client">The direct MXAccess client substitute to inject into the service.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithMxAccessClient(IMxAccessClient? client)
|
||||
{
|
||||
_mxAccessClient = client;
|
||||
@@ -61,6 +88,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds a convenience fake repository with Galaxy hierarchy and attribute rows for address-space tests.
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">The object hierarchy to expose through the test OPC UA namespace.</param>
|
||||
/// <param name="attributes">The attribute rows to attach to the hierarchy.</param>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder WithHierarchy(List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes)
|
||||
{
|
||||
if (!_galaxyRepositorySet)
|
||||
@@ -79,18 +112,30 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the embedded dashboard so tests can focus on the runtime bridge without binding the HTTP listener.
|
||||
/// </summary>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder DisableDashboard()
|
||||
{
|
||||
_config.Dashboard.Enabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Effectively disables Galaxy change detection by pushing the polling interval beyond realistic test durations.
|
||||
/// </summary>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
public OpcUaServiceBuilder DisableChangeDetection()
|
||||
{
|
||||
_config.GalaxyRepository.ChangeDetectionIntervalSeconds = int.MaxValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="OpcUaService"/> using the accumulated test doubles and configuration overrides.
|
||||
/// </summary>
|
||||
/// <returns>A service instance ready for integration-style testing.</returns>
|
||||
public OpcUaService Build()
|
||||
{
|
||||
return new OpcUaService(
|
||||
@@ -106,16 +151,50 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
/// </summary>
|
||||
private class FakeBuilderGalaxyRepository : IGalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the fake repository wants to simulate a Galaxy deploy change.
|
||||
/// </summary>
|
||||
public event System.Action? OnGalaxyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hierarchy rows that the fake repository returns to the service.
|
||||
/// </summary>
|
||||
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attribute rows that the fake repository returns to the service.
|
||||
/// </summary>
|
||||
public List<GalaxyAttributeInfo> Attributes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the seeded hierarchy rows for address-space construction.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured hierarchy rows.</returns>
|
||||
public System.Threading.Tasks.Task<List<GalaxyObjectInfo>> GetHierarchyAsync(System.Threading.CancellationToken ct = default)
|
||||
=> System.Threading.Tasks.Task.FromResult(Hierarchy);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the seeded attribute rows for address-space construction.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured attribute rows.</returns>
|
||||
public System.Threading.Tasks.Task<List<GalaxyAttributeInfo>> GetAttributesAsync(System.Threading.CancellationToken ct = default)
|
||||
=> System.Threading.Tasks.Task.FromResult(Attributes);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current UTC time so change-detection tests have a deploy timestamp to compare against.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
/// <returns>The current UTC time.</returns>
|
||||
public System.Threading.Tasks.Task<System.DateTime?> GetLastDeployTimeAsync(System.Threading.CancellationToken ct = default)
|
||||
=> System.Threading.Tasks.Task.FromResult<System.DateTime?>(System.DateTime.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// Reports a healthy repository connection for builder-based test setups.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token that is ignored by the in-memory fake.</param>
|
||||
/// <returns>A completed task returning <see langword="true"/>.</returns>
|
||||
public System.Threading.Tasks.Task<bool> TestConnectionAsync(System.Threading.CancellationToken ct = default)
|
||||
=> System.Threading.Tasks.Task.FromResult(true);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
/// </summary>
|
||||
public class HealthCheckService
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates bridge health from runtime connectivity and recorded performance metrics.
|
||||
/// </summary>
|
||||
/// <param name="connectionState">The current MXAccess connection state.</param>
|
||||
/// <param name="metrics">The recorded performance metrics, if available.</param>
|
||||
/// <returns>A dashboard health snapshot describing the current service condition.</returns>
|
||||
public HealthInfo CheckHealth(ConnectionState connectionState, PerformanceMetrics? metrics)
|
||||
{
|
||||
// Rule 1: Not connected → Unhealthy
|
||||
@@ -48,6 +54,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the bridge should currently be treated as healthy.
|
||||
/// </summary>
|
||||
/// <param name="connectionState">The current MXAccess connection state.</param>
|
||||
/// <param name="metrics">The recorded performance metrics, if available.</param>
|
||||
/// <returns><see langword="true"/> when the bridge is not unhealthy; otherwise, <see langword="false"/>.</returns>
|
||||
public bool IsHealthy(ConnectionState connectionState, PerformanceMetrics? metrics)
|
||||
{
|
||||
var health = CheckHealth(connectionState, metrics);
|
||||
|
||||
@@ -9,46 +9,139 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
/// </summary>
|
||||
public class StatusData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current MXAccess and service connectivity summary shown on the dashboard.
|
||||
/// </summary>
|
||||
public ConnectionInfo Connection { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the overall health state communicated to operators.
|
||||
/// </summary>
|
||||
public HealthInfo Health { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets subscription counts that show how many live tag streams the bridge is maintaining.
|
||||
/// </summary>
|
||||
public SubscriptionInfo Subscriptions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Galaxy-specific metadata such as deploy timing and address-space counts.
|
||||
/// </summary>
|
||||
public GalaxyInfo Galaxy { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets per-operation performance statistics used to diagnose bridge throughput and latency.
|
||||
/// </summary>
|
||||
public Dictionary<string, MetricsStatistics> Operations { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets footer details such as the snapshot timestamp and service version.
|
||||
/// </summary>
|
||||
public FooterInfo Footer { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard model for current runtime connection details.
|
||||
/// </summary>
|
||||
public class ConnectionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current MXAccess connection state shown to operators.
|
||||
/// </summary>
|
||||
public string State { get; set; } = "Disconnected";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how many reconnect attempts have occurred since the service started.
|
||||
/// </summary>
|
||||
public int ReconnectCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of active OPC UA sessions connected to the bridge.
|
||||
/// </summary>
|
||||
public int ActiveSessions { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard model for the overall health banner.
|
||||
/// </summary>
|
||||
public class HealthInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the high-level health state, such as Healthy, Degraded, or Unhealthy.
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "Unknown";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator-facing explanation for the current health state.
|
||||
/// </summary>
|
||||
public string Message { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color token used by the dashboard UI to render the health banner.
|
||||
/// </summary>
|
||||
public string Color { get; set; } = "gray";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard model for subscription load.
|
||||
/// </summary>
|
||||
public class SubscriptionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the number of active tag subscriptions mirrored from MXAccess into OPC UA.
|
||||
/// </summary>
|
||||
public int ActiveCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard model for Galaxy metadata and rebuild status.
|
||||
/// </summary>
|
||||
public class GalaxyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy name currently being bridged into OPC UA.
|
||||
/// </summary>
|
||||
public string GalaxyName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the repository database is currently reachable.
|
||||
/// </summary>
|
||||
public bool DbConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the most recent deploy timestamp observed in the Galaxy repository.
|
||||
/// </summary>
|
||||
public DateTime? LastDeployTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of Galaxy objects currently represented in the address space.
|
||||
/// </summary>
|
||||
public int ObjectCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of Galaxy attributes currently represented as OPC UA variables.
|
||||
/// </summary>
|
||||
public int AttributeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC timestamp of the last completed address-space rebuild.
|
||||
/// </summary>
|
||||
public DateTime? LastRebuildTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard model for the status page footer.
|
||||
/// </summary>
|
||||
public class FooterInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC time when the status snapshot was generated.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service version displayed to operators for support and traceability.
|
||||
/// </summary>
|
||||
public string Version { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
private GalaxyRepositoryStats? _galaxyStats;
|
||||
private OpcUaServerHost? _serverHost;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new status report service for the dashboard using the supplied health-check policy and refresh interval.
|
||||
/// </summary>
|
||||
/// <param name="healthCheck">The health-check component used to derive the overall dashboard health status.</param>
|
||||
/// <param name="refreshIntervalSeconds">The HTML auto-refresh interval, in seconds, for the dashboard page.</param>
|
||||
public StatusReportService(HealthCheckService healthCheck, int refreshIntervalSeconds)
|
||||
{
|
||||
_healthCheck = healthCheck;
|
||||
_refreshIntervalSeconds = refreshIntervalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies the live bridge components whose status should be reflected in generated dashboard snapshots.
|
||||
/// </summary>
|
||||
/// <param name="mxAccessClient">The runtime client whose connection and subscription state should be reported.</param>
|
||||
/// <param name="metrics">The performance metrics collector whose operation statistics should be reported.</param>
|
||||
/// <param name="galaxyStats">The Galaxy repository statistics to surface on the dashboard.</param>
|
||||
/// <param name="serverHost">The OPC UA server host whose active session count should be reported.</param>
|
||||
public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics,
|
||||
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost)
|
||||
{
|
||||
@@ -36,6 +48,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
_serverHost = serverHost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the structured dashboard snapshot consumed by the HTML and JSON renderers.
|
||||
/// </summary>
|
||||
/// <returns>The current dashboard status data for the bridge.</returns>
|
||||
public StatusData GetStatusData()
|
||||
{
|
||||
var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected;
|
||||
@@ -71,6 +87,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the operator-facing HTML dashboard for the current bridge status.
|
||||
/// </summary>
|
||||
/// <returns>An HTML document containing the latest dashboard snapshot.</returns>
|
||||
public string GenerateHtml()
|
||||
{
|
||||
var data = GetStatusData();
|
||||
@@ -131,12 +151,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an indented JSON status payload for API consumers.
|
||||
/// </summary>
|
||||
/// <returns>A JSON representation of the current dashboard snapshot.</returns>
|
||||
public string GenerateJson()
|
||||
{
|
||||
var data = GetStatusData();
|
||||
return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the bridge should currently be considered healthy for the dashboard health endpoint.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when the bridge meets the health policy; otherwise, <see langword="false"/>.</returns>
|
||||
public bool IsHealthy()
|
||||
{
|
||||
var state = _mxAccessClient?.State ?? ConnectionState.Disconnected;
|
||||
|
||||
@@ -19,14 +19,25 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
private HttpListener? _listener;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dashboard listener is currently accepting requests.
|
||||
/// </summary>
|
||||
public bool IsRunning => _listener?.IsListening ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new dashboard web server bound to the supplied report service and HTTP port.
|
||||
/// </summary>
|
||||
/// <param name="reportService">The report service used to generate dashboard responses.</param>
|
||||
/// <param name="port">The HTTP port to listen on.</param>
|
||||
public StatusWebServer(StatusReportService reportService, int port)
|
||||
{
|
||||
_reportService = reportService;
|
||||
_port = port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the HTTP listener and background request loop for the status dashboard.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
@@ -47,6 +58,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the dashboard listener and releases its HTTP resources.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
@@ -139,6 +153,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
response.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the dashboard listener and releases its resources.
|
||||
/// </summary>
|
||||
public void Dispose() => Stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests that exercise the real Galaxy repository queries against the test database configuration.
|
||||
/// </summary>
|
||||
public class GalaxyRepositoryServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads repository configuration from the integration test settings and controls whether extended attributes are enabled.
|
||||
/// </summary>
|
||||
/// <param name="extendedAttributes">A value indicating whether the extended attribute query path should be enabled.</param>
|
||||
/// <returns>The repository configuration used by the integration test.</returns>
|
||||
private static GalaxyRepositoryConfiguration LoadConfig(bool extendedAttributes = false)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -22,6 +30,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the standard attribute query returns rows from the repository.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_StandardMode_ReturnsRows()
|
||||
{
|
||||
@@ -35,6 +46,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.PrimitiveName == "" && r.AttributeSource == "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended attribute query returns more rows than the standard query path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_ReturnsMoreRows()
|
||||
{
|
||||
@@ -49,6 +63,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
extendedResults.Count.ShouldBeGreaterThan(standardResults.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended attribute query includes both primitive and dynamic attribute sources.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_IncludesPrimitiveAttributes()
|
||||
{
|
||||
@@ -61,6 +78,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldContain(r => r.AttributeSource == "dynamic");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended mode populates attribute-source metadata across the result set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_PrimitiveNamePopulated()
|
||||
{
|
||||
@@ -76,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.AttributeSource == "primitive" || r.AttributeSource == "dynamic");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that standard-mode results always include fully qualified tag references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_StandardMode_AllHaveFullTagReference()
|
||||
{
|
||||
@@ -88,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.FullTagReference.Contains("."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-mode results always include fully qualified tag references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_AllHaveFullTagReference()
|
||||
{
|
||||
|
||||
@@ -3,8 +3,14 @@ using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder integration test that keeps the integration test project wired into the solution.
|
||||
/// </summary>
|
||||
public class SampleIntegrationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the integration test assembly is executing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Placeholder_ShouldPass()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,15 @@ using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that application configuration binds correctly from appsettings and that validation catches invalid bridge settings.
|
||||
/// </summary>
|
||||
public class ConfigurationLoadingTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the application configuration from the repository appsettings file for binding tests.
|
||||
/// </summary>
|
||||
/// <returns>The bound application configuration snapshot.</returns>
|
||||
private static AppConfiguration LoadFromJson()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -21,6 +28,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUa_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -33,6 +43,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.OpcUa.SessionTimeoutMinutes.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MxAccess_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -46,6 +59,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.MxAccess.ProbeStaleThresholdSeconds.ShouldBe(60);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -56,6 +72,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.GalaxyRepository.ExtendedAttributes.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-attribute loading defaults to disabled when not configured.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_DefaultsFalse()
|
||||
{
|
||||
@@ -63,6 +82,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.ExtendedAttributes.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_BindsFromJson()
|
||||
{
|
||||
@@ -76,6 +98,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.ExtendedAttributes.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dashboard_Section_BindsCorrectly()
|
||||
{
|
||||
@@ -85,6 +110,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.Dashboard.RefreshIntervalSeconds.ShouldBe(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the default configuration objects start with the expected bridge defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
{
|
||||
@@ -95,6 +123,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.Dashboard.Enabled.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a valid configuration passes startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_ValidConfig_ReturnsTrue()
|
||||
{
|
||||
@@ -102,6 +133,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an invalid OPC UA port is rejected by startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_InvalidPort_ReturnsFalse()
|
||||
{
|
||||
@@ -110,6 +144,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_EmptyGalaxyName_ReturnsFalse()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies default and extended-field behavior for Galaxy attribute metadata objects.
|
||||
/// </summary>
|
||||
public class GalaxyAttributeInfoTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a default attribute metadata object starts with empty strings for its text fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreEmpty()
|
||||
{
|
||||
@@ -18,6 +24,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
info.DataTypeName.ShouldBe("");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that primitive-name and attribute-source fields can be populated for extended metadata rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedFields_CanBeSet()
|
||||
{
|
||||
@@ -30,6 +39,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
info.AttributeSource.ShouldBe("primitive");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that standard attribute rows leave the extended metadata fields empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StandardAttributes_HaveEmptyExtendedFields()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how Galaxy MX data types are mapped into OPC UA and CLR types by the bridge.
|
||||
/// </summary>
|
||||
public class MxDataTypeMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that known Galaxy MX data types map to the expected OPC UA data type node identifiers.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <param name="expectedNodeId">The expected OPC UA data type node identifier.</param>
|
||||
[Theory]
|
||||
[InlineData(1, 1u)] // Boolean
|
||||
[InlineData(2, 6u)] // Integer → Int32
|
||||
@@ -25,6 +33,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(expectedNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX data types default to the OPC UA string data type.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The unsupported MX data type code.</param>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(99)]
|
||||
@@ -34,6 +46,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(12u); // String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that known MX data types map to the expected CLR runtime types.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <param name="expectedType">The expected CLR type used by the bridge.</param>
|
||||
[Theory]
|
||||
[InlineData(1, typeof(bool))]
|
||||
[InlineData(2, typeof(int))]
|
||||
@@ -50,18 +67,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.MapToClrType(mxDataType).ShouldBe(expectedType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX data types default to the CLR string type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToClrType_UnknownDefaultsToString()
|
||||
{
|
||||
MxDataTypeMapper.MapToClrType(999).ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean MX type reports the expected OPC UA type name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetOpcUaTypeName_Boolean()
|
||||
{
|
||||
MxDataTypeMapper.GetOpcUaTypeName(1).ShouldBe("Boolean");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX types report the fallback OPC UA type name of string.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetOpcUaTypeName_Unknown_ReturnsString()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the operator-facing error messages and quality mappings derived from MXAccess error codes.
|
||||
/// </summary>
|
||||
public class MxErrorCodesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that known MXAccess error codes produce readable operator-facing descriptions.
|
||||
/// </summary>
|
||||
/// <param name="code">The MXAccess error code.</param>
|
||||
/// <param name="expectedSubstring">A substring expected in the returned description.</param>
|
||||
[Theory]
|
||||
[InlineData(1008, "Invalid reference")]
|
||||
[InlineData(1012, "Wrong data type")]
|
||||
@@ -18,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.GetMessage(code).ShouldContain(expectedSubstring);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MXAccess error codes are reported as unknown while preserving the numeric code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetMessage_UnknownCode_ReturnsUnknown()
|
||||
{
|
||||
@@ -25,6 +36,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.GetMessage(9999).ShouldContain("9999");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that known MXAccess error codes map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="code">The MXAccess error code.</param>
|
||||
/// <param name="expected">The expected bridge quality value.</param>
|
||||
[Theory]
|
||||
[InlineData(1008, Quality.BadConfigError)]
|
||||
[InlineData(1012, Quality.BadConfigError)]
|
||||
@@ -37,6 +53,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.MapToQuality(code).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MXAccess error codes map to the generic bad quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToQuality_UnknownCode_ReturnsBad()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the mapping between MXAccess quality codes, bridge quality values, and OPC UA status codes.
|
||||
/// </summary>
|
||||
public class QualityMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that bad-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw MXAccess quality code.</param>
|
||||
/// <param name="expected">The bridge quality value expected for the code.</param>
|
||||
[Theory]
|
||||
[InlineData(0, Quality.Bad)]
|
||||
[InlineData(4, Quality.BadConfigError)]
|
||||
@@ -16,6 +24,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw MXAccess quality code.</param>
|
||||
/// <param name="expected">The bridge quality value expected for the code.</param>
|
||||
[Theory]
|
||||
[InlineData(64, Quality.Uncertain)]
|
||||
[InlineData(68, Quality.UncertainLastUsable)]
|
||||
@@ -25,6 +38,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw MXAccess quality code.</param>
|
||||
/// <param name="expected">The bridge quality value expected for the code.</param>
|
||||
[Theory]
|
||||
[InlineData(192, Quality.Good)]
|
||||
[InlineData(216, Quality.GoodLocalOverride)]
|
||||
@@ -33,48 +51,72 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown bad-family values collapse to the generic bad quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownBadValue_ReturnsBad()
|
||||
{
|
||||
QualityMapper.MapFromMxAccessQuality(63).ShouldBe(Quality.Bad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown uncertain-family values collapse to the generic uncertain quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownUncertainValue_ReturnsUncertain()
|
||||
{
|
||||
QualityMapper.MapFromMxAccessQuality(100).ShouldBe(Quality.Uncertain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown good-family values collapse to the generic good quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownGoodValue_ReturnsGood()
|
||||
{
|
||||
QualityMapper.MapFromMxAccessQuality(200).ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic good quality maps to the OPC UA good status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Good_Returns0()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.Good).ShouldBe(0x00000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic bad quality maps to the OPC UA bad status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Bad_Returns80000000()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.Bad).ShouldBe(0x80000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that communication failures map to the OPC UA bad communication-failure status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_BadCommFailure()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.BadCommFailure).ShouldBe(0x80050000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic uncertain quality maps to the OPC UA uncertain status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Uncertain()
|
||||
{
|
||||
QualityMapper.MapToOpcUaStatusCode(Quality.Uncertain).ShouldBe(0x40000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsGood()
|
||||
{
|
||||
@@ -83,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
Quality.Good.IsUncertain().ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsBad()
|
||||
{
|
||||
@@ -90,6 +135,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
Quality.Bad.IsGood().ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsUncertain()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
/// </summary>
|
||||
public class FullDataFlowTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the fake-backed bridge can start, build the address space, and expose coherent status data end to end.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FullDataFlow_EndToEnd()
|
||||
{
|
||||
|
||||
@@ -8,8 +8,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the polling service that detects Galaxy deploy changes and triggers address-space rebuilds.
|
||||
/// </summary>
|
||||
public class ChangeDetectionServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the first poll always triggers an initial rebuild notification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FirstPoll_AlwaysTriggers()
|
||||
{
|
||||
@@ -26,6 +32,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated polls with the same deploy timestamp do not retrigger rebuilds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SameTimestamp_DoesNotTriggerAgain()
|
||||
{
|
||||
@@ -42,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a changed deploy timestamp triggers another rebuild notification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangedTimestamp_TriggersAgain()
|
||||
{
|
||||
@@ -62,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transient polling failures do not crash the service and allow later recovery.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FailedPoll_DoesNotCrash_RetriesNext()
|
||||
{
|
||||
@@ -88,6 +103,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
service.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that stopping the service before it starts is a harmless no-op.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stop_BeforeStart_DoesNotThrow()
|
||||
{
|
||||
|
||||
@@ -6,40 +6,88 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// In-memory Galaxy repository used by tests to control hierarchy rows, attribute rows, and deploy metadata without SQL access.
|
||||
/// </summary>
|
||||
public class FakeGalaxyRepository : IGalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the fake repository simulates a Galaxy deploy change.
|
||||
/// </summary>
|
||||
public event Action? OnGalaxyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hierarchy rows returned to address-space construction logic.
|
||||
/// </summary>
|
||||
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new List<GalaxyObjectInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attribute rows returned to address-space construction logic.
|
||||
/// </summary>
|
||||
public List<GalaxyAttributeInfo> Attributes { get; set; } = new List<GalaxyAttributeInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the deploy timestamp returned to change-detection logic.
|
||||
/// </summary>
|
||||
public DateTime? LastDeployTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether connection checks should report success.
|
||||
/// </summary>
|
||||
public bool ConnectionSucceeds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether repository calls should throw to simulate database failures.
|
||||
/// </summary>
|
||||
public bool ShouldThrow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured hierarchy rows or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured hierarchy rows.</returns>
|
||||
public Task<List<GalaxyObjectInfo>> GetHierarchyAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(Hierarchy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured attribute rows or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured attribute rows.</returns>
|
||||
public Task<List<GalaxyAttributeInfo>> GetAttributesAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(Attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured deploy timestamp or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured deploy timestamp.</returns>
|
||||
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(LastDeployTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured connection result or throws to simulate a repository failure.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
|
||||
/// <returns>The configured connection result.</returns>
|
||||
public Task<bool> TestConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (ShouldThrow) throw new Exception("Simulated DB failure");
|
||||
return Task.FromResult(ConnectionSucceeds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the deploy-change event so tests can trigger rebuild logic.
|
||||
/// </summary>
|
||||
public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,44 +7,99 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.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
|
||||
{
|
||||
/// <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>
|
||||
/// 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;
|
||||
|
||||
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>
|
||||
/// 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)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
_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 (TagValues.TryGetValue(fullTagReference, out var vtq))
|
||||
@@ -52,6 +107,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
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)
|
||||
{
|
||||
WrittenValues.Add((fullTagReference, value));
|
||||
@@ -60,6 +122,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return Task.FromResult(WriteResult);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
@@ -67,12 +134,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the fake client. No unmanaged resources are held.
|
||||
/// </summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,71 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
private int _connectionHandle;
|
||||
private bool _registered;
|
||||
|
||||
/// <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>
|
||||
/// 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 ConcurrentDictionary<int, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new ConcurrentDictionary<int, bool>();
|
||||
|
||||
/// <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 List<(string, object)>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the fake runtime is currently considered registered.
|
||||
/// </summary>
|
||||
public bool IsRegistered => _registered;
|
||||
|
||||
/// <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>
|
||||
/// 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++;
|
||||
@@ -41,6 +91,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
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++;
|
||||
@@ -48,6 +102,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
_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);
|
||||
@@ -55,21 +115,43 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
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)");
|
||||
@@ -95,6 +177,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <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];
|
||||
@@ -106,6 +192,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <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)
|
||||
|
||||
@@ -21,8 +21,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
private static int _nextPort = 16000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the started service instance managed by the fixture.
|
||||
/// </summary>
|
||||
public OpcUaService Service { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA port assigned to this fixture instance.
|
||||
/// </summary>
|
||||
public int OpcUaPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL exposed by the fixture.
|
||||
/// </summary>
|
||||
public string EndpointUrl => $"opc.tcp://localhost:{OpcUaPort}/LmxOpcUa";
|
||||
|
||||
/// <summary>
|
||||
@@ -44,6 +55,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
private readonly OpcUaServiceBuilder _builder;
|
||||
private bool _started;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a fixture around a prepared service builder and optional fake dependencies.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder used to construct the service under test.</param>
|
||||
/// <param name="repo">The optional fake Galaxy repository exposed to tests.</param>
|
||||
/// <param name="mxClient">The optional fake MXAccess client exposed to tests.</param>
|
||||
/// <param name="mxProxy">The optional fake MXAccess proxy exposed to tests.</param>
|
||||
private OpcUaServerFixture(OpcUaServiceBuilder builder,
|
||||
FakeGalaxyRepository? repo = null,
|
||||
FakeMxAccessClient? mxClient = null,
|
||||
@@ -62,6 +80,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Creates fixture with FakeMxProxy + FakeGalaxyRepository (standard test data).
|
||||
/// The STA thread and COM interop run against FakeMxProxy.
|
||||
/// </summary>
|
||||
/// <param name="proxy">An optional fake proxy to inject; otherwise a default fake is created.</param>
|
||||
/// <param name="repo">An optional fake repository to inject; otherwise standard test data is used.</param>
|
||||
/// <returns>A fixture configured to exercise the COM-style runtime path.</returns>
|
||||
public static OpcUaServerFixture WithFakes(
|
||||
FakeMxProxy? proxy = null,
|
||||
FakeGalaxyRepository? repo = null)
|
||||
@@ -85,6 +106,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Creates fixture using FakeMxAccessClient directly — skips STA thread + COM entirely.
|
||||
/// Fastest option for tests that don't need real COM interop.
|
||||
/// </summary>
|
||||
/// <param name="mxClient">An optional fake MXAccess client to inject; otherwise a default fake is created.</param>
|
||||
/// <param name="repo">An optional fake repository to inject; otherwise standard test data is used.</param>
|
||||
/// <returns>A fixture configured to exercise the direct fake-client path.</returns>
|
||||
public static OpcUaServerFixture WithFakeMxAccessClient(
|
||||
FakeMxAccessClient? mxClient = null,
|
||||
FakeGalaxyRepository? repo = null)
|
||||
@@ -104,6 +128,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return new OpcUaServerFixture(builder, repo: r, mxClient: client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and starts the OPC UA service for the current fixture.
|
||||
/// </summary>
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
Service = _builder.Build();
|
||||
@@ -112,6 +139,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the OPC UA service when the fixture had previously been started.
|
||||
/// </summary>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
if (_started)
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the reusable OPC UA server fixture used by integration and wiring tests.
|
||||
/// </summary>
|
||||
public class OpcUaServerFixtureTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the standard fake-backed fixture starts the bridge and tears it down cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakes_StartsAndStops()
|
||||
{
|
||||
@@ -25,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakeMxAccessClient_SkipsCom()
|
||||
{
|
||||
@@ -38,6 +47,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleFixtures_GetUniquePortsAutomatically()
|
||||
{
|
||||
@@ -57,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture2.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Shutdown_CompletesWithin30Seconds()
|
||||
{
|
||||
@@ -70,6 +85,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
sw.Elapsed.TotalSeconds.ShouldBeLessThan(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakes_BuildsAddressSpace()
|
||||
{
|
||||
|
||||
@@ -16,11 +16,16 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
private Session? _session;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
|
||||
/// </summary>
|
||||
public Session Session => _session ?? throw new InvalidOperationException("Not connected");
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the namespace index for a given namespace URI (e.g., "urn:TestGalaxy:LmxOpcUa").
|
||||
/// </summary>
|
||||
/// <param name="galaxyName">The Galaxy name whose OPC UA namespace should be resolved on the test server.</param>
|
||||
/// <returns>The namespace index assigned by the server for the requested Galaxy namespace.</returns>
|
||||
public ushort GetNamespaceIndex(string galaxyName = "TestGalaxy")
|
||||
{
|
||||
var nsUri = $"urn:{galaxyName}:LmxOpcUa";
|
||||
@@ -32,11 +37,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The string identifier for the node inside the Galaxy namespace.</param>
|
||||
/// <param name="galaxyName">The Galaxy name whose namespace should be used for the node identifier.</param>
|
||||
/// <returns>A node identifier that targets the requested node on the test server.</returns>
|
||||
public NodeId MakeNodeId(string identifier, string galaxyName = "TestGalaxy")
|
||||
{
|
||||
return new NodeId(identifier, GetNamespaceIndex(galaxyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
|
||||
/// </summary>
|
||||
/// <param name="endpointUrl">The OPC UA endpoint URL to connect to.</param>
|
||||
public async Task ConnectAsync(string endpointUrl)
|
||||
{
|
||||
var config = new ApplicationConfiguration
|
||||
@@ -87,6 +99,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose hierarchical children should be browsed.</param>
|
||||
/// <returns>The child nodes exposed beneath the requested node.</returns>
|
||||
public async Task<List<(string Name, NodeId NodeId, NodeClass NodeClass)>> BrowseAsync(NodeId nodeId)
|
||||
{
|
||||
var results = new List<(string, NodeId, NodeClass)>();
|
||||
@@ -109,6 +123,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// <summary>
|
||||
/// Read a node's value.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose current value should be read from the server.</param>
|
||||
/// <returns>The OPC UA data value returned by the server.</returns>
|
||||
public DataValue Read(NodeId nodeId)
|
||||
{
|
||||
return Session.ReadValue(nodeId);
|
||||
@@ -118,6 +134,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Write a node's value, optionally using an OPC UA index range for array element writes.
|
||||
/// Returns the server status code for the write.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value should be written.</param>
|
||||
/// <param name="value">The value to send to the server.</param>
|
||||
/// <param name="indexRange">An optional OPC UA index range used for array element writes.</param>
|
||||
/// <returns>The server status code returned for the write request.</returns>
|
||||
public StatusCode Write(NodeId nodeId, object value, string? indexRange = null)
|
||||
{
|
||||
var nodesToWrite = new WriteValueCollection
|
||||
@@ -139,6 +159,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// Create a subscription with a monitored item on the given node.
|
||||
/// Returns the subscription and monitored item for inspection.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value changes should be monitored.</param>
|
||||
/// <param name="intervalMs">The publishing and sampling interval, in milliseconds, for the test subscription.</param>
|
||||
/// <returns>The created subscription and monitored item pair for later assertions and cleanup.</returns>
|
||||
public async Task<(Subscription Sub, MonitoredItem Item)> SubscribeAsync(
|
||||
NodeId nodeId, int intervalMs = 250)
|
||||
{
|
||||
@@ -162,6 +185,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return (subscription, item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the test session and releases OPC UA client resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_session != null)
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
/// </summary>
|
||||
public static class TestData
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the standard Galaxy hierarchy used by integration and wiring tests.
|
||||
/// </summary>
|
||||
/// <returns>The standard hierarchy rows for the fake repository.</returns>
|
||||
public static List<GalaxyObjectInfo> CreateStandardHierarchy()
|
||||
{
|
||||
return new List<GalaxyObjectInfo>
|
||||
@@ -20,6 +24,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the standard attribute set used by integration and wiring tests.
|
||||
/// </summary>
|
||||
/// <returns>The standard attribute rows for the fake repository.</returns>
|
||||
public static List<GalaxyAttributeInfo> CreateStandardAttributes()
|
||||
{
|
||||
return new List<GalaxyAttributeInfo>
|
||||
@@ -33,6 +41,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal hierarchy containing a single object for focused unit tests.
|
||||
/// </summary>
|
||||
/// <returns>A minimal hierarchy row set.</returns>
|
||||
public static List<GalaxyObjectInfo> CreateMinimalHierarchy()
|
||||
{
|
||||
return new List<GalaxyObjectInfo>
|
||||
@@ -41,6 +53,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal attribute set containing a single scalar attribute for focused unit tests.
|
||||
/// </summary>
|
||||
/// <returns>A minimal attribute row set.</returns>
|
||||
public static List<GalaxyAttributeInfo> CreateMinimalAttributes()
|
||||
{
|
||||
return new List<GalaxyAttributeInfo>
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
/// </summary>
|
||||
public class AddressSpaceRebuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_ReturnsInitialHierarchy()
|
||||
{
|
||||
@@ -38,6 +41,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AfterAddingObject_NewNodeAppears()
|
||||
{
|
||||
@@ -81,6 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AfterRemovingObject_NodeDisappears()
|
||||
{
|
||||
@@ -114,6 +123,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_RemovedNode_PublishesBadQuality()
|
||||
{
|
||||
@@ -160,6 +172,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_SurvivingNode_StillWorksAfterRebuild()
|
||||
{
|
||||
@@ -196,6 +211,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AddAttribute_NewVariableAppears()
|
||||
{
|
||||
@@ -230,6 +248,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_RemoveAttribute_VariableDisappears()
|
||||
{
|
||||
@@ -260,6 +281,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Rebuild_PreservesSubscriptionBookkeeping_ForSurvivingNodes()
|
||||
{
|
||||
|
||||
@@ -8,8 +8,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies OPC UA indexed array writes against the bridge's whole-array runtime update behavior.
|
||||
/// </summary>
|
||||
public class ArrayWriteTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that writing a single array element updates the correct slot while preserving the rest of the array.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SingleArrayElement_UpdatesWholeArrayValue()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
// ── Subscription Sync ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that multiple OPC UA clients subscribed to the same tag all receive the same runtime update.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleClients_SubscribeToSameTag_AllReceiveDataChanges()
|
||||
{
|
||||
@@ -70,6 +73,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Client_Disconnects_OtherClientsStillReceive()
|
||||
{
|
||||
@@ -119,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Client_Unsubscribes_OtherClientsStillReceive()
|
||||
{
|
||||
@@ -159,6 +168,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleClients_SubscribeToDifferentTags_EachGetsOwnData()
|
||||
{
|
||||
@@ -206,6 +218,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
// ── Concurrent Operation Tests ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent browse operations from several clients all complete successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowseFromMultipleClients_AllSucceed()
|
||||
{
|
||||
@@ -246,6 +261,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent browse requests return consistent results across clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowse_AllReturnSameResults()
|
||||
{
|
||||
@@ -283,6 +301,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowseAndSubscribe_NoInterference()
|
||||
{
|
||||
@@ -318,6 +339,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentSubscribeAndRead_NoDeadlock()
|
||||
{
|
||||
@@ -355,6 +379,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated client churn does not leave the server in an unstable state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RapidConnectDisconnect_ServerStaysStable()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies operation timing aggregation, rolling buffers, and success tracking used by the bridge metrics subsystem.
|
||||
/// </summary>
|
||||
public class PerformanceMetricsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a fresh metrics collector reports no statistics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyState_ReturnsZeroStatistics()
|
||||
{
|
||||
@@ -15,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated operation recordings update total and successful execution counts.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecordOperation_TracksCounts()
|
||||
{
|
||||
@@ -29,6 +38,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats["Read"].SuccessRate.ShouldBe(0.5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that min, max, and average timing values are calculated from recorded operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecordOperation_TracksMinMaxAverage()
|
||||
{
|
||||
@@ -43,6 +55,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.AverageMilliseconds.ShouldBe(20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the 95th percentile is calculated from the recorded timing sample.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void P95_CalculatedCorrectly()
|
||||
{
|
||||
@@ -54,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.Percentile95Milliseconds.ShouldBe(95);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the rolling buffer keeps the most recent operation durations for percentile calculations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RollingBuffer_EvictsOldEntries()
|
||||
{
|
||||
@@ -67,6 +85,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.Percentile95Milliseconds.ShouldBeGreaterThan(1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a timing scope records an operation when disposed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BeginOperation_TimingScopeRecordsOnDispose()
|
||||
{
|
||||
@@ -85,6 +106,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats["Test"].AverageMilliseconds.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a timing scope can mark an operation as failed before disposal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BeginOperation_SetSuccessFalse()
|
||||
{
|
||||
@@ -100,6 +124,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats.SuccessCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that looking up an unknown operation returns no metrics bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetMetrics_UnknownOperation_ReturnsNull()
|
||||
{
|
||||
@@ -107,6 +134,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
metrics.GetMetrics("NonExistent").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that operation names are tracked without case sensitivity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OperationNames_AreCaseInsensitive()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies MXAccess client connection lifecycle behavior, including transitions, registration, and reconnect handling.
|
||||
/// </summary>
|
||||
public class MxAccessClientConnectionTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
@@ -19,6 +22,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
private readonly MxAccessClient _client;
|
||||
private readonly List<(ConnectionState Previous, ConnectionState Current)> _stateChanges = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the connection test fixture with a fake runtime proxy and state-change recorder.
|
||||
/// </summary>
|
||||
public MxAccessClientConnectionTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -30,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ConnectionStateChanged += (_, e) => _stateChanges.Add((e.PreviousState, e.CurrentState));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the connection test fixture and its supporting resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
@@ -37,12 +46,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a newly created MXAccess client starts in the disconnected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InitialState_IsDisconnected()
|
||||
{
|
||||
_client.State.ShouldBe(ConnectionState.Disconnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that connecting drives the expected disconnected-to-connecting-to-connected transitions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_TransitionsToConnected()
|
||||
{
|
||||
@@ -53,6 +68,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_stateChanges.ShouldContain(s => s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a successful connect registers exactly once with the runtime proxy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_RegistersCalled()
|
||||
{
|
||||
@@ -60,6 +78,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.RegisterCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disconnecting drives the expected shutdown transitions back to disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -71,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disconnecting unregisters the runtime proxy session.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_UnregistersCalled()
|
||||
{
|
||||
@@ -79,6 +103,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.UnregisterCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that registration failures move the client into the error state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectFails_TransitionsToError()
|
||||
{
|
||||
@@ -88,6 +115,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.State.ShouldBe(ConnectionState.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated connect calls do not perform duplicate runtime registrations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DoubleConnect_NoOp()
|
||||
{
|
||||
@@ -96,6 +126,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.RegisterCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reconnect increments the reconnect counter and restores the connected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Reconnect_IncrementsCount()
|
||||
{
|
||||
|
||||
@@ -10,12 +10,18 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the background connectivity monitor used to reconnect the MXAccess bridge after faults or stale probes.
|
||||
/// </summary>
|
||||
public class MxAccessClientMonitorTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector.
|
||||
/// </summary>
|
||||
public MxAccessClientMonitorTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -24,12 +30,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics = new PerformanceMetrics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the monitor test fixture resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_staThread.Dispose();
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the monitor reconnects the client after an observed disconnect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ReconnectsOnDisconnect()
|
||||
{
|
||||
@@ -54,6 +66,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the monitor can be started and stopped without throwing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_StopsOnCancel()
|
||||
{
|
||||
@@ -69,6 +84,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ProbeStale_ForcesReconnect()
|
||||
{
|
||||
@@ -93,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that fresh probe updates prevent unnecessary reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ProbeDataChange_PreventsStaleReconnect()
|
||||
{
|
||||
@@ -122,6 +143,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that enabling the monitor without a probe tag does not trigger false reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_NoProbeConfigured_NoFalseReconnect()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies MXAccess client read and write behavior against the fake runtime proxy used by the bridge.
|
||||
/// </summary>
|
||||
public class MxAccessClientReadWriteTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
@@ -18,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the COM-threaded MXAccess test fixture with a fake runtime proxy and metrics collector.
|
||||
/// </summary>
|
||||
public MxAccessClientReadWriteTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -28,6 +34,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client = new MxAccessClient(_staThread, _proxy, config, _metrics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the MXAccess client fixture and its supporting STA thread and metrics collector.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
@@ -35,6 +44,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reads fail with bad-not-connected quality when the runtime session is offline.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_NotConnected_ReturnsBad()
|
||||
{
|
||||
@@ -42,6 +54,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.Quality.ShouldBe(Quality.BadNotConnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a runtime data-change callback completes a pending read with the published value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_ReturnsValueOnDataChange()
|
||||
{
|
||||
@@ -59,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reads time out with bad communication-failure quality when the runtime never responds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_Timeout_ReturnsBadCommFailure()
|
||||
{
|
||||
@@ -69,6 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.Quality.ShouldBe(Quality.BadCommFailure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that timed-out reads are recorded as failed read operations in the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_Timeout_RecordsFailedMetrics()
|
||||
{
|
||||
@@ -83,6 +104,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Read"].SuccessCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that writes are rejected when the runtime session is not connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_NotConnected_ReturnsFalse()
|
||||
{
|
||||
@@ -90,6 +114,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that successful runtime write acknowledgments return success and record the written payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_Success_ReturnsTrue()
|
||||
{
|
||||
@@ -101,6 +128,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.WrittenValues.ShouldContain(w => w.Address == "TestTag.Attr" && (int)w.Value == 42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that MXAccess error codes on write completion are surfaced as failed writes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_ErrorCode_ReturnsFalse()
|
||||
{
|
||||
@@ -111,6 +141,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that write timeouts are recorded as failed write operations in the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_Timeout_ReturnsFalse_AndRecordsFailedMetrics()
|
||||
{
|
||||
@@ -126,6 +159,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Write"].SuccessCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that successful reads contribute a read entry to the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_RecordsMetrics()
|
||||
{
|
||||
@@ -141,6 +177,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Read"].TotalCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that writes contribute a write entry to the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_RecordsMetrics()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how the MXAccess client manages persistent subscriptions, reconnect replay, and probe-tag behavior.
|
||||
/// </summary>
|
||||
public class MxAccessClientSubscriptionTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
@@ -18,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the subscription test fixture with a fake runtime proxy and STA thread.
|
||||
/// </summary>
|
||||
public MxAccessClientSubscriptionTests()
|
||||
{
|
||||
_staThread = new StaComThread();
|
||||
@@ -27,6 +33,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client = new MxAccessClient(_staThread, _proxy, new MxAccessConfiguration(), _metrics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the subscription test fixture and its supporting resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
@@ -34,6 +43,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_metrics.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscribing creates a runtime item, advises it, and increments the active subscription count.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_CreatesItemAndAdvises()
|
||||
{
|
||||
@@ -45,6 +57,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unsubscribing clears the active subscription count after a tag was previously monitored.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_RemovesItemAndUnadvises()
|
||||
{
|
||||
@@ -55,6 +70,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime data changes are delivered to the per-subscription callback.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnDataChange_InvokesCallback()
|
||||
{
|
||||
@@ -70,6 +88,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
received.Value.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime data changes are also delivered to the client's global tag-change event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnDataChange_InvokesGlobalHandler()
|
||||
{
|
||||
@@ -84,6 +105,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
globalAddr.ShouldBe("TestTag.Attr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that stored subscriptions are replayed after reconnect so live updates resume automatically.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StoredSubscriptions_ReplayedAfterReconnect()
|
||||
{
|
||||
@@ -102,6 +126,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
callbackInvoked.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one-shot reads do not remove persistent subscriptions when the client reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OneShotRead_DoesNotRemovePersistentSubscription_OnReconnect()
|
||||
{
|
||||
@@ -122,6 +149,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transient writes do not prevent later removal of a persistent subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OneShotWrite_DoesNotBreakPersistentUnsubscribe()
|
||||
{
|
||||
@@ -138,6 +168,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_proxy.Items.Values.ShouldNotContain("TestTag.Attr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the configured probe tag is subscribed during connect so connectivity monitoring can start immediately.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProbeTag_SubscribedOnConnect()
|
||||
{
|
||||
@@ -152,6 +185,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the probe tag cannot be unsubscribed accidentally because it is reserved for connection monitoring.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProbeTag_ProtectedFromUnsubscribe()
|
||||
{
|
||||
|
||||
@@ -7,18 +7,30 @@ using ZB.MOM.WW.LmxOpcUa.Host.MxAccess;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the single-threaded apartment worker used to marshal COM calls for the MXAccess bridge.
|
||||
/// </summary>
|
||||
public class StaComThreadTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _thread;
|
||||
|
||||
/// <summary>
|
||||
/// Starts a fresh STA thread instance for each test.
|
||||
/// </summary>
|
||||
public StaComThreadTests()
|
||||
{
|
||||
_thread = new StaComThread();
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the STA thread after each test.
|
||||
/// </summary>
|
||||
public void Dispose() => _thread.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that queued work runs on a thread configured for STA apartment state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ExecutesOnStaThread()
|
||||
{
|
||||
@@ -26,6 +38,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
apartmentState.ShouldBe(ApartmentState.STA);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that action delegates run to completion on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Action_Completes()
|
||||
{
|
||||
@@ -34,6 +49,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
executed.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that function delegates can return results from the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Func_ReturnsResult()
|
||||
{
|
||||
@@ -41,6 +59,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
result.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that exceptions thrown on the STA thread propagate back to the caller.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_PropagatesException()
|
||||
{
|
||||
@@ -48,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_thread.RunAsync(() => throw new InvalidOperationException("test error")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disposing the STA thread stops it from accepting additional work.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dispose_Stops_Thread()
|
||||
{
|
||||
@@ -59,6 +83,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
Should.Throw<ObjectDisposedException>(() => thread.RunAsync(() => { }).GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that multiple queued work items all execute successfully on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleWorkItems_ExecuteInOrder()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how bridge VTQ values are translated to and from OPC UA data values for the published namespace.
|
||||
/// </summary>
|
||||
public class DataValueConverterTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that boolean runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Boolean()
|
||||
{
|
||||
@@ -17,6 +23,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
Opc.Ua.StatusCode.IsGood(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that integer runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Int32()
|
||||
{
|
||||
@@ -25,6 +34,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that float runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Float()
|
||||
{
|
||||
@@ -33,6 +45,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(3.14f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that double runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Double()
|
||||
{
|
||||
@@ -41,6 +56,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(3.14159);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that string runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_String()
|
||||
{
|
||||
@@ -49,6 +67,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe("hello");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that UTC timestamps remain UTC when a VTQ is converted for OPC UA clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_DateTime_IsUtc()
|
||||
{
|
||||
@@ -58,6 +79,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
((DateTime)dv.Value).Kind.ShouldBe(DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that elapsed-time values are exposed to OPC UA clients in seconds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_TimeSpan_ConvertedToSeconds()
|
||||
{
|
||||
@@ -66,6 +90,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(150.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that string arrays remain arrays when exposed through OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_StringArray()
|
||||
{
|
||||
@@ -75,6 +102,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(arr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that integer arrays remain arrays when exposed through OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_IntArray()
|
||||
{
|
||||
@@ -84,6 +114,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBe(arr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad runtime quality is translated to a bad OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_BadQuality_MapsToStatusCode()
|
||||
{
|
||||
@@ -92,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
Opc.Ua.StatusCode.IsBad(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain runtime quality is translated to an uncertain OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_UncertainQuality()
|
||||
{
|
||||
@@ -100,6 +136,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
Opc.Ua.StatusCode.IsUncertain(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that null runtime values remain null when converted for OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_NullValue()
|
||||
{
|
||||
@@ -108,6 +147,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
dv.Value.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a data value can round-trip back into a VTQ without losing the process value or quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToVtq_RoundTrip()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,15 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the in-memory address-space model built from Galaxy hierarchy and attribute rows.
|
||||
/// </summary>
|
||||
public class LmxNodeManagerBuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates representative Galaxy hierarchy and attribute rows for address-space builder tests.
|
||||
/// </summary>
|
||||
/// <returns>The hierarchy and attribute rows used by the tests.</returns>
|
||||
private static (List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes) CreateTestData()
|
||||
{
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
@@ -29,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
return (hierarchy, attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that object and variable counts are computed correctly from the seeded Galaxy model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_CreatesCorrectNodeCounts()
|
||||
{
|
||||
@@ -39,6 +49,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.VariableCount.ShouldBe(4); // MachineID, DownloadPath, JobStepNumber, BatchItems
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime tag references are populated for every published variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_TagReferencesPopulated()
|
||||
{
|
||||
@@ -51,6 +64,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that array attributes are represented in the tag-reference map.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_ArrayVariable_HasCorrectInfo()
|
||||
{
|
||||
@@ -60,6 +76,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that Galaxy areas are not counted as object nodes in the resulting model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_Areas_AreNotCountedAsObjects()
|
||||
{
|
||||
@@ -73,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.ObjectCount.ShouldBe(1); // Only Obj1, not Area1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that only top-level Galaxy nodes are returned as roots in the model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_RootNodes_AreTopLevel()
|
||||
{
|
||||
@@ -86,6 +108,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.RootNodes.Count.ShouldBe(1); // Only Root1 is a root
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that variables for multiple MX data types are included in the model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_DataTypeMappings()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies rebuild behavior by comparing address-space models before and after metadata changes.
|
||||
/// </summary>
|
||||
public class LmxNodeManagerRebuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that rebuilding with new metadata replaces the old tag-reference set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rebuild_NewBuild_ReplacesOldData()
|
||||
{
|
||||
@@ -40,6 +46,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model2.NodeIdToTagReference.ContainsKey("NewObj.NewAttr").ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that object counts are recalculated from the latest rebuild input.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rebuild_UpdatesNodeCounts()
|
||||
{
|
||||
@@ -59,6 +68,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model2.ObjectCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that empty metadata produces an empty address-space model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyHierarchy_ProducesEmptyModel()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies translation between bridge quality values and OPC UA status codes.
|
||||
/// </summary>
|
||||
public class OpcUaQualityMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that good bridge quality maps to an OPC UA good status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Good_MapsToGoodStatusCode()
|
||||
{
|
||||
@@ -15,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsGood(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad bridge quality maps to an OPC UA bad status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bad_MapsToBadStatusCode()
|
||||
{
|
||||
@@ -22,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsBad(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain bridge quality maps to an OPC UA uncertain status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Uncertain_MapsToUncertainStatusCode()
|
||||
{
|
||||
@@ -29,6 +41,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsUncertain(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that communication failures map to a bad OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BadCommFailure_MapsCorrectly()
|
||||
{
|
||||
@@ -36,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
StatusCode.IsBad(sc).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA good status maps back to bridge good quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Good()
|
||||
{
|
||||
@@ -43,6 +61,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
q.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA bad status maps back to bridge bad quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Bad()
|
||||
{
|
||||
@@ -50,6 +71,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
q.ShouldBe(Quality.Bad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA uncertain status maps back to bridge uncertain quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Uncertain()
|
||||
{
|
||||
|
||||
@@ -3,8 +3,14 @@ using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder unit test that keeps the unit test project wired into the solution.
|
||||
/// </summary>
|
||||
public class SampleTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the unit test assembly is executing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Placeholder_ShouldPass()
|
||||
{
|
||||
|
||||
@@ -7,10 +7,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Status;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies how the dashboard health service classifies bridge health from connection state and metrics.
|
||||
/// </summary>
|
||||
public class HealthCheckServiceTests
|
||||
{
|
||||
private readonly HealthCheckService _sut = new();
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a disconnected runtime is reported as unhealthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NotConnected_ReturnsUnhealthy()
|
||||
{
|
||||
@@ -20,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Message.ShouldContain("not connected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a connected runtime with no metrics history is still considered healthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_NoMetrics_ReturnsHealthy()
|
||||
{
|
||||
@@ -28,6 +37,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Color.ShouldBe("green");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good success-rate metrics keep the service in a healthy state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_GoodMetrics_ReturnsHealthy()
|
||||
{
|
||||
@@ -39,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Status.ShouldBe("Healthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that poor operation success rates degrade the reported health state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_LowSuccessRate_ReturnsDegraded()
|
||||
{
|
||||
@@ -53,18 +68,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Color.ShouldBe("yellow");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean health helper reports true when the runtime is connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_Connected_ReturnsTrue()
|
||||
{
|
||||
_sut.IsHealthy(ConnectionState.Connected, null).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean health helper reports false when the runtime is disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_Disconnected_ReturnsFalse()
|
||||
{
|
||||
_sut.IsHealthy(ConnectionState.Disconnected, null).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the error connection state is treated as unhealthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Error_ReturnsUnhealthy()
|
||||
{
|
||||
@@ -72,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Status.ShouldBe("Unhealthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the reconnecting state is treated as unhealthy while recovery is in progress.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Reconnecting_ReturnsUnhealthy()
|
||||
{
|
||||
|
||||
@@ -9,8 +9,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the HTML, JSON, and health snapshots generated for the operator status dashboard.
|
||||
/// </summary>
|
||||
public class StatusReportServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the generated HTML contains every dashboard panel expected by operators.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ContainsAllPanels()
|
||||
{
|
||||
@@ -25,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("Footer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generated HTML includes the configured auto-refresh meta tag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ContainsMetaRefresh()
|
||||
{
|
||||
@@ -33,6 +42,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("meta http-equiv='refresh' content='10'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the connection panel renders the current runtime connection state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ConnectionPanel_ShowsState()
|
||||
{
|
||||
@@ -41,6 +53,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("Connected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy panel renders the bridged Galaxy name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_GalaxyPanel_ShowsName()
|
||||
{
|
||||
@@ -49,6 +64,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("TestGalaxy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the operations table renders the expected performance metric headers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_OperationsTable_ShowsHeaders()
|
||||
{
|
||||
@@ -62,6 +80,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("P95 (ms)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the footer renders timestamp and version information.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_Footer_ContainsTimestampAndVersion()
|
||||
{
|
||||
@@ -71,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
html.ShouldContain("Version:");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generated JSON includes the major dashboard sections.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateJson_Deserializes()
|
||||
{
|
||||
@@ -86,6 +110,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
json.ShouldContain("Footer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the report service reports healthy when the runtime connection is up.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_WhenConnected_ReturnsTrue()
|
||||
{
|
||||
@@ -93,6 +120,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
sut.IsHealthy().ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the report service reports unhealthy when the runtime connection is down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_WhenDisconnected_ReturnsFalse()
|
||||
{
|
||||
@@ -102,6 +132,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
sut.IsHealthy().ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data.
|
||||
/// </summary>
|
||||
/// <returns>A configured status report service for dashboard assertions.</returns>
|
||||
private static StatusReportService CreateService()
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
|
||||
@@ -9,12 +9,18 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the lightweight HTTP dashboard host that exposes bridge status to operators.
|
||||
/// </summary>
|
||||
public class StatusWebServerTests : IDisposable
|
||||
{
|
||||
private readonly StatusWebServer _server;
|
||||
private readonly HttpClient _client;
|
||||
private readonly int _port;
|
||||
|
||||
/// <summary>
|
||||
/// Starts a status web server on a random test port and prepares an HTTP client for endpoint assertions.
|
||||
/// </summary>
|
||||
public StatusWebServerTests()
|
||||
{
|
||||
_port = new Random().Next(18000, 19000);
|
||||
@@ -26,12 +32,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
_client = new HttpClient { BaseAddress = new Uri($"http://localhost:{_port}") };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the test HTTP client and stops the status web server.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
_server.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard root responds with HTML content.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Root_ReturnsHtml200()
|
||||
{
|
||||
@@ -40,6 +52,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("text/html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the JSON status endpoint responds successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiStatus_ReturnsJson200()
|
||||
{
|
||||
@@ -48,6 +63,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the health endpoint returns HTTP 200 when the bridge is healthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiHealth_Returns200WhenHealthy()
|
||||
{
|
||||
@@ -58,6 +76,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
body.ShouldContain("healthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown dashboard routes return HTTP 404.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnknownPath_Returns404()
|
||||
{
|
||||
@@ -65,6 +86,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unsupported HTTP methods are rejected with HTTP 405.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task PostMethod_Returns405()
|
||||
{
|
||||
@@ -72,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that cache-control headers disable caching for dashboard responses.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CacheHeaders_Present()
|
||||
{
|
||||
@@ -80,6 +107,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Headers.CacheControl?.NoStore.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the server can be started and stopped cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StartStop_DoesNotThrow()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class ChangeDetectionToRebuildWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a changed deploy timestamp causes the change-detection pipeline to raise another rebuild signal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangedTimestamp_TriggersRebuild()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class MxAccessToNodeManagerWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a simulated data change reaches the global tag-value-changed event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DataChange_ReachesGlobalHandler()
|
||||
{
|
||||
@@ -33,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
receivedVtq.Value.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a simulated data change reaches the stored per-tag subscription callback.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DataChange_ReachesSubscriptionCallback()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class OpcUaReadToMxAccessWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the resolved OPC UA read path uses the expected full Galaxy tag reference.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_ResolvesCorrectTagReference()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class OpcUaWriteToMxAccessWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the resolved OPC UA write path targets the expected Galaxy tag reference and payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SendsCorrectTagAndValue()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class ServiceStartupSequenceTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that startup with fake dependencies creates the expected bridge components and state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Start_WithFakes_AllComponentsCreated()
|
||||
{
|
||||
@@ -71,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that when MXAccess is initially unavailable, the background monitor reconnects it later.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Start_WhenMxAccessIsInitiallyDown_MonitorReconnectsInBackground()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
/// </summary>
|
||||
public class ShutdownCompletesTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a started service can shut down within the required time budget.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Shutdown_CompletesWithin30Seconds()
|
||||
{
|
||||
|
||||
@@ -9,18 +9,34 @@ namespace OpcUaCli.Commands;
|
||||
[Command("browse", Description = "Browse the OPC UA address space")]
|
||||
public class BrowseCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL to connect to before browsing.
|
||||
/// </summary>
|
||||
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
||||
public string Url { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional node identifier to browse from; defaults to the OPC UA Objects folder.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID to browse (default: Objects folder)")]
|
||||
public string? NodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum browse depth when recursive traversal is enabled.
|
||||
/// </summary>
|
||||
[CommandOption("depth", 'd', Description = "Maximum browse depth")]
|
||||
public int Depth { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether browse recursion should continue into child objects.
|
||||
/// </summary>
|
||||
[CommandOption("recursive", 'r', Description = "Browse recursively (uses --depth as max depth)")]
|
||||
public bool Recursive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the OPC UA endpoint and writes the browse tree to the console.
|
||||
/// </summary>
|
||||
/// <param name="console">The console used to emit browse output.</param>
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
using var session = await OpcUaHelper.ConnectAsync(Url);
|
||||
|
||||
@@ -7,9 +7,16 @@ namespace OpcUaCli.Commands;
|
||||
[Command("connect", Description = "Test connection to an OPC UA server")]
|
||||
public class ConnectCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL to test.
|
||||
/// </summary>
|
||||
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
||||
public string Url { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the OPC UA endpoint and prints the resolved server metadata.
|
||||
/// </summary>
|
||||
/// <param name="console">The console used to report connection results.</param>
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
using var session = await OpcUaHelper.ConnectAsync(Url);
|
||||
|
||||
@@ -9,12 +9,22 @@ namespace OpcUaCli.Commands;
|
||||
[Command("read", Description = "Read a value from a node")]
|
||||
public class ReadCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL to connect to before reading.
|
||||
/// </summary>
|
||||
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
||||
public string Url { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node identifier whose value should be read.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the endpoint, reads the target node, and prints the returned value details.
|
||||
/// </summary>
|
||||
/// <param name="console">The console used to report the read result.</param>
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
using var session = await OpcUaHelper.ConnectAsync(Url);
|
||||
|
||||
@@ -9,15 +9,28 @@ namespace OpcUaCli.Commands;
|
||||
[Command("subscribe", Description = "Monitor a node for value changes")]
|
||||
public class SubscribeCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL to connect to before subscribing.
|
||||
/// </summary>
|
||||
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
||||
public string Url { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node identifier to monitor for value changes.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID to monitor", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sampling and publishing interval, in milliseconds, for the monitored item.
|
||||
/// </summary>
|
||||
[CommandOption("interval", 'i', Description = "Polling interval in milliseconds")]
|
||||
public int Interval { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the OPC UA endpoint and streams monitored-item notifications until cancellation.
|
||||
/// </summary>
|
||||
/// <param name="console">The console used to display subscription updates.</param>
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
using var session = await OpcUaHelper.ConnectAsync(Url);
|
||||
|
||||
@@ -9,15 +9,28 @@ namespace OpcUaCli.Commands;
|
||||
[Command("write", Description = "Write a value to a node")]
|
||||
public class WriteCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the OPC UA endpoint URL to connect to before issuing the write.
|
||||
/// </summary>
|
||||
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
||||
public string Url { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node identifier that should receive the write.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the textual value supplied on the command line before type conversion.
|
||||
/// </summary>
|
||||
[CommandOption("value", 'v', Description = "Value to write", IsRequired = true)]
|
||||
public string Value { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the OPC UA endpoint, converts the supplied value, and writes it to the target node.
|
||||
/// </summary>
|
||||
/// <param name="console">The console used to report the write result.</param>
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
using var session = await OpcUaHelper.ConnectAsync(Url);
|
||||
|
||||
@@ -6,6 +6,11 @@ namespace OpcUaCli;
|
||||
|
||||
public static class OpcUaHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an OPC UA client session for the specified endpoint URL.
|
||||
/// </summary>
|
||||
/// <param name="endpointUrl">The OPC UA endpoint URL to connect to.</param>
|
||||
/// <returns>An active OPC UA client session.</returns>
|
||||
public static async Task<Session> ConnectAsync(string endpointUrl)
|
||||
{
|
||||
var config = new ApplicationConfiguration
|
||||
@@ -61,6 +66,12 @@ public static class OpcUaHelper
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a raw command-line string into the runtime type expected by the target node.
|
||||
/// </summary>
|
||||
/// <param name="rawValue">The raw string supplied by the user.</param>
|
||||
/// <param name="currentValue">The current node value used to infer the target type.</param>
|
||||
/// <returns>A typed value suitable for an OPC UA write request.</returns>
|
||||
public static object ConvertValue(string rawValue, object? currentValue)
|
||||
{
|
||||
return currentValue switch
|
||||
|
||||
Reference in New Issue
Block a user