fix(opcuaclient): review fixes - drop driver-side OperationLimits chunking (SDK batches), diagnostics as OTel counter, safe discovery selection, options validation at load (#273 #275 #276 #277 #278)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+67
-8
@@ -20,7 +20,6 @@ public sealed class OpcUaClientTuningAndDiscoveryTests
|
||||
o.SubscriptionMaxNotificationsPerPublish.ShouldBe(0u);
|
||||
o.SubscriptionPriority.ShouldBe((byte)0);
|
||||
o.SubscriptionPublishingInterval.ShouldBeNull();
|
||||
o.HonorServerOperationLimits.ShouldBeTrue();
|
||||
o.DiscoveryUrl.ShouldBeNull();
|
||||
o.CertificateValidation.RejectUnknownRevocationStatus.ShouldBeFalse();
|
||||
o.CertificateValidation.IssuerStorePath.ShouldBeNull();
|
||||
@@ -33,8 +32,7 @@ public sealed class OpcUaClientTuningAndDiscoveryTests
|
||||
var o = OpcUaClientDriverFactoryExtensions.ParseOptions("d", """
|
||||
{"subscriptionPublishingInterval":"00:00:02","subscriptionKeepAliveCount":20,
|
||||
"subscriptionLifetimeCount":600,"subscriptionMaxNotificationsPerPublish":500,
|
||||
"subscriptionPriority":9,"honorServerOperationLimits":false,
|
||||
"discoveryUrl":"opc.tcp://lds:4840",
|
||||
"subscriptionPriority":9, "discoveryUrl":"opc.tcp://lds:4840","discoveryServerUri":"urn:plant:l1",
|
||||
"certificateValidation":{"rejectUnknownRevocationStatus":true,"issuerStorePath":"/pki/issuers"}}
|
||||
""");
|
||||
|
||||
@@ -43,8 +41,8 @@ public sealed class OpcUaClientTuningAndDiscoveryTests
|
||||
o.SubscriptionLifetimeCount.ShouldBe(600u);
|
||||
o.SubscriptionMaxNotificationsPerPublish.ShouldBe(500u);
|
||||
o.SubscriptionPriority.ShouldBe((byte)9);
|
||||
o.HonorServerOperationLimits.ShouldBeFalse();
|
||||
o.DiscoveryUrl.ShouldBe("opc.tcp://lds:4840");
|
||||
o.DiscoveryServerUri.ShouldBe("urn:plant:l1");
|
||||
o.CertificateValidation.RejectUnknownRevocationStatus.ShouldBeTrue();
|
||||
o.CertificateValidation.IssuerStorePath.ShouldBe("/pki/issuers");
|
||||
|
||||
@@ -119,21 +117,82 @@ public sealed class OpcUaClientTuningAndDiscoveryTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_prepends_discovered_urls_and_dedups_case_insensitively()
|
||||
public void ResolveEndpointCandidates_puts_configured_endpoints_before_discovered_and_dedups()
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions { EndpointUrls = ["opc.tcp://Static:1", "opc.tcp://found:2"] };
|
||||
|
||||
var merged = OpcUaClientDriver.ResolveEndpointCandidates(o, ["opc.tcp://found:2", "opc.tcp://new:3"]);
|
||||
|
||||
merged.ShouldBe(["opc.tcp://found:2", "opc.tcp://new:3", "opc.tcp://Static:1"]);
|
||||
merged.ShouldBe(["opc.tcp://Static:1", "opc.tcp://found:2", "opc.tcp://new:3"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpointCandidates_uses_the_single_EndpointUrl_after_discovered_when_no_list()
|
||||
public void ResolveEndpointCandidates_uses_the_single_EndpointUrl_before_discovered_when_no_list()
|
||||
{
|
||||
var o = new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://only:9" };
|
||||
|
||||
OpcUaClientDriver.ResolveEndpointCandidates(o, ["opc.tcp://d:1"]).ShouldBe(["opc.tcp://d:1", "opc.tcp://only:9"]);
|
||||
OpcUaClientDriver.ResolveEndpointCandidates(o, ["opc.tcp://d:1"]).ShouldBe(["opc.tcp://only:9", "opc.tcp://d:1"]);
|
||||
}
|
||||
|
||||
private static OpcUaClientDriver.DiscoveredEndpoint De(string appUri, string url) => new(appUri, url);
|
||||
|
||||
[Fact]
|
||||
public void SelectDiscoveredUrls_with_one_distinct_server_uses_it_and_dedups()
|
||||
{
|
||||
var urls = OpcUaClientDriver.SelectDiscoveredUrls(
|
||||
[De("urn:a", "opc.tcp://a:1"), De("urn:a", "opc.tcp://A:1"), De("urn:a", "opc.tcp://a:2")], null);
|
||||
|
||||
urls.ShouldBe(["opc.tcp://a:1", "opc.tcp://a:2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectDiscoveredUrls_with_several_servers_and_no_filter_fails_naming_them()
|
||||
{
|
||||
var ex = Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.SelectDiscoveredUrls(
|
||||
[De("urn:line1", "opc.tcp://l1:1"), De("urn:line2", "opc.tcp://l2:1")], null));
|
||||
|
||||
ex.Message.ShouldContain("urn:line1");
|
||||
ex.Message.ShouldContain("urn:line2");
|
||||
ex.Message.ShouldContain("DiscoveryServerUri");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectDiscoveredUrls_filter_picks_only_that_server_even_when_several_are_listed()
|
||||
{
|
||||
var urls = OpcUaClientDriver.SelectDiscoveredUrls(
|
||||
[De("urn:line1", "opc.tcp://l1:1"), De("urn:line2", "opc.tcp://l2:1"), De("urn:line2", "opc.tcp://l2:2")],
|
||||
"urn:line2");
|
||||
|
||||
urls.ShouldBe(["opc.tcp://l2:1", "opc.tcp://l2:2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectDiscoveredUrls_filter_matching_nothing_yields_empty_not_another_server()
|
||||
{
|
||||
OpcUaClientDriver.SelectDiscoveredUrls([De("urn:line1", "opc.tcp://l1:1")], "urn:other").ShouldBeEmpty();
|
||||
OpcUaClientDriver.SelectDiscoveredUrls([], null).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// ---- #273: validation at options load ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("""{"subscriptionKeepAliveCount":0}""")]
|
||||
[InlineData("""{"subscriptionKeepAliveCount":10,"subscriptionLifetimeCount":29}""")]
|
||||
[InlineData("""{"discoveryUrl":"not a uri"}""")]
|
||||
public void ParseOptions_rejects_out_of_range_tuning_with_a_clear_message(string json)
|
||||
{
|
||||
var ex = Should.Throw<InvalidOperationException>(
|
||||
() => OpcUaClientDriverFactoryExtensions.ParseOptions("drv-bad", json));
|
||||
|
||||
ex.Message.ShouldContain("drv-bad");
|
||||
ex.Message.ShouldContain("invalid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Factory_CreateInstance_fails_fast_on_a_bad_tuning_config()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriverFactoryExtensions.CreateInstance(
|
||||
"drv-bad", """{"subscriptionLifetimeCount":5}"""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user