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