Apply code style formatting and restore partial modifiers on Avalonia views
Linter/formatter pass across the full codebase. Restores required partial keyword on AXAML code-behind classes that the formatter incorrectly removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,10 +20,7 @@ public class AlarmsCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -47,10 +44,7 @@ public class AlarmsCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -74,10 +68,7 @@ public class AlarmsCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -104,10 +95,7 @@ public class AlarmsCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -129,10 +117,7 @@ public class AlarmsCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -153,10 +138,7 @@ public class AlarmsCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -165,4 +147,4 @@ public class AlarmsCommandTests
|
||||
fakeService.DisconnectCalled.ShouldBeTrue();
|
||||
fakeService.DisposeCalled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests.Fakes;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests;
|
||||
@@ -16,9 +15,9 @@ public class BrowseCommandTests
|
||||
{
|
||||
BrowseResults = new List<BrowseResult>
|
||||
{
|
||||
new BrowseResult("ns=2;s=Obj1", "Object1", "Object", true),
|
||||
new BrowseResult("ns=2;s=Var1", "Variable1", "Variable", false),
|
||||
new BrowseResult("ns=2;s=Meth1", "Method1", "Method", false)
|
||||
new("ns=2;s=Obj1", "Object1", "Object", true),
|
||||
new("ns=2;s=Var1", "Variable1", "Variable", false),
|
||||
new("ns=2;s=Meth1", "Method1", "Method", false)
|
||||
}
|
||||
};
|
||||
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
||||
@@ -85,7 +84,7 @@ public class BrowseCommandTests
|
||||
{
|
||||
BrowseResults = new List<BrowseResult>
|
||||
{
|
||||
new BrowseResult("ns=2;s=Child", "Child", "Object", true)
|
||||
new("ns=2;s=Child", "Child", "Object", true)
|
||||
}
|
||||
};
|
||||
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
||||
@@ -143,4 +142,4 @@ public class BrowseCommandTests
|
||||
fakeService.DisconnectCalled.ShouldBeTrue();
|
||||
fakeService.DisposeCalled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using CliFx.Infrastructure;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
@@ -87,4 +86,4 @@ public class CommandBaseTests
|
||||
|
||||
fakeService.LastConnectionSettings!.FailoverUrls.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,6 @@ public class ConnectCommandTests
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
// The command should propagate the exception but still clean up.
|
||||
// Since connect fails, service is null in finally, so no disconnect.
|
||||
await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await command.ExecuteAsync(console));
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () => await command.ExecuteAsync(console));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Fake implementation of <see cref="IOpcUaClientService"/> for unit testing commands.
|
||||
/// Records all method calls and returns configurable results.
|
||||
/// Fake implementation of <see cref="IOpcUaClientService" /> for unit testing commands.
|
||||
/// Records all method calls and returns configurable results.
|
||||
/// </summary>
|
||||
public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
@@ -16,20 +16,24 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public ConnectionSettings? LastConnectionSettings { get; private set; }
|
||||
public bool DisconnectCalled { get; private set; }
|
||||
public bool DisposeCalled { get; private set; }
|
||||
public List<NodeId> ReadNodeIds { get; } = new();
|
||||
public List<(NodeId NodeId, object Value)> WriteValues { get; } = new();
|
||||
public List<NodeId?> BrowseNodeIds { get; } = new();
|
||||
public List<(NodeId NodeId, int IntervalMs)> SubscribeCalls { get; } = new();
|
||||
public List<NodeId> UnsubscribeCalls { get; } = new();
|
||||
public List<(NodeId? SourceNodeId, int IntervalMs)> SubscribeAlarmsCalls { get; } = new();
|
||||
public List<NodeId> ReadNodeIds { get; } = [];
|
||||
public List<(NodeId NodeId, object Value)> WriteValues { get; } = [];
|
||||
public List<NodeId?> BrowseNodeIds { get; } = [];
|
||||
public List<(NodeId NodeId, int IntervalMs)> SubscribeCalls { get; } = [];
|
||||
public List<NodeId> UnsubscribeCalls { get; } = [];
|
||||
public List<(NodeId? SourceNodeId, int IntervalMs)> SubscribeAlarmsCalls { get; } = [];
|
||||
public bool UnsubscribeAlarmsCalled { get; private set; }
|
||||
public bool RequestConditionRefreshCalled { get; private set; }
|
||||
public List<(NodeId NodeId, DateTime Start, DateTime End, int MaxValues)> HistoryReadRawCalls { get; } = new();
|
||||
public List<(NodeId NodeId, DateTime Start, DateTime End, AggregateType Aggregate, double IntervalMs)> HistoryReadAggregateCalls { get; } = new();
|
||||
public List<(NodeId NodeId, DateTime Start, DateTime End, int MaxValues)> HistoryReadRawCalls { get; } = [];
|
||||
|
||||
public List<(NodeId NodeId, DateTime Start, DateTime End, AggregateType Aggregate, double IntervalMs)>
|
||||
HistoryReadAggregateCalls { get; } =
|
||||
[];
|
||||
|
||||
public bool GetRedundancyInfoCalled { get; private set; }
|
||||
|
||||
// Configurable results
|
||||
public ConnectionInfo ConnectionInfoResult { get; set; } = new ConnectionInfo(
|
||||
public ConnectionInfo ConnectionInfoResult { get; set; } = new(
|
||||
"opc.tcp://localhost:4840",
|
||||
"TestServer",
|
||||
"None",
|
||||
@@ -37,7 +41,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
"session-1",
|
||||
"TestSession");
|
||||
|
||||
public DataValue ReadValueResult { get; set; } = new DataValue(
|
||||
public DataValue ReadValueResult { get; set; } = new(
|
||||
new Variant(42),
|
||||
StatusCodes.Good,
|
||||
DateTime.UtcNow,
|
||||
@@ -47,18 +51,18 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
|
||||
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = new List<BrowseResult>
|
||||
{
|
||||
new BrowseResult("ns=2;s=Node1", "Node1", "Object", true),
|
||||
new BrowseResult("ns=2;s=Node2", "Node2", "Variable", false)
|
||||
new("ns=2;s=Node1", "Node1", "Object", true),
|
||||
new("ns=2;s=Node2", "Node2", "Variable", false)
|
||||
};
|
||||
|
||||
public IReadOnlyList<DataValue> HistoryReadResult { get; set; } = new List<DataValue>
|
||||
{
|
||||
new DataValue(new Variant(10.0), StatusCodes.Good, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow),
|
||||
new DataValue(new Variant(20.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
|
||||
new(new Variant(10.0), StatusCodes.Good, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow),
|
||||
new(new Variant(20.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
|
||||
};
|
||||
|
||||
public RedundancyInfo RedundancyInfoResult { get; set; } = new RedundancyInfo(
|
||||
"Warm", 200, new[] { "urn:server1", "urn:server2" }, "urn:app:test");
|
||||
public RedundancyInfo RedundancyInfoResult { get; set; } = new(
|
||||
"Warm", 200, ["urn:server1", "urn:server2"], "urn:app:test");
|
||||
|
||||
public Exception? ConnectException { get; set; }
|
||||
public Exception? ReadException { get; set; }
|
||||
@@ -159,6 +163,11 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(RedundancyInfoResult);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeCalled = true;
|
||||
}
|
||||
|
||||
/// <summary>Raises the DataChanged event for testing subscribe commands.</summary>
|
||||
public void RaiseDataChanged(string nodeId, DataValue value)
|
||||
{
|
||||
@@ -176,9 +185,4 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(oldState, newState, endpointUrl));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeCalled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Fake factory that returns a pre-configured <see cref="FakeOpcUaClientService"/> for testing.
|
||||
/// Fake factory that returns a pre-configured <see cref="FakeOpcUaClientService" /> for testing.
|
||||
/// </summary>
|
||||
public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
{
|
||||
@@ -14,5 +14,8 @@ public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public IOpcUaClientService Create() => _service;
|
||||
}
|
||||
public IOpcUaClientService Create()
|
||||
{
|
||||
return _service;
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ public class HistoryReadCommandTests
|
||||
{
|
||||
HistoryReadResult = new List<DataValue>
|
||||
{
|
||||
new DataValue(new Variant(10.5), StatusCodes.Good, time1, time1),
|
||||
new DataValue(new Variant(20.3), StatusCodes.Good, time2, time2)
|
||||
new(new Variant(10.5), StatusCodes.Good, time1, time1),
|
||||
new(new Variant(20.3), StatusCodes.Good, time2, time2)
|
||||
}
|
||||
};
|
||||
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
||||
@@ -117,8 +117,7 @@ public class HistoryReadCommandTests
|
||||
};
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
await Should.ThrowAsync<ArgumentException>(
|
||||
async () => await command.ExecuteAsync(console));
|
||||
await Should.ThrowAsync<ArgumentException>(async () => await command.ExecuteAsync(console));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -138,4 +137,4 @@ public class HistoryReadCommandTests
|
||||
fakeService.DisconnectCalled.ShouldBeTrue();
|
||||
fakeService.DisposeCalled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,4 +84,4 @@ public class NodeIdParserTests
|
||||
result.ShouldNotBeNull();
|
||||
result.Identifier.ShouldBe("TestNode");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
// This file intentionally left empty. Real tests are in separate files.
|
||||
|
||||
|
||||
@@ -90,10 +90,9 @@ public class ReadCommandTests
|
||||
};
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
await Should.ThrowAsync<InvalidOperationException>(
|
||||
async () => await command.ExecuteAsync(console));
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () => await command.ExecuteAsync(console));
|
||||
|
||||
fakeService.DisconnectCalled.ShouldBeTrue();
|
||||
fakeService.DisposeCalled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public class RedundancyCommandTests
|
||||
var fakeService = new FakeOpcUaClientService
|
||||
{
|
||||
RedundancyInfoResult = new RedundancyInfo(
|
||||
"Hot", 250, new[] { "urn:server:primary", "urn:server:secondary" }, "urn:app:myserver")
|
||||
"Hot", 250, ["urn:server:primary", "urn:server:secondary"], "urn:app:myserver")
|
||||
};
|
||||
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
||||
var command = new RedundancyCommand(factory)
|
||||
@@ -40,7 +40,7 @@ public class RedundancyCommandTests
|
||||
var fakeService = new FakeOpcUaClientService
|
||||
{
|
||||
RedundancyInfoResult = new RedundancyInfo(
|
||||
"None", 100, Array.Empty<string>(), "urn:app:standalone")
|
||||
"None", 100, [], "urn:app:standalone")
|
||||
};
|
||||
var factory = new FakeOpcUaClientServiceFactory(fakeService);
|
||||
var command = new RedundancyCommand(factory)
|
||||
@@ -90,4 +90,4 @@ public class RedundancyCommandTests
|
||||
fakeService.DisconnectCalled.ShouldBeTrue();
|
||||
fakeService.DisposeCalled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
@@ -24,10 +23,7 @@ public class SubscribeCommandTests
|
||||
|
||||
// The subscribe command waits for cancellation. We need to cancel it.
|
||||
// Use the console's cancellation to trigger stop.
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
// Give it a moment to subscribe, then cancel
|
||||
await Task.Delay(100);
|
||||
@@ -52,10 +48,7 @@ public class SubscribeCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -77,10 +70,7 @@ public class SubscribeCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -104,10 +94,7 @@ public class SubscribeCommandTests
|
||||
|
||||
using var console = TestConsoleHelper.CreateConsole();
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
await command.ExecuteAsync(console);
|
||||
});
|
||||
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
|
||||
|
||||
await Task.Delay(100);
|
||||
console.RequestCancellation();
|
||||
@@ -117,4 +104,4 @@ public class SubscribeCommandTests
|
||||
output.ShouldContain("Subscribed to ns=2;s=TestVar (interval: 2000ms)");
|
||||
output.ShouldContain("Unsubscribed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ using CliFx.Infrastructure;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for creating CliFx <see cref="FakeInMemoryConsole"/> instances and reading their output.
|
||||
/// Helper for creating CliFx <see cref="FakeInMemoryConsole" /> instances and reading their output.
|
||||
/// </summary>
|
||||
public static class TestConsoleHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FakeInMemoryConsole"/> for testing.
|
||||
/// Creates a new <see cref="FakeInMemoryConsole" /> for testing.
|
||||
/// </summary>
|
||||
public static FakeInMemoryConsole CreateConsole()
|
||||
{
|
||||
@@ -16,7 +16,7 @@ public static class TestConsoleHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all text written to the console's standard output.
|
||||
/// Reads all text written to the console's standard output.
|
||||
/// </summary>
|
||||
public static string GetOutput(FakeInMemoryConsole console)
|
||||
{
|
||||
@@ -25,11 +25,11 @@ public static class TestConsoleHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all text written to the console's standard error.
|
||||
/// Reads all text written to the console's standard error.
|
||||
/// </summary>
|
||||
public static string GetError(FakeInMemoryConsole console)
|
||||
{
|
||||
console.Error.Flush();
|
||||
return console.ReadErrorString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,4 +100,4 @@ public class WriteCommandTests
|
||||
fakeService.DisconnectCalled.ShouldBeTrue();
|
||||
fakeService.DisposeCalled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="CliFx" Version="2.3.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="CliFx" Version="2.3.6"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.CLI\ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.CLI\ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -35,4 +35,4 @@ internal sealed class FakeApplicationConfigurationFactory : IApplicationConfigur
|
||||
|
||||
return Task.FromResult(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ internal sealed class FakeEndpointDiscovery : IEndpointDiscovery
|
||||
public int SelectCallCount { get; private set; }
|
||||
public string? LastEndpointUrl { get; private set; }
|
||||
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode)
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode)
|
||||
{
|
||||
SelectCallCount++;
|
||||
LastEndpointUrl = endpointUrl;
|
||||
@@ -31,4 +32,4 @@ internal sealed class FakeEndpointDiscovery : IEndpointDiscovery
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes;
|
||||
|
||||
internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
{
|
||||
private readonly List<FakeSubscriptionAdapter> _createdSubscriptions = [];
|
||||
private Action<bool>? _keepAliveCallback;
|
||||
private readonly List<FakeSubscriptionAdapter> _createdSubscriptions = new();
|
||||
|
||||
public bool Connected { get; set; } = true;
|
||||
public string SessionId { get; set; } = "ns=0;i=12345";
|
||||
public string SessionName { get; set; } = "FakeSession";
|
||||
public string EndpointUrl { get; set; } = "opc.tcp://localhost:4840";
|
||||
public string ServerName { get; set; } = "FakeServer";
|
||||
public string SecurityMode { get; set; } = "None";
|
||||
public string SecurityPolicyUri { get; set; } = "http://opcfoundation.org/UA/SecurityPolicy#None";
|
||||
public NamespaceTable NamespaceUris { get; set; } = new();
|
||||
|
||||
public bool Closed { get; private set; }
|
||||
public bool Disposed { get; private set; }
|
||||
@@ -35,38 +26,39 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
public bool ThrowOnWrite { get; set; }
|
||||
public bool ThrowOnBrowse { get; set; }
|
||||
|
||||
public ReferenceDescriptionCollection BrowseResponse { get; set; } = new();
|
||||
public ReferenceDescriptionCollection BrowseResponse { get; set; } = [];
|
||||
public byte[]? BrowseContinuationPoint { get; set; }
|
||||
public ReferenceDescriptionCollection BrowseNextResponse { get; set; } = new();
|
||||
public ReferenceDescriptionCollection BrowseNextResponse { get; set; } = [];
|
||||
public byte[]? BrowseNextContinuationPoint { get; set; }
|
||||
public bool HasChildrenResponse { get; set; } = false;
|
||||
|
||||
public List<DataValue> HistoryReadRawResponse { get; set; } = new();
|
||||
public List<DataValue> HistoryReadAggregateResponse { get; set; } = new();
|
||||
public List<DataValue> HistoryReadRawResponse { get; set; } = [];
|
||||
public List<DataValue> HistoryReadAggregateResponse { get; set; } = [];
|
||||
public bool ThrowOnHistoryReadRaw { get; set; }
|
||||
public bool ThrowOnHistoryReadAggregate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The next FakeSubscriptionAdapter to return from CreateSubscriptionAsync.
|
||||
/// If null, a new one is created automatically.
|
||||
/// The next FakeSubscriptionAdapter to return from CreateSubscriptionAsync.
|
||||
/// If null, a new one is created automatically.
|
||||
/// </summary>
|
||||
public FakeSubscriptionAdapter? NextSubscription { get; set; }
|
||||
|
||||
public IReadOnlyList<FakeSubscriptionAdapter> CreatedSubscriptions => _createdSubscriptions;
|
||||
|
||||
public bool Connected { get; set; } = true;
|
||||
public string SessionId { get; set; } = "ns=0;i=12345";
|
||||
public string SessionName { get; set; } = "FakeSession";
|
||||
public string EndpointUrl { get; set; } = "opc.tcp://localhost:4840";
|
||||
public string ServerName { get; set; } = "FakeServer";
|
||||
public string SecurityMode { get; set; } = "None";
|
||||
public string SecurityPolicyUri { get; set; } = "http://opcfoundation.org/UA/SecurityPolicy#None";
|
||||
public NamespaceTable NamespaceUris { get; set; } = new();
|
||||
|
||||
public void RegisterKeepAliveHandler(Action<bool> callback)
|
||||
{
|
||||
_keepAliveCallback = callback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a keep-alive event.
|
||||
/// </summary>
|
||||
public void SimulateKeepAlive(bool isGood)
|
||||
{
|
||||
_keepAliveCallback?.Invoke(isGood);
|
||||
}
|
||||
|
||||
public Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct)
|
||||
{
|
||||
ReadCount++;
|
||||
@@ -119,7 +111,8 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct)
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
HistoryReadAggregateCount++;
|
||||
if (ThrowOnHistoryReadAggregate)
|
||||
@@ -147,4 +140,12 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
Disposed = true;
|
||||
Connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a keep-alive event.
|
||||
/// </summary>
|
||||
public void SimulateKeepAlive(bool isGood)
|
||||
{
|
||||
_keepAliveCallback?.Invoke(isGood);
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes;
|
||||
|
||||
internal sealed class FakeSessionFactory : ISessionFactory
|
||||
{
|
||||
private readonly List<FakeSessionAdapter> _createdSessions = [];
|
||||
private readonly Queue<FakeSessionAdapter> _sessions = new();
|
||||
private readonly List<FakeSessionAdapter> _createdSessions = new();
|
||||
|
||||
public int CreateCallCount { get; private set; }
|
||||
public bool ThrowOnCreate { get; set; }
|
||||
public string? LastEndpointUrl { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a session adapter to be returned on the next call to CreateSessionAsync.
|
||||
/// </summary>
|
||||
public void EnqueueSession(FakeSessionAdapter session)
|
||||
{
|
||||
_sessions.Enqueue(session);
|
||||
}
|
||||
|
||||
public IReadOnlyList<FakeSessionAdapter> CreatedSessions => _createdSessions;
|
||||
|
||||
public Task<ISessionAdapter> CreateSessionAsync(
|
||||
@@ -34,11 +26,8 @@ internal sealed class FakeSessionFactory : ISessionFactory
|
||||
|
||||
FakeSessionAdapter session;
|
||||
if (_sessions.Count > 0)
|
||||
{
|
||||
session = _sessions.Dequeue();
|
||||
}
|
||||
else
|
||||
{
|
||||
session = new FakeSessionAdapter
|
||||
{
|
||||
EndpointUrl = endpoint.EndpointUrl,
|
||||
@@ -46,11 +35,18 @@ internal sealed class FakeSessionFactory : ISessionFactory
|
||||
SecurityMode = endpoint.SecurityMode.ToString(),
|
||||
SecurityPolicyUri = endpoint.SecurityPolicyUri ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure endpoint URL matches
|
||||
session.EndpointUrl = endpoint.EndpointUrl;
|
||||
_createdSessions.Add(session);
|
||||
return Task.FromResult<ISessionAdapter>(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a session adapter to be returned on the next call to CreateSessionAsync.
|
||||
/// </summary>
|
||||
public void EnqueueSession(FakeSessionAdapter session)
|
||||
{
|
||||
_sessions.Enqueue(session);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes;
|
||||
|
||||
internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
{
|
||||
private uint _nextHandle = 100;
|
||||
private readonly Dictionary<uint, (NodeId NodeId, Action<string, DataValue>? DataCallback, Action<EventFieldList>? EventCallback)> _items = new();
|
||||
private readonly
|
||||
Dictionary<uint, (NodeId NodeId, Action<string, DataValue>? DataCallback, Action<EventFieldList>? EventCallback
|
||||
)> _items = new();
|
||||
|
||||
public uint SubscriptionId { get; set; } = 42;
|
||||
private uint _nextHandle = 100;
|
||||
public bool Deleted { get; private set; }
|
||||
public bool ConditionRefreshCalled { get; private set; }
|
||||
public bool ThrowOnConditionRefresh { get; set; }
|
||||
@@ -16,7 +17,15 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
public int AddEventCount { get; private set; }
|
||||
public int RemoveCount { get; private set; }
|
||||
|
||||
public Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, Action<string, DataValue> onDataChange, CancellationToken ct)
|
||||
/// <summary>
|
||||
/// Gets the handles of all active items.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<uint> ActiveHandles => _items.Keys.ToList();
|
||||
|
||||
public uint SubscriptionId { get; set; } = 42;
|
||||
|
||||
public Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs,
|
||||
Action<string, DataValue> onDataChange, CancellationToken ct)
|
||||
{
|
||||
AddDataChangeCount++;
|
||||
var handle = _nextHandle++;
|
||||
@@ -31,7 +40,8 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter, Action<EventFieldList> onEvent, CancellationToken ct)
|
||||
public Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter,
|
||||
Action<EventFieldList> onEvent, CancellationToken ct)
|
||||
{
|
||||
AddEventCount++;
|
||||
var handle = _nextHandle++;
|
||||
@@ -60,29 +70,19 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a data change notification for testing.
|
||||
/// Simulates a data change notification for testing.
|
||||
/// </summary>
|
||||
public void SimulateDataChange(uint handle, DataValue value)
|
||||
{
|
||||
if (_items.TryGetValue(handle, out var item) && item.DataCallback != null)
|
||||
{
|
||||
item.DataCallback(item.NodeId.ToString(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates an event notification for testing.
|
||||
/// Simulates an event notification for testing.
|
||||
/// </summary>
|
||||
public void SimulateEvent(uint handle, EventFieldList eventFields)
|
||||
{
|
||||
if (_items.TryGetValue(handle, out var item) && item.EventCallback != null)
|
||||
{
|
||||
item.EventCallback(eventFields);
|
||||
}
|
||||
if (_items.TryGetValue(handle, out var item) && item.EventCallback != null) item.EventCallback(eventFields);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handles of all active items.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<uint> ActiveHandles => _items.Keys.ToList();
|
||||
}
|
||||
}
|
||||
@@ -64,4 +64,4 @@ public class AggregateTypeMapperTests
|
||||
Should.Throw<ArgumentOutOfRangeException>(() =>
|
||||
AggregateTypeMapper.ToNodeId((AggregateType)99));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,101 +10,101 @@ public class FailoverUrlParserTests
|
||||
public void Parse_CsvNull_ReturnsPrimaryOnly()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", (string?)null);
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CsvEmpty_ReturnsPrimaryOnly()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", "");
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CsvWhitespace_ReturnsPrimaryOnly()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", " ");
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SingleFailover_ReturnsBoth()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", "opc.tcp://backup:4840");
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleFailovers_ReturnsAll()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", "opc.tcp://backup1:4840,opc.tcp://backup2:4840");
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup1:4840", "opc.tcp://backup2:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup1:4840", "opc.tcp://backup2:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_TrimsWhitespace()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", " opc.tcp://backup:4840 ");
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_DeduplicatesPrimaryInFailoverList()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", "opc.tcp://primary:4840,opc.tcp://backup:4840");
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_DeduplicatesCaseInsensitive()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://Primary:4840", "opc.tcp://primary:4840");
|
||||
result.ShouldBe(new[] { "opc.tcp://Primary:4840" });
|
||||
result.ShouldBe(["opc.tcp://Primary:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayNull_ReturnsPrimaryOnly()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", (string[]?)null);
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840" });
|
||||
result.ShouldBe(["opc.tcp://primary:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayEmpty_ReturnsPrimaryOnly()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", Array.Empty<string>());
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840" });
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840", []);
|
||||
result.ShouldBe(["opc.tcp://primary:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayWithUrls_ReturnsAll()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840",
|
||||
new[] { "opc.tcp://backup1:4840", "opc.tcp://backup2:4840" });
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup1:4840", "opc.tcp://backup2:4840" });
|
||||
["opc.tcp://backup1:4840", "opc.tcp://backup2:4840"]);
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup1:4840", "opc.tcp://backup2:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayDeduplicates()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840",
|
||||
new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayTrimsWhitespace()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840",
|
||||
new[] { " opc.tcp://backup:4840 " });
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
[" opc.tcp://backup:4840 "]);
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArraySkipsNullAndEmpty()
|
||||
{
|
||||
var result = FailoverUrlParser.Parse("opc.tcp://primary:4840",
|
||||
new[] { null!, "", "opc.tcp://backup:4840" });
|
||||
result.ShouldBe(new[] { "opc.tcp://primary:4840", "opc.tcp://backup:4840" });
|
||||
[null!, "", "opc.tcp://backup:4840"]);
|
||||
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,4 @@ public class SecurityModeMapperTests
|
||||
{
|
||||
SecurityModeMapper.FromString(null!).ShouldBe(SecurityMode.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,4 +107,4 @@ public class ValueConverterTests
|
||||
{
|
||||
Should.Throw<OverflowException>(() => ValueConverter.ConvertValue("256", (byte)0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,4 +92,4 @@ public class ConnectionSettingsTests
|
||||
};
|
||||
Should.NotThrow(() => settings.Validate());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Models;
|
||||
|
||||
@@ -10,7 +11,7 @@ public class ModelConstructionTests
|
||||
[Fact]
|
||||
public void BrowseResult_ConstructsCorrectly()
|
||||
{
|
||||
var result = new ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult("ns=2;s=MyNode", "MyNode", "Variable", true);
|
||||
var result = new BrowseResult("ns=2;s=MyNode", "MyNode", "Variable", true);
|
||||
|
||||
result.NodeId.ShouldBe("ns=2;s=MyNode");
|
||||
result.DisplayName.ShouldBe("MyNode");
|
||||
@@ -21,7 +22,7 @@ public class ModelConstructionTests
|
||||
[Fact]
|
||||
public void BrowseResult_WithoutChildren()
|
||||
{
|
||||
var result = new ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult("ns=2;s=Leaf", "Leaf", "Variable", false);
|
||||
var result = new BrowseResult("ns=2;s=Leaf", "Leaf", "Variable", false);
|
||||
result.HasChildren.ShouldBeFalse();
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ public class ModelConstructionTests
|
||||
[Fact]
|
||||
public void RedundancyInfo_WithEmptyUris()
|
||||
{
|
||||
var info = new RedundancyInfo("None", 0, Array.Empty<string>(), string.Empty);
|
||||
var info = new RedundancyInfo("None", 0, [], string.Empty);
|
||||
info.ServerUris.ShouldBeEmpty();
|
||||
info.ApplicationUri.ShouldBeEmpty();
|
||||
}
|
||||
@@ -126,4 +127,4 @@ public class ModelConstructionTests
|
||||
{
|
||||
Enum.GetValues<AggregateType>().Length.ShouldBe(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
private readonly FakeApplicationConfigurationFactory _configFactory = new();
|
||||
private readonly FakeEndpointDiscovery _endpointDiscovery = new();
|
||||
private readonly FakeSessionFactory _sessionFactory = new();
|
||||
private readonly OpcUaClientService _service;
|
||||
private readonly FakeSessionFactory _sessionFactory = new();
|
||||
|
||||
public OpcUaClientServiceTests()
|
||||
{
|
||||
@@ -23,11 +23,14 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.Dispose();
|
||||
}
|
||||
|
||||
private ConnectionSettings ValidSettings(string url = "opc.tcp://localhost:4840") => new()
|
||||
private ConnectionSettings ValidSettings(string url = "opc.tcp://localhost:4840")
|
||||
{
|
||||
EndpointUrl = url,
|
||||
SessionTimeoutSeconds = 60
|
||||
};
|
||||
return new ConnectionSettings
|
||||
{
|
||||
EndpointUrl = url,
|
||||
SessionTimeoutSeconds = 60
|
||||
};
|
||||
}
|
||||
|
||||
// --- Connection tests ---
|
||||
|
||||
@@ -243,15 +246,15 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
var session = new FakeSessionAdapter
|
||||
{
|
||||
BrowseResponse = new ReferenceDescriptionCollection
|
||||
{
|
||||
BrowseResponse =
|
||||
[
|
||||
new ReferenceDescription
|
||||
{
|
||||
NodeId = new ExpandedNodeId("ns=2;s=Child1"),
|
||||
DisplayName = new LocalizedText("Child1"),
|
||||
NodeClass = NodeClass.Variable
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
await _service.ConnectAsync(ValidSettings());
|
||||
@@ -269,12 +272,12 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
var session = new FakeSessionAdapter
|
||||
{
|
||||
BrowseResponse = new ReferenceDescriptionCollection()
|
||||
BrowseResponse = []
|
||||
};
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
await _service.ConnectAsync(ValidSettings());
|
||||
|
||||
await _service.BrowseAsync(null);
|
||||
await _service.BrowseAsync();
|
||||
|
||||
session.BrowseCount.ShouldBe(1);
|
||||
}
|
||||
@@ -284,15 +287,15 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
var session = new FakeSessionAdapter
|
||||
{
|
||||
BrowseResponse = new ReferenceDescriptionCollection
|
||||
{
|
||||
BrowseResponse =
|
||||
[
|
||||
new ReferenceDescription
|
||||
{
|
||||
NodeId = new ExpandedNodeId("ns=2;s=Folder1"),
|
||||
DisplayName = new LocalizedText("Folder1"),
|
||||
NodeClass = NodeClass.Object
|
||||
}
|
||||
},
|
||||
],
|
||||
HasChildrenResponse = true
|
||||
};
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
@@ -309,25 +312,25 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
var session = new FakeSessionAdapter
|
||||
{
|
||||
BrowseResponse = new ReferenceDescriptionCollection
|
||||
{
|
||||
BrowseResponse =
|
||||
[
|
||||
new ReferenceDescription
|
||||
{
|
||||
NodeId = new ExpandedNodeId("ns=2;s=A"),
|
||||
DisplayName = new LocalizedText("A"),
|
||||
NodeClass = NodeClass.Variable
|
||||
}
|
||||
},
|
||||
BrowseContinuationPoint = new byte[] { 1, 2, 3 },
|
||||
BrowseNextResponse = new ReferenceDescriptionCollection
|
||||
{
|
||||
],
|
||||
BrowseContinuationPoint = [1, 2, 3],
|
||||
BrowseNextResponse =
|
||||
[
|
||||
new ReferenceDescription
|
||||
{
|
||||
NodeId = new ExpandedNodeId("ns=2;s=B"),
|
||||
DisplayName = new LocalizedText("B"),
|
||||
NodeClass = NodeClass.Variable
|
||||
}
|
||||
},
|
||||
],
|
||||
BrowseNextContinuationPoint = null
|
||||
};
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
@@ -436,7 +439,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
await _service.ConnectAsync(ValidSettings());
|
||||
|
||||
await _service.SubscribeAlarmsAsync(null, 1000);
|
||||
await _service.SubscribeAlarmsAsync();
|
||||
|
||||
session.CreatedSubscriptions.Count.ShouldBe(1);
|
||||
session.CreatedSubscriptions[0].AddEventCount.ShouldBe(1);
|
||||
@@ -472,21 +475,21 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
var handle = fakeSub.ActiveHandles.First();
|
||||
var fields = new EventFieldList
|
||||
{
|
||||
EventFields = new VariantCollection
|
||||
{
|
||||
new Variant(new byte[] { 1, 2, 3 }), // 0: EventId
|
||||
new Variant(ObjectTypeIds.AlarmConditionType), // 1: EventType
|
||||
new Variant("Source1"), // 2: SourceName
|
||||
new Variant(DateTime.UtcNow), // 3: Time
|
||||
new Variant(new LocalizedText("High temp")), // 4: Message
|
||||
new Variant((ushort)500), // 5: Severity
|
||||
new Variant("HighTemp"), // 6: ConditionName
|
||||
new Variant(true), // 7: Retain
|
||||
new Variant(false), // 8: AckedState
|
||||
new Variant(true), // 9: ActiveState
|
||||
new Variant(true), // 10: EnabledState
|
||||
new Variant(false) // 11: SuppressedOrShelved
|
||||
}
|
||||
EventFields =
|
||||
[
|
||||
new Variant(new byte[] { 1, 2, 3 }), // 0: EventId
|
||||
new Variant(ObjectTypeIds.AlarmConditionType), // 1: EventType
|
||||
new Variant("Source1"), // 2: SourceName
|
||||
new Variant(DateTime.UtcNow), // 3: Time
|
||||
new Variant(new LocalizedText("High temp")), // 4: Message
|
||||
new Variant((ushort)500), // 5: Severity
|
||||
new Variant("HighTemp"), // 6: ConditionName
|
||||
new Variant(true), // 7: Retain
|
||||
new Variant(false), // 8: AckedState
|
||||
new Variant(true), // 9: ActiveState
|
||||
new Variant(true), // 10: EnabledState
|
||||
new Variant(false)
|
||||
]
|
||||
};
|
||||
fakeSub.SimulateEvent(handle, fields);
|
||||
|
||||
@@ -563,8 +566,8 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
var expectedValues = new List<DataValue>
|
||||
{
|
||||
new DataValue(new Variant(1.0), StatusCodes.Good),
|
||||
new DataValue(new Variant(2.0), StatusCodes.Good)
|
||||
new(new Variant(1.0), StatusCodes.Good),
|
||||
new(new Variant(2.0), StatusCodes.Good)
|
||||
};
|
||||
var session = new FakeSessionAdapter { HistoryReadRawResponse = expectedValues };
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
@@ -600,7 +603,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
var expectedValues = new List<DataValue>
|
||||
{
|
||||
new DataValue(new Variant(1.5), StatusCodes.Good)
|
||||
new(new Variant(1.5), StatusCodes.Good)
|
||||
};
|
||||
var session = new FakeSessionAdapter { HistoryReadAggregateResponse = expectedValues };
|
||||
_sessionFactory.EnqueueSession(session);
|
||||
@@ -650,9 +653,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
if (nodeId == VariableIds.Server_ServiceLevel)
|
||||
return new DataValue(new Variant((byte)200), StatusCodes.Good);
|
||||
if (nodeId == VariableIds.Server_ServerRedundancy_ServerUriArray)
|
||||
return new DataValue(new Variant(new[] { "urn:server1", "urn:server2" }), StatusCodes.Good);
|
||||
return new DataValue(new Variant(["urn:server1", "urn:server2"]), StatusCodes.Good);
|
||||
if (nodeId == VariableIds.Server_ServerArray)
|
||||
return new DataValue(new Variant(new[] { "urn:server1" }), StatusCodes.Good);
|
||||
return new DataValue(new Variant(["urn:server1"]), StatusCodes.Good);
|
||||
return new DataValue(StatusCodes.BadNodeIdUnknown);
|
||||
}
|
||||
};
|
||||
@@ -663,14 +666,14 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
info.Mode.ShouldBe("Warm");
|
||||
info.ServiceLevel.ShouldBe((byte)200);
|
||||
info.ServerUris.ShouldBe(new[] { "urn:server1", "urn:server2" });
|
||||
info.ServerUris.ShouldBe(["urn:server1", "urn:server2"]);
|
||||
info.ApplicationUri.ShouldBe("urn:server1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_MissingOptionalArrays_ReturnsGracefully()
|
||||
{
|
||||
int readCallIndex = 0;
|
||||
var readCallIndex = 0;
|
||||
var session = new FakeSessionAdapter
|
||||
{
|
||||
ReadResponseFunc = nodeId =>
|
||||
@@ -712,7 +715,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_sessionFactory.EnqueueSession(session2);
|
||||
|
||||
var settings = ValidSettings("opc.tcp://primary:4840");
|
||||
settings.FailoverUrls = new[] { "opc.tcp://backup:4840" };
|
||||
settings.FailoverUrls = ["opc.tcp://backup:4840"];
|
||||
|
||||
var stateChanges = new List<ConnectionStateChangedEventArgs>();
|
||||
_service.ConnectionStateChanged += (_, e) => stateChanges.Add(e);
|
||||
@@ -728,7 +731,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
// Should have reconnected
|
||||
stateChanges.ShouldContain(e => e.NewState == ConnectionState.Reconnecting);
|
||||
stateChanges.ShouldContain(e => e.NewState == ConnectionState.Connected &&
|
||||
e.EndpointUrl == "opc.tcp://backup:4840");
|
||||
e.EndpointUrl == "opc.tcp://backup:4840");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -744,7 +747,7 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_sessionFactory.EnqueueSession(session2);
|
||||
|
||||
var settings = ValidSettings("opc.tcp://primary:4840");
|
||||
settings.FailoverUrls = new[] { "opc.tcp://backup:4840" };
|
||||
settings.FailoverUrls = ["opc.tcp://backup:4840"];
|
||||
|
||||
await _service.ConnectAsync(settings);
|
||||
session1.SimulateKeepAlive(false);
|
||||
@@ -813,4 +816,4 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
service.ShouldBeAssignableTo<IOpcUaClientService>();
|
||||
service.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.Shared\ZB.MOM.WW.LmxOpcUa.Client.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.Shared\ZB.MOM.WW.LmxOpcUa.Client.Shared.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -138,4 +138,4 @@ public class AlarmsViewModelTests
|
||||
{
|
||||
_vm.Interval.ShouldBe(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests;
|
||||
|
||||
public class BrowseTreeViewModelTests
|
||||
{
|
||||
private readonly FakeOpcUaClientService _service;
|
||||
private readonly SynchronousUiDispatcher _dispatcher;
|
||||
private readonly FakeOpcUaClientService _service;
|
||||
private readonly BrowseTreeViewModel _vm;
|
||||
|
||||
public BrowseTreeViewModelTests()
|
||||
{
|
||||
_service = new FakeOpcUaClientService
|
||||
{
|
||||
BrowseResults = new[]
|
||||
{
|
||||
BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Node1", "Node1", "Object", true),
|
||||
new BrowseResult("ns=2;s=Node2", "Node2", "Variable", false)
|
||||
}
|
||||
]
|
||||
};
|
||||
_dispatcher = new SynchronousUiDispatcher();
|
||||
_vm = new BrowseTreeViewModel(_service, _dispatcher);
|
||||
@@ -79,18 +79,18 @@ public class BrowseTreeViewModelTests
|
||||
[Fact]
|
||||
public async Task TreeNode_FirstExpand_TriggersChildBrowse()
|
||||
{
|
||||
_service.BrowseResults = new[]
|
||||
{
|
||||
_service.BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Parent", "Parent", "Object", true)
|
||||
};
|
||||
];
|
||||
|
||||
await _vm.LoadRootsAsync();
|
||||
|
||||
// Reset browse results for child browse
|
||||
_service.BrowseResults = new[]
|
||||
{
|
||||
_service.BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Child1", "Child1", "Variable", false)
|
||||
};
|
||||
];
|
||||
|
||||
var parent = _vm.RootNodes[0];
|
||||
var initialBrowseCount = _service.BrowseCallCount;
|
||||
@@ -108,17 +108,17 @@ public class BrowseTreeViewModelTests
|
||||
[Fact]
|
||||
public async Task TreeNode_SecondExpand_DoesNotBrowseAgain()
|
||||
{
|
||||
_service.BrowseResults = new[]
|
||||
{
|
||||
_service.BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Parent", "Parent", "Object", true)
|
||||
};
|
||||
];
|
||||
|
||||
await _vm.LoadRootsAsync();
|
||||
|
||||
_service.BrowseResults = new[]
|
||||
{
|
||||
_service.BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Child1", "Child1", "Variable", false)
|
||||
};
|
||||
];
|
||||
|
||||
var parent = _vm.RootNodes[0];
|
||||
parent.IsExpanded = true;
|
||||
@@ -136,14 +136,14 @@ public class BrowseTreeViewModelTests
|
||||
[Fact]
|
||||
public async Task TreeNode_IsLoading_TransitionsDuringBrowse()
|
||||
{
|
||||
_service.BrowseResults = new[]
|
||||
{
|
||||
_service.BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Parent", "Parent", "Object", true)
|
||||
};
|
||||
];
|
||||
|
||||
await _vm.LoadRootsAsync();
|
||||
|
||||
_service.BrowseResults = Array.Empty<BrowseResult>();
|
||||
_service.BrowseResults = [];
|
||||
|
||||
var parent = _vm.RootNodes[0];
|
||||
parent.IsExpanded = true;
|
||||
@@ -152,4 +152,4 @@ public class BrowseTreeViewModelTests
|
||||
// After completion, IsLoading should be false
|
||||
parent.IsLoading.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,30 @@ using Opc.Ua;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
using ConnectionState = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.ConnectionState;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Fake IOpcUaClientService for unit testing.
|
||||
/// Fake IOpcUaClientService for unit testing.
|
||||
/// </summary>
|
||||
public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
// Configurable responses
|
||||
public ConnectionInfo? ConnectResult { get; set; }
|
||||
public Exception? ConnectException { get; set; }
|
||||
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = Array.Empty<BrowseResult>();
|
||||
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = [];
|
||||
public Exception? BrowseException { get; set; }
|
||||
public DataValue ReadResult { get; set; } = new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow);
|
||||
|
||||
public DataValue ReadResult { get; set; } =
|
||||
new(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow);
|
||||
|
||||
public Exception? ReadException { get; set; }
|
||||
public StatusCode WriteResult { get; set; } = StatusCodes.Good;
|
||||
public Exception? WriteException { get; set; }
|
||||
public RedundancyInfo? RedundancyResult { get; set; }
|
||||
public Exception? RedundancyException { get; set; }
|
||||
public IReadOnlyList<DataValue> HistoryRawResult { get; set; } = Array.Empty<DataValue>();
|
||||
public IReadOnlyList<DataValue> HistoryAggregateResult { get; set; } = Array.Empty<DataValue>();
|
||||
public IReadOnlyList<DataValue> HistoryRawResult { get; set; } = [];
|
||||
public IReadOnlyList<DataValue> HistoryAggregateResult { get; set; } = [];
|
||||
public Exception? HistoryException { get; set; }
|
||||
|
||||
// Call tracking
|
||||
@@ -134,14 +136,16 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues = 1000, CancellationToken ct = default)
|
||||
{
|
||||
HistoryReadRawCallCount++;
|
||||
if (HistoryException != null) throw HistoryException;
|
||||
return Task.FromResult(HistoryRawResult);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default)
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default)
|
||||
{
|
||||
HistoryReadAggregateCallCount++;
|
||||
LastAggregateType = aggregate;
|
||||
@@ -156,13 +160,24 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(RedundancyResult!);
|
||||
}
|
||||
|
||||
// Methods to raise events from tests
|
||||
public void RaiseDataChanged(DataChangedEventArgs args) => DataChanged?.Invoke(this, args);
|
||||
public void RaiseAlarmEvent(AlarmEventArgs args) => AlarmEvent?.Invoke(this, args);
|
||||
public void RaiseConnectionStateChanged(ConnectionStateChangedEventArgs args) => ConnectionStateChanged?.Invoke(this, args);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No-op for testing
|
||||
}
|
||||
}
|
||||
|
||||
// Methods to raise events from tests
|
||||
public void RaiseDataChanged(DataChangedEventArgs args)
|
||||
{
|
||||
DataChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
public void RaiseAlarmEvent(AlarmEventArgs args)
|
||||
{
|
||||
AlarmEvent?.Invoke(this, args);
|
||||
}
|
||||
|
||||
public void RaiseConnectionStateChanged(ConnectionStateChangedEventArgs args)
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, args);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Fake factory that returns a preconfigured FakeOpcUaClientService.
|
||||
/// Fake factory that returns a preconfigured FakeOpcUaClientService.
|
||||
/// </summary>
|
||||
public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
{
|
||||
@@ -14,5 +14,8 @@ public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public IOpcUaClientService Create() => _service;
|
||||
}
|
||||
public IOpcUaClientService Create()
|
||||
{
|
||||
return _service;
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,15 @@ public class HistoryViewModelTests
|
||||
{
|
||||
_service = new FakeOpcUaClientService
|
||||
{
|
||||
HistoryRawResult = new[]
|
||||
{
|
||||
HistoryRawResult =
|
||||
[
|
||||
new DataValue(new Variant(10), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow),
|
||||
new DataValue(new Variant(20), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
|
||||
},
|
||||
HistoryAggregateResult = new[]
|
||||
{
|
||||
],
|
||||
HistoryAggregateResult =
|
||||
[
|
||||
new DataValue(new Variant(15.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
|
||||
}
|
||||
]
|
||||
};
|
||||
var dispatcher = new SynchronousUiDispatcher();
|
||||
_vm = new HistoryViewModel(_service, dispatcher);
|
||||
@@ -157,4 +157,4 @@ public class HistoryViewModelTests
|
||||
_vm.Results.Count.ShouldBe(1);
|
||||
_vm.Results[0].Value.ShouldContain("History not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,11 @@ public class MainWindowViewModelTests
|
||||
"http://opcfoundation.org/UA/SecurityPolicy#None",
|
||||
"session-1",
|
||||
"TestSession"),
|
||||
BrowseResults = new[]
|
||||
{
|
||||
BrowseResults =
|
||||
[
|
||||
new BrowseResult("ns=2;s=Root", "Root", "Object", true)
|
||||
},
|
||||
RedundancyResult = new RedundancyInfo("None", 200, new[] { "urn:test" }, "urn:test")
|
||||
],
|
||||
RedundancyResult = new RedundancyInfo("None", 200, ["urn:test"], "urn:test")
|
||||
};
|
||||
|
||||
var factory = new FakeOpcUaClientServiceFactory(_service);
|
||||
@@ -123,7 +123,8 @@ public class MainWindowViewModelTests
|
||||
public void ConnectionStateChangedEvent_UpdatesState()
|
||||
{
|
||||
_service.RaiseConnectionStateChanged(
|
||||
new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Reconnecting, "opc.tcp://localhost:4840"));
|
||||
new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Reconnecting,
|
||||
"opc.tcp://localhost:4840"));
|
||||
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Reconnecting);
|
||||
_vm.StatusMessage.ShouldBe("Reconnecting...");
|
||||
@@ -182,7 +183,8 @@ public class MainWindowViewModelTests
|
||||
_vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!);
|
||||
|
||||
_service.RaiseConnectionStateChanged(
|
||||
new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Connected, "opc.tcp://localhost:4840"));
|
||||
new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Connected,
|
||||
"opc.tcp://localhost:4840"));
|
||||
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.IsConnected));
|
||||
@@ -325,4 +327,4 @@ public class MainWindowViewModelTests
|
||||
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,4 +149,4 @@ public class ReadWriteViewModelTests
|
||||
_vm.SelectedNodeId = null;
|
||||
_vm.IsNodeSelected.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,4 +168,4 @@ public class SubscriptionsViewModelTests
|
||||
_vm.ActiveSubscriptions.ShouldBeEmpty();
|
||||
_service.SubscribeCallCount.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.UI.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.UI.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.UI\ZB.MOM.WW.LmxOpcUa.Client.UI.csproj" />
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.Shared\ZB.MOM.WW.LmxOpcUa.Client.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.UI\ZB.MOM.WW.LmxOpcUa.Client.UI.csproj"/>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Client.Shared\ZB.MOM.WW.LmxOpcUa.Client.Shared.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shouldly;
|
||||
@@ -9,19 +8,20 @@ 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.
|
||||
/// 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.
|
||||
/// 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()
|
||||
.AddJsonFile("appsettings.test.json", optional: false)
|
||||
.AddJsonFile("appsettings.test.json", false)
|
||||
.Build();
|
||||
|
||||
var config = new GalaxyRepositoryConfiguration();
|
||||
@@ -31,12 +31,12 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the standard attribute query returns rows from the repository.
|
||||
/// Confirms that the standard attribute query returns rows from the repository.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_StandardMode_ReturnsRows()
|
||||
{
|
||||
var config = LoadConfig(extendedAttributes: false);
|
||||
var config = LoadConfig(false);
|
||||
var service = new GalaxyRepositoryService(config);
|
||||
|
||||
var results = await service.GetAttributesAsync();
|
||||
@@ -47,13 +47,13 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended attribute query returns more rows than the standard query path.
|
||||
/// Confirms that the extended attribute query returns more rows than the standard query path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_ReturnsMoreRows()
|
||||
{
|
||||
var standardConfig = LoadConfig(extendedAttributes: false);
|
||||
var extendedConfig = LoadConfig(extendedAttributes: true);
|
||||
var standardConfig = LoadConfig(false);
|
||||
var extendedConfig = LoadConfig(true);
|
||||
var standardService = new GalaxyRepositoryService(standardConfig);
|
||||
var extendedService = new GalaxyRepositoryService(extendedConfig);
|
||||
|
||||
@@ -64,12 +64,12 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended attribute query includes both primitive and dynamic attribute sources.
|
||||
/// Confirms that the extended attribute query includes both primitive and dynamic attribute sources.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_IncludesPrimitiveAttributes()
|
||||
{
|
||||
var config = LoadConfig(extendedAttributes: true);
|
||||
var config = LoadConfig(true);
|
||||
var service = new GalaxyRepositoryService(config);
|
||||
|
||||
var results = await service.GetAttributesAsync();
|
||||
@@ -79,12 +79,12 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended mode populates attribute-source metadata across the result set.
|
||||
/// Confirms that extended mode populates attribute-source metadata across the result set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_PrimitiveNamePopulated()
|
||||
{
|
||||
var config = LoadConfig(extendedAttributes: true);
|
||||
var config = LoadConfig(true);
|
||||
var service = new GalaxyRepositoryService(config);
|
||||
|
||||
var results = await service.GetAttributesAsync();
|
||||
@@ -97,12 +97,12 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that standard-mode results always include fully qualified tag references.
|
||||
/// Confirms that standard-mode results always include fully qualified tag references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_StandardMode_AllHaveFullTagReference()
|
||||
{
|
||||
var config = LoadConfig(extendedAttributes: false);
|
||||
var config = LoadConfig(false);
|
||||
var service = new GalaxyRepositoryService(config);
|
||||
|
||||
var results = await service.GetAttributesAsync();
|
||||
@@ -112,12 +112,12 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-mode results always include fully qualified tag references.
|
||||
/// Confirms that extended-mode results always include fully qualified tag references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetAttributesAsync_ExtendedMode_AllHaveFullTagReference()
|
||||
{
|
||||
var config = LoadConfig(extendedAttributes: true);
|
||||
var config = LoadConfig(true);
|
||||
var service = new GalaxyRepositoryService(config);
|
||||
|
||||
var results = await service.GetAttributesAsync();
|
||||
@@ -126,4 +126,4 @@ namespace ZB.MOM.WW.LmxOpcUa.IntegrationTests
|
||||
results.ShouldAllBe(r => r.FullTagReference.Contains("."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.IntegrationTests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.IntegrationTests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit" Version="2.9.3"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.2.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Host\ZB.MOM.WW.LmxOpcUa.Host.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Host\ZB.MOM.WW.LmxOpcUa.Host.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.test.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.test.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
@@ -53,10 +54,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
{
|
||||
provider.ValidateCredentials("readonly", "readonly123").ShouldBeTrue();
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
// GLAuth not running - skip gracefully
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +70,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
{
|
||||
provider.ValidateCredentials("readonly", "wrongpassword").ShouldBeFalse();
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return; // GLAuth not running
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +85,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
{
|
||||
provider.ValidateCredentials("nonexistent", "anything").ShouldBeFalse();
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return; // GLAuth not running
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +104,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
roles.ShouldNotContain("WriteOperate");
|
||||
roles.ShouldNotContain("AlarmAck");
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return; // GLAuth not running
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,9 +122,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
roles.ShouldContain("WriteOperate");
|
||||
roles.ShouldNotContain("AlarmAck");
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return; // GLAuth not running
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,9 +140,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
roles.ShouldContain("AlarmAck");
|
||||
roles.ShouldNotContain("WriteOperate");
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return; // GLAuth not running
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +161,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
roles.ShouldContain("WriteConfigure");
|
||||
roles.ShouldContain("AlarmAck");
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return; // GLAuth not running
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,4 +228,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Authentication
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -6,18 +7,19 @@ 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.
|
||||
/// 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.
|
||||
/// 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()
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.AddJsonFile("appsettings.json", false)
|
||||
.Build();
|
||||
|
||||
var config = new AppConfiguration();
|
||||
@@ -30,7 +32,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
|
||||
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUa_Section_BindsCorrectly()
|
||||
@@ -46,7 +48,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
|
||||
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MxAccess_Section_BindsCorrectly()
|
||||
@@ -62,7 +64,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
|
||||
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_Section_BindsCorrectly()
|
||||
@@ -75,7 +77,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-attribute loading defaults to disabled when not configured.
|
||||
/// Confirms that extended-attribute loading defaults to disabled when not configured.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_DefaultsFalse()
|
||||
@@ -85,14 +87,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
|
||||
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_BindsFromJson()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair<string, string>("GalaxyRepository:ExtendedAttributes", "true") })
|
||||
.AddJsonFile("appsettings.json", false)
|
||||
.AddInMemoryCollection(new[]
|
||||
{ new KeyValuePair<string, string>("GalaxyRepository:ExtendedAttributes", "true") })
|
||||
.Build();
|
||||
|
||||
var config = new GalaxyRepositoryConfiguration();
|
||||
@@ -101,7 +104,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
|
||||
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dashboard_Section_BindsCorrectly()
|
||||
@@ -113,7 +116,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the default configuration objects start with the expected bridge defaults.
|
||||
/// Confirms that the default configuration objects start with the expected bridge defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
@@ -127,7 +130,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that BindAddress can be overridden to a specific hostname or IP.
|
||||
/// Confirms that BindAddress can be overridden to a specific hostname or IP.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUa_BindAddress_CanBeOverridden()
|
||||
@@ -135,7 +138,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("OpcUa:BindAddress", "localhost"),
|
||||
new KeyValuePair<string, string>("OpcUa:BindAddress", "localhost")
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -145,7 +148,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a valid configuration passes startup validation.
|
||||
/// Confirms that a valid configuration passes startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_ValidConfig_ReturnsTrue()
|
||||
@@ -155,7 +158,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an invalid OPC UA port is rejected by startup validation.
|
||||
/// Confirms that an invalid OPC UA port is rejected by startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_InvalidPort_ReturnsFalse()
|
||||
@@ -166,7 +169,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
|
||||
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_EmptyGalaxyName_ReturnsFalse()
|
||||
@@ -177,7 +180,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Security section binds profile list from appsettings.json.
|
||||
/// Confirms that the Security section binds profile list from appsettings.json.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Security_Section_BindsProfilesCorrectly()
|
||||
@@ -189,7 +192,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a minimum key size below 2048 is rejected by the validator.
|
||||
/// Confirms that a minimum key size below 2048 is rejected by the validator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_InvalidMinKeySize_ReturnsFalse()
|
||||
@@ -200,7 +203,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a valid configuration with security defaults passes validation.
|
||||
/// Confirms that a valid configuration with security defaults passes validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_DefaultSecurityConfig_ReturnsTrue()
|
||||
@@ -210,7 +213,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that custom security profiles can be bound from in-memory configuration.
|
||||
/// Confirms that custom security profiles can be bound from in-memory configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Security_Section_BindsCustomProfiles()
|
||||
@@ -218,10 +221,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Security:Profiles:0", "None"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Security:Profiles:1", "Basic256Sha256-SignAndEncrypt"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Security:AutoAcceptClientCertificates", "false"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Security:MinimumCertificateKeySize", "4096"),
|
||||
new KeyValuePair<string, string>("Security:Profiles:0", "None"),
|
||||
new KeyValuePair<string, string>("Security:Profiles:1", "Basic256Sha256-SignAndEncrypt"),
|
||||
new KeyValuePair<string, string>("Security:AutoAcceptClientCertificates", "false"),
|
||||
new KeyValuePair<string, string>("Security:MinimumCertificateKeySize", "4096")
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -253,12 +256,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Redundancy:Enabled", "true"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Redundancy:Mode", "Hot"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Redundancy:Role", "Secondary"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Redundancy:ServiceLevelBase", "180"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Redundancy:ServerUris:0", "urn:a"),
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("Redundancy:ServerUris:1", "urn:b"),
|
||||
new KeyValuePair<string, string>("Redundancy:Enabled", "true"),
|
||||
new KeyValuePair<string, string>("Redundancy:Mode", "Hot"),
|
||||
new KeyValuePair<string, string>("Redundancy:Role", "Secondary"),
|
||||
new KeyValuePair<string, string>("Redundancy:ServiceLevelBase", "180"),
|
||||
new KeyValuePair<string, string>("Redundancy:ServerUris:0", "urn:a"),
|
||||
new KeyValuePair<string, string>("Redundancy:ServerUris:1", "urn:b")
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -304,7 +307,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new System.Collections.Generic.KeyValuePair<string, string>("OpcUa:ApplicationUri", "urn:test:app"),
|
||||
new KeyValuePair<string, string>("OpcUa:ApplicationUri", "urn:test:app")
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -313,4 +316,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
||||
config.ApplicationUri.ShouldBe("urn:test:app");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that a default attribute metadata object starts with empty strings for its text fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreEmpty()
|
||||
@@ -28,7 +28,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that primitive-name and attribute-source fields can be populated for extended metadata rows.
|
||||
/// Confirms that primitive-name and attribute-source fields can be populated for extended metadata rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedFields_CanBeSet()
|
||||
@@ -43,7 +43,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that standard attribute rows leave the extended metadata fields empty.
|
||||
/// Confirms that standard attribute rows leave the extended metadata fields empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StandardAttributes_HaveEmptyExtendedFields()
|
||||
@@ -60,4 +60,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
info.AttributeSource.ShouldBe("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,35 +6,35 @@ 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.
|
||||
/// 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.
|
||||
/// 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
|
||||
[InlineData(3, 10u)] // Float
|
||||
[InlineData(4, 11u)] // Double
|
||||
[InlineData(5, 12u)] // String
|
||||
[InlineData(6, 13u)] // DateTime
|
||||
[InlineData(7, 11u)] // ElapsedTime → Double
|
||||
[InlineData(8, 12u)] // Reference → String
|
||||
[InlineData(13, 6u)] // Enumeration → Int32
|
||||
[InlineData(14, 12u)] // Custom → String
|
||||
[InlineData(15, 21u)] // InternationalizedString → LocalizedText
|
||||
[InlineData(16, 12u)] // Custom → String
|
||||
[InlineData(1, 1u)] // Boolean
|
||||
[InlineData(2, 6u)] // Integer → Int32
|
||||
[InlineData(3, 10u)] // Float
|
||||
[InlineData(4, 11u)] // Double
|
||||
[InlineData(5, 12u)] // String
|
||||
[InlineData(6, 13u)] // DateTime
|
||||
[InlineData(7, 11u)] // ElapsedTime → Double
|
||||
[InlineData(8, 12u)] // Reference → String
|
||||
[InlineData(13, 6u)] // Enumeration → Int32
|
||||
[InlineData(14, 12u)] // Custom → String
|
||||
[InlineData(15, 21u)] // InternationalizedString → LocalizedText
|
||||
[InlineData(16, 12u)] // Custom → String
|
||||
public void MapToOpcUaDataType_AllKnownTypes(int mxDataType, uint expectedNodeId)
|
||||
{
|
||||
MxDataTypeMapper.MapToOpcUaDataType(mxDataType).ShouldBe(expectedNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX data types default to the OPC UA string data type.
|
||||
/// 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]
|
||||
@@ -47,7 +47,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that known MX data types map to the expected CLR runtime types.
|
||||
/// 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>
|
||||
@@ -68,7 +68,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX data types default to the CLR string type.
|
||||
/// Confirms that unknown MX data types default to the CLR string type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToClrType_UnknownDefaultsToString()
|
||||
@@ -77,7 +77,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean MX type reports the expected OPC UA type name.
|
||||
/// Confirms that the boolean MX type reports the expected OPC UA type name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetOpcUaTypeName_Boolean()
|
||||
@@ -86,7 +86,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MX types report the fallback OPC UA type name of string.
|
||||
/// Confirms that unknown MX types report the fallback OPC UA type name of string.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetOpcUaTypeName_Unknown_ReturnsString()
|
||||
@@ -94,4 +94,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxDataTypeMapper.GetOpcUaTypeName(999).ShouldBe("String");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ 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.
|
||||
/// 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.
|
||||
/// 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>
|
||||
@@ -27,7 +27,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MXAccess error codes are reported as unknown while preserving the numeric code.
|
||||
/// Confirms that unknown MXAccess error codes are reported as unknown while preserving the numeric code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetMessage_UnknownCode_ReturnsUnknown()
|
||||
@@ -37,7 +37,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that known MXAccess error codes map to the expected bridge quality values.
|
||||
/// 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>
|
||||
@@ -54,7 +54,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown MXAccess error codes map to the generic bad quality bucket.
|
||||
/// Confirms that unknown MXAccess error codes map to the generic bad quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToQuality_UnknownCode_ReturnsBad()
|
||||
@@ -62,4 +62,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
MxErrorCodes.MapToQuality(9999).ShouldBe(Quality.Bad);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ 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.
|
||||
/// 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.
|
||||
/// 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>
|
||||
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// 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>
|
||||
@@ -39,7 +39,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good-family MXAccess quality values map to the expected bridge quality values.
|
||||
/// 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>
|
||||
@@ -52,7 +52,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown bad-family values collapse to the generic bad quality bucket.
|
||||
/// Confirms that unknown bad-family values collapse to the generic bad quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownBadValue_ReturnsBad()
|
||||
@@ -61,7 +61,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown uncertain-family values collapse to the generic uncertain quality bucket.
|
||||
/// Confirms that unknown uncertain-family values collapse to the generic uncertain quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownUncertainValue_ReturnsUncertain()
|
||||
@@ -70,7 +70,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown good-family values collapse to the generic good quality bucket.
|
||||
/// Confirms that unknown good-family values collapse to the generic good quality bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapFromMxAccess_UnknownGoodValue_ReturnsGood()
|
||||
@@ -79,7 +79,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic good quality maps to the OPC UA good status code.
|
||||
/// Confirms that the generic good quality maps to the OPC UA good status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Good_Returns0()
|
||||
@@ -88,7 +88,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic bad quality maps to the OPC UA bad status code.
|
||||
/// Confirms that the generic bad quality maps to the OPC UA bad status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Bad_Returns80000000()
|
||||
@@ -97,7 +97,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that communication failures map to the OPC UA bad communication-failure status code.
|
||||
/// Confirms that communication failures map to the OPC UA bad communication-failure status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_BadCommFailure()
|
||||
@@ -106,7 +106,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generic uncertain quality maps to the OPC UA uncertain status code.
|
||||
/// Confirms that the generic uncertain quality maps to the OPC UA uncertain status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapToOpcUa_Uncertain()
|
||||
@@ -115,7 +115,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good quality values are classified correctly by the quality extension helpers.
|
||||
/// Confirms that good quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsGood()
|
||||
@@ -126,7 +126,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad quality values are classified correctly by the quality extension helpers.
|
||||
/// Confirms that bad quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsBad()
|
||||
@@ -136,7 +136,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain quality values are classified correctly by the quality extension helpers.
|
||||
/// Confirms that uncertain quality values are classified correctly by the quality extension helpers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QualityExtensions_IsUncertain()
|
||||
@@ -146,4 +146,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
Quality.Uncertain.IsBad().ShouldBe(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,22 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
public class SecurityClassificationMapperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that Galaxy classifications intended for operator and engineering writes remain writable through OPC UA.
|
||||
/// Verifies that Galaxy classifications intended for operator and engineering writes remain writable through OPC UA.
|
||||
/// </summary>
|
||||
/// <param name="classification">The Galaxy security classification value being evaluated for write access.</param>
|
||||
/// <param name="expected">The expected writable result for the supplied Galaxy classification.</param>
|
||||
[Theory]
|
||||
[InlineData(0, true)] // FreeAccess
|
||||
[InlineData(1, true)] // Operate
|
||||
[InlineData(4, true)] // Tune
|
||||
[InlineData(5, true)] // Configure
|
||||
[InlineData(0, true)] // FreeAccess
|
||||
[InlineData(1, true)] // Operate
|
||||
[InlineData(4, true)] // Tune
|
||||
[InlineData(5, true)] // Configure
|
||||
public void Writable_SecurityLevels(int classification, bool expected)
|
||||
{
|
||||
SecurityClassificationMapper.IsWritable(classification).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that secured or view-only Galaxy classifications are exposed as read-only attributes.
|
||||
/// Verifies that secured or view-only Galaxy classifications are exposed as read-only attributes.
|
||||
/// </summary>
|
||||
/// <param name="classification">The Galaxy security classification value expected to block writes.</param>
|
||||
/// <param name="expected">The expected writable result for the supplied read-only Galaxy classification.</param>
|
||||
@@ -36,9 +36,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that unknown security classifications do not accidentally block writes for unmapped Galaxy values.
|
||||
/// Verifies that unknown security classifications do not accidentally block writes for unmapped Galaxy values.
|
||||
/// </summary>
|
||||
/// <param name="classification">An unmapped Galaxy security classification value that should fall back to writable behavior.</param>
|
||||
/// <param name="classification">
|
||||
/// An unmapped Galaxy security classification value that should fall back to writable
|
||||
/// behavior.
|
||||
/// </param>
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(7)]
|
||||
@@ -48,4 +51,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Domain
|
||||
SecurityClassificationMapper.IsWritable(classification).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
{
|
||||
/// <summary>
|
||||
/// THE ULTIMATE SMOKE TEST: Full service with fakes, verifying the complete data flow.
|
||||
/// (1) Address space built, (2) MXAccess data change → callback, (3) read → correct tag ref,
|
||||
/// (4) write → correct tag+value, (5) dashboard has real data.
|
||||
/// THE ULTIMATE SMOKE TEST: Full service with fakes, verifying the complete data flow.
|
||||
/// (1) Address space built, (2) MXAccess data change → callback, (3) read → correct tag ref,
|
||||
/// (4) write → correct tag+value, (5) dashboard has real data.
|
||||
/// </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.
|
||||
/// 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()
|
||||
@@ -26,7 +26,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
var config = new AppConfiguration
|
||||
{
|
||||
OpcUa = new OpcUaConfiguration { Port = 14842, GalaxyName = "TestGalaxy", EndpointPath = "/LmxOpcUa" },
|
||||
MxAccess = new MxAccessConfiguration { ClientName = "Test", ReadTimeoutSeconds = 2, WriteTimeoutSeconds = 2 },
|
||||
MxAccess = new MxAccessConfiguration
|
||||
{ ClientName = "Test", ReadTimeoutSeconds = 2, WriteTimeoutSeconds = 2 },
|
||||
GalaxyRepository = new GalaxyRepositoryConfiguration { ChangeDetectionIntervalSeconds = 60 },
|
||||
Dashboard = new DashboardConfiguration { Enabled = false }
|
||||
};
|
||||
@@ -36,15 +37,35 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 1, IsArea = false },
|
||||
new GalaxyObjectInfo { GobjectId = 3, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 2, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true },
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 1,
|
||||
IsArea = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver",
|
||||
BrowseName = "DelmiaReceiver", ParentGobjectId = 2, IsArea = false
|
||||
}
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 2, TagName = "TestMachine_001", AttributeName = "MachineID", FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 3, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 3, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber", FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "TestMachine_001", AttributeName = "MachineID",
|
||||
FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath",
|
||||
FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber",
|
||||
FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,7 +83,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
service.MxClient!.State.ShouldBe(ConnectionState.Connected);
|
||||
|
||||
// (3) Address space model can be built from the same data
|
||||
var model = Host.OpcUa.AddressSpaceBuilder.Build(repo.Hierarchy, repo.Attributes);
|
||||
var model = AddressSpaceBuilder.Build(repo.Hierarchy, repo.Attributes);
|
||||
model.NodeIdToTagReference.ContainsKey("TestMachine_001.MachineID").ShouldBe(true);
|
||||
model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.DownloadPath").ShouldBe(true);
|
||||
model.NodeIdToTagReference.ContainsKey("DelmiaReceiver_001.JobStepNumber").ShouldBe(true);
|
||||
@@ -103,4 +124,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.EndToEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,18 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that the first poll always triggers an initial rebuild notification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FirstPoll_AlwaysTriggers()
|
||||
{
|
||||
var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) };
|
||||
var service = new ChangeDetectionService(repo, intervalSeconds: 1);
|
||||
var service = new ChangeDetectionService(repo, 1);
|
||||
var triggered = false;
|
||||
service.OnGalaxyChanged += () => triggered = true;
|
||||
|
||||
@@ -33,13 +33,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated polls with the same deploy timestamp do not retrigger rebuilds.
|
||||
/// Confirms that repeated polls with the same deploy timestamp do not retrigger rebuilds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SameTimestamp_DoesNotTriggerAgain()
|
||||
{
|
||||
var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) };
|
||||
var service = new ChangeDetectionService(repo, intervalSeconds: 1);
|
||||
var service = new ChangeDetectionService(repo, 1);
|
||||
var triggerCount = 0;
|
||||
service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount);
|
||||
|
||||
@@ -52,13 +52,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a changed deploy timestamp triggers another rebuild notification.
|
||||
/// Confirms that a changed deploy timestamp triggers another rebuild notification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangedTimestamp_TriggersAgain()
|
||||
{
|
||||
var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) };
|
||||
var service = new ChangeDetectionService(repo, intervalSeconds: 1);
|
||||
var service = new ChangeDetectionService(repo, 1);
|
||||
var triggerCount = 0;
|
||||
service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount);
|
||||
|
||||
@@ -75,13 +75,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transient polling failures do not crash the service and allow later recovery.
|
||||
/// Confirms that transient polling failures do not crash the service and allow later recovery.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FailedPoll_DoesNotCrash_RetriesNext()
|
||||
{
|
||||
var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) };
|
||||
var service = new ChangeDetectionService(repo, intervalSeconds: 1);
|
||||
var service = new ChangeDetectionService(repo, 1);
|
||||
var triggerCount = 0;
|
||||
service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount);
|
||||
|
||||
@@ -104,15 +104,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that stopping the service before it starts is a harmless no-op.
|
||||
/// Confirms that stopping the service before it starts is a harmless no-op.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stop_BeforeStart_DoesNotThrow()
|
||||
{
|
||||
var repo = new FakeGalaxyRepository();
|
||||
var service = new ChangeDetectionService(repo, intervalSeconds: 30);
|
||||
var service = new ChangeDetectionService(repo, 30);
|
||||
service.Stop(); // Should not throw
|
||||
service.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,19 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Deterministic authentication provider for integration tests.
|
||||
/// Validates credentials against hardcoded username/password pairs
|
||||
/// and returns configured role sets per user.
|
||||
/// Deterministic authentication provider for integration tests.
|
||||
/// Validates credentials against hardcoded username/password pairs
|
||||
/// and returns configured role sets per user.
|
||||
/// </summary>
|
||||
internal class FakeAuthenticationProvider : IUserAuthenticationProvider, IRoleProvider
|
||||
{
|
||||
private readonly Dictionary<string, string> _credentials =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, IReadOnlyList<string>> _roles =
|
||||
new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _credentials = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public FakeAuthenticationProvider AddUser(string username, string password, params string[] roles)
|
||||
private readonly Dictionary<string, IReadOnlyList<string>> _roles = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IReadOnlyList<string> GetUserRoles(string username)
|
||||
{
|
||||
_credentials[username] = password;
|
||||
_roles[username] = roles;
|
||||
return this;
|
||||
return _roles.TryGetValue(username, out var roles) ? roles : new[] { AppRoles.ReadOnly };
|
||||
}
|
||||
|
||||
public bool ValidateCredentials(string username, string password)
|
||||
@@ -28,9 +25,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
return _credentials.TryGetValue(username, out var expected) && expected == password;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetUserRoles(string username)
|
||||
public FakeAuthenticationProvider AddUser(string username, string password, params string[] roles)
|
||||
{
|
||||
return _roles.TryGetValue(username, out var roles) ? roles : new[] { AppRoles.ReadOnly };
|
||||
_credentials[username] = password;
|
||||
_roles[username] = roles;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,42 +7,43 @@ 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.
|
||||
/// 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.
|
||||
/// Gets or sets the hierarchy rows returned to address-space construction logic.
|
||||
/// </summary>
|
||||
public event Action? OnGalaxyChanged;
|
||||
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hierarchy rows returned to address-space construction logic.
|
||||
/// Gets or sets the attribute rows returned to address-space construction logic.
|
||||
/// </summary>
|
||||
public List<GalaxyObjectInfo> Hierarchy { get; set; } = new List<GalaxyObjectInfo>();
|
||||
public List<GalaxyAttributeInfo> Attributes { get; set; } = new();
|
||||
|
||||
/// <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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// Occurs when the fake repository simulates a Galaxy deploy change.
|
||||
/// </summary>
|
||||
public event Action? OnGalaxyChanged;
|
||||
|
||||
/// <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>
|
||||
@@ -53,7 +54,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured attribute rows or throws to simulate a repository failure.
|
||||
/// 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>
|
||||
@@ -64,7 +65,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured deploy timestamp or throws to simulate a repository failure.
|
||||
/// 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>
|
||||
@@ -75,7 +76,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the configured connection result or throws to simulate a repository failure.
|
||||
/// 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>
|
||||
@@ -86,8 +87,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the deploy-change event so tests can trigger rebuild logic.
|
||||
/// Raises the deploy-change event so tests can trigger rebuild logic.
|
||||
/// </summary>
|
||||
public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke();
|
||||
public void RaiseGalaxyChanged()
|
||||
{
|
||||
OnGalaxyChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,54 +8,56 @@ 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.
|
||||
/// 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;
|
||||
private readonly ConcurrentDictionary<string, Action<string, Vtq>> _subscriptions =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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;
|
||||
|
||||
/// <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)
|
||||
@@ -65,7 +67,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates disconnecting from the runtime.
|
||||
/// Simulates disconnecting from the runtime.
|
||||
/// </summary>
|
||||
public Task DisconnectAsync()
|
||||
{
|
||||
@@ -74,7 +76,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a subscription callback so later simulated data changes can target it.
|
||||
/// 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>
|
||||
@@ -85,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a stored subscription callback for the specified tag reference.
|
||||
/// 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)
|
||||
@@ -95,7 +97,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current in-memory VTQ for a tag reference or a bad-quality placeholder when none has been seeded.
|
||||
/// 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>
|
||||
@@ -108,7 +110,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a write request, optionally updates the in-memory tag table, and returns the configured write result.
|
||||
/// 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>
|
||||
@@ -123,7 +125,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a simulated tag-value change to both the event stream and any stored subscription callback.
|
||||
/// Releases the fake client. No unmanaged resources are held.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -135,7 +144,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a simulated connection-state transition for health and reconnect tests.
|
||||
/// 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>
|
||||
@@ -144,10 +153,5 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
State = curr;
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(prev, curr));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the fake client. No unmanaged resources are held.
|
||||
/// </summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,77 +8,76 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Fake IMxProxy for testing without the MxAccess COM runtime.
|
||||
/// Simulates connections, subscriptions, data changes, and writes.
|
||||
/// Fake IMxProxy for testing without the MxAccess COM runtime.
|
||||
/// Simulates connections, subscriptions, data changes, and writes.
|
||||
/// </summary>
|
||||
public class FakeMxProxy : IMxProxy
|
||||
{
|
||||
private int _nextHandle = 1;
|
||||
private int _connectionHandle;
|
||||
private bool _registered;
|
||||
private int _nextHandle = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test.
|
||||
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
|
||||
/// </summary>
|
||||
public event MxDataChangeHandler? OnDataChange;
|
||||
public ConcurrentDictionary<int, string> Items { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test.
|
||||
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
|
||||
/// </summary>
|
||||
public event MxWriteCompleteHandler? OnWriteComplete;
|
||||
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime.
|
||||
/// Gets the values written through the fake runtime so write scenarios can assert the final payload.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, string> Items { get; } = new ConcurrentDictionary<int, string>();
|
||||
public List<(string Address, object Value)> WrittenValues { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item handles currently marked as advised so tests can assert subscription behavior.
|
||||
/// Gets a value indicating whether the fake runtime is currently considered registered.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<int, bool> AdvisedItems { get; } = new ConcurrentDictionary<int, bool>();
|
||||
public bool IsRegistered { get; private set; }
|
||||
|
||||
/// <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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test.
|
||||
/// </summary>
|
||||
public event MxDataChangeHandler? OnDataChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test.
|
||||
/// </summary>
|
||||
public event MxWriteCompleteHandler? OnWriteComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the MXAccess registration handshake and returns a synthetic connection handle.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client name supplied by the code under test.</param>
|
||||
/// <returns>A synthetic connection handle for subsequent fake operations.</returns>
|
||||
@@ -86,24 +85,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
RegisterCallCount++;
|
||||
if (ShouldFailRegister) throw new InvalidOperationException("Register failed (simulated)");
|
||||
_registered = true;
|
||||
IsRegistered = true;
|
||||
_connectionHandle = Interlocked.Increment(ref _nextHandle);
|
||||
return _connectionHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates tearing down the fake MXAccess connection.
|
||||
/// 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++;
|
||||
_registered = false;
|
||||
IsRegistered = false;
|
||||
_connectionHandle = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates resolving a tag reference into a fake runtime item handle.
|
||||
/// 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>
|
||||
@@ -116,7 +115,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates removing an item from the fake runtime session.
|
||||
/// 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>
|
||||
@@ -126,7 +125,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item as actively advised so tests can assert subscription activation.
|
||||
/// 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>
|
||||
@@ -136,7 +135,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an item as no longer advised so tests can assert subscription teardown.
|
||||
/// 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>
|
||||
@@ -146,7 +145,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a runtime write, records the written value, and optionally raises the write-complete callback.
|
||||
/// 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>
|
||||
@@ -170,12 +169,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
status[0].success = 0;
|
||||
status[0].detail = (short)WriteCompleteStatus;
|
||||
}
|
||||
|
||||
if (!SkipWriteCompleteCallback)
|
||||
OnWriteComplete?.Invoke(_connectionHandle, itemHandle, ref status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates an MXAccess data change event for a specific item handle.
|
||||
/// 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>
|
||||
@@ -186,26 +186,25 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
var status = new MXSTATUS_PROXY[1];
|
||||
status[0].success = 1;
|
||||
OnDataChange?.Invoke(_connectionHandle, itemHandle, value, quality,
|
||||
(object)(timestamp ?? DateTime.UtcNow), ref status);
|
||||
timestamp ?? DateTime.UtcNow, ref status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates data change for a specific address (finds handle by address).
|
||||
/// 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)
|
||||
public void SimulateDataChangeByAddress(string address, object value, int quality = 192,
|
||||
DateTime? timestamp = null)
|
||||
{
|
||||
foreach (var kvp in Items)
|
||||
{
|
||||
if (string.Equals(kvp.Value, address, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SimulateDataChange(kvp.Key, value, quality, timestamp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,57 +8,24 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// xUnit fixture that manages an OpcUaService lifecycle with automatic port allocation.
|
||||
/// Guarantees no port conflicts between parallel tests.
|
||||
///
|
||||
/// Usage (per-test):
|
||||
/// var fixture = OpcUaServerFixture.WithFakes();
|
||||
/// await fixture.InitializeAsync();
|
||||
/// try { ... } finally { await fixture.DisposeAsync(); }
|
||||
///
|
||||
/// Usage (skip COM entirely):
|
||||
/// var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
|
||||
/// xUnit fixture that manages an OpcUaService lifecycle with automatic port allocation.
|
||||
/// Guarantees no port conflicts between parallel tests.
|
||||
/// Usage (per-test):
|
||||
/// var fixture = OpcUaServerFixture.WithFakes();
|
||||
/// await fixture.InitializeAsync();
|
||||
/// try { ... } finally { await fixture.DisposeAsync(); }
|
||||
/// Usage (skip COM entirely):
|
||||
/// var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
|
||||
/// </summary>
|
||||
internal class OpcUaServerFixture : IAsyncLifetime
|
||||
{
|
||||
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>
|
||||
/// The fake Galaxy repository injected into the service. Mutate Hierarchy/Attributes
|
||||
/// then call Service.TriggerRebuild() to simulate a Galaxy redeployment.
|
||||
/// </summary>
|
||||
public FakeGalaxyRepository? GalaxyRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The fake MxAccess client injected into the service (when using WithFakeMxAccessClient).
|
||||
/// </summary>
|
||||
public FakeMxAccessClient? MxAccessClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The fake MxProxy injected into the service (when using WithFakes).
|
||||
/// </summary>
|
||||
public FakeMxProxy? MxProxy { get; }
|
||||
|
||||
private readonly OpcUaServiceBuilder _builder;
|
||||
private bool _started;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a fixture around a prepared service builder and optional fake dependencies.
|
||||
/// 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>
|
||||
@@ -79,8 +46,68 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates fixture with FakeMxProxy + FakeGalaxyRepository (standard test data).
|
||||
/// The STA thread and COM interop run against FakeMxProxy.
|
||||
/// 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>
|
||||
/// The fake Galaxy repository injected into the service. Mutate Hierarchy/Attributes
|
||||
/// then call Service.TriggerRebuild() to simulate a Galaxy redeployment.
|
||||
/// </summary>
|
||||
public FakeGalaxyRepository? GalaxyRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The fake MxAccess client injected into the service (when using WithFakeMxAccessClient).
|
||||
/// </summary>
|
||||
public FakeMxAccessClient? MxAccessClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The fake MxProxy injected into the service (when using WithFakes).
|
||||
/// </summary>
|
||||
public FakeMxProxy? MxProxy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Builds and starts the OPC UA service for the current fixture.
|
||||
/// </summary>
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
Service = _builder.Build();
|
||||
Service.Start();
|
||||
_started = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the OPC UA service when the fixture had previously been started.
|
||||
/// </summary>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
if (_started)
|
||||
try
|
||||
{
|
||||
Service.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* swallow cleanup errors */
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
@@ -101,12 +128,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
.WithGalaxyRepository(r)
|
||||
.WithGalaxyName("TestGalaxy");
|
||||
|
||||
return new OpcUaServerFixture(builder, repo: r, mxProxy: p);
|
||||
return new OpcUaServerFixture(builder, r, mxProxy: p);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates fixture using FakeMxAccessClient directly — skips STA thread + COM entirely.
|
||||
/// Fastest option for tests that don't need real COM interop.
|
||||
/// 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>
|
||||
@@ -150,31 +177,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
if (authProvider != null)
|
||||
builder.WithAuthProvider(authProvider);
|
||||
|
||||
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();
|
||||
Service.Start();
|
||||
_started = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the OPC UA service when the fixture had previously been started.
|
||||
/// </summary>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
try { Service.Stop(); }
|
||||
catch { /* swallow cleanup errors */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
return new OpcUaServerFixture(builder, r, client);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that the standard fake-backed fixture starts the bridge and tears it down cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakes_StartsAndStops()
|
||||
@@ -32,7 +32,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
|
||||
/// Confirms that the fake-client fixture bypasses COM wiring and uses the provided fake runtime client.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakeMxAccessClient_SkipsCom()
|
||||
@@ -48,7 +48,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
|
||||
/// Confirms that separate fixture instances automatically allocate unique OPC UA ports.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleFixtures_GetUniquePortsAutomatically()
|
||||
@@ -70,7 +70,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
|
||||
/// Confirms that fixture shutdown completes quickly enough for the integration test suite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Shutdown_CompletesWithin30Seconds()
|
||||
@@ -86,7 +86,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime callbacks arriving after shutdown are ignored cleanly.
|
||||
/// Confirms that runtime callbacks arriving after shutdown are ignored cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Stop_UnhooksNodeManagerFromMxAccessCallbacks()
|
||||
@@ -101,7 +101,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
|
||||
/// Confirms that the fake-backed fixture builds the seeded address space and Galaxy statistics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WithFakes_BuildsAddressSpace()
|
||||
@@ -116,4 +116,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,40 @@ using Opc.Ua.Configuration;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// OPC UA client helper for integration tests. Connects to a test server,
|
||||
/// browses, reads, and subscribes to nodes programmatically.
|
||||
/// OPC UA client helper for integration tests. Connects to a test server,
|
||||
/// browses, reads, and subscribes to nodes programmatically.
|
||||
/// </summary>
|
||||
internal class OpcUaTestClient : IDisposable
|
||||
{
|
||||
private Session? _session;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
|
||||
/// 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").
|
||||
/// Closes the test session and releases OPC UA client resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_session != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_session.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
_session.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -35,7 +55,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
|
||||
/// 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>
|
||||
@@ -46,7 +66,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
|
||||
/// 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>
|
||||
/// <param name="securityMode">The requested message security mode (default: None).</param>
|
||||
@@ -115,7 +135,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
var endpointConfig = EndpointConfiguration.Create(config);
|
||||
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);
|
||||
|
||||
UserIdentity identity = username != null
|
||||
var identity = username != null
|
||||
? new UserIdentity(username, password ?? "")
|
||||
: new UserIdentity();
|
||||
|
||||
@@ -130,30 +150,26 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
var endpoints = client.GetEndpoints(null);
|
||||
|
||||
foreach (var ep in endpoints)
|
||||
{
|
||||
if (ep.SecurityMode == mode && ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)
|
||||
{
|
||||
ep.EndpointUrl = endpointUrl;
|
||||
return ep;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to any matching mode
|
||||
foreach (var ep in endpoints)
|
||||
{
|
||||
if (ep.SecurityMode == mode)
|
||||
{
|
||||
ep.EndpointUrl = endpointUrl;
|
||||
return ep;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No endpoint with security mode {mode} found on {endpointUrl}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
|
||||
/// 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>
|
||||
@@ -170,14 +186,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
|
||||
var refs = browser.Browse(nodeId);
|
||||
foreach (var rd in refs)
|
||||
{
|
||||
results.Add((rd.DisplayName.Text, ExpandedNodeId.ToNodeId(rd.NodeId, Session.NamespaceUris), rd.NodeClass));
|
||||
}
|
||||
results.Add((rd.DisplayName.Text, ExpandedNodeId.ToNodeId(rd.NodeId, Session.NamespaceUris),
|
||||
rd.NodeClass));
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a node's value.
|
||||
/// 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>
|
||||
@@ -187,7 +202,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a specific OPC UA attribute from a node.
|
||||
/// Read a specific OPC UA attribute from a node.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose attribute should be read.</param>
|
||||
/// <param name="attributeId">The OPC UA attribute identifier to read.</param>
|
||||
@@ -215,8 +230,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a node's value, optionally using an OPC UA index range for array element writes.
|
||||
/// Returns the server status code for the write.
|
||||
/// 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>
|
||||
@@ -240,8 +255,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a subscription with a monitored item on the given node.
|
||||
/// Returns the subscription and monitored item for inspection.
|
||||
/// 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>
|
||||
@@ -268,18 +283,5 @@ 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)
|
||||
{
|
||||
try { _session.Close(); }
|
||||
catch { /* ignore */ }
|
||||
_session.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,65 +4,117 @@ using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Reusable test data matching the Galaxy hierarchy from gr/layout.md.
|
||||
/// Reusable test data matching the Galaxy hierarchy from gr/layout.md.
|
||||
/// </summary>
|
||||
public static class TestData
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the standard Galaxy hierarchy used by integration and wiring tests.
|
||||
/// 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>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea", ParentGobjectId = 1, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false },
|
||||
new GalaxyObjectInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false },
|
||||
new GalaxyObjectInfo { GobjectId = 5, TagName = "MESReceiver_001", ContainedName = "MESReceiver", BrowseName = "MESReceiver", ParentGobjectId = 3, IsArea = false },
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0,
|
||||
IsArea = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea",
|
||||
ParentGobjectId = 1, IsArea = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001",
|
||||
BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver",
|
||||
BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 5, TagName = "MESReceiver_001", ContainedName = "MESReceiver",
|
||||
BrowseName = "MESReceiver", ParentGobjectId = 3, IsArea = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the standard attribute set used by integration and wiring tests.
|
||||
/// 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>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID", FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineCode", FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber", FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInBatchID", FullTagReference = "MESReceiver_001.MoveInBatchID", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInPartNumbers", FullTagReference = "MESReceiver_001.MoveInPartNumbers[]", MxDataType = 5, IsArray = true, ArrayDimension = 50 },
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID",
|
||||
FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineCode",
|
||||
FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath",
|
||||
FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber",
|
||||
FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInBatchID",
|
||||
FullTagReference = "MESReceiver_001.MoveInBatchID", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 5, TagName = "MESReceiver_001", AttributeName = "MoveInPartNumbers",
|
||||
FullTagReference = "MESReceiver_001.MoveInPartNumbers[]", MxDataType = 5, IsArray = true,
|
||||
ArrayDimension = 50
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal hierarchy containing a single object for focused unit tests.
|
||||
/// 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>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal attribute set containing a single scalar attribute for focused unit tests.
|
||||
/// 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>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr", FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr",
|
||||
FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Historian
|
||||
public class HistorianQualityMappingTests
|
||||
{
|
||||
private static StatusCode MapHistorianQuality(byte quality)
|
||||
=> QualityMapper.MapToOpcUaStatusCode(QualityMapper.MapFromMxAccessQuality(quality));
|
||||
{
|
||||
return QualityMapper.MapToOpcUaStatusCode(QualityMapper.MapFromMxAccessQuality(quality));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(192)] // Quality.Good
|
||||
@@ -19,10 +21,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Historian
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(64)] // Quality.Uncertain
|
||||
[InlineData(68)] // Quality.UncertainLastUsable
|
||||
[InlineData(80)] // Quality.UncertainSensorNotAccurate
|
||||
[InlineData(88)] // Quality.UncertainSubNormal
|
||||
[InlineData(64)] // Quality.Uncertain
|
||||
[InlineData(68)] // Quality.UncertainLastUsable
|
||||
[InlineData(80)] // Quality.UncertainSensorNotAccurate
|
||||
[InlineData(88)] // Quality.UncertainSubNormal
|
||||
[InlineData(128)] // Uncertain range (no exact enum match)
|
||||
public void UncertainQualityRange_MapsToUncertain(byte quality)
|
||||
{
|
||||
@@ -30,15 +32,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Historian
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // Quality.Bad
|
||||
[InlineData(1)] // Bad range
|
||||
[InlineData(4)] // Quality.BadConfigError
|
||||
[InlineData(8)] // Quality.BadNotConnected
|
||||
[InlineData(20)] // Quality.BadCommFailure
|
||||
[InlineData(50)] // Bad range (no exact enum match)
|
||||
[InlineData(0)] // Quality.Bad
|
||||
[InlineData(1)] // Bad range
|
||||
[InlineData(4)] // Quality.BadConfigError
|
||||
[InlineData(8)] // Quality.BadNotConnected
|
||||
[InlineData(20)] // Quality.BadCommFailure
|
||||
[InlineData(50)] // Bad range (no exact enum match)
|
||||
public void BadQualityRange_MapsToBad(byte quality)
|
||||
{
|
||||
StatusCode.IsBad(MapHistorianQuality(quality)).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,23 +16,54 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
|
||||
}
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "FreeAttr", FullTagReference = "TestObj.FreeAttr", MxDataType = 5, SecurityClassification = 0 },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "OperateAttr", FullTagReference = "TestObj.OperateAttr", MxDataType = 5, SecurityClassification = 1 },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "SecuredAttr", FullTagReference = "TestObj.SecuredAttr", MxDataType = 5, SecurityClassification = 2 },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "VerifiedAttr", FullTagReference = "TestObj.VerifiedAttr", MxDataType = 5, SecurityClassification = 3 },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TuneAttr", FullTagReference = "TestObj.TuneAttr", MxDataType = 5, SecurityClassification = 4 },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "ConfigAttr", FullTagReference = "TestObj.ConfigAttr", MxDataType = 5, SecurityClassification = 5 },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "ViewOnlyAttr", FullTagReference = "TestObj.ViewOnlyAttr", MxDataType = 5, SecurityClassification = 6 },
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "FreeAttr",
|
||||
FullTagReference = "TestObj.FreeAttr", MxDataType = 5, SecurityClassification = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "OperateAttr",
|
||||
FullTagReference = "TestObj.OperateAttr", MxDataType = 5, SecurityClassification = 1
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "SecuredAttr",
|
||||
FullTagReference = "TestObj.SecuredAttr", MxDataType = 5, SecurityClassification = 2
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "VerifiedAttr",
|
||||
FullTagReference = "TestObj.VerifiedAttr", MxDataType = 5, SecurityClassification = 3
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "TuneAttr",
|
||||
FullTagReference = "TestObj.TuneAttr", MxDataType = 5, SecurityClassification = 4
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "ConfigAttr",
|
||||
FullTagReference = "TestObj.ConfigAttr", MxDataType = 5, SecurityClassification = 5
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "ViewOnlyAttr",
|
||||
FullTagReference = "TestObj.ViewOnlyAttr", MxDataType = 5, SecurityClassification = 6
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that writable Galaxy security classifications publish OPC UA variables with read-write access.
|
||||
/// Verifies that writable Galaxy security classifications publish OPC UA variables with read-write access.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadWriteAttribute_HasCurrentReadOrWrite_AccessLevel()
|
||||
@@ -52,11 +83,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
$"{attrName} should be ReadWrite");
|
||||
}
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that secured and view-only Galaxy classifications publish OPC UA variables with read-only access.
|
||||
/// Verifies that secured and view-only Galaxy classifications publish OPC UA variables with read-only access.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadOnlyAttribute_HasCurrentRead_AccessLevel()
|
||||
@@ -76,11 +110,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
$"{attrName} should be ReadOnly");
|
||||
}
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the bridge rejects writes against Galaxy attributes whose security classification is read-only.
|
||||
/// Verifies that the bridge rejects writes against Galaxy attributes whose security classification is read-only.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_ToReadOnlyAttribute_IsRejected()
|
||||
@@ -96,17 +133,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var result = client.Write(nodeId, "test");
|
||||
StatusCode.IsBad(result).ShouldBeTrue("Write to ReadOnly attribute should be rejected");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that writes succeed for Galaxy attributes whose security classification permits operator updates.
|
||||
/// Verifies that writes succeed for Galaxy attributes whose security classification permits operator updates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_ToReadWriteAttribute_Succeeds()
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient: mxClient, repo: CreateRepoWithSecurityLevels());
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient, CreateRepoWithSecurityLevels());
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
@@ -117,7 +157,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var result = client.Write(nodeId, "test");
|
||||
StatusCode.IsGood(result).ShouldBeTrue("Write to ReadWrite attribute should succeed");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
@@ -11,13 +9,13 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests verifying dynamic address space changes via a real OPC UA client.
|
||||
/// Tests browse, subscribe, add/remove nodes at runtime, and subscription quality changes.
|
||||
/// Integration tests verifying dynamic address space changes via a real OPC UA client.
|
||||
/// Tests browse, subscribe, add/remove nodes at runtime, and subscription quality changes.
|
||||
/// </summary>
|
||||
public class AddressSpaceRebuildTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
|
||||
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_ReturnsInitialHierarchy()
|
||||
@@ -42,7 +40,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
|
||||
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AfterAddingObject_NewNodeAppears()
|
||||
@@ -88,7 +86,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
|
||||
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AfterRemovingObject_NodeDisappears()
|
||||
@@ -124,7 +122,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
|
||||
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_RemovedNode_PublishesBadQuality()
|
||||
@@ -138,7 +136,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
// Subscribe to an attribute that will be removed
|
||||
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInBatchID");
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
|
||||
|
||||
// Collect notifications
|
||||
var notifications = new List<MonitoredItemNotification>();
|
||||
@@ -173,7 +171,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
|
||||
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_SurvivingNode_StillWorksAfterRebuild()
|
||||
@@ -187,7 +185,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
// Subscribe to an attribute that will survive the rebuild
|
||||
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
@@ -212,7 +210,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
|
||||
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_AddAttribute_NewVariableAppears()
|
||||
@@ -249,7 +247,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
|
||||
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Browse_RemoveAttribute_VariableDisappears()
|
||||
@@ -266,8 +264,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
initialChildren.ShouldContain(c => c.Name == "MachineCode");
|
||||
|
||||
// Remove MachineCode attribute
|
||||
fixture.GalaxyRepository!.Attributes.RemoveAll(
|
||||
a => a.TagName == "TestMachine_001" && a.AttributeName == "MachineCode");
|
||||
fixture.GalaxyRepository!.Attributes.RemoveAll(a =>
|
||||
a.TagName == "TestMachine_001" && a.AttributeName == "MachineCode");
|
||||
|
||||
fixture.Service.TriggerRebuild();
|
||||
await Task.Delay(500);
|
||||
@@ -282,7 +280,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
|
||||
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Rebuild_PreservesSubscriptionBookkeeping_ForSurvivingNodes()
|
||||
@@ -312,7 +310,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transferred monitored items recreate MXAccess subscriptions when the service has no local subscription state.
|
||||
/// Confirms that transferred monitored items recreate MXAccess subscriptions when the service has no local
|
||||
/// subscription state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TransferSubscriptions_RestoresMxAccessSubscriptionState_WhenLocalStateIsMissing()
|
||||
@@ -347,7 +346,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transferring monitored items does not double-count subscriptions already tracked in memory.
|
||||
/// Confirms that transferring monitored items does not double-count subscriptions already tracked in memory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TransferSubscriptions_DoesNotDoubleCount_WhenSubscriptionAlreadyTracked()
|
||||
@@ -380,4 +379,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
@@ -11,12 +10,12 @@ 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.
|
||||
/// 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.
|
||||
/// 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()
|
||||
@@ -39,7 +38,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
before.Length.ShouldBe(50);
|
||||
before[1].ShouldBe("PART-01");
|
||||
|
||||
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
|
||||
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, "1");
|
||||
StatusCode.IsGood(status).ShouldBe(true);
|
||||
|
||||
var after = client.Read(nodeId).Value as string[];
|
||||
@@ -56,7 +55,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that array nodes use bracketless OPC UA node identifiers while still exposing one-dimensional array metadata.
|
||||
/// Confirms that array nodes use bracketless OPC UA node identifiers while still exposing one-dimensional array
|
||||
/// metadata.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ArrayNode_UsesBracketlessNodeId_AndPublishesArrayDimensions()
|
||||
@@ -91,7 +91,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a null runtime value for a statically sized array is exposed as a typed fixed-length array.
|
||||
/// Confirms that a null runtime value for a statically sized array is exposed as a typed fixed-length array.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_NullStaticArray_ReturnsDefaultTypedArray()
|
||||
@@ -122,7 +122,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an indexed write also updates the published OPC UA value seen by subscribed clients.
|
||||
/// Confirms that an indexed write also updates the published OPC UA value seen by subscribed clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SingleArrayElement_PublishesUpdatedArrayToSubscribers()
|
||||
@@ -139,7 +139,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInPartNumbers");
|
||||
var notifications = new ConcurrentBag<MonitoredItemNotification>();
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
|
||||
item.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification notification)
|
||||
@@ -148,7 +148,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
|
||||
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, "1");
|
||||
StatusCode.IsGood(status).ShouldBe(true);
|
||||
|
||||
await Task.Delay(1000);
|
||||
@@ -169,7 +169,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that indexed writes succeed even when the current runtime array value is null.
|
||||
/// Confirms that indexed writes succeed even when the current runtime array value is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SingleArrayElement_WhenCurrentArrayIsNull_UsesDefaultArray()
|
||||
@@ -184,7 +184,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
await client.ConnectAsync(fixture.EndpointUrl);
|
||||
|
||||
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInPartNumbers");
|
||||
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
|
||||
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, "1");
|
||||
StatusCode.IsGood(status).ShouldBe(true);
|
||||
|
||||
var after = client.Read(nodeId).Value as string[];
|
||||
@@ -200,4 +200,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
|
||||
}
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "HistorizedAttr", FullTagReference = "TestObj.HistorizedAttr", MxDataType = 2, IsHistorized = true },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "NormalAttr", FullTagReference = "TestObj.NormalAttr", MxDataType = 5, IsHistorized = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "AlarmAttr", FullTagReference = "TestObj.AlarmAttr", MxDataType = 1, IsAlarm = true },
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "HistorizedAttr",
|
||||
FullTagReference = "TestObj.HistorizedAttr", MxDataType = 2, IsHistorized = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "NormalAttr",
|
||||
FullTagReference = "TestObj.NormalAttr", MxDataType = 5, IsHistorized = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "AlarmAttr",
|
||||
FullTagReference = "TestObj.AlarmAttr", MxDataType = 1, IsAlarm = true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that historized Galaxy attributes advertise OPC UA historizing support and history-read access.
|
||||
/// Verifies that historized Galaxy attributes advertise OPC UA historizing support and history-read access.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistorizedAttribute_HasHistorizingTrue_AndHistoryReadAccess()
|
||||
@@ -49,11 +64,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
(level & AccessLevels.HistoryRead).ShouldBe(AccessLevels.HistoryRead,
|
||||
"HistoryRead bit should be set");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that non-historized Galaxy attributes do not claim OPC UA history support.
|
||||
/// Verifies that non-historized Galaxy attributes do not claim OPC UA history support.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task NormalAttribute_HasHistorizingFalse_AndNoHistoryReadAccess()
|
||||
@@ -71,10 +89,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
var accessLevel = client.ReadAttribute(nodeId, Attributes.AccessLevel);
|
||||
var level = (byte)accessLevel.Value;
|
||||
(level & AccessLevels.HistoryRead).ShouldBe((byte)0,
|
||||
(level & AccessLevels.HistoryRead).ShouldBe(0,
|
||||
"HistoryRead bit should not be set");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Opc.Ua;
|
||||
@@ -12,7 +11,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
public class IncrementalSyncTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that adding a new Galaxy object and attribute causes the corresponding OPC UA node subtree to appear after sync.
|
||||
/// Verifies that adding a new Galaxy object and attribute causes the corresponding OPC UA node subtree to appear after
|
||||
/// sync.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sync_AddObject_NewNodeAppears()
|
||||
@@ -56,11 +56,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
// Original object should still be there
|
||||
children.Select(c => c.Name).ShouldContain("TestMachine_001");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing a Galaxy object tears down the corresponding OPC UA subtree without affecting siblings.
|
||||
/// Verifies that removing a Galaxy object tears down the corresponding OPC UA subtree without affecting siblings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sync_RemoveObject_NodeDisappears()
|
||||
@@ -90,11 +93,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
// DelmiaReceiver should still be there
|
||||
children.Select(c => c.Name).ShouldContain("DelmiaReceiver");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that adding a Galaxy attribute creates a new OPC UA variable during incremental rebuild.
|
||||
/// Verifies that adding a Galaxy attribute creates a new OPC UA variable during incremental rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sync_AddAttribute_NewVariableAppears()
|
||||
@@ -120,17 +126,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
children.Select(c => c.Name).ShouldContain("NewAttr");
|
||||
children.Select(c => c.Name).ShouldContain("MachineID");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that subscriptions on unchanged objects continue receiving data after unrelated subtree rebuilds.
|
||||
/// Verifies that subscriptions on unchanged objects continue receiving data after unrelated subtree rebuilds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sync_UnchangedObject_SubscriptionSurvives()
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient: mxClient);
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient);
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
@@ -139,7 +148,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
// Subscribe to MachineID on TestMachine_001
|
||||
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, 250);
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId);
|
||||
await Task.Delay(500);
|
||||
|
||||
// Modify a DIFFERENT object (MESReceiver) — TestMachine_001 should be unaffected
|
||||
@@ -157,11 +166,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var lastValue = (item.LastValue as MonitoredItemNotification)?.Value?.Value;
|
||||
lastValue.ShouldBe("UPDATED");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a rebuild request with no repository changes leaves the published namespace intact.
|
||||
/// Verifies that a rebuild request with no repository changes leaves the published namespace intact.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sync_NoChanges_NothingHappens()
|
||||
@@ -181,7 +193,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001"));
|
||||
children.Select(c => c.Name).ShouldContain("MachineID");
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,19 @@ using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests verifying multi-client subscription sync and concurrent operations.
|
||||
/// Integration tests verifying multi-client subscription sync and concurrent operations.
|
||||
/// </summary>
|
||||
public class MultiClientTests
|
||||
{
|
||||
// ── Subscription Sync ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that multiple OPC UA clients subscribed to the same tag all receive the same runtime update.
|
||||
/// 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()
|
||||
@@ -33,14 +32,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var notifications = new ConcurrentDictionary<int, List<MonitoredItemNotification>>();
|
||||
var subscriptions = new List<Subscription>();
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var client = new OpcUaTestClient();
|
||||
await client.ConnectAsync(fixture.EndpointUrl);
|
||||
clients.Add(client);
|
||||
|
||||
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
|
||||
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
|
||||
subscriptions.Add(sub);
|
||||
|
||||
var clientIndex = i;
|
||||
@@ -55,14 +54,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
await Task.Delay(500); // let subscriptions settle
|
||||
|
||||
// Simulate data change
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "MACHINE_42", 192);
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "MACHINE_42");
|
||||
await Task.Delay(1000); // let publish cycle deliver
|
||||
|
||||
// All 3 clients should have received the notification
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
notifications[i].Count.ShouldBeGreaterThan(0, $"Client {i} did not receive notification");
|
||||
}
|
||||
|
||||
foreach (var sub in subscriptions) await sub.DeleteAsync(true);
|
||||
foreach (var c in clients) c.Dispose();
|
||||
@@ -74,7 +71,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
|
||||
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Client_Disconnects_OtherClientsStillReceive()
|
||||
@@ -97,8 +94,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var (sub2, _) = await client2.SubscribeAsync(client2.MakeNodeId("TestMachine_001.MachineID"), 100);
|
||||
var (sub3, item3) = await client3.SubscribeAsync(client3.MakeNodeId("TestMachine_001.MachineID"), 100);
|
||||
|
||||
item1.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n); };
|
||||
item3.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications3.Add(n); };
|
||||
item1.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n);
|
||||
};
|
||||
item3.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification n) notifications3.Add(n);
|
||||
};
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
@@ -108,11 +111,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
await Task.Delay(500); // let server process disconnect
|
||||
|
||||
// Simulate data change — should not crash, clients 1+3 should still receive
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_DISCONNECT", 192);
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_DISCONNECT");
|
||||
await Task.Delay(1000);
|
||||
|
||||
notifications1.Count.ShouldBeGreaterThan(0, "Client 1 should still receive after client 2 disconnected");
|
||||
notifications3.Count.ShouldBeGreaterThan(0, "Client 3 should still receive after client 2 disconnected");
|
||||
notifications1.Count.ShouldBeGreaterThan(0,
|
||||
"Client 1 should still receive after client 2 disconnected");
|
||||
notifications3.Count.ShouldBeGreaterThan(0,
|
||||
"Client 3 should still receive after client 2 disconnected");
|
||||
|
||||
await sub1.DeleteAsync(true);
|
||||
await sub3.DeleteAsync(true);
|
||||
@@ -126,7 +131,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
|
||||
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Client_Unsubscribes_OtherClientsStillReceive()
|
||||
@@ -144,7 +149,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
var (sub1, _) = await client1.SubscribeAsync(client1.MakeNodeId("TestMachine_001.MachineID"), 100);
|
||||
var (sub2, item2) = await client2.SubscribeAsync(client2.MakeNodeId("TestMachine_001.MachineID"), 100);
|
||||
item2.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n); };
|
||||
item2.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n);
|
||||
};
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
@@ -153,10 +161,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
await Task.Delay(500);
|
||||
|
||||
// Simulate data change — client 2 should still receive
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_UNSUB", 192);
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_UNSUB");
|
||||
await Task.Delay(1000);
|
||||
|
||||
notifications2.Count.ShouldBeGreaterThan(0, "Client 2 should still receive after client 1 unsubscribed");
|
||||
notifications2.Count.ShouldBeGreaterThan(0,
|
||||
"Client 2 should still receive after client 1 unsubscribed");
|
||||
|
||||
await sub2.DeleteAsync(true);
|
||||
client1.Dispose();
|
||||
@@ -169,7 +178,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
|
||||
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleClients_SubscribeToDifferentTags_EachGetsOwnData()
|
||||
@@ -187,15 +196,22 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var notifications2 = new ConcurrentBag<MonitoredItemNotification>();
|
||||
|
||||
var (sub1, item1) = await client1.SubscribeAsync(client1.MakeNodeId("TestMachine_001.MachineID"), 100);
|
||||
var (sub2, item2) = await client2.SubscribeAsync(client2.MakeNodeId("DelmiaReceiver_001.DownloadPath"), 100);
|
||||
var (sub2, item2) =
|
||||
await client2.SubscribeAsync(client2.MakeNodeId("DelmiaReceiver_001.DownloadPath"), 100);
|
||||
|
||||
item1.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n); };
|
||||
item2.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n); };
|
||||
item1.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n);
|
||||
};
|
||||
item2.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n);
|
||||
};
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// Only change MachineID
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "CHANGED", 192);
|
||||
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "CHANGED");
|
||||
await Task.Delay(1000);
|
||||
|
||||
notifications1.Count.ShouldBeGreaterThan(0, "Client 1 should receive MachineID change");
|
||||
@@ -219,7 +235,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
// ── Concurrent Operation Tests ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent browse operations from several clients all complete successfully.
|
||||
/// Confirms that concurrent browse operations from several clients all complete successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowseFromMultipleClients_AllSucceed()
|
||||
@@ -230,7 +246,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
try
|
||||
{
|
||||
var clients = new List<OpcUaTestClient>();
|
||||
for (int i = 0; i < 5; i++)
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var c = new OpcUaTestClient();
|
||||
await c.ConnectAsync(fixture.EndpointUrl);
|
||||
@@ -262,7 +278,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent browse requests return consistent results across clients.
|
||||
/// Confirms that concurrent browse requests return consistent results across clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowse_AllReturnSameResults()
|
||||
@@ -272,7 +288,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
try
|
||||
{
|
||||
var clients = new List<OpcUaTestClient>();
|
||||
for (int i = 0; i < 5; i++)
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var c = new OpcUaTestClient();
|
||||
await c.ConnectAsync(fixture.EndpointUrl);
|
||||
@@ -287,7 +303,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
|
||||
// All should get identical child lists
|
||||
var firstResult = results[0].Select(r => r.Name).OrderBy(n => n).ToList();
|
||||
for (int i = 1; i < results.Length; i++)
|
||||
for (var i = 1; i < results.Length; i++)
|
||||
{
|
||||
var thisResult = results[i].Select(r => r.Name).OrderBy(n => n).ToList();
|
||||
thisResult.ShouldBe(firstResult, $"Client {i} got different browse results");
|
||||
@@ -302,7 +318,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
|
||||
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentBrowseAndSubscribe_NoInterference()
|
||||
@@ -312,7 +328,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
try
|
||||
{
|
||||
var clients = new List<OpcUaTestClient>();
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var c = new OpcUaTestClient();
|
||||
await c.ConnectAsync(fixture.EndpointUrl);
|
||||
@@ -340,7 +356,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
|
||||
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConcurrentSubscribeAndRead_NoDeadlock()
|
||||
@@ -380,7 +396,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated client churn does not leave the server in an unstable state.
|
||||
/// Confirms that repeated client churn does not leave the server in an unstable state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RapidConnectDisconnect_ServerStaysStable()
|
||||
@@ -390,7 +406,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
try
|
||||
{
|
||||
// Rapidly connect, browse, disconnect — 10 iterations
|
||||
for (int i = 0; i < 10; i++)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
using var client = new OpcUaTestClient();
|
||||
await client.ConnectAsync(fixture.EndpointUrl);
|
||||
@@ -411,4 +427,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
.AddUser("writetune", "writetune123", AppRoles.WriteTune)
|
||||
.AddUser("writeconfig", "writeconfig123", AppRoles.WriteConfigure)
|
||||
.AddUser("alarmack", "alarmack123", AppRoles.AlarmAck)
|
||||
.AddUser("admin", "admin123", AppRoles.ReadOnly, AppRoles.WriteOperate, AppRoles.WriteTune, AppRoles.WriteConfigure, AppRoles.AlarmAck);
|
||||
.AddUser("admin", "admin123", AppRoles.ReadOnly, AppRoles.WriteOperate, AppRoles.WriteTune,
|
||||
AppRoles.WriteConfigure, AppRoles.AlarmAck);
|
||||
}
|
||||
|
||||
private static AuthenticationConfiguration CreateAuthConfig(bool anonymousCanWrite = false)
|
||||
@@ -36,7 +37,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("hello");
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
mxClient: mxClient,
|
||||
mxClient,
|
||||
authConfig: CreateAuthConfig(),
|
||||
authProvider: CreateTestAuthProvider());
|
||||
await fixture.InitializeAsync();
|
||||
@@ -48,14 +49,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var result = client.Read(client.MakeNodeId("TestMachine_001.MachineID"));
|
||||
result.StatusCode.ShouldNotBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnonymousWrite_Denied_WhenAnonymousCanWriteFalse()
|
||||
{
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
authConfig: CreateAuthConfig(anonymousCanWrite: false),
|
||||
authConfig: CreateAuthConfig(false),
|
||||
authProvider: CreateTestAuthProvider());
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
@@ -66,7 +70,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
|
||||
status.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -75,8 +82,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("initial");
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
mxClient: mxClient,
|
||||
authConfig: CreateAuthConfig(anonymousCanWrite: true),
|
||||
mxClient,
|
||||
authConfig: CreateAuthConfig(true),
|
||||
authProvider: CreateTestAuthProvider());
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
@@ -87,7 +94,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
|
||||
status.Code.ShouldNotBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -105,7 +115,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
|
||||
status.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -114,7 +127,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("initial");
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
mxClient: mxClient,
|
||||
mxClient,
|
||||
authConfig: CreateAuthConfig(),
|
||||
authProvider: CreateTestAuthProvider());
|
||||
await fixture.InitializeAsync();
|
||||
@@ -126,7 +139,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
|
||||
status.Code.ShouldNotBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -144,7 +160,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
|
||||
status.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -153,7 +172,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("initial");
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
mxClient: mxClient,
|
||||
mxClient,
|
||||
authConfig: CreateAuthConfig(),
|
||||
authProvider: CreateTestAuthProvider());
|
||||
await fixture.InitializeAsync();
|
||||
@@ -165,7 +184,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
|
||||
status.Code.ShouldNotBe(StatusCodes.BadUserAccessDenied);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -182,7 +204,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
await Should.ThrowAsync<ServiceResultException>(async () =>
|
||||
await client.ConnectAsync(fixture.EndpointUrl, username: "readonly", password: "wrongpassword"));
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var serviceLevel = client.Read(VariableIds.Server_ServiceLevel);
|
||||
((byte)serviceLevel.Value).ShouldBe((byte)255);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -53,7 +56,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
var redundancySupport = client.Read(VariableIds.Server_ServerRedundancy_RedundancySupport);
|
||||
((int)redundancySupport.Value).ShouldBe((int)RedundancySupport.Warm);
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -130,7 +136,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
uris.ShouldContain("urn:test:server2");
|
||||
}
|
||||
}
|
||||
finally { await fixture.DisposeAsync(); }
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -176,4 +185,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
@@ -6,12 +7,12 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that a fresh metrics collector reports no statistics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyState_ReturnsZeroStatistics()
|
||||
@@ -22,13 +23,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated operation recordings update total and successful execution counts.
|
||||
/// Confirms that repeated operation recordings update total and successful execution counts.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecordOperation_TracksCounts()
|
||||
{
|
||||
using var metrics = new PerformanceMetrics();
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true);
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10));
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(20), false);
|
||||
|
||||
var stats = metrics.GetStatistics();
|
||||
@@ -39,7 +40,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that min, max, and average timing values are calculated from recorded operations.
|
||||
/// Confirms that min, max, and average timing values are calculated from recorded operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecordOperation_TracksMinMaxAverage()
|
||||
@@ -56,13 +57,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the 95th percentile is calculated from the recorded timing sample.
|
||||
/// Confirms that the 95th percentile is calculated from the recorded timing sample.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void P95_CalculatedCorrectly()
|
||||
{
|
||||
using var metrics = new PerformanceMetrics();
|
||||
for (int i = 1; i <= 100; i++)
|
||||
for (var i = 1; i <= 100; i++)
|
||||
metrics.RecordOperation("Op", TimeSpan.FromMilliseconds(i));
|
||||
|
||||
var stats = metrics.GetStatistics()["Op"];
|
||||
@@ -70,13 +71,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the rolling buffer keeps the most recent operation durations for percentile calculations.
|
||||
/// Confirms that the rolling buffer keeps the most recent operation durations for percentile calculations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RollingBuffer_EvictsOldEntries()
|
||||
{
|
||||
var opMetrics = new OperationMetrics();
|
||||
for (int i = 0; i < 1100; i++)
|
||||
for (var i = 0; i < 1100; i++)
|
||||
opMetrics.Record(TimeSpan.FromMilliseconds(i), true);
|
||||
|
||||
var stats = opMetrics.GetStatistics();
|
||||
@@ -86,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a timing scope records an operation when disposed.
|
||||
/// Confirms that a timing scope records an operation when disposed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BeginOperation_TimingScopeRecordsOnDispose()
|
||||
@@ -96,7 +97,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
using (var scope = metrics.BeginOperation("Test"))
|
||||
{
|
||||
// Simulate some work
|
||||
System.Threading.Thread.Sleep(5);
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
|
||||
var stats = metrics.GetStatistics();
|
||||
@@ -107,7 +108,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a timing scope can mark an operation as failed before disposal.
|
||||
/// Confirms that a timing scope can mark an operation as failed before disposal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BeginOperation_SetSuccessFalse()
|
||||
@@ -125,7 +126,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that looking up an unknown operation returns no metrics bucket.
|
||||
/// Confirms that looking up an unknown operation returns no metrics bucket.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetMetrics_UnknownOperation_ReturnsNull()
|
||||
@@ -135,7 +136,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that operation names are tracked without case sensitivity.
|
||||
/// Confirms that operation names are tracked without case sensitivity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OperationNames_AreCaseInsensitive()
|
||||
@@ -149,4 +150,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Metrics
|
||||
stats["READ"].TotalCount.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,19 @@ 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.
|
||||
/// Verifies MXAccess client connection lifecycle behavior, including transitions, registration, and reconnect
|
||||
/// handling.
|
||||
/// </summary>
|
||||
public class MxAccessClientConnectionTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly List<(ConnectionState Previous, ConnectionState Current)> _stateChanges = new();
|
||||
private readonly StaComThread _staThread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the connection test fixture with a fake runtime proxy and state-change recorder.
|
||||
/// Initializes the connection test fixture with a fake runtime proxy and state-change recorder.
|
||||
/// </summary>
|
||||
public MxAccessClientConnectionTests()
|
||||
{
|
||||
@@ -37,7 +38,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the connection test fixture and its supporting resources.
|
||||
/// Disposes the connection test fixture and its supporting resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -47,7 +48,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a newly created MXAccess client starts in the disconnected state.
|
||||
/// Confirms that a newly created MXAccess client starts in the disconnected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InitialState_IsDisconnected()
|
||||
@@ -56,7 +57,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that connecting drives the expected disconnected-to-connecting-to-connected transitions.
|
||||
/// Confirms that connecting drives the expected disconnected-to-connecting-to-connected transitions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_TransitionsToConnected()
|
||||
@@ -64,12 +65,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
await _client.ConnectAsync();
|
||||
|
||||
_client.State.ShouldBe(ConnectionState.Connected);
|
||||
_stateChanges.ShouldContain(s => s.Previous == ConnectionState.Disconnected && s.Current == ConnectionState.Connecting);
|
||||
_stateChanges.ShouldContain(s => s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected);
|
||||
_stateChanges.ShouldContain(s =>
|
||||
s.Previous == ConnectionState.Disconnected && s.Current == ConnectionState.Connecting);
|
||||
_stateChanges.ShouldContain(s =>
|
||||
s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a successful connect registers exactly once with the runtime proxy.
|
||||
/// Confirms that a successful connect registers exactly once with the runtime proxy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_RegistersCalled()
|
||||
@@ -79,7 +82,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disconnecting drives the expected shutdown transitions back to disconnected.
|
||||
/// Confirms that disconnecting drives the expected shutdown transitions back to disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_TransitionsToDisconnected()
|
||||
@@ -93,7 +96,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disconnecting unregisters the runtime proxy session.
|
||||
/// Confirms that disconnecting unregisters the runtime proxy session.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_UnregistersCalled()
|
||||
@@ -104,7 +107,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that registration failures move the client into the error state.
|
||||
/// Confirms that registration failures move the client into the error state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectFails_TransitionsToError()
|
||||
@@ -116,7 +119,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that repeated connect calls do not perform duplicate runtime registrations.
|
||||
/// Confirms that repeated connect calls do not perform duplicate runtime registrations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DoubleConnect_NoOp()
|
||||
@@ -127,7 +130,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reconnect increments the reconnect counter and restores the connected state.
|
||||
/// Confirms that reconnect increments the reconnect counter and restores the connected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Reconnect_IncrementsCount()
|
||||
@@ -140,4 +143,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.State.ShouldBe(ConnectionState.Connected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,16 @@ 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.
|
||||
/// 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;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly StaComThread _staThread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector.
|
||||
/// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector.
|
||||
/// </summary>
|
||||
public MxAccessClientMonitorTests()
|
||||
{
|
||||
@@ -31,7 +31,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the monitor test fixture resources.
|
||||
/// Disposes the monitor test fixture resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the monitor reconnects the client after an observed disconnect.
|
||||
/// Confirms that the monitor reconnects the client after an observed disconnect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ReconnectsOnDisconnect()
|
||||
@@ -67,7 +67,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the monitor can be started and stopped without throwing.
|
||||
/// Confirms that the monitor can be started and stopped without throwing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_StopsOnCancel()
|
||||
@@ -85,7 +85,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled.
|
||||
/// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ProbeStale_ForcesReconnect()
|
||||
@@ -112,7 +112,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that fresh probe updates prevent unnecessary reconnects.
|
||||
/// Confirms that fresh probe updates prevent unnecessary reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_ProbeDataChange_PreventsStaleReconnect()
|
||||
@@ -130,10 +130,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.StartMonitor();
|
||||
|
||||
// Continuously simulate probe data changes to keep it fresh
|
||||
for (int i = 0; i < 8; i++)
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
_proxy.SimulateDataChangeByAddress("TestProbe", i, 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestProbe", i);
|
||||
}
|
||||
|
||||
client.StopMonitor();
|
||||
@@ -144,7 +144,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that enabling the monitor without a probe tag does not trigger false reconnects.
|
||||
/// Confirms that enabling the monitor without a probe tag does not trigger false reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_NoProbeConfigured_NoFalseReconnect()
|
||||
@@ -169,4 +169,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -12,17 +11,17 @@ 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.
|
||||
/// 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;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly StaComThread _staThread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the COM-threaded MXAccess test fixture with a fake runtime proxy and metrics collector.
|
||||
/// Initializes the COM-threaded MXAccess test fixture with a fake runtime proxy and metrics collector.
|
||||
/// </summary>
|
||||
public MxAccessClientReadWriteTests()
|
||||
{
|
||||
@@ -35,7 +34,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the MXAccess client fixture and its supporting STA thread and metrics collector.
|
||||
/// Disposes the MXAccess client fixture and its supporting STA thread and metrics collector.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -45,7 +44,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reads fail with bad-not-connected quality when the runtime session is offline.
|
||||
/// Confirms that reads fail with bad-not-connected quality when the runtime session is offline.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_NotConnected_ReturnsBad()
|
||||
@@ -55,7 +54,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a runtime data-change callback completes a pending read with the published value.
|
||||
/// Confirms that a runtime data-change callback completes a pending read with the published value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_ReturnsValueOnDataChange()
|
||||
@@ -67,7 +66,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
|
||||
// Give it a moment to set up subscription, then simulate data change
|
||||
await Task.Delay(50);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 42);
|
||||
|
||||
var result = await readTask;
|
||||
result.Value.ShouldBe(42);
|
||||
@@ -75,7 +74,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that reads time out with bad communication-failure quality when the runtime never responds.
|
||||
/// Confirms that reads time out with bad communication-failure quality when the runtime never responds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_Timeout_ReturnsBadCommFailure()
|
||||
@@ -88,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that timed-out reads are recorded as failed read operations in the metrics collector.
|
||||
/// Confirms that timed-out reads are recorded as failed read operations in the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_Timeout_RecordsFailedMetrics()
|
||||
@@ -105,7 +104,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that writes are rejected when the runtime session is not connected.
|
||||
/// Confirms that writes are rejected when the runtime session is not connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_NotConnected_ReturnsFalse()
|
||||
@@ -115,7 +114,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that successful runtime write acknowledgments return success and record the written payload.
|
||||
/// Confirms that successful runtime write acknowledgments return success and record the written payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_Success_ReturnsTrue()
|
||||
@@ -129,7 +128,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that MXAccess error codes on write completion are surfaced as failed writes.
|
||||
/// Confirms that MXAccess error codes on write completion are surfaced as failed writes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_ErrorCode_ReturnsFalse()
|
||||
@@ -142,7 +141,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that write timeouts are recorded as failed write operations in the metrics collector.
|
||||
/// Confirms that write timeouts are recorded as failed write operations in the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_Timeout_ReturnsFalse_AndRecordsFailedMetrics()
|
||||
@@ -160,7 +159,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that successful reads contribute a read entry to the metrics collector.
|
||||
/// Confirms that successful reads contribute a read entry to the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_RecordsMetrics()
|
||||
@@ -169,7 +168,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
|
||||
var readTask = _client.ReadAsync("TestTag.Attr");
|
||||
await Task.Delay(50);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 1, 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 1);
|
||||
await readTask;
|
||||
|
||||
var stats = _metrics.GetStatistics();
|
||||
@@ -178,7 +177,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that writes contribute a write entry to the metrics collector.
|
||||
/// Confirms that writes contribute a write entry to the metrics collector.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_RecordsMetrics()
|
||||
@@ -191,4 +190,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
stats["Write"].TotalCount.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
@@ -13,17 +12,17 @@ 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.
|
||||
/// Verifies how the MXAccess client manages persistent subscriptions, reconnect replay, and probe-tag behavior.
|
||||
/// </summary>
|
||||
public class MxAccessClientSubscriptionTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _staThread;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly MxAccessClient _client;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly FakeMxProxy _proxy;
|
||||
private readonly StaComThread _staThread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the subscription test fixture with a fake runtime proxy and STA thread.
|
||||
/// Initializes the subscription test fixture with a fake runtime proxy and STA thread.
|
||||
/// </summary>
|
||||
public MxAccessClientSubscriptionTests()
|
||||
{
|
||||
@@ -35,7 +34,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the subscription test fixture and its supporting resources.
|
||||
/// Disposes the subscription test fixture and its supporting resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -45,7 +44,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscribing creates a runtime item, advises it, and increments the active subscription count.
|
||||
/// Confirms that subscribing creates a runtime item, advises it, and increments the active subscription count.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_CreatesItemAndAdvises()
|
||||
@@ -59,7 +58,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that subscribing to the same address twice reuses the existing runtime item.
|
||||
/// Confirms that subscribing to the same address twice reuses the existing runtime item.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_SameAddressTwice_ReusesExistingRuntimeItem()
|
||||
@@ -78,7 +77,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unsubscribing clears the active subscription count after a tag was previously monitored.
|
||||
/// Confirms that unsubscribing clears the active subscription count after a tag was previously monitored.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_RemovesItemAndUnadvises()
|
||||
@@ -91,7 +90,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime data changes are delivered to the per-subscription callback.
|
||||
/// Confirms that runtime data changes are delivered to the per-subscription callback.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnDataChange_InvokesCallback()
|
||||
@@ -101,7 +100,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
Vtq? received = null;
|
||||
await _client.SubscribeAsync("TestTag.Attr", (addr, vtq) => received = vtq);
|
||||
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 42);
|
||||
|
||||
received.ShouldNotBeNull();
|
||||
received.Value.Value.ShouldBe(42);
|
||||
@@ -109,7 +108,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime data changes are also delivered to the client's global tag-change event.
|
||||
/// Confirms that runtime data changes are also delivered to the client's global tag-change event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnDataChange_InvokesGlobalHandler()
|
||||
@@ -120,13 +119,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.OnTagValueChanged += (addr, vtq) => globalAddr = addr;
|
||||
|
||||
await _client.SubscribeAsync("TestTag.Attr", (_, _) => { });
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", "hello", 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", "hello");
|
||||
|
||||
globalAddr.ShouldBe("TestTag.Attr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that stored subscriptions are replayed after reconnect so live updates resume automatically.
|
||||
/// Confirms that stored subscriptions are replayed after reconnect so live updates resume automatically.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StoredSubscriptions_ReplayedAfterReconnect()
|
||||
@@ -142,12 +141,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
_client.ActiveSubscriptionCount.ShouldBe(1);
|
||||
|
||||
// Simulate data change on the re-subscribed item
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", "value", 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", "value");
|
||||
callbackInvoked.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that one-shot reads do not remove persistent subscriptions when the client reconnects.
|
||||
/// Confirms that one-shot reads do not remove persistent subscriptions when the client reconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OneShotRead_DoesNotRemovePersistentSubscription_OnReconnect()
|
||||
@@ -158,19 +157,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
|
||||
var readTask = _client.ReadAsync("TestTag.Attr");
|
||||
await Task.Delay(50);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", 42);
|
||||
(await readTask).Value.ShouldBe(42);
|
||||
callbackInvoked = false;
|
||||
|
||||
await _client.ReconnectAsync();
|
||||
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", "after_reconnect", 192);
|
||||
_proxy.SimulateDataChangeByAddress("TestTag.Attr", "after_reconnect");
|
||||
callbackInvoked.ShouldBe(true);
|
||||
_client.ActiveSubscriptionCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that transient writes do not prevent later removal of a persistent subscription.
|
||||
/// Confirms that transient writes do not prevent later removal of a persistent subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OneShotWrite_DoesNotBreakPersistentUnsubscribe()
|
||||
@@ -189,7 +188,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the configured probe tag is subscribed during connect so connectivity monitoring can start immediately.
|
||||
/// Confirms that the configured probe tag is subscribed during connect so connectivity monitoring can start
|
||||
/// immediately.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProbeTag_SubscribedOnConnect()
|
||||
@@ -206,7 +206,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the probe tag cannot be unsubscribed accidentally because it is reserved for connection monitoring.
|
||||
/// Confirms that the probe tag cannot be unsubscribed accidentally because it is reserved for connection monitoring.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProbeTag_ProtectedFromUnsubscribe()
|
||||
@@ -226,4 +226,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
@@ -8,14 +9,14 @@ 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.
|
||||
/// 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.
|
||||
/// Starts a fresh STA thread instance for each test.
|
||||
/// </summary>
|
||||
public StaComThreadTests()
|
||||
{
|
||||
@@ -24,12 +25,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the STA thread after each test.
|
||||
/// Disposes the STA thread after each test.
|
||||
/// </summary>
|
||||
public void Dispose() => _thread.Dispose();
|
||||
public void Dispose()
|
||||
{
|
||||
_thread.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that queued work runs on a thread configured for STA apartment state.
|
||||
/// Confirms that queued work runs on a thread configured for STA apartment state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ExecutesOnStaThread()
|
||||
@@ -39,7 +43,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that action delegates run to completion on the STA thread.
|
||||
/// Confirms that action delegates run to completion on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Action_Completes()
|
||||
@@ -50,7 +54,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that function delegates can return results from the STA thread.
|
||||
/// Confirms that function delegates can return results from the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Func_ReturnsResult()
|
||||
@@ -60,7 +64,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that exceptions thrown on the STA thread propagate back to the caller.
|
||||
/// Confirms that exceptions thrown on the STA thread propagate back to the caller.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_PropagatesException()
|
||||
@@ -70,7 +74,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that disposing the STA thread stops it from accepting additional work.
|
||||
/// Confirms that disposing the STA thread stops it from accepting additional work.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dispose_Stops_Thread()
|
||||
@@ -84,12 +88,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that multiple queued work items all execute successfully on the STA thread.
|
||||
/// Confirms that multiple queued work items all execute successfully on the STA thread.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MultipleWorkItems_ExecuteInOrder()
|
||||
{
|
||||
var results = new System.Collections.Concurrent.ConcurrentBag<int>();
|
||||
var results = new ConcurrentBag<int>();
|
||||
await Task.WhenAll(
|
||||
_thread.RunAsync(() => results.Add(1)),
|
||||
_thread.RunAsync(() => results.Add(2)),
|
||||
@@ -98,4 +102,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
|
||||
results.Count.ShouldBe(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,25 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
public class AddressSpaceDiffTests
|
||||
{
|
||||
private static GalaxyObjectInfo Obj(int id, string tag, int parent = 0, bool isArea = false)
|
||||
=> new GalaxyObjectInfo { GobjectId = id, TagName = tag, BrowseName = tag, ContainedName = tag, ParentGobjectId = parent, IsArea = isArea };
|
||||
{
|
||||
return new GalaxyObjectInfo
|
||||
{
|
||||
GobjectId = id, TagName = tag, BrowseName = tag, ContainedName = tag, ParentGobjectId = parent,
|
||||
IsArea = isArea
|
||||
};
|
||||
}
|
||||
|
||||
private static GalaxyAttributeInfo Attr(int gobjectId, string name, string tagName = "Obj", int mxDataType = 5)
|
||||
=> new GalaxyAttributeInfo { GobjectId = gobjectId, AttributeName = name, FullTagReference = $"{tagName}.{name}", MxDataType = mxDataType, TagName = tagName };
|
||||
{
|
||||
return new GalaxyAttributeInfo
|
||||
{
|
||||
GobjectId = gobjectId, AttributeName = name, FullTagReference = $"{tagName}.{name}",
|
||||
MxDataType = mxDataType, TagName = tagName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that identical Galaxy hierarchy and attribute snapshots produce no incremental rebuild work.
|
||||
/// Verifies that identical Galaxy hierarchy and attribute snapshots produce no incremental rebuild work.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NoChanges_ReturnsEmptySet()
|
||||
@@ -28,7 +40,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that newly deployed Galaxy objects are flagged for OPC UA subtree creation.
|
||||
/// Verifies that newly deployed Galaxy objects are flagged for OPC UA subtree creation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddedObject_Detected()
|
||||
@@ -43,7 +55,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removed Galaxy objects are flagged so their OPC UA subtree can be torn down.
|
||||
/// Verifies that removed Galaxy objects are flagged so their OPC UA subtree can be torn down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RemovedObject_Detected()
|
||||
@@ -58,13 +70,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that browse-name changes are treated as address-space changes for the affected Galaxy object.
|
||||
/// Verifies that browse-name changes are treated as address-space changes for the affected Galaxy object.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ModifiedObject_BrowseNameChange_Detected()
|
||||
{
|
||||
var oldH = new List<GalaxyObjectInfo> { Obj(1, "A") };
|
||||
var newH = new List<GalaxyObjectInfo> { new GalaxyObjectInfo { GobjectId = 1, TagName = "A", BrowseName = "A_Renamed", ContainedName = "A" } };
|
||||
var newH = new List<GalaxyObjectInfo>
|
||||
{ new() { GobjectId = 1, TagName = "A", BrowseName = "A_Renamed", ContainedName = "A" } };
|
||||
var a = new List<GalaxyAttributeInfo>();
|
||||
|
||||
var changed = AddressSpaceDiff.FindChangedGobjectIds(oldH, a, newH, a);
|
||||
@@ -72,13 +85,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that parent changes are treated as subtree moves that require rebuilding the affected object.
|
||||
/// Verifies that parent changes are treated as subtree moves that require rebuilding the affected object.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ModifiedObject_ParentChange_Detected()
|
||||
{
|
||||
var oldH = new List<GalaxyObjectInfo> { Obj(1, "A"), Obj(2, "B", parent: 1) };
|
||||
var newH = new List<GalaxyObjectInfo> { Obj(1, "A"), new GalaxyObjectInfo { GobjectId = 2, TagName = "B", BrowseName = "B", ContainedName = "B", ParentGobjectId = 0 } };
|
||||
var oldH = new List<GalaxyObjectInfo> { Obj(1, "A"), Obj(2, "B", 1) };
|
||||
var newH = new List<GalaxyObjectInfo>
|
||||
{
|
||||
Obj(1, "A"),
|
||||
new() { GobjectId = 2, TagName = "B", BrowseName = "B", ContainedName = "B", ParentGobjectId = 0 }
|
||||
};
|
||||
var a = new List<GalaxyAttributeInfo>();
|
||||
|
||||
var changed = AddressSpaceDiff.FindChangedGobjectIds(oldH, a, newH, a);
|
||||
@@ -86,7 +103,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that adding a Galaxy attribute marks the owning object for OPC UA variable rebuild.
|
||||
/// Verifies that adding a Galaxy attribute marks the owning object for OPC UA variable rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AttributeAdded_Detected()
|
||||
@@ -100,7 +117,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing a Galaxy attribute marks the owning object for OPC UA variable rebuild.
|
||||
/// Verifies that removing a Galaxy attribute marks the owning object for OPC UA variable rebuild.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AttributeRemoved_Detected()
|
||||
@@ -114,7 +131,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that changes to attribute field metadata such as MX data type trigger rebuild of the owning object.
|
||||
/// Verifies that changes to attribute field metadata such as MX data type trigger rebuild of the owning object.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AttributeFieldChange_Detected()
|
||||
@@ -128,21 +145,23 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that security-classification changes are treated as address-space changes for the owning attribute.
|
||||
/// Verifies that security-classification changes are treated as address-space changes for the owning attribute.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AttributeSecurityChange_Detected()
|
||||
{
|
||||
var h = new List<GalaxyObjectInfo> { Obj(1, "A") };
|
||||
var oldA = new List<GalaxyAttributeInfo> { new GalaxyAttributeInfo { GobjectId = 1, AttributeName = "X", FullTagReference = "A.X", SecurityClassification = 1 } };
|
||||
var newA = new List<GalaxyAttributeInfo> { new GalaxyAttributeInfo { GobjectId = 1, AttributeName = "X", FullTagReference = "A.X", SecurityClassification = 2 } };
|
||||
var oldA = new List<GalaxyAttributeInfo>
|
||||
{ new() { GobjectId = 1, AttributeName = "X", FullTagReference = "A.X", SecurityClassification = 1 } };
|
||||
var newA = new List<GalaxyAttributeInfo>
|
||||
{ new() { GobjectId = 1, AttributeName = "X", FullTagReference = "A.X", SecurityClassification = 2 } };
|
||||
|
||||
var changed = AddressSpaceDiff.FindChangedGobjectIds(h, oldA, h, newA);
|
||||
changed.ShouldContain(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that subtree expansion includes all descendants of a changed Galaxy object.
|
||||
/// Verifies that subtree expansion includes all descendants of a changed Galaxy object.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExpandToSubtrees_IncludesChildren()
|
||||
@@ -150,9 +169,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
var h = new List<GalaxyObjectInfo>
|
||||
{
|
||||
Obj(1, "Root"),
|
||||
Obj(2, "Child", parent: 1),
|
||||
Obj(3, "Grandchild", parent: 2),
|
||||
Obj(4, "Sibling", parent: 1),
|
||||
Obj(2, "Child", 1),
|
||||
Obj(3, "Grandchild", 2),
|
||||
Obj(4, "Sibling", 1),
|
||||
Obj(5, "Unrelated")
|
||||
};
|
||||
|
||||
@@ -167,7 +186,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that subtree expansion does not introduce unrelated nodes when the changed object is already a leaf.
|
||||
/// Verifies that subtree expansion does not introduce unrelated nodes when the changed object is already a leaf.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExpandToSubtrees_LeafNode_NoExpansion()
|
||||
@@ -175,8 +194,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
var h = new List<GalaxyObjectInfo>
|
||||
{
|
||||
Obj(1, "Root"),
|
||||
Obj(2, "Child", parent: 1),
|
||||
Obj(3, "Sibling", parent: 1)
|
||||
Obj(2, "Child", 1),
|
||||
Obj(3, "Sibling", 1)
|
||||
};
|
||||
|
||||
var changed = new HashSet<int> { 2 };
|
||||
@@ -187,4 +206,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
expanded.ShouldNotContain(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
@@ -7,12 +8,12 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that boolean runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Boolean()
|
||||
@@ -20,11 +21,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
var vtq = Vtq.Good(true);
|
||||
var dv = DataValueConverter.FromVtq(vtq);
|
||||
dv.Value.ShouldBe(true);
|
||||
Opc.Ua.StatusCode.IsGood(dv.StatusCode).ShouldBe(true);
|
||||
StatusCode.IsGood(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that integer runtime values are preserved when converted to OPC UA data values.
|
||||
/// Confirms that integer runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Int32()
|
||||
@@ -35,7 +36,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that float runtime values are preserved when converted to OPC UA data values.
|
||||
/// Confirms that float runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Float()
|
||||
@@ -46,7 +47,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that double runtime values are preserved when converted to OPC UA data values.
|
||||
/// Confirms that double runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_Double()
|
||||
@@ -57,7 +58,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that string runtime values are preserved when converted to OPC UA data values.
|
||||
/// Confirms that string runtime values are preserved when converted to OPC UA data values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_String()
|
||||
@@ -68,7 +69,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that UTC timestamps remain UTC when a VTQ is converted for OPC UA clients.
|
||||
/// Confirms that UTC timestamps remain UTC when a VTQ is converted for OPC UA clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_DateTime_IsUtc()
|
||||
@@ -80,7 +81,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that elapsed-time values are exposed to OPC UA clients in seconds.
|
||||
/// Confirms that elapsed-time values are exposed to OPC UA clients in seconds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_TimeSpan_ConvertedToSeconds()
|
||||
@@ -91,7 +92,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that string arrays remain arrays when exposed through OPC UA.
|
||||
/// Confirms that string arrays remain arrays when exposed through OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_StringArray()
|
||||
@@ -103,7 +104,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that integer arrays remain arrays when exposed through OPC UA.
|
||||
/// Confirms that integer arrays remain arrays when exposed through OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_IntArray()
|
||||
@@ -115,29 +116,29 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad runtime quality is translated to a bad OPC UA status code.
|
||||
/// Confirms that bad runtime quality is translated to a bad OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_BadQuality_MapsToStatusCode()
|
||||
{
|
||||
var vtq = Vtq.Bad(Quality.BadCommFailure);
|
||||
var dv = DataValueConverter.FromVtq(vtq);
|
||||
Opc.Ua.StatusCode.IsBad(dv.StatusCode).ShouldBe(true);
|
||||
StatusCode.IsBad(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain runtime quality is translated to an uncertain OPC UA status code.
|
||||
/// Confirms that uncertain runtime quality is translated to an uncertain OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_UncertainQuality()
|
||||
{
|
||||
var vtq = Vtq.Uncertain(42);
|
||||
var dv = DataValueConverter.FromVtq(vtq);
|
||||
Opc.Ua.StatusCode.IsUncertain(dv.StatusCode).ShouldBe(true);
|
||||
StatusCode.IsUncertain(dv.StatusCode).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that null runtime values remain null when converted for OPC UA.
|
||||
/// Confirms that null runtime values remain null when converted for OPC UA.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromVtq_NullValue()
|
||||
@@ -148,7 +149,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a data value can round-trip back into a VTQ without losing the process value or quality.
|
||||
/// 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()
|
||||
@@ -161,4 +162,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
roundTrip.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,37 +7,70 @@ 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.
|
||||
/// 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.
|
||||
/// 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>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea", ParentGobjectId = 1, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false },
|
||||
new GalaxyObjectInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false },
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "DEV", ContainedName = "DEV", BrowseName = "DEV", ParentGobjectId = 0,
|
||||
IsArea = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "TestArea", ContainedName = "TestArea", BrowseName = "TestArea",
|
||||
ParentGobjectId = 1, IsArea = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "TestMachine_001", ContainedName = "TestMachine_001",
|
||||
BrowseName = "TestMachine_001", ParentGobjectId = 2, IsArea = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 4, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver",
|
||||
BrowseName = "DelmiaReceiver", ParentGobjectId = 3, IsArea = false
|
||||
}
|
||||
};
|
||||
|
||||
var attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID", FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber", FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "BatchItems", FullTagReference = "TestMachine_001.BatchItems[]", MxDataType = 5, IsArray = true, ArrayDimension = 50 },
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "MachineID",
|
||||
FullTagReference = "TestMachine_001.MachineID", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath",
|
||||
FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 4, TagName = "DelmiaReceiver_001", AttributeName = "JobStepNumber",
|
||||
FullTagReference = "DelmiaReceiver_001.JobStepNumber", MxDataType = 2, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "BatchItems",
|
||||
FullTagReference = "TestMachine_001.BatchItems[]", MxDataType = 5, IsArray = true,
|
||||
ArrayDimension = 50
|
||||
}
|
||||
};
|
||||
|
||||
return (hierarchy, attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that object and variable counts are computed correctly from the seeded Galaxy model.
|
||||
/// Confirms that object and variable counts are computed correctly from the seeded Galaxy model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_CreatesCorrectNodeCounts()
|
||||
@@ -50,7 +83,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that runtime tag references are populated for every published variable.
|
||||
/// Confirms that runtime tag references are populated for every published variable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_TagReferencesPopulated()
|
||||
@@ -65,7 +98,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that array attributes are represented in the tag-reference map.
|
||||
/// Confirms that array attributes are represented in the tag-reference map.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_ArrayVariable_HasCorrectInfo()
|
||||
@@ -78,15 +111,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that Galaxy areas are not counted as object nodes in the resulting model.
|
||||
/// Confirms that Galaxy areas are not counted as object nodes in the resulting model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_Areas_AreNotCountedAsObjects()
|
||||
{
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "Area1", BrowseName = "Area1", ParentGobjectId = 0, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 1, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "Area1", BrowseName = "Area1", ParentGobjectId = 0, IsArea = true },
|
||||
new() { GobjectId = 2, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 1, IsArea = false }
|
||||
};
|
||||
|
||||
var model = AddressSpaceBuilder.Build(hierarchy, new List<GalaxyAttributeInfo>());
|
||||
@@ -94,15 +127,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that only top-level Galaxy nodes are returned as roots in the model.
|
||||
/// Confirms that only top-level Galaxy nodes are returned as roots in the model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_RootNodes_AreTopLevel()
|
||||
{
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "Root1", BrowseName = "Root1", ParentGobjectId = 0, IsArea = true },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "Child1", BrowseName = "Child1", ParentGobjectId = 1, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "Root1", BrowseName = "Root1", ParentGobjectId = 0, IsArea = true },
|
||||
new() { GobjectId = 2, TagName = "Child1", BrowseName = "Child1", ParentGobjectId = 1, IsArea = false }
|
||||
};
|
||||
|
||||
var model = AddressSpaceBuilder.Build(hierarchy, new List<GalaxyAttributeInfo>());
|
||||
@@ -110,21 +143,37 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that variables for multiple MX data types are included in the model.
|
||||
/// Confirms that variables for multiple MX data types are included in the model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildAddressSpace_DataTypeMappings()
|
||||
{
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "Obj", BrowseName = "Obj", ParentGobjectId = 0, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "Obj", BrowseName = "Obj", ParentGobjectId = 0, IsArea = false }
|
||||
};
|
||||
var attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "BoolAttr", FullTagReference = "Obj.BoolAttr", MxDataType = 1, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "IntAttr", FullTagReference = "Obj.IntAttr", MxDataType = 2, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "FloatAttr", FullTagReference = "Obj.FloatAttr", MxDataType = 3, IsArray = false },
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj", AttributeName = "StrAttr", FullTagReference = "Obj.StrAttr", MxDataType = 5, IsArray = false },
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "Obj", AttributeName = "BoolAttr", FullTagReference = "Obj.BoolAttr",
|
||||
MxDataType = 1, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "Obj", AttributeName = "IntAttr", FullTagReference = "Obj.IntAttr",
|
||||
MxDataType = 2, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "Obj", AttributeName = "FloatAttr", FullTagReference = "Obj.FloatAttr",
|
||||
MxDataType = 3, IsArray = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "Obj", AttributeName = "StrAttr", FullTagReference = "Obj.StrAttr",
|
||||
MxDataType = 5, IsArray = false
|
||||
}
|
||||
};
|
||||
|
||||
var model = AddressSpaceBuilder.Build(hierarchy, attributes);
|
||||
@@ -132,4 +181,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.NodeIdToTagReference.Count.ShouldBe(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,23 +7,27 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that rebuilding with new metadata replaces the old tag-reference set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rebuild_NewBuild_ReplacesOldData()
|
||||
{
|
||||
var hierarchy1 = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "OldObj", BrowseName = "OldObj", ParentGobjectId = 0, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "OldObj", BrowseName = "OldObj", ParentGobjectId = 0, IsArea = false }
|
||||
};
|
||||
var attrs1 = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "OldObj", AttributeName = "OldAttr", FullTagReference = "OldObj.OldAttr", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "OldObj", AttributeName = "OldAttr", FullTagReference = "OldObj.OldAttr",
|
||||
MxDataType = 5, IsArray = false
|
||||
}
|
||||
};
|
||||
|
||||
var model1 = AddressSpaceBuilder.Build(hierarchy1, attrs1);
|
||||
@@ -32,11 +36,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
// Rebuild with new data
|
||||
var hierarchy2 = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "NewObj", BrowseName = "NewObj", ParentGobjectId = 0, IsArea = false }
|
||||
new() { GobjectId = 2, TagName = "NewObj", BrowseName = "NewObj", ParentGobjectId = 0, IsArea = false }
|
||||
};
|
||||
var attrs2 = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 2, TagName = "NewObj", AttributeName = "NewAttr", FullTagReference = "NewObj.NewAttr", MxDataType = 2, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "NewObj", AttributeName = "NewAttr", FullTagReference = "NewObj.NewAttr",
|
||||
MxDataType = 2, IsArray = false
|
||||
}
|
||||
};
|
||||
|
||||
var model2 = AddressSpaceBuilder.Build(hierarchy2, attrs2);
|
||||
@@ -47,29 +55,29 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that object counts are recalculated from the latest rebuild input.
|
||||
/// Confirms that object counts are recalculated from the latest rebuild input.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rebuild_UpdatesNodeCounts()
|
||||
{
|
||||
var hierarchy1 = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 0, IsArea = false },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "Obj2", BrowseName = "Obj2", ParentGobjectId = 0, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 0, IsArea = false },
|
||||
new() { GobjectId = 2, TagName = "Obj2", BrowseName = "Obj2", ParentGobjectId = 0, IsArea = false }
|
||||
};
|
||||
var model1 = AddressSpaceBuilder.Build(hierarchy1, new List<GalaxyAttributeInfo>());
|
||||
model1.ObjectCount.ShouldBe(2);
|
||||
|
||||
var hierarchy2 = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 3, TagName = "Obj3", BrowseName = "Obj3", ParentGobjectId = 0, IsArea = false }
|
||||
new() { GobjectId = 3, TagName = "Obj3", BrowseName = "Obj3", ParentGobjectId = 0, IsArea = false }
|
||||
};
|
||||
var model2 = AddressSpaceBuilder.Build(hierarchy2, new List<GalaxyAttributeInfo>());
|
||||
model2.ObjectCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that empty metadata produces an empty address-space model.
|
||||
/// Confirms that empty metadata produces an empty address-space model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyHierarchy_ProducesEmptyModel()
|
||||
@@ -81,4 +89,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
model.VariableCount.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that good bridge quality maps to an OPC UA good status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Good_MapsToGoodStatusCode()
|
||||
@@ -22,7 +22,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that bad bridge quality maps to an OPC UA bad status.
|
||||
/// Confirms that bad bridge quality maps to an OPC UA bad status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bad_MapsToBadStatusCode()
|
||||
@@ -32,7 +32,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that uncertain bridge quality maps to an OPC UA uncertain status.
|
||||
/// Confirms that uncertain bridge quality maps to an OPC UA uncertain status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Uncertain_MapsToUncertainStatusCode()
|
||||
@@ -42,7 +42,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that communication failures map to a bad OPC UA status code.
|
||||
/// Confirms that communication failures map to a bad OPC UA status code.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BadCommFailure_MapsCorrectly()
|
||||
@@ -52,7 +52,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA good status maps back to bridge good quality.
|
||||
/// Confirms that the OPC UA good status maps back to bridge good quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Good()
|
||||
@@ -62,7 +62,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA bad status maps back to bridge bad quality.
|
||||
/// Confirms that the OPC UA bad status maps back to bridge bad quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Bad()
|
||||
@@ -72,7 +72,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA uncertain status maps back to bridge uncertain quality.
|
||||
/// Confirms that the OPC UA uncertain status maps back to bridge uncertain quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromStatusCode_Uncertain()
|
||||
@@ -81,4 +81,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.OpcUa
|
||||
q.ShouldBe(Quality.Uncertain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Redundancy
|
||||
config.ServiceLevelBase.ShouldBe(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,45 +10,45 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Redundancy
|
||||
[Fact]
|
||||
public void Resolve_Disabled_ReturnsNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Warm", enabled: false).ShouldBe(RedundancySupport.None);
|
||||
RedundancyModeResolver.Resolve("Warm", false).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Warm_ReturnsWarm()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Warm", enabled: true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("Warm", true).ShouldBe(RedundancySupport.Warm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Hot_ReturnsHot()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Hot", enabled: true).ShouldBe(RedundancySupport.Hot);
|
||||
RedundancyModeResolver.Resolve("Hot", true).ShouldBe(RedundancySupport.Hot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Unknown_FallsBackToNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("Transparent", enabled: true).ShouldBe(RedundancySupport.None);
|
||||
RedundancyModeResolver.Resolve("Transparent", true).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_CaseInsensitive()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("warm", enabled: true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("WARM", enabled: true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("hot", enabled: true).ShouldBe(RedundancySupport.Hot);
|
||||
RedundancyModeResolver.Resolve("warm", true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("WARM", true).ShouldBe(RedundancySupport.Warm);
|
||||
RedundancyModeResolver.Resolve("hot", true).ShouldBe(RedundancySupport.Hot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Null_FallsBackToNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve(null!, enabled: true).ShouldBe(RedundancySupport.None);
|
||||
RedundancyModeResolver.Resolve(null!, true).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Empty_FallsBackToNone()
|
||||
{
|
||||
RedundancyModeResolver.Resolve("", enabled: true).ShouldBe(RedundancySupport.None);
|
||||
RedundancyModeResolver.Resolve("", true).ShouldBe(RedundancySupport.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,54 +6,54 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Redundancy
|
||||
{
|
||||
public class ServiceLevelCalculatorTests
|
||||
{
|
||||
private readonly ServiceLevelCalculator _calculator = new ServiceLevelCalculator();
|
||||
private readonly ServiceLevelCalculator _calculator = new();
|
||||
|
||||
[Fact]
|
||||
public void FullyHealthy_Primary_ReturnsBase()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)200);
|
||||
_calculator.Calculate(200, true, true).ShouldBe((byte)200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullyHealthy_Secondary_ReturnsBaseMinusFifty()
|
||||
{
|
||||
_calculator.Calculate(150, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)150);
|
||||
_calculator.Calculate(150, true, true).ShouldBe((byte)150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MxAccessDown_ReducesServiceLevel()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: false, dbConnected: true).ShouldBe((byte)100);
|
||||
_calculator.Calculate(200, false, true).ShouldBe((byte)100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DbDown_ReducesServiceLevel()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: true, dbConnected: false).ShouldBe((byte)150);
|
||||
_calculator.Calculate(200, true, false).ShouldBe((byte)150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BothDown_ReturnsZero()
|
||||
{
|
||||
_calculator.Calculate(200, mxAccessConnected: false, dbConnected: false).ShouldBe((byte)0);
|
||||
_calculator.Calculate(200, false, false).ShouldBe((byte)0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClampedTo255()
|
||||
{
|
||||
_calculator.Calculate(255, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)255);
|
||||
_calculator.Calculate(255, true, true).ShouldBe((byte)255);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClampedToZero()
|
||||
{
|
||||
_calculator.Calculate(50, mxAccessConnected: false, dbConnected: true).ShouldBe((byte)0);
|
||||
_calculator.Calculate(50, false, true).ShouldBe((byte)0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroBase_BothHealthy_ReturnsZero()
|
||||
{
|
||||
_calculator.Calculate(0, mxAccessConnected: true, dbConnected: true).ShouldBe((byte)0);
|
||||
_calculator.Calculate(0, true, true).ShouldBe((byte)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ using Xunit;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder unit test that keeps the unit test project wired into the solution.
|
||||
/// 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.
|
||||
/// Confirms that the unit test assembly is executing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Placeholder_ShouldPass()
|
||||
@@ -17,4 +17,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests
|
||||
true.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,4 +49,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Security
|
||||
config.CertificateSubject.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -134,4 +133,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Security
|
||||
names.Count.ShouldBe(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,14 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that a disconnected runtime is reported as unhealthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NotConnected_ReturnsUnhealthy()
|
||||
@@ -27,7 +27,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a connected runtime with no metrics history is still considered healthy.
|
||||
/// Confirms that a connected runtime with no metrics history is still considered healthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_NoMetrics_ReturnsHealthy()
|
||||
@@ -38,29 +38,29 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that good success-rate metrics keep the service in a healthy state.
|
||||
/// Confirms that good success-rate metrics keep the service in a healthy state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_GoodMetrics_ReturnsHealthy()
|
||||
{
|
||||
using var metrics = new PerformanceMetrics();
|
||||
for (int i = 0; i < 200; i++)
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true);
|
||||
for (var i = 0; i < 200; i++)
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10));
|
||||
|
||||
var result = _sut.CheckHealth(ConnectionState.Connected, metrics);
|
||||
result.Status.ShouldBe("Healthy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that poor operation success rates degrade the reported health state.
|
||||
/// Confirms that poor operation success rates degrade the reported health state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Connected_LowSuccessRate_ReturnsDegraded()
|
||||
{
|
||||
using var metrics = new PerformanceMetrics();
|
||||
for (int i = 0; i < 40; i++)
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true);
|
||||
for (int i = 0; i < 80; i++)
|
||||
for (var i = 0; i < 40; i++)
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10));
|
||||
for (var i = 0; i < 80; i++)
|
||||
metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false);
|
||||
|
||||
var result = _sut.CheckHealth(ConnectionState.Connected, metrics);
|
||||
@@ -69,7 +69,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean health helper reports true when the runtime is connected.
|
||||
/// Confirms that the boolean health helper reports true when the runtime is connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_Connected_ReturnsTrue()
|
||||
@@ -78,7 +78,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the boolean health helper reports false when the runtime is disconnected.
|
||||
/// Confirms that the boolean health helper reports false when the runtime is disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_Disconnected_ReturnsFalse()
|
||||
@@ -87,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the error connection state is treated as unhealthy.
|
||||
/// Confirms that the error connection state is treated as unhealthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Error_ReturnsUnhealthy()
|
||||
@@ -97,7 +97,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the reconnecting state is treated as unhealthy while recovery is in progress.
|
||||
/// Confirms that the reconnecting state is treated as unhealthy while recovery is in progress.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Reconnecting_ReturnsUnhealthy()
|
||||
@@ -106,4 +106,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
result.Status.ShouldBe("Unhealthy");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
@@ -10,12 +11,12 @@ 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.
|
||||
/// 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.
|
||||
/// Confirms that the generated HTML contains every dashboard panel expected by operators.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ContainsAllPanels()
|
||||
@@ -32,7 +33,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generated HTML includes the configured auto-refresh meta tag.
|
||||
/// Confirms that the generated HTML includes the configured auto-refresh meta tag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ContainsMetaRefresh()
|
||||
@@ -43,7 +44,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the connection panel renders the current runtime connection state.
|
||||
/// Confirms that the connection panel renders the current runtime connection state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_ConnectionPanel_ShowsState()
|
||||
@@ -54,7 +55,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy panel renders the bridged Galaxy name.
|
||||
/// Confirms that the Galaxy panel renders the bridged Galaxy name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_GalaxyPanel_ShowsName()
|
||||
@@ -65,7 +66,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the operations table renders the expected performance metric headers.
|
||||
/// Confirms that the operations table renders the expected performance metric headers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_OperationsTable_ShowsHeaders()
|
||||
@@ -81,7 +82,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the footer renders timestamp and version information.
|
||||
/// Confirms that the footer renders timestamp and version information.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateHtml_Footer_ContainsTimestampAndVersion()
|
||||
@@ -93,7 +94,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the generated JSON includes the major dashboard sections.
|
||||
/// Confirms that the generated JSON includes the major dashboard sections.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateJson_Deserializes()
|
||||
@@ -111,7 +112,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the report service reports healthy when the runtime connection is up.
|
||||
/// Confirms that the report service reports healthy when the runtime connection is up.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_WhenConnected_ReturnsTrue()
|
||||
@@ -121,7 +122,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the report service reports unhealthy when the runtime connection is down.
|
||||
/// Confirms that the report service reports unhealthy when the runtime connection is down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsHealthy_WhenDisconnected_ReturnsFalse()
|
||||
@@ -266,7 +267,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data.
|
||||
/// 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()
|
||||
@@ -295,7 +296,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
var galaxyStats = new GalaxyRepositoryStats { GalaxyName = "TestGalaxy", DbConnected = true };
|
||||
var redundancyConfig = new Host.Configuration.RedundancyConfiguration
|
||||
var redundancyConfig = new RedundancyConfiguration
|
||||
{
|
||||
Enabled = true,
|
||||
Mode = "Warm",
|
||||
@@ -307,4 +308,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
return sut;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,16 @@ 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.
|
||||
/// 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;
|
||||
private readonly StatusWebServer _server;
|
||||
|
||||
/// <summary>
|
||||
/// Starts a status web server on a random test port and prepares an HTTP client for endpoint assertions.
|
||||
/// Starts a status web server on a random test port and prepares an HTTP client for endpoint assertions.
|
||||
/// </summary>
|
||||
public StatusWebServerTests()
|
||||
{
|
||||
@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the test HTTP client and stops the status web server.
|
||||
/// Disposes the test HTTP client and stops the status web server.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -42,7 +42,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard root responds with HTML content.
|
||||
/// Confirms that the dashboard root responds with HTML content.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Root_ReturnsHtml200()
|
||||
@@ -53,7 +53,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the JSON status endpoint responds successfully.
|
||||
/// Confirms that the JSON status endpoint responds successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiStatus_ReturnsJson200()
|
||||
@@ -64,7 +64,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the health endpoint returns HTTP 200 when the bridge is healthy.
|
||||
/// Confirms that the health endpoint returns HTTP 200 when the bridge is healthy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiHealth_Returns200WhenHealthy()
|
||||
@@ -77,7 +77,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unknown dashboard routes return HTTP 404.
|
||||
/// Confirms that unknown dashboard routes return HTTP 404.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnknownPath_Returns404()
|
||||
@@ -87,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that unsupported HTTP methods are rejected with HTTP 405.
|
||||
/// Confirms that unsupported HTTP methods are rejected with HTTP 405.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task PostMethod_Returns405()
|
||||
@@ -97,7 +97,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that cache-control headers disable caching for dashboard responses.
|
||||
/// Confirms that cache-control headers disable caching for dashboard responses.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CacheHeaders_Present()
|
||||
@@ -108,7 +108,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the /health route returns an HTML health page.
|
||||
/// Confirms that the /health route returns an HTML health page.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HealthPage_ReturnsHtml200()
|
||||
@@ -122,7 +122,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that /api/health returns rich JSON with component health details.
|
||||
/// Confirms that /api/health returns rich JSON with component health details.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiHealth_ReturnsRichJson()
|
||||
@@ -137,7 +137,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the server can be started and stopped cleanly.
|
||||
/// Confirms that the server can be started and stopped cleanly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StartStop_DoesNotThrow()
|
||||
@@ -150,4 +150,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
server2.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,17 @@ using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies: Galaxy change detection → OnGalaxyChanged → address space rebuild
|
||||
/// Verifies: Galaxy change detection → OnGalaxyChanged → address space rebuild
|
||||
/// </summary>
|
||||
public class ChangeDetectionToRebuildWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a changed deploy timestamp causes the change-detection pipeline to raise another rebuild signal.
|
||||
/// Confirms that a changed deploy timestamp causes the change-detection pipeline to raise another rebuild signal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangedTimestamp_TriggersRebuild()
|
||||
@@ -27,16 +26,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
LastDeployTime = new DateTime(2024, 1, 1),
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 0, IsArea = false }
|
||||
new() { GobjectId = 1, TagName = "Obj1", BrowseName = "Obj1", ParentGobjectId = 0, IsArea = false }
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "Obj1", AttributeName = "Attr1", FullTagReference = "Obj1.Attr1", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "Obj1", AttributeName = "Attr1", FullTagReference = "Obj1.Attr1",
|
||||
MxDataType = 5, IsArray = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rebuildCount = 0;
|
||||
var service = new ChangeDetectionService(repo, intervalSeconds: 1);
|
||||
var service = new ChangeDetectionService(repo, 1);
|
||||
service.OnGalaxyChanged += () => Interlocked.Increment(ref rebuildCount);
|
||||
|
||||
service.Start();
|
||||
@@ -52,4 +55,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
service.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,17 @@ using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies: FakeMxProxy OnDataChange → MxAccessClient → OnTagValueChanged → node manager delivery
|
||||
/// Verifies: FakeMxProxy OnDataChange → MxAccessClient → OnTagValueChanged → node manager delivery
|
||||
/// </summary>
|
||||
public class MxAccessToNodeManagerWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a simulated data change reaches the global tag-value-changed event.
|
||||
/// Confirms that a simulated data change reaches the global tag-value-changed event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DataChange_ReachesGlobalHandler()
|
||||
@@ -37,7 +36,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a simulated data change reaches the stored per-tag subscription callback.
|
||||
/// Confirms that a simulated data change reaches the stored per-tag subscription callback.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DataChange_ReachesSubscriptionCallback()
|
||||
@@ -52,4 +51,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
received.Value.Value.ShouldBe(99);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies: OPC UA Read → NodeManager → IMxAccessClient.ReadAsync with correct full_tag_reference
|
||||
/// Verifies: OPC UA Read → NodeManager → IMxAccessClient.ReadAsync with correct full_tag_reference
|
||||
/// </summary>
|
||||
public class OpcUaReadToMxAccessWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the resolved OPC UA read path uses the expected full Galaxy tag reference.
|
||||
/// Confirms that the resolved OPC UA read path uses the expected full Galaxy tag reference.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Read_ResolvesCorrectTagReference()
|
||||
@@ -24,12 +24,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 0, IsArea = false },
|
||||
new GalaxyObjectInfo { GobjectId = 2, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", BrowseName = "DelmiaReceiver", ParentGobjectId = 1, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 0,
|
||||
IsArea = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver",
|
||||
BrowseName = "DelmiaReceiver", ParentGobjectId = 1, IsArea = false
|
||||
}
|
||||
};
|
||||
var attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 2, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 2, TagName = "DelmiaReceiver_001", AttributeName = "DownloadPath",
|
||||
FullTagReference = "DelmiaReceiver_001.DownloadPath", MxDataType = 5, IsArray = false
|
||||
}
|
||||
};
|
||||
|
||||
var model = AddressSpaceBuilder.Build(hierarchy, attributes);
|
||||
@@ -44,4 +56,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
vtq.Quality.ShouldBe(Quality.Good);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -10,12 +9,12 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies: OPC UA Write → NodeManager → IMxAccessClient.WriteAsync with correct tag+value
|
||||
/// Verifies: OPC UA Write → NodeManager → IMxAccessClient.WriteAsync with correct tag+value
|
||||
/// </summary>
|
||||
public class OpcUaWriteToMxAccessWiringTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that the resolved OPC UA write path targets the expected Galaxy tag reference and payload.
|
||||
/// Confirms that the resolved OPC UA write path targets the expected Galaxy tag reference and payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Write_SendsCorrectTagAndValue()
|
||||
@@ -24,11 +23,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
|
||||
var hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 0, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestMachine_001", BrowseName = "TestMachine_001", ParentGobjectId = 0,
|
||||
IsArea = false
|
||||
}
|
||||
};
|
||||
var attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestMachine_001", AttributeName = "MachineCode", FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestMachine_001", AttributeName = "MachineCode",
|
||||
FullTagReference = "TestMachine_001.MachineCode", MxDataType = 5, IsArray = false
|
||||
}
|
||||
};
|
||||
|
||||
var model = AddressSpaceBuilder.Build(hierarchy, attributes);
|
||||
@@ -38,7 +45,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
var result = await mxClient.WriteAsync(tagRef, "NEW_CODE");
|
||||
|
||||
result.ShouldBe(true);
|
||||
mxClient.WrittenValues.ShouldContain(w => w.Tag == "TestMachine_001.MachineCode" && (string)w.Value == "NEW_CODE");
|
||||
mxClient.WrittenValues.ShouldContain(w =>
|
||||
w.Tag == "TestMachine_001.MachineCode" && (string)w.Value == "NEW_CODE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,12 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies: OpcUaService Start() creates and wires all components with fakes.
|
||||
/// Verifies: OpcUaService Start() creates and wires all components with fakes.
|
||||
/// </summary>
|
||||
public class ServiceStartupSequenceTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that startup with fake dependencies creates the expected bridge components and state.
|
||||
/// Confirms that startup with fake dependencies creates the expected bridge components and state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Start_WithFakes_AllComponentsCreated()
|
||||
@@ -38,11 +38,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
|
||||
}
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr", FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr",
|
||||
FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,7 +82,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that when MXAccess is initially unavailable, the background monitor reconnects it later.
|
||||
/// Confirms that when MXAccess is initially unavailable, the background monitor reconnects it later.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Start_WhenMxAccessIsInitiallyDown_MonitorReconnectsInBackground()
|
||||
@@ -103,11 +110,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
|
||||
}
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>
|
||||
{
|
||||
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr", FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false }
|
||||
new()
|
||||
{
|
||||
GobjectId = 1, TagName = "TestObj", AttributeName = "TestAttr",
|
||||
FullTagReference = "TestObj.TestAttr", MxDataType = 5, IsArray = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -132,4 +146,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies: Start then Stop completes within 30 seconds. (SVC-004)
|
||||
/// Verifies: Start then Stop completes within 30 seconds. (SVC-004)
|
||||
/// </summary>
|
||||
public class ShutdownCompletesTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirms that a started service can shut down within the required time budget.
|
||||
/// Confirms that a started service can shut down within the required time budget.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Shutdown_CompletesWithin30Seconds()
|
||||
@@ -41,4 +38,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Wiring
|
||||
sw.Elapsed.TotalSeconds.ShouldBeLessThan(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.374.126" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.374.126" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit" Version="2.9.3"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.2.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0"/>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.374.126"/>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.374.126"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Host\ZB.MOM.WW.LmxOpcUa.Host.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Host\ZB.MOM.WW.LmxOpcUa.Host.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="ArchestrA.MxAccess">
|
||||
<HintPath>..\..\lib\ArchestrA.MxAccess.dll</HintPath>
|
||||
<EmbedInteropTypes>false</EmbedInteropTypes>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="ArchestrA.MxAccess">
|
||||
<HintPath>..\..\lib\ArchestrA.MxAccess.dll</HintPath>
|
||||
<EmbedInteropTypes>false</EmbedInteropTypes>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Host\appsettings.json" Link="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\ZB.MOM.WW.LmxOpcUa.Host\appsettings.json" Link="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user