Expand XML docs across bridge and test code

This commit is contained in:
Joseph Doherty
2026-03-25 11:45:12 -04:00
parent 3f813b3869
commit 4833765606
86 changed files with 2323 additions and 0 deletions

View File

@@ -8,8 +8,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
{
/// <summary>
/// Integration tests that exercise the real Galaxy repository queries against the test database configuration.
/// </summary>
public class GalaxyRepositoryServiceTests
{
/// <summary>
/// Loads repository configuration from the integration test settings and controls whether extended attributes are enabled.
/// </summary>
/// <param name="extendedAttributes">A value indicating whether the extended attribute query path should be enabled.</param>
/// <returns>The repository configuration used by the integration test.</returns>
private static GalaxyRepositoryConfiguration LoadConfig(bool extendedAttributes = false)
{
var configuration = new ConfigurationBuilder()
@@ -22,6 +30,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
return config;
}
/// <summary>
/// Confirms that the standard attribute query returns rows from the repository.
/// </summary>
[Fact]
public async Task GetAttributesAsync_StandardMode_ReturnsRows()
{
@@ -35,6 +46,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
results.ShouldAllBe(r => r.PrimitiveName == "" && r.AttributeSource == "");
}
/// <summary>
/// Confirms that the extended attribute query returns more rows than the standard query path.
/// </summary>
[Fact]
public async Task GetAttributesAsync_ExtendedMode_ReturnsMoreRows()
{
@@ -49,6 +63,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
extendedResults.Count.ShouldBeGreaterThan(standardResults.Count);
}
/// <summary>
/// Confirms that the extended attribute query includes both primitive and dynamic attribute sources.
/// </summary>
[Fact]
public async Task GetAttributesAsync_ExtendedMode_IncludesPrimitiveAttributes()
{
@@ -61,6 +78,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
results.ShouldContain(r => r.AttributeSource == "dynamic");
}
/// <summary>
/// Confirms that extended mode populates attribute-source metadata across the result set.
/// </summary>
[Fact]
public async Task GetAttributesAsync_ExtendedMode_PrimitiveNamePopulated()
{
@@ -76,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
results.ShouldAllBe(r => r.AttributeSource == "primitive" || r.AttributeSource == "dynamic");
}
/// <summary>
/// Confirms that standard-mode results always include fully qualified tag references.
/// </summary>
[Fact]
public async Task GetAttributesAsync_StandardMode_AllHaveFullTagReference()
{
@@ -88,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
results.ShouldAllBe(r => r.FullTagReference.Contains("."));
}
/// <summary>
/// Confirms that extended-mode results always include fully qualified tag references.
/// </summary>
[Fact]
public async Task GetAttributesAsync_ExtendedMode_AllHaveFullTagReference()
{

View File

@@ -3,8 +3,14 @@ using Xunit;
namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
{
/// <summary>
/// Placeholder integration test that keeps the integration test project wired into the solution.
/// </summary>
public class SampleIntegrationTest
{
/// <summary>
/// Confirms that the integration test assembly is executing.
/// </summary>
[Fact]
public void Placeholder_ShouldPass()
{

View File

@@ -5,8 +5,15 @@ using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
{
/// <summary>
/// Verifies that application configuration binds correctly from appsettings and that validation catches invalid bridge settings.
/// </summary>
public class ConfigurationLoadingTests
{
/// <summary>
/// Loads the application configuration from the repository appsettings file for binding tests.
/// </summary>
/// <returns>The bound application configuration snapshot.</returns>
private static AppConfiguration LoadFromJson()
{
var configuration = new ConfigurationBuilder()
@@ -21,6 +28,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
return config;
}
/// <summary>
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
/// </summary>
[Fact]
public void OpcUa_Section_BindsCorrectly()
{
@@ -33,6 +43,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.OpcUa.SessionTimeoutMinutes.ShouldBe(30);
}
/// <summary>
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
/// </summary>
[Fact]
public void MxAccess_Section_BindsCorrectly()
{
@@ -46,6 +59,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.MxAccess.ProbeStaleThresholdSeconds.ShouldBe(60);
}
/// <summary>
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
/// </summary>
[Fact]
public void GalaxyRepository_Section_BindsCorrectly()
{
@@ -56,6 +72,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.GalaxyRepository.ExtendedAttributes.ShouldBe(false);
}
/// <summary>
/// Confirms that extended-attribute loading defaults to disabled when not configured.
/// </summary>
[Fact]
public void GalaxyRepository_ExtendedAttributes_DefaultsFalse()
{
@@ -63,6 +82,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.ExtendedAttributes.ShouldBe(false);
}
/// <summary>
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
/// </summary>
[Fact]
public void GalaxyRepository_ExtendedAttributes_BindsFromJson()
{
@@ -76,6 +98,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.ExtendedAttributes.ShouldBe(true);
}
/// <summary>
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
/// </summary>
[Fact]
public void Dashboard_Section_BindsCorrectly()
{
@@ -85,6 +110,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.Dashboard.RefreshIntervalSeconds.ShouldBe(10);
}
/// <summary>
/// Confirms that the default configuration objects start with the expected bridge defaults.
/// </summary>
[Fact]
public void DefaultValues_AreCorrect()
{
@@ -95,6 +123,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
config.Dashboard.Enabled.ShouldBe(true);
}
/// <summary>
/// Confirms that a valid configuration passes startup validation.
/// </summary>
[Fact]
public void Validator_ValidConfig_ReturnsTrue()
{
@@ -102,6 +133,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
}
/// <summary>
/// Confirms that an invalid OPC UA port is rejected by startup validation.
/// </summary>
[Fact]
public void Validator_InvalidPort_ReturnsFalse()
{
@@ -110,6 +144,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
}
/// <summary>
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
/// </summary>
[Fact]
public void Validator_EmptyGalaxyName_ReturnsFalse()
{

View File

@@ -4,8 +4,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
{
/// <summary>
/// Verifies default and extended-field behavior for Galaxy attribute metadata objects.
/// </summary>
public class GalaxyAttributeInfoTests
{
/// <summary>
/// Confirms that a default attribute metadata object starts with empty strings for its text fields.
/// </summary>
[Fact]
public void DefaultValues_AreEmpty()
{
@@ -18,6 +24,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
info.DataTypeName.ShouldBe("");
}
/// <summary>
/// Confirms that primitive-name and attribute-source fields can be populated for extended metadata rows.
/// </summary>
[Fact]
public void ExtendedFields_CanBeSet()
{
@@ -30,6 +39,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
info.AttributeSource.ShouldBe("primitive");
}
/// <summary>
/// Confirms that standard attribute rows leave the extended metadata fields empty.
/// </summary>
[Fact]
public void StandardAttributes_HaveEmptyExtendedFields()
{

View File

@@ -5,8 +5,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
{
/// <summary>
/// Verifies how Galaxy MX data types are mapped into OPC UA and CLR types by the bridge.
/// </summary>
public class MxDataTypeMapperTests
{
/// <summary>
/// Confirms that known Galaxy MX data types map to the expected OPC UA data type node identifiers.
/// </summary>
/// <param name="mxDataType">The Galaxy MX data type code.</param>
/// <param name="expectedNodeId">The expected OPC UA data type node identifier.</param>
[Theory]
[InlineData(1, 1u)] // Boolean
[InlineData(2, 6u)] // Integer → Int32
@@ -25,6 +33,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(expectedNodeId);
}
/// <summary>
/// Confirms that unknown MX data types default to the OPC UA string data type.
/// </summary>
/// <param name="mxDataType">The unsupported MX data type code.</param>
[Theory]
[InlineData(0)]
[InlineData(99)]
@@ -34,6 +46,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(12u); // String
}
/// <summary>
/// Confirms that known MX data types map to the expected CLR runtime types.
/// </summary>
/// <param name="mxDataType">The Galaxy MX data type code.</param>
/// <param name="expectedType">The expected CLR type used by the bridge.</param>
[Theory]
[InlineData(1, typeof(bool))]
[InlineData(2, typeof(int))]
@@ -50,18 +67,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
MxDataTypeMapper.MapToClrType(mxDataType).ShouldBe(expectedType);
}
/// <summary>
/// Confirms that unknown MX data types default to the CLR string type.
/// </summary>
[Fact]
public void MapToClrType_UnknownDefaultsToString()
{
MxDataTypeMapper.MapToClrType(999).ShouldBe(typeof(string));
}
/// <summary>
/// Confirms that the boolean MX type reports the expected OPC UA type name.
/// </summary>
[Fact]
public void GetOpcUaTypeName_Boolean()
{
MxDataTypeMapper.GetOpcUaTypeName(1).ShouldBe("Boolean");
}
/// <summary>
/// Confirms that unknown MX types report the fallback OPC UA type name of string.
/// </summary>
[Fact]
public void GetOpcUaTypeName_Unknown_ReturnsString()
{

View File

@@ -4,8 +4,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
{
/// <summary>
/// Verifies the operator-facing error messages and quality mappings derived from MXAccess error codes.
/// </summary>
public class MxErrorCodesTests
{
/// <summary>
/// Confirms that known MXAccess error codes produce readable operator-facing descriptions.
/// </summary>
/// <param name="code">The MXAccess error code.</param>
/// <param name="expectedSubstring">A substring expected in the returned description.</param>
[Theory]
[InlineData(1008, "Invalid reference")]
[InlineData(1012, "Wrong data type")]
@@ -18,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
MxErrorCodes.GetMessage(code).ShouldContain(expectedSubstring);
}
/// <summary>
/// Confirms that unknown MXAccess error codes are reported as unknown while preserving the numeric code.
/// </summary>
[Fact]
public void GetMessage_UnknownCode_ReturnsUnknown()
{
@@ -25,6 +36,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
MxErrorCodes.GetMessage(9999).ShouldContain("9999");
}
/// <summary>
/// Confirms that known MXAccess error codes map to the expected bridge quality values.
/// </summary>
/// <param name="code">The MXAccess error code.</param>
/// <param name="expected">The expected bridge quality value.</param>
[Theory]
[InlineData(1008, Quality.BadConfigError)]
[InlineData(1012, Quality.BadConfigError)]
@@ -37,6 +53,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
MxErrorCodes.MapToQuality(code).ShouldBe(expected);
}
/// <summary>
/// Confirms that unknown MXAccess error codes map to the generic bad quality bucket.
/// </summary>
[Fact]
public void MapToQuality_UnknownCode_ReturnsBad()
{

View File

@@ -4,8 +4,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
{
/// <summary>
/// Verifies the mapping between MXAccess quality codes, bridge quality values, and OPC UA status codes.
/// </summary>
public class QualityMapperTests
{
/// <summary>
/// Confirms that bad-family MXAccess quality values map to the expected bridge quality values.
/// </summary>
/// <param name="input">The raw MXAccess quality code.</param>
/// <param name="expected">The bridge quality value expected for the code.</param>
[Theory]
[InlineData(0, Quality.Bad)]
[InlineData(4, Quality.BadConfigError)]
@@ -16,6 +24,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
}
/// <summary>
/// Confirms that uncertain-family MXAccess quality values map to the expected bridge quality values.
/// </summary>
/// <param name="input">The raw MXAccess quality code.</param>
/// <param name="expected">The bridge quality value expected for the code.</param>
[Theory]
[InlineData(64, Quality.Uncertain)]
[InlineData(68, Quality.UncertainLastUsable)]
@@ -25,6 +38,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
}
/// <summary>
/// Confirms that good-family MXAccess quality values map to the expected bridge quality values.
/// </summary>
/// <param name="input">The raw MXAccess quality code.</param>
/// <param name="expected">The bridge quality value expected for the code.</param>
[Theory]
[InlineData(192, Quality.Good)]
[InlineData(216, Quality.GoodLocalOverride)]
@@ -33,48 +51,72 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
QualityMapper.MapFromMxAccessQuality(input).ShouldBe(expected);
}
/// <summary>
/// Confirms that unknown bad-family values collapse to the generic bad quality bucket.
/// </summary>
[Fact]
public void MapFromMxAccess_UnknownBadValue_ReturnsBad()
{
QualityMapper.MapFromMxAccessQuality(63).ShouldBe(Quality.Bad);
}
/// <summary>
/// Confirms that unknown uncertain-family values collapse to the generic uncertain quality bucket.
/// </summary>
[Fact]
public void MapFromMxAccess_UnknownUncertainValue_ReturnsUncertain()
{
QualityMapper.MapFromMxAccessQuality(100).ShouldBe(Quality.Uncertain);
}
/// <summary>
/// Confirms that unknown good-family values collapse to the generic good quality bucket.
/// </summary>
[Fact]
public void MapFromMxAccess_UnknownGoodValue_ReturnsGood()
{
QualityMapper.MapFromMxAccessQuality(200).ShouldBe(Quality.Good);
}
/// <summary>
/// Confirms that the generic good quality maps to the OPC UA good status code.
/// </summary>
[Fact]
public void MapToOpcUa_Good_Returns0()
{
QualityMapper.MapToOpcUaStatusCode(Quality.Good).ShouldBe(0x00000000u);
}
/// <summary>
/// Confirms that the generic bad quality maps to the OPC UA bad status code.
/// </summary>
[Fact]
public void MapToOpcUa_Bad_Returns80000000()
{
QualityMapper.MapToOpcUaStatusCode(Quality.Bad).ShouldBe(0x80000000u);
}
/// <summary>
/// Confirms that communication failures map to the OPC UA bad communication-failure status code.
/// </summary>
[Fact]
public void MapToOpcUa_BadCommFailure()
{
QualityMapper.MapToOpcUaStatusCode(Quality.BadCommFailure).ShouldBe(0x80050000u);
}
/// <summary>
/// Confirms that the generic uncertain quality maps to the OPC UA uncertain status code.
/// </summary>
[Fact]
public void MapToOpcUa_Uncertain()
{
QualityMapper.MapToOpcUaStatusCode(Quality.Uncertain).ShouldBe(0x40000000u);
}
/// <summary>
/// Confirms that good quality values are classified correctly by the quality extension helpers.
/// </summary>
[Fact]
public void QualityExtensions_IsGood()
{
@@ -83,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
Quality.Good.IsUncertain().ShouldBe(false);
}
/// <summary>
/// Confirms that bad quality values are classified correctly by the quality extension helpers.
/// </summary>
[Fact]
public void QualityExtensions_IsBad()
{
@@ -90,6 +135,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
Quality.Bad.IsGood().ShouldBe(false);
}
/// <summary>
/// Confirms that uncertain quality values are classified correctly by the quality extension helpers.
/// </summary>
[Fact]
public void QualityExtensions_IsUncertain()
{

View File

@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
/// </summary>
public class FullDataFlowTest
{
/// <summary>
/// Confirms that the fake-backed bridge can start, build the address space, and expose coherent status data end to end.
/// </summary>
[Fact]
public void FullDataFlow_EndToEnd()
{

View File

@@ -8,8 +8,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
{
/// <summary>
/// Verifies the polling service that detects Galaxy deploy changes and triggers address-space rebuilds.
/// </summary>
public class ChangeDetectionServiceTests
{
/// <summary>
/// Confirms that the first poll always triggers an initial rebuild notification.
/// </summary>
[Fact]
public async Task FirstPoll_AlwaysTriggers()
{
@@ -26,6 +32,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
service.Dispose();
}
/// <summary>
/// Confirms that repeated polls with the same deploy timestamp do not retrigger rebuilds.
/// </summary>
[Fact]
public async Task SameTimestamp_DoesNotTriggerAgain()
{
@@ -42,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
service.Dispose();
}
/// <summary>
/// Confirms that a changed deploy timestamp triggers another rebuild notification.
/// </summary>
[Fact]
public async Task ChangedTimestamp_TriggersAgain()
{
@@ -62,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
service.Dispose();
}
/// <summary>
/// Confirms that transient polling failures do not crash the service and allow later recovery.
/// </summary>
[Fact]
public async Task FailedPoll_DoesNotCrash_RetriesNext()
{
@@ -88,6 +103,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
service.Dispose();
}
/// <summary>
/// Confirms that stopping the service before it starts is a harmless no-op.
/// </summary>
[Fact]
public void Stop_BeforeStart_DoesNotThrow()
{

View File

@@ -6,40 +6,88 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// In-memory Galaxy repository used by tests to control hierarchy rows, attribute rows, and deploy metadata without SQL access.
/// </summary>
public class FakeGalaxyRepository : IGalaxyRepository
{
/// <summary>
/// Occurs when the fake repository simulates a Galaxy deploy change.
/// </summary>
public event Action? OnGalaxyChanged;
/// <summary>
/// Gets or sets the hierarchy rows returned to address-space construction logic.
/// </summary>
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new List<GalaxyObjectInfo>();
/// <summary>
/// Gets or sets the attribute rows returned to address-space construction logic.
/// </summary>
public List<GalaxyAttributeInfo> Attributes { get; set; } = new List<GalaxyAttributeInfo>();
/// <summary>
/// Gets or sets the deploy timestamp returned to change-detection logic.
/// </summary>
public DateTime? LastDeployTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// Gets or sets a value indicating whether connection checks should report success.
/// </summary>
public bool ConnectionSucceeds { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether repository calls should throw to simulate database failures.
/// </summary>
public bool ShouldThrow { get; set; }
/// <summary>
/// Returns the configured hierarchy rows or throws to simulate a repository failure.
/// </summary>
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
/// <returns>The configured hierarchy rows.</returns>
public Task<List<GalaxyObjectInfo>> GetHierarchyAsync(CancellationToken ct = default)
{
if (ShouldThrow) throw new Exception("Simulated DB failure");
return Task.FromResult(Hierarchy);
}
/// <summary>
/// Returns the configured attribute rows or throws to simulate a repository failure.
/// </summary>
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
/// <returns>The configured attribute rows.</returns>
public Task<List<GalaxyAttributeInfo>> GetAttributesAsync(CancellationToken ct = default)
{
if (ShouldThrow) throw new Exception("Simulated DB failure");
return Task.FromResult(Attributes);
}
/// <summary>
/// Returns the configured deploy timestamp or throws to simulate a repository failure.
/// </summary>
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
/// <returns>The configured deploy timestamp.</returns>
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
{
if (ShouldThrow) throw new Exception("Simulated DB failure");
return Task.FromResult(LastDeployTime);
}
/// <summary>
/// Returns the configured connection result or throws to simulate a repository failure.
/// </summary>
/// <param name="ct">A cancellation token ignored by the in-memory fake.</param>
/// <returns>The configured connection result.</returns>
public Task<bool> TestConnectionAsync(CancellationToken ct = default)
{
if (ShouldThrow) throw new Exception("Simulated DB failure");
return Task.FromResult(ConnectionSucceeds);
}
/// <summary>
/// Raises the deploy-change event so tests can trigger rebuild logic.
/// </summary>
public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke();
}
}

View File

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

View File

@@ -17,21 +17,71 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
private int _connectionHandle;
private bool _registered;
/// <summary>
/// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test.
/// </summary>
public event MxDataChangeHandler? OnDataChange;
/// <summary>
/// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test.
/// </summary>
public event MxWriteCompleteHandler? OnWriteComplete;
/// <summary>
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
/// </summary>
public ConcurrentDictionary<int, string> Items { get; } = new ConcurrentDictionary<int, string>();
/// <summary>
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
/// </summary>
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new ConcurrentDictionary<int, bool>();
/// <summary>
/// Gets the values written through the fake runtime so write scenarios can assert the final payload.
/// </summary>
public List<(string Address, object Value)> WrittenValues { get; } = new List<(string, object)>();
/// <summary>
/// Gets a value indicating whether the fake runtime is currently considered registered.
/// </summary>
public bool IsRegistered => _registered;
/// <summary>
/// Gets the number of times the system under test attempted to register with the fake runtime.
/// </summary>
public int RegisterCallCount { get; private set; }
/// <summary>
/// Gets the number of times the system under test attempted to unregister from the fake runtime.
/// </summary>
public int UnregisterCallCount { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether registration should fail to exercise connection-error paths.
/// </summary>
public bool ShouldFailRegister { get; set; }
/// <summary>
/// Gets or sets a value indicating whether writes should fail to exercise runtime write-error paths.
/// </summary>
public bool ShouldFailWrite { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the fake should suppress the write-complete callback for timeout scenarios.
/// </summary>
public bool SkipWriteCompleteCallback { get; set; }
/// <summary>
/// Gets or sets the status code returned in the simulated write-complete callback.
/// </summary>
public int WriteCompleteStatus { get; set; } = 0; // 0 = success
/// <summary>
/// Simulates the MXAccess registration handshake and returns a synthetic connection handle.
/// </summary>
/// <param name="clientName">The client name supplied by the code under test.</param>
/// <returns>A synthetic connection handle for subsequent fake operations.</returns>
public int Register(string clientName)
{
RegisterCallCount++;
@@ -41,6 +91,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return _connectionHandle;
}
/// <summary>
/// Simulates tearing down the fake MXAccess connection.
/// </summary>
/// <param name="handle">The connection handle supplied by the code under test.</param>
public void Unregister(int handle)
{
UnregisterCallCount++;
@@ -48,6 +102,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
_connectionHandle = 0;
}
/// <summary>
/// Simulates resolving a tag reference into a fake runtime item handle.
/// </summary>
/// <param name="handle">The synthetic connection handle.</param>
/// <param name="address">The Galaxy attribute reference being registered.</param>
/// <returns>A synthetic item handle.</returns>
public int AddItem(int handle, string address)
{
var itemHandle = Interlocked.Increment(ref _nextHandle);
@@ -55,21 +115,43 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return itemHandle;
}
/// <summary>
/// Simulates removing an item from the fake runtime session.
/// </summary>
/// <param name="handle">The synthetic connection handle.</param>
/// <param name="itemHandle">The synthetic item handle to remove.</param>
public void RemoveItem(int handle, int itemHandle)
{
Items.TryRemove(itemHandle, out _);
}
/// <summary>
/// Marks an item as actively advised so tests can assert subscription activation.
/// </summary>
/// <param name="handle">The synthetic connection handle.</param>
/// <param name="itemHandle">The synthetic item handle being monitored.</param>
public void AdviseSupervisory(int handle, int itemHandle)
{
AdvisedItems[itemHandle] = true;
}
/// <summary>
/// Marks an item as no longer advised so tests can assert subscription teardown.
/// </summary>
/// <param name="handle">The synthetic connection handle.</param>
/// <param name="itemHandle">The synthetic item handle no longer being monitored.</param>
public void UnAdviseSupervisory(int handle, int itemHandle)
{
AdvisedItems.TryRemove(itemHandle, out _);
}
/// <summary>
/// Simulates a runtime write, records the written value, and optionally raises the write-complete callback.
/// </summary>
/// <param name="handle">The synthetic connection handle.</param>
/// <param name="itemHandle">The synthetic item handle to write.</param>
/// <param name="value">The value supplied by the system under test.</param>
/// <param name="securityClassification">The security classification supplied with the write request.</param>
public void Write(int handle, int itemHandle, object value, int securityClassification)
{
if (ShouldFailWrite) throw new InvalidOperationException("Write failed (simulated)");
@@ -95,6 +177,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// <summary>
/// Simulates an MXAccess data change event for a specific item handle.
/// </summary>
/// <param name="itemHandle">The synthetic item handle that should receive the new value.</param>
/// <param name="value">The value to publish to the system under test.</param>
/// <param name="quality">The runtime quality code to send with the value.</param>
/// <param name="timestamp">The optional timestamp to send with the value; defaults to the current UTC time.</param>
public void SimulateDataChange(int itemHandle, object value, int quality = 192, DateTime? timestamp = null)
{
var status = new MXSTATUS_PROXY[1];
@@ -106,6 +192,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// <summary>
/// Simulates data change for a specific address (finds handle by address).
/// </summary>
/// <param name="address">The Galaxy attribute reference whose registered handle should receive the new value.</param>
/// <param name="value">The value to publish to the system under test.</param>
/// <param name="quality">The runtime quality code to send with the value.</param>
/// <param name="timestamp">The optional timestamp to send with the value; defaults to the current UTC time.</param>
public void SimulateDataChangeByAddress(string address, object value, int quality = 192, DateTime? timestamp = null)
{
foreach (var kvp in Items)

View File

@@ -21,8 +21,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
private static int _nextPort = 16000;
/// <summary>
/// Gets the started service instance managed by the fixture.
/// </summary>
public OpcUaService Service { get; private set; } = null!;
/// <summary>
/// Gets the OPC UA port assigned to this fixture instance.
/// </summary>
public int OpcUaPort { get; }
/// <summary>
/// Gets the OPC UA endpoint URL exposed by the fixture.
/// </summary>
public string EndpointUrl => $"opc.tcp://localhost:{OpcUaPort}/LmxOpcUa";
/// <summary>
@@ -44,6 +55,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
private readonly OpcUaServiceBuilder _builder;
private bool _started;
/// <summary>
/// Initializes a fixture around a prepared service builder and optional fake dependencies.
/// </summary>
/// <param name="builder">The builder used to construct the service under test.</param>
/// <param name="repo">The optional fake Galaxy repository exposed to tests.</param>
/// <param name="mxClient">The optional fake MXAccess client exposed to tests.</param>
/// <param name="mxProxy">The optional fake MXAccess proxy exposed to tests.</param>
private OpcUaServerFixture(OpcUaServiceBuilder builder,
FakeGalaxyRepository? repo = null,
FakeMxAccessClient? mxClient = null,
@@ -62,6 +80,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// Creates fixture with FakeMxProxy + FakeGalaxyRepository (standard test data).
/// The STA thread and COM interop run against FakeMxProxy.
/// </summary>
/// <param name="proxy">An optional fake proxy to inject; otherwise a default fake is created.</param>
/// <param name="repo">An optional fake repository to inject; otherwise standard test data is used.</param>
/// <returns>A fixture configured to exercise the COM-style runtime path.</returns>
public static OpcUaServerFixture WithFakes(
FakeMxProxy? proxy = null,
FakeGalaxyRepository? repo = null)
@@ -85,6 +106,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// Creates fixture using FakeMxAccessClient directly — skips STA thread + COM entirely.
/// Fastest option for tests that don't need real COM interop.
/// </summary>
/// <param name="mxClient">An optional fake MXAccess client to inject; otherwise a default fake is created.</param>
/// <param name="repo">An optional fake repository to inject; otherwise standard test data is used.</param>
/// <returns>A fixture configured to exercise the direct fake-client path.</returns>
public static OpcUaServerFixture WithFakeMxAccessClient(
FakeMxAccessClient? mxClient = null,
FakeGalaxyRepository? repo = null)
@@ -104,6 +128,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return new OpcUaServerFixture(builder, repo: r, mxClient: client);
}
/// <summary>
/// Builds and starts the OPC UA service for the current fixture.
/// </summary>
public Task InitializeAsync()
{
Service = _builder.Build();
@@ -112,6 +139,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return Task.CompletedTask;
}
/// <summary>
/// Stops the OPC UA service when the fixture had previously been started.
/// </summary>
public Task DisposeAsync()
{
if (_started)

View File

@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// Verifies the reusable OPC UA server fixture used by integration and wiring tests.
/// </summary>
public class OpcUaServerFixtureTests
{
/// <summary>
/// Confirms that the standard fake-backed fixture starts the bridge and tears it down cleanly.
/// </summary>
[Fact]
public async Task WithFakes_StartsAndStops()
{
@@ -25,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
await fixture.DisposeAsync();
}
/// <summary>
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
/// </summary>
[Fact]
public async Task WithFakeMxAccessClient_SkipsCom()
{
@@ -38,6 +47,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
await fixture.DisposeAsync();
}
/// <summary>
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
/// </summary>
[Fact]
public async Task MultipleFixtures_GetUniquePortsAutomatically()
{
@@ -57,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
await fixture2.DisposeAsync();
}
/// <summary>
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
/// </summary>
[Fact]
public async Task Shutdown_CompletesWithin30Seconds()
{
@@ -70,6 +85,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
sw.Elapsed.TotalSeconds.ShouldBeLessThan(30);
}
/// <summary>
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
/// </summary>
[Fact]
public async Task WithFakes_BuildsAddressSpace()
{

View File

@@ -16,11 +16,16 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
private Session? _session;
/// <summary>
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
/// </summary>
public Session Session => _session ?? throw new InvalidOperationException("Not connected");
/// <summary>
/// Resolves the namespace index for a given namespace URI (e.g., "urn:TestGalaxy:LmxOpcUa").
/// </summary>
/// <param name="galaxyName">The Galaxy name whose OPC UA namespace should be resolved on the test server.</param>
/// <returns>The namespace index assigned by the server for the requested Galaxy namespace.</returns>
public ushort GetNamespaceIndex(string galaxyName = "TestGalaxy")
{
var nsUri = $"urn:{galaxyName}:LmxOpcUa";
@@ -32,11 +37,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// <summary>
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
/// </summary>
/// <param name="identifier">The string identifier for the node inside the Galaxy namespace.</param>
/// <param name="galaxyName">The Galaxy name whose namespace should be used for the node identifier.</param>
/// <returns>A node identifier that targets the requested node on the test server.</returns>
public NodeId MakeNodeId(string identifier, string galaxyName = "TestGalaxy")
{
return new NodeId(identifier, GetNamespaceIndex(galaxyName));
}
/// <summary>
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
/// </summary>
/// <param name="endpointUrl">The OPC UA endpoint URL to connect to.</param>
public async Task ConnectAsync(string endpointUrl)
{
var config = new ApplicationConfiguration
@@ -87,6 +99,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// <summary>
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
/// </summary>
/// <param name="nodeId">The node whose hierarchical children should be browsed.</param>
/// <returns>The child nodes exposed beneath the requested node.</returns>
public async Task<List<(string Name, NodeId NodeId, NodeClass NodeClass)>> BrowseAsync(NodeId nodeId)
{
var results = new List<(string, NodeId, NodeClass)>();
@@ -109,6 +123,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// <summary>
/// Read a node's value.
/// </summary>
/// <param name="nodeId">The node whose current value should be read from the server.</param>
/// <returns>The OPC UA data value returned by the server.</returns>
public DataValue Read(NodeId nodeId)
{
return Session.ReadValue(nodeId);
@@ -118,6 +134,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// Write a node's value, optionally using an OPC UA index range for array element writes.
/// Returns the server status code for the write.
/// </summary>
/// <param name="nodeId">The node whose value should be written.</param>
/// <param name="value">The value to send to the server.</param>
/// <param name="indexRange">An optional OPC UA index range used for array element writes.</param>
/// <returns>The server status code returned for the write request.</returns>
public StatusCode Write(NodeId nodeId, object value, string? indexRange = null)
{
var nodesToWrite = new WriteValueCollection
@@ -139,6 +159,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// Create a subscription with a monitored item on the given node.
/// Returns the subscription and monitored item for inspection.
/// </summary>
/// <param name="nodeId">The node whose value changes should be monitored.</param>
/// <param name="intervalMs">The publishing and sampling interval, in milliseconds, for the test subscription.</param>
/// <returns>The created subscription and monitored item pair for later assertions and cleanup.</returns>
public async Task<(Subscription Sub, MonitoredItem Item)> SubscribeAsync(
NodeId nodeId, int intervalMs = 250)
{
@@ -162,6 +185,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
return (subscription, item);
}
/// <summary>
/// Closes the test session and releases OPC UA client resources.
/// </summary>
public void Dispose()
{
if (_session != null)

View File

@@ -8,6 +8,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
/// </summary>
public static class TestData
{
/// <summary>
/// Creates the standard Galaxy hierarchy used by integration and wiring tests.
/// </summary>
/// <returns>The standard hierarchy rows for the fake repository.</returns>
public static List<GalaxyObjectInfo> CreateStandardHierarchy()
{
return new List<GalaxyObjectInfo>
@@ -20,6 +24,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
};
}
/// <summary>
/// Creates the standard attribute set used by integration and wiring tests.
/// </summary>
/// <returns>The standard attribute rows for the fake repository.</returns>
public static List<GalaxyAttributeInfo> CreateStandardAttributes()
{
return new List<GalaxyAttributeInfo>
@@ -33,6 +41,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
};
}
/// <summary>
/// Creates a minimal hierarchy containing a single object for focused unit tests.
/// </summary>
/// <returns>A minimal hierarchy row set.</returns>
public static List<GalaxyObjectInfo> CreateMinimalHierarchy()
{
return new List<GalaxyObjectInfo>
@@ -41,6 +53,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
};
}
/// <summary>
/// Creates a minimal attribute set containing a single scalar attribute for focused unit tests.
/// </summary>
/// <returns>A minimal attribute row set.</returns>
public static List<GalaxyAttributeInfo> CreateMinimalAttributes()
{
return new List<GalaxyAttributeInfo>

View File

@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
/// </summary>
public class AddressSpaceRebuildTests
{
/// <summary>
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
/// </summary>
[Fact]
public async Task Browse_ReturnsInitialHierarchy()
{
@@ -38,6 +41,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
/// </summary>
[Fact]
public async Task Browse_AfterAddingObject_NewNodeAppears()
{
@@ -81,6 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
/// </summary>
[Fact]
public async Task Browse_AfterRemovingObject_NodeDisappears()
{
@@ -114,6 +123,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
/// </summary>
[Fact]
public async Task Subscribe_RemovedNode_PublishesBadQuality()
{
@@ -160,6 +172,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
/// </summary>
[Fact]
public async Task Subscribe_SurvivingNode_StillWorksAfterRebuild()
{
@@ -196,6 +211,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
/// </summary>
[Fact]
public async Task Browse_AddAttribute_NewVariableAppears()
{
@@ -230,6 +248,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
/// </summary>
[Fact]
public async Task Browse_RemoveAttribute_VariableDisappears()
{
@@ -260,6 +281,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
/// </summary>
[Fact]
public async Task Rebuild_PreservesSubscriptionBookkeeping_ForSurvivingNodes()
{

View File

@@ -8,8 +8,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
/// <summary>
/// Verifies OPC UA indexed array writes against the bridge's whole-array runtime update behavior.
/// </summary>
public class ArrayWriteTests
{
/// <summary>
/// Confirms that writing a single array element updates the correct slot while preserving the rest of the array.
/// </summary>
[Fact]
public async Task Write_SingleArrayElement_UpdatesWholeArrayValue()
{

View File

@@ -19,6 +19,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
// ── Subscription Sync ─────────────────────────────────────────────
/// <summary>
/// Confirms that multiple OPC UA clients subscribed to the same tag all receive the same runtime update.
/// </summary>
[Fact]
public async Task MultipleClients_SubscribeToSameTag_AllReceiveDataChanges()
{
@@ -70,6 +73,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
/// </summary>
[Fact]
public async Task Client_Disconnects_OtherClientsStillReceive()
{
@@ -119,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
/// </summary>
[Fact]
public async Task Client_Unsubscribes_OtherClientsStillReceive()
{
@@ -159,6 +168,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
/// </summary>
[Fact]
public async Task MultipleClients_SubscribeToDifferentTags_EachGetsOwnData()
{
@@ -206,6 +218,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// ── Concurrent Operation Tests ────────────────────────────────────
/// <summary>
/// Confirms that concurrent browse operations from several clients all complete successfully.
/// </summary>
[Fact]
public async Task ConcurrentBrowseFromMultipleClients_AllSucceed()
{
@@ -246,6 +261,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that concurrent browse requests return consistent results across clients.
/// </summary>
[Fact]
public async Task ConcurrentBrowse_AllReturnSameResults()
{
@@ -283,6 +301,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
/// </summary>
[Fact]
public async Task ConcurrentBrowseAndSubscribe_NoInterference()
{
@@ -318,6 +339,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
/// </summary>
[Fact]
public async Task ConcurrentSubscribeAndRead_NoDeadlock()
{
@@ -355,6 +379,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
/// <summary>
/// Confirms that repeated client churn does not leave the server in an unstable state.
/// </summary>
[Fact]
public async Task RapidConnectDisconnect_ServerStaysStable()
{

View File

@@ -5,8 +5,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
{
/// <summary>
/// Verifies operation timing aggregation, rolling buffers, and success tracking used by the bridge metrics subsystem.
/// </summary>
public class PerformanceMetricsTests
{
/// <summary>
/// Confirms that a fresh metrics collector reports no statistics.
/// </summary>
[Fact]
public void EmptyState_ReturnsZeroStatistics()
{
@@ -15,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats.ShouldBeEmpty();
}
/// <summary>
/// Confirms that repeated operation recordings update total and successful execution counts.
/// </summary>
[Fact]
public void RecordOperation_TracksCounts()
{
@@ -29,6 +38,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats["Read"].SuccessRate.ShouldBe(0.5);
}
/// <summary>
/// Confirms that min, max, and average timing values are calculated from recorded operations.
/// </summary>
[Fact]
public void RecordOperation_TracksMinMaxAverage()
{
@@ -43,6 +55,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats.AverageMilliseconds.ShouldBe(20);
}
/// <summary>
/// Confirms that the 95th percentile is calculated from the recorded timing sample.
/// </summary>
[Fact]
public void P95_CalculatedCorrectly()
{
@@ -54,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats.Percentile95Milliseconds.ShouldBe(95);
}
/// <summary>
/// Confirms that the rolling buffer keeps the most recent operation durations for percentile calculations.
/// </summary>
[Fact]
public void RollingBuffer_EvictsOldEntries()
{
@@ -67,6 +85,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats.Percentile95Milliseconds.ShouldBeGreaterThan(1000);
}
/// <summary>
/// Confirms that a timing scope records an operation when disposed.
/// </summary>
[Fact]
public void BeginOperation_TimingScopeRecordsOnDispose()
{
@@ -85,6 +106,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats["Test"].AverageMilliseconds.ShouldBeGreaterThan(0);
}
/// <summary>
/// Confirms that a timing scope can mark an operation as failed before disposal.
/// </summary>
[Fact]
public void BeginOperation_SetSuccessFalse()
{
@@ -100,6 +124,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
stats.SuccessCount.ShouldBe(0);
}
/// <summary>
/// Confirms that looking up an unknown operation returns no metrics bucket.
/// </summary>
[Fact]
public void GetMetrics_UnknownOperation_ReturnsNull()
{
@@ -107,6 +134,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
metrics.GetMetrics("NonExistent").ShouldBeNull();
}
/// <summary>
/// Confirms that operation names are tracked without case sensitivity.
/// </summary>
[Fact]
public void OperationNames_AreCaseInsensitive()
{

View File

@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
{
/// <summary>
/// Verifies MXAccess client connection lifecycle behavior, including transitions, registration, and reconnect handling.
/// </summary>
public class MxAccessClientConnectionTests : IDisposable
{
private readonly StaComThread _staThread;
@@ -19,6 +22,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
private readonly MxAccessClient _client;
private readonly List<(ConnectionState Previous, ConnectionState Current)> _stateChanges = new();
/// <summary>
/// Initializes the connection test fixture with a fake runtime proxy and state-change recorder.
/// </summary>
public MxAccessClientConnectionTests()
{
_staThread = new StaComThread();
@@ -30,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client.ConnectionStateChanged += (_, e) => _stateChanges.Add((e.PreviousState, e.CurrentState));
}
/// <summary>
/// Disposes the connection test fixture and its supporting resources.
/// </summary>
public void Dispose()
{
_client.Dispose();
@@ -37,12 +46,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_metrics.Dispose();
}
/// <summary>
/// Confirms that a newly created MXAccess client starts in the disconnected state.
/// </summary>
[Fact]
public void InitialState_IsDisconnected()
{
_client.State.ShouldBe(ConnectionState.Disconnected);
}
/// <summary>
/// Confirms that connecting drives the expected disconnected-to-connecting-to-connected transitions.
/// </summary>
[Fact]
public async Task Connect_TransitionsToConnected()
{
@@ -53,6 +68,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_stateChanges.ShouldContain(s => s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected);
}
/// <summary>
/// Confirms that a successful connect registers exactly once with the runtime proxy.
/// </summary>
[Fact]
public async Task Connect_RegistersCalled()
{
@@ -60,6 +78,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_proxy.RegisterCallCount.ShouldBe(1);
}
/// <summary>
/// Confirms that disconnecting drives the expected shutdown transitions back to disconnected.
/// </summary>
[Fact]
public async Task Disconnect_TransitionsToDisconnected()
{
@@ -71,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnected);
}
/// <summary>
/// Confirms that disconnecting unregisters the runtime proxy session.
/// </summary>
[Fact]
public async Task Disconnect_UnregistersCalled()
{
@@ -79,6 +103,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_proxy.UnregisterCallCount.ShouldBe(1);
}
/// <summary>
/// Confirms that registration failures move the client into the error state.
/// </summary>
[Fact]
public async Task ConnectFails_TransitionsToError()
{
@@ -88,6 +115,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client.State.ShouldBe(ConnectionState.Error);
}
/// <summary>
/// Confirms that repeated connect calls do not perform duplicate runtime registrations.
/// </summary>
[Fact]
public async Task DoubleConnect_NoOp()
{
@@ -96,6 +126,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_proxy.RegisterCallCount.ShouldBe(1);
}
/// <summary>
/// Confirms that reconnect increments the reconnect counter and restores the connected state.
/// </summary>
[Fact]
public async Task Reconnect_IncrementsCount()
{

View File

@@ -10,12 +10,18 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
{
/// <summary>
/// Verifies the background connectivity monitor used to reconnect the MXAccess bridge after faults or stale probes.
/// </summary>
public class MxAccessClientMonitorTests : IDisposable
{
private readonly StaComThread _staThread;
private readonly FakeMxProxy _proxy;
private readonly PerformanceMetrics _metrics;
/// <summary>
/// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector.
/// </summary>
public MxAccessClientMonitorTests()
{
_staThread = new StaComThread();
@@ -24,12 +30,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_metrics = new PerformanceMetrics();
}
/// <summary>
/// Disposes the monitor test fixture resources.
/// </summary>
public void Dispose()
{
_staThread.Dispose();
_metrics.Dispose();
}
/// <summary>
/// Confirms that the monitor reconnects the client after an observed disconnect.
/// </summary>
[Fact]
public async Task Monitor_ReconnectsOnDisconnect()
{
@@ -54,6 +66,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
client.Dispose();
}
/// <summary>
/// Confirms that the monitor can be started and stopped without throwing.
/// </summary>
[Fact]
public async Task Monitor_StopsOnCancel()
{
@@ -69,6 +84,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
client.Dispose();
}
/// <summary>
/// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled.
/// </summary>
[Fact]
public async Task Monitor_ProbeStale_ForcesReconnect()
{
@@ -93,6 +111,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
client.Dispose();
}
/// <summary>
/// Confirms that fresh probe updates prevent unnecessary reconnects.
/// </summary>
[Fact]
public async Task Monitor_ProbeDataChange_PreventsStaleReconnect()
{
@@ -122,6 +143,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
client.Dispose();
}
/// <summary>
/// Confirms that enabling the monitor without a probe tag does not trigger false reconnects.
/// </summary>
[Fact]
public async Task Monitor_NoProbeConfigured_NoFalseReconnect()
{

View File

@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
{
/// <summary>
/// Verifies MXAccess client read and write behavior against the fake runtime proxy used by the bridge.
/// </summary>
public class MxAccessClientReadWriteTests : IDisposable
{
private readonly StaComThread _staThread;
@@ -18,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
private readonly PerformanceMetrics _metrics;
private readonly MxAccessClient _client;
/// <summary>
/// Initializes the COM-threaded MXAccess test fixture with a fake runtime proxy and metrics collector.
/// </summary>
public MxAccessClientReadWriteTests()
{
_staThread = new StaComThread();
@@ -28,6 +34,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client = new MxAccessClient(_staThread, _proxy, config, _metrics);
}
/// <summary>
/// Disposes the MXAccess client fixture and its supporting STA thread and metrics collector.
/// </summary>
public void Dispose()
{
_client.Dispose();
@@ -35,6 +44,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_metrics.Dispose();
}
/// <summary>
/// Confirms that reads fail with bad-not-connected quality when the runtime session is offline.
/// </summary>
[Fact]
public async Task Read_NotConnected_ReturnsBad()
{
@@ -42,6 +54,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
result.Quality.ShouldBe(Quality.BadNotConnected);
}
/// <summary>
/// Confirms that a runtime data-change callback completes a pending read with the published value.
/// </summary>
[Fact]
public async Task Read_ReturnsValueOnDataChange()
{
@@ -59,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
result.Quality.ShouldBe(Quality.Good);
}
/// <summary>
/// Confirms that reads time out with bad communication-failure quality when the runtime never responds.
/// </summary>
[Fact]
public async Task Read_Timeout_ReturnsBadCommFailure()
{
@@ -69,6 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
result.Quality.ShouldBe(Quality.BadCommFailure);
}
/// <summary>
/// Confirms that timed-out reads are recorded as failed read operations in the metrics collector.
/// </summary>
[Fact]
public async Task Read_Timeout_RecordsFailedMetrics()
{
@@ -83,6 +104,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
stats["Read"].SuccessCount.ShouldBe(0);
}
/// <summary>
/// Confirms that writes are rejected when the runtime session is not connected.
/// </summary>
[Fact]
public async Task Write_NotConnected_ReturnsFalse()
{
@@ -90,6 +114,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
result.ShouldBe(false);
}
/// <summary>
/// Confirms that successful runtime write acknowledgments return success and record the written payload.
/// </summary>
[Fact]
public async Task Write_Success_ReturnsTrue()
{
@@ -101,6 +128,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_proxy.WrittenValues.ShouldContain(w => w.Address == "TestTag.Attr" && (int)w.Value == 42);
}
/// <summary>
/// Confirms that MXAccess error codes on write completion are surfaced as failed writes.
/// </summary>
[Fact]
public async Task Write_ErrorCode_ReturnsFalse()
{
@@ -111,6 +141,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
result.ShouldBe(false);
}
/// <summary>
/// Confirms that write timeouts are recorded as failed write operations in the metrics collector.
/// </summary>
[Fact]
public async Task Write_Timeout_ReturnsFalse_AndRecordsFailedMetrics()
{
@@ -126,6 +159,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
stats["Write"].SuccessCount.ShouldBe(0);
}
/// <summary>
/// Confirms that successful reads contribute a read entry to the metrics collector.
/// </summary>
[Fact]
public async Task Read_RecordsMetrics()
{
@@ -141,6 +177,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
stats["Read"].TotalCount.ShouldBe(1);
}
/// <summary>
/// Confirms that writes contribute a write entry to the metrics collector.
/// </summary>
[Fact]
public async Task Write_RecordsMetrics()
{

View File

@@ -11,6 +11,9 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
{
/// <summary>
/// Verifies how the MXAccess client manages persistent subscriptions, reconnect replay, and probe-tag behavior.
/// </summary>
public class MxAccessClientSubscriptionTests : IDisposable
{
private readonly StaComThread _staThread;
@@ -18,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
private readonly PerformanceMetrics _metrics;
private readonly MxAccessClient _client;
/// <summary>
/// Initializes the subscription test fixture with a fake runtime proxy and STA thread.
/// </summary>
public MxAccessClientSubscriptionTests()
{
_staThread = new StaComThread();
@@ -27,6 +33,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client = new MxAccessClient(_staThread, _proxy, new MxAccessConfiguration(), _metrics);
}
/// <summary>
/// Disposes the subscription test fixture and its supporting resources.
/// </summary>
public void Dispose()
{
_client.Dispose();
@@ -34,6 +43,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_metrics.Dispose();
}
/// <summary>
/// Confirms that subscribing creates a runtime item, advises it, and increments the active subscription count.
/// </summary>
[Fact]
public async Task Subscribe_CreatesItemAndAdvises()
{
@@ -45,6 +57,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client.ActiveSubscriptionCount.ShouldBe(1);
}
/// <summary>
/// Confirms that unsubscribing clears the active subscription count after a tag was previously monitored.
/// </summary>
[Fact]
public async Task Unsubscribe_RemovesItemAndUnadvises()
{
@@ -55,6 +70,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client.ActiveSubscriptionCount.ShouldBe(0);
}
/// <summary>
/// Confirms that runtime data changes are delivered to the per-subscription callback.
/// </summary>
[Fact]
public async Task OnDataChange_InvokesCallback()
{
@@ -70,6 +88,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
received.Value.Quality.ShouldBe(Quality.Good);
}
/// <summary>
/// Confirms that runtime data changes are also delivered to the client's global tag-change event.
/// </summary>
[Fact]
public async Task OnDataChange_InvokesGlobalHandler()
{
@@ -84,6 +105,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
globalAddr.ShouldBe("TestTag.Attr");
}
/// <summary>
/// Confirms that stored subscriptions are replayed after reconnect so live updates resume automatically.
/// </summary>
[Fact]
public async Task StoredSubscriptions_ReplayedAfterReconnect()
{
@@ -102,6 +126,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
callbackInvoked.ShouldBe(true);
}
/// <summary>
/// Confirms that one-shot reads do not remove persistent subscriptions when the client reconnects.
/// </summary>
[Fact]
public async Task OneShotRead_DoesNotRemovePersistentSubscription_OnReconnect()
{
@@ -122,6 +149,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_client.ActiveSubscriptionCount.ShouldBe(1);
}
/// <summary>
/// Confirms that transient writes do not prevent later removal of a persistent subscription.
/// </summary>
[Fact]
public async Task OneShotWrite_DoesNotBreakPersistentUnsubscribe()
{
@@ -138,6 +168,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_proxy.Items.Values.ShouldNotContain("TestTag.Attr");
}
/// <summary>
/// Confirms that the configured probe tag is subscribed during connect so connectivity monitoring can start immediately.
/// </summary>
[Fact]
public async Task ProbeTag_SubscribedOnConnect()
{
@@ -152,6 +185,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
client.Dispose();
}
/// <summary>
/// Confirms that the probe tag cannot be unsubscribed accidentally because it is reserved for connection monitoring.
/// </summary>
[Fact]
public async Task ProbeTag_ProtectedFromUnsubscribe()
{

View File

@@ -7,18 +7,30 @@ using ZB.MOM.WW.LmxOpcUa.Host.MxAccess;
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
{
/// <summary>
/// Verifies the single-threaded apartment worker used to marshal COM calls for the MXAccess bridge.
/// </summary>
public class StaComThreadTests : IDisposable
{
private readonly StaComThread _thread;
/// <summary>
/// Starts a fresh STA thread instance for each test.
/// </summary>
public StaComThreadTests()
{
_thread = new StaComThread();
_thread.Start();
}
/// <summary>
/// Disposes the STA thread after each test.
/// </summary>
public void Dispose() => _thread.Dispose();
/// <summary>
/// Confirms that queued work runs on a thread configured for STA apartment state.
/// </summary>
[Fact]
public async Task RunAsync_ExecutesOnStaThread()
{
@@ -26,6 +38,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
apartmentState.ShouldBe(ApartmentState.STA);
}
/// <summary>
/// Confirms that action delegates run to completion on the STA thread.
/// </summary>
[Fact]
public async Task RunAsync_Action_Completes()
{
@@ -34,6 +49,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
executed.ShouldBe(true);
}
/// <summary>
/// Confirms that function delegates can return results from the STA thread.
/// </summary>
[Fact]
public async Task RunAsync_Func_ReturnsResult()
{
@@ -41,6 +59,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
result.ShouldBe(42);
}
/// <summary>
/// Confirms that exceptions thrown on the STA thread propagate back to the caller.
/// </summary>
[Fact]
public async Task RunAsync_PropagatesException()
{
@@ -48,6 +69,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
_thread.RunAsync(() => throw new InvalidOperationException("test error")));
}
/// <summary>
/// Confirms that disposing the STA thread stops it from accepting additional work.
/// </summary>
[Fact]
public void Dispose_Stops_Thread()
{
@@ -59,6 +83,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
Should.Throw<ObjectDisposedException>(() => thread.RunAsync(() => { }).GetAwaiter().GetResult());
}
/// <summary>
/// Confirms that multiple queued work items all execute successfully on the STA thread.
/// </summary>
[Fact]
public async Task MultipleWorkItems_ExecuteInOrder()
{

View File

@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
{
/// <summary>
/// Verifies how bridge VTQ values are translated to and from OPC UA data values for the published namespace.
/// </summary>
public class DataValueConverterTests
{
/// <summary>
/// Confirms that boolean runtime values are preserved when converted to OPC UA data values.
/// </summary>
[Fact]
public void FromVtq_Boolean()
{
@@ -17,6 +23,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
Opc.Ua.StatusCode.IsGood(dv.StatusCode).ShouldBe(true);
}
/// <summary>
/// Confirms that integer runtime values are preserved when converted to OPC UA data values.
/// </summary>
[Fact]
public void FromVtq_Int32()
{
@@ -25,6 +34,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe(42);
}
/// <summary>
/// Confirms that float runtime values are preserved when converted to OPC UA data values.
/// </summary>
[Fact]
public void FromVtq_Float()
{
@@ -33,6 +45,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe(3.14f);
}
/// <summary>
/// Confirms that double runtime values are preserved when converted to OPC UA data values.
/// </summary>
[Fact]
public void FromVtq_Double()
{
@@ -41,6 +56,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe(3.14159);
}
/// <summary>
/// Confirms that string runtime values are preserved when converted to OPC UA data values.
/// </summary>
[Fact]
public void FromVtq_String()
{
@@ -49,6 +67,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe("hello");
}
/// <summary>
/// Confirms that UTC timestamps remain UTC when a VTQ is converted for OPC UA clients.
/// </summary>
[Fact]
public void FromVtq_DateTime_IsUtc()
{
@@ -58,6 +79,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
((DateTime)dv.Value).Kind.ShouldBe(DateTimeKind.Utc);
}
/// <summary>
/// Confirms that elapsed-time values are exposed to OPC UA clients in seconds.
/// </summary>
[Fact]
public void FromVtq_TimeSpan_ConvertedToSeconds()
{
@@ -66,6 +90,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe(150.0);
}
/// <summary>
/// Confirms that string arrays remain arrays when exposed through OPC UA.
/// </summary>
[Fact]
public void FromVtq_StringArray()
{
@@ -75,6 +102,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe(arr);
}
/// <summary>
/// Confirms that integer arrays remain arrays when exposed through OPC UA.
/// </summary>
[Fact]
public void FromVtq_IntArray()
{
@@ -84,6 +114,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBe(arr);
}
/// <summary>
/// Confirms that bad runtime quality is translated to a bad OPC UA status code.
/// </summary>
[Fact]
public void FromVtq_BadQuality_MapsToStatusCode()
{
@@ -92,6 +125,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
Opc.Ua.StatusCode.IsBad(dv.StatusCode).ShouldBe(true);
}
/// <summary>
/// Confirms that uncertain runtime quality is translated to an uncertain OPC UA status code.
/// </summary>
[Fact]
public void FromVtq_UncertainQuality()
{
@@ -100,6 +136,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
Opc.Ua.StatusCode.IsUncertain(dv.StatusCode).ShouldBe(true);
}
/// <summary>
/// Confirms that null runtime values remain null when converted for OPC UA.
/// </summary>
[Fact]
public void FromVtq_NullValue()
{
@@ -108,6 +147,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
dv.Value.ShouldBeNull();
}
/// <summary>
/// Confirms that a data value can round-trip back into a VTQ without losing the process value or quality.
/// </summary>
[Fact]
public void ToVtq_RoundTrip()
{

View File

@@ -6,8 +6,15 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
{
/// <summary>
/// Verifies the in-memory address-space model built from Galaxy hierarchy and attribute rows.
/// </summary>
public class LmxNodeManagerBuildTests
{
/// <summary>
/// Creates representative Galaxy hierarchy and attribute rows for address-space builder tests.
/// </summary>
/// <returns>The hierarchy and attribute rows used by the tests.</returns>
private static (List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> attributes) CreateTestData()
{
var hierarchy = new List<GalaxyObjectInfo>
@@ -29,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
return (hierarchy, attributes);
}
/// <summary>
/// Confirms that object and variable counts are computed correctly from the seeded Galaxy model.
/// </summary>
[Fact]
public void BuildAddressSpace_CreatesCorrectNodeCounts()
{
@@ -39,6 +49,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model.VariableCount.ShouldBe(4); // MachineID, DownloadPath, JobStepNumber, BatchItems
}
/// <summary>
/// Confirms that runtime tag references are populated for every published variable.
/// </summary>
[Fact]
public void BuildAddressSpace_TagReferencesPopulated()
{
@@ -51,6 +64,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true);
}
/// <summary>
/// Confirms that array attributes are represented in the tag-reference map.
/// </summary>
[Fact]
public void BuildAddressSpace_ArrayVariable_HasCorrectInfo()
{
@@ -60,6 +76,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model.NodeIdToTagReference.ContainsKey("TestMachine_001.BatchItems[]").ShouldBe(true);
}
/// <summary>
/// Confirms that Galaxy areas are not counted as object nodes in the resulting model.
/// </summary>
[Fact]
public void BuildAddressSpace_Areas_AreNotCountedAsObjects()
{
@@ -73,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model.ObjectCount.ShouldBe(1); // Only Obj1, not Area1
}
/// <summary>
/// Confirms that only top-level Galaxy nodes are returned as roots in the model.
/// </summary>
[Fact]
public void BuildAddressSpace_RootNodes_AreTopLevel()
{
@@ -86,6 +108,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model.RootNodes.Count.ShouldBe(1); // Only Root1 is a root
}
/// <summary>
/// Confirms that variables for multiple MX data types are included in the model.
/// </summary>
[Fact]
public void BuildAddressSpace_DataTypeMappings()
{

View File

@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
{
/// <summary>
/// Verifies rebuild behavior by comparing address-space models before and after metadata changes.
/// </summary>
public class LmxNodeManagerRebuildTests
{
/// <summary>
/// Confirms that rebuilding with new metadata replaces the old tag-reference set.
/// </summary>
[Fact]
public void Rebuild_NewBuild_ReplacesOldData()
{
@@ -40,6 +46,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model2.NodeIdToTagReference.ContainsKey("NewObj.NewAttr").ShouldBe(true);
}
/// <summary>
/// Confirms that object counts are recalculated from the latest rebuild input.
/// </summary>
[Fact]
public void Rebuild_UpdatesNodeCounts()
{
@@ -59,6 +68,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
model2.ObjectCount.ShouldBe(1);
}
/// <summary>
/// Confirms that empty metadata produces an empty address-space model.
/// </summary>
[Fact]
public void EmptyHierarchy_ProducesEmptyModel()
{

View File

@@ -6,8 +6,14 @@ using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
{
/// <summary>
/// Verifies translation between bridge quality values and OPC UA status codes.
/// </summary>
public class OpcUaQualityMapperTests
{
/// <summary>
/// Confirms that good bridge quality maps to an OPC UA good status.
/// </summary>
[Fact]
public void Good_MapsToGoodStatusCode()
{
@@ -15,6 +21,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
StatusCode.IsGood(sc).ShouldBe(true);
}
/// <summary>
/// Confirms that bad bridge quality maps to an OPC UA bad status.
/// </summary>
[Fact]
public void Bad_MapsToBadStatusCode()
{
@@ -22,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
StatusCode.IsBad(sc).ShouldBe(true);
}
/// <summary>
/// Confirms that uncertain bridge quality maps to an OPC UA uncertain status.
/// </summary>
[Fact]
public void Uncertain_MapsToUncertainStatusCode()
{
@@ -29,6 +41,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
StatusCode.IsUncertain(sc).ShouldBe(true);
}
/// <summary>
/// Confirms that communication failures map to a bad OPC UA status code.
/// </summary>
[Fact]
public void BadCommFailure_MapsCorrectly()
{
@@ -36,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
StatusCode.IsBad(sc).ShouldBe(true);
}
/// <summary>
/// Confirms that the OPC UA good status maps back to bridge good quality.
/// </summary>
[Fact]
public void FromStatusCode_Good()
{
@@ -43,6 +61,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
q.ShouldBe(Quality.Good);
}
/// <summary>
/// Confirms that the OPC UA bad status maps back to bridge bad quality.
/// </summary>
[Fact]
public void FromStatusCode_Bad()
{
@@ -50,6 +71,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
q.ShouldBe(Quality.Bad);
}
/// <summary>
/// Confirms that the OPC UA uncertain status maps back to bridge uncertain quality.
/// </summary>
[Fact]
public void FromStatusCode_Uncertain()
{

View File

@@ -3,8 +3,14 @@ using Xunit;
namespace ZB.MOM.WW.LmxOpcUa.Tests
{
/// <summary>
/// Placeholder unit test that keeps the unit test project wired into the solution.
/// </summary>
public class SampleTest
{
/// <summary>
/// Confirms that the unit test assembly is executing.
/// </summary>
[Fact]
public void Placeholder_ShouldPass()
{

View File

@@ -7,10 +7,16 @@ using ZB.MOM.WW.LmxOpcUa.Host.Status;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
{
/// <summary>
/// Verifies how the dashboard health service classifies bridge health from connection state and metrics.
/// </summary>
public class HealthCheckServiceTests
{
private readonly HealthCheckService _sut = new();
/// <summary>
/// Confirms that a disconnected runtime is reported as unhealthy.
/// </summary>
[Fact]
public void NotConnected_ReturnsUnhealthy()
{
@@ -20,6 +26,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
result.Message.ShouldContain("not connected");
}
/// <summary>
/// Confirms that a connected runtime with no metrics history is still considered healthy.
/// </summary>
[Fact]
public void Connected_NoMetrics_ReturnsHealthy()
{
@@ -28,6 +37,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
result.Color.ShouldBe("green");
}
/// <summary>
/// Confirms that good success-rate metrics keep the service in a healthy state.
/// </summary>
[Fact]
public void Connected_GoodMetrics_ReturnsHealthy()
{
@@ -39,6 +51,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
result.Status.ShouldBe("Healthy");
}
/// <summary>
/// Confirms that poor operation success rates degrade the reported health state.
/// </summary>
[Fact]
public void Connected_LowSuccessRate_ReturnsDegraded()
{
@@ -53,18 +68,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
result.Color.ShouldBe("yellow");
}
/// <summary>
/// Confirms that the boolean health helper reports true when the runtime is connected.
/// </summary>
[Fact]
public void IsHealthy_Connected_ReturnsTrue()
{
_sut.IsHealthy(ConnectionState.Connected, null).ShouldBe(true);
}
/// <summary>
/// Confirms that the boolean health helper reports false when the runtime is disconnected.
/// </summary>
[Fact]
public void IsHealthy_Disconnected_ReturnsFalse()
{
_sut.IsHealthy(ConnectionState.Disconnected, null).ShouldBe(false);
}
/// <summary>
/// Confirms that the error connection state is treated as unhealthy.
/// </summary>
[Fact]
public void Error_ReturnsUnhealthy()
{
@@ -72,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
result.Status.ShouldBe("Unhealthy");
}
/// <summary>
/// Confirms that the reconnecting state is treated as unhealthy while recovery is in progress.
/// </summary>
[Fact]
public void Reconnecting_ReturnsUnhealthy()
{

View File

@@ -9,8 +9,14 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
{
/// <summary>
/// Verifies the HTML, JSON, and health snapshots generated for the operator status dashboard.
/// </summary>
public class StatusReportServiceTests
{
/// <summary>
/// Confirms that the generated HTML contains every dashboard panel expected by operators.
/// </summary>
[Fact]
public void GenerateHtml_ContainsAllPanels()
{
@@ -25,6 +31,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
html.ShouldContain("Footer");
}
/// <summary>
/// Confirms that the generated HTML includes the configured auto-refresh meta tag.
/// </summary>
[Fact]
public void GenerateHtml_ContainsMetaRefresh()
{
@@ -33,6 +42,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
html.ShouldContain("meta http-equiv='refresh' content='10'");
}
/// <summary>
/// Confirms that the connection panel renders the current runtime connection state.
/// </summary>
[Fact]
public void GenerateHtml_ConnectionPanel_ShowsState()
{
@@ -41,6 +53,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
html.ShouldContain("Connected");
}
/// <summary>
/// Confirms that the Galaxy panel renders the bridged Galaxy name.
/// </summary>
[Fact]
public void GenerateHtml_GalaxyPanel_ShowsName()
{
@@ -49,6 +64,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
html.ShouldContain("TestGalaxy");
}
/// <summary>
/// Confirms that the operations table renders the expected performance metric headers.
/// </summary>
[Fact]
public void GenerateHtml_OperationsTable_ShowsHeaders()
{
@@ -62,6 +80,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
html.ShouldContain("P95 (ms)");
}
/// <summary>
/// Confirms that the footer renders timestamp and version information.
/// </summary>
[Fact]
public void GenerateHtml_Footer_ContainsTimestampAndVersion()
{
@@ -71,6 +92,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
html.ShouldContain("Version:");
}
/// <summary>
/// Confirms that the generated JSON includes the major dashboard sections.
/// </summary>
[Fact]
public void GenerateJson_Deserializes()
{
@@ -86,6 +110,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
json.ShouldContain("Footer");
}
/// <summary>
/// Confirms that the report service reports healthy when the runtime connection is up.
/// </summary>
[Fact]
public void IsHealthy_WhenConnected_ReturnsTrue()
{
@@ -93,6 +120,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
sut.IsHealthy().ShouldBe(true);
}
/// <summary>
/// Confirms that the report service reports unhealthy when the runtime connection is down.
/// </summary>
[Fact]
public void IsHealthy_WhenDisconnected_ReturnsFalse()
{
@@ -102,6 +132,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
sut.IsHealthy().ShouldBe(false);
}
/// <summary>
/// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data.
/// </summary>
/// <returns>A configured status report service for dashboard assertions.</returns>
private static StatusReportService CreateService()
{
var mxClient = new FakeMxAccessClient();

View File

@@ -9,12 +9,18 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
{
/// <summary>
/// Verifies the lightweight HTTP dashboard host that exposes bridge status to operators.
/// </summary>
public class StatusWebServerTests : IDisposable
{
private readonly StatusWebServer _server;
private readonly HttpClient _client;
private readonly int _port;
/// <summary>
/// Starts a status web server on a random test port and prepares an HTTP client for endpoint assertions.
/// </summary>
public StatusWebServerTests()
{
_port = new Random().Next(18000, 19000);
@@ -26,12 +32,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
_client = new HttpClient { BaseAddress = new Uri($"http://localhost:{_port}") };
}
/// <summary>
/// Disposes the test HTTP client and stops the status web server.
/// </summary>
public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
/// <summary>
/// Confirms that the dashboard root responds with HTML content.
/// </summary>
[Fact]
public async Task Root_ReturnsHtml200()
{
@@ -40,6 +52,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
response.Content.Headers.ContentType?.MediaType.ShouldBe("text/html");
}
/// <summary>
/// Confirms that the JSON status endpoint responds successfully.
/// </summary>
[Fact]
public async Task ApiStatus_ReturnsJson200()
{
@@ -48,6 +63,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json");
}
/// <summary>
/// Confirms that the health endpoint returns HTTP 200 when the bridge is healthy.
/// </summary>
[Fact]
public async Task ApiHealth_Returns200WhenHealthy()
{
@@ -58,6 +76,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
body.ShouldContain("healthy");
}
/// <summary>
/// Confirms that unknown dashboard routes return HTTP 404.
/// </summary>
[Fact]
public async Task UnknownPath_Returns404()
{
@@ -65,6 +86,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
}
/// <summary>
/// Confirms that unsupported HTTP methods are rejected with HTTP 405.
/// </summary>
[Fact]
public async Task PostMethod_Returns405()
{
@@ -72,6 +96,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
response.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed);
}
/// <summary>
/// Confirms that cache-control headers disable caching for dashboard responses.
/// </summary>
[Fact]
public async Task CacheHeaders_Present()
{
@@ -80,6 +107,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
response.Headers.CacheControl?.NoStore.ShouldBe(true);
}
/// <summary>
/// Confirms that the server can be started and stopped cleanly.
/// </summary>
[Fact]
public void StartStop_DoesNotThrow()
{

View File

@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
/// </summary>
public class ChangeDetectionToRebuildWiringTest
{
/// <summary>
/// Confirms that a changed deploy timestamp causes the change-detection pipeline to raise another rebuild signal.
/// </summary>
[Fact]
public async Task ChangedTimestamp_TriggersRebuild()
{

View File

@@ -12,6 +12,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
/// </summary>
public class MxAccessToNodeManagerWiringTest
{
/// <summary>
/// Confirms that a simulated data change reaches the global tag-value-changed event.
/// </summary>
[Fact]
public async Task DataChange_ReachesGlobalHandler()
{
@@ -33,6 +36,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
receivedVtq.Value.Quality.ShouldBe(Quality.Good);
}
/// <summary>
/// Confirms that a simulated data change reaches the stored per-tag subscription callback.
/// </summary>
[Fact]
public async Task DataChange_ReachesSubscriptionCallback()
{

View File

@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
/// </summary>
public class OpcUaReadToMxAccessWiringTest
{
/// <summary>
/// Confirms that the resolved OPC UA read path uses the expected full Galaxy tag reference.
/// </summary>
[Fact]
public async Task Read_ResolvesCorrectTagReference()
{

View File

@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
/// </summary>
public class OpcUaWriteToMxAccessWiringTest
{
/// <summary>
/// Confirms that the resolved OPC UA write path targets the expected Galaxy tag reference and payload.
/// </summary>
[Fact]
public async Task Write_SendsCorrectTagAndValue()
{

View File

@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
/// </summary>
public class ServiceStartupSequenceTest
{
/// <summary>
/// Confirms that startup with fake dependencies creates the expected bridge components and state.
/// </summary>
[Fact]
public void Start_WithFakes_AllComponentsCreated()
{
@@ -71,6 +74,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
}
}
/// <summary>
/// Confirms that when MXAccess is initially unavailable, the background monitor reconnects it later.
/// </summary>
[Fact]
public async Task Start_WhenMxAccessIsInitiallyDown_MonitorReconnectsInBackground()
{

View File

@@ -15,6 +15,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
/// </summary>
public class ShutdownCompletesTest
{
/// <summary>
/// Confirms that a started service can shut down within the required time budget.
/// </summary>
[Fact]
public void Shutdown_CompletesWithin30Seconds()
{