341417f193
- Ab.Transport: AbOpcUaStatusMap is the single AbStatus->OPC UA table; AbCip/AbLegacy mappers delegate. - Remove dead MapCipGeneralStatus / MapPcccStatus / AbLegacy MapException (+ tests). - LibplctagOrphanRelease is one source file (Ab.LibplctagShared) compiled into both drivers. - LibplctagConfinementGuardTests: tokenising comment/literal stripper, block comments, csproj PackageReference guard. - LibplctagStatusTranslationTests: real LibPlcTagException round trip, WrapSync/Wrap OCE pass-through, adapter routes decode via WrapSync. - AbCipTypeMismatchTests: libplctag width-mismatch path (BadTypeMismatch, handle kept, no Degraded, warn once). - Host: AddAbCipDriverDefaults extension + Program.cs source pin for Drivers:AbCip:DefaultTransport. - docs/drivers/AbCip.md: layout rows + benign Degraded-after-outage note. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
175 lines
8.0 KiB
C#
175 lines
8.0 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
|
|
|
|
/// <summary>
|
|
/// Final review I1 — the node-level <c>Drivers:AbCip:DefaultTransport</c>: binding (appsettings and the
|
|
/// <c>__</c> env-var form), precedence (explicit per-driver key > node default > compiled Native), the
|
|
/// probe agreeing with the driver factory, and fail-fast validation. Every precedence test runs a pinned
|
|
/// node (Libplctag) against the compiled default (Native), so a wiring that ignores the node value goes red.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
[Collection(AbTransportSharedPoolCollection.Name)] // resolves the registry, which configures the shared pool
|
|
public sealed class AbCipDriverDefaultsOptionsTests
|
|
{
|
|
private const string NoKey = """{"devices":[{"hostAddress":"ab://10.0.0.1/1,0"}],"probe":{"enabled":false}}""";
|
|
private const string ExplicitNative =
|
|
"""{"transport":"Native","devices":[{"hostAddress":"ab://10.0.0.1/1,0"}],"probe":{"enabled":false}}""";
|
|
|
|
private static ServiceProvider Build(IConfiguration configuration)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddSingleton<ISecretResolver>(new StubSecretResolver());
|
|
services.AddAbCipDriverDefaults(configuration);
|
|
services.AddOtOpcUaDriverFactories();
|
|
return services.BuildServiceProvider();
|
|
}
|
|
|
|
private static IConfiguration Config(string? value) =>
|
|
new ConfigurationBuilder()
|
|
.AddInMemoryCollection(value is null
|
|
? []
|
|
: new Dictionary<string, string?> { ["Drivers:AbCip:DefaultTransport"] = value })
|
|
.Build();
|
|
|
|
private static AbCipDriver Create(ServiceProvider sp, string json) =>
|
|
(AbCipDriver)sp.GetRequiredService<DriverFactoryRegistry>().TryGet(DriverTypeNames.AbCip)!("drv-i1", json);
|
|
|
|
/// <summary>
|
|
/// #585 composition-root pin: <c>Program.cs</c> is top-level statements and cannot be booted in a unit test, so
|
|
/// the registration lives in <see cref="AbCipDriverDefaultsRegistration.AddAbCipDriverDefaults"/> (which every
|
|
/// test here builds through) and this asserts Program.cs still CALLS it. Deleting the call silently ignores an
|
|
/// operator's <c>Drivers:AbCip:DefaultTransport</c> pin — every behaviour test above would stay green.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Program_registers_the_node_default_transport()
|
|
{
|
|
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) dir = dir.Parent;
|
|
dir.ShouldNotBeNull();
|
|
var program = File.ReadAllText(Path.Combine(dir.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.Host", "Program.cs"));
|
|
|
|
System.Text.RegularExpressions.Regex.IsMatch(
|
|
program, @"^builder\.Services\s*\.\s*AddAbCipDriverDefaults\(\s*builder\.Configuration\s*\)\s*;",
|
|
System.Text.RegularExpressions.RegexOptions.Multiline)
|
|
.ShouldBeTrue("Program.cs must call builder.Services.AddAbCipDriverDefaults(builder.Configuration), unconditionally " +
|
|
"(at top level, not inside an if (hasDriver/hasAdmin) block — the probe on admin nodes needs it too)");
|
|
}
|
|
|
|
[Fact]
|
|
public void Absent_section_defaults_to_Native()
|
|
{
|
|
using var sp = Build(Config(null));
|
|
sp.GetRequiredService<IOptions<AbCipDriverDefaultsOptions>>().Value.ResolveDefaultTransport()
|
|
.ShouldBe(AbCipTransport.Native);
|
|
Create(sp, NoKey).EffectiveTransport.ShouldBe(AbCipTransport.Native);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Libplctag", AbCipTransport.Libplctag)]
|
|
[InlineData("libplctag", AbCipTransport.Libplctag)]
|
|
[InlineData("NATIVE", AbCipTransport.Native)]
|
|
[InlineData("", AbCipTransport.Native)]
|
|
public void The_appsettings_key_binds(string value, AbCipTransport expected)
|
|
{
|
|
using var sp = Build(Config(value));
|
|
sp.GetRequiredService<IOptions<AbCipDriverDefaultsOptions>>().Value.ResolveDefaultTransport()
|
|
.ShouldBe(expected);
|
|
}
|
|
|
|
/// <summary>The documented env-var override form, <c>Drivers__AbCip__DefaultTransport</c>.</summary>
|
|
[Fact]
|
|
public void The_double_underscore_environment_variable_binds()
|
|
{
|
|
const string prefix = "OTOPCUA_I1_TEST_";
|
|
Environment.SetEnvironmentVariable(prefix + "Drivers__AbCip__DefaultTransport", "Libplctag");
|
|
try
|
|
{
|
|
var configuration = new ConfigurationBuilder().AddEnvironmentVariables(prefix).Build();
|
|
using var sp = Build(configuration);
|
|
Create(sp, NoKey).EffectiveTransport.ShouldBe(AbCipTransport.Libplctag);
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable(prefix + "Drivers__AbCip__DefaultTransport", null);
|
|
}
|
|
}
|
|
|
|
/// <summary>A pinned node moves no-key drivers; an explicit per-driver key still wins over the pin.</summary>
|
|
[Fact]
|
|
public void Node_default_applies_only_to_drivers_without_a_key()
|
|
{
|
|
using var sp = Build(Config("Libplctag"));
|
|
Create(sp, NoKey).EffectiveTransport.ShouldBe(AbCipTransport.Libplctag);
|
|
Create(sp, ExplicitNative).EffectiveTransport.ShouldBe(AbCipTransport.Native);
|
|
}
|
|
|
|
/// <summary>The Test Connect probe resolves an absent key to the SAME node default the driver does.</summary>
|
|
[Theory]
|
|
[InlineData("Libplctag", AbCipTransport.Libplctag)]
|
|
[InlineData(null, AbCipTransport.Native)]
|
|
public void The_probe_uses_the_node_default(string? value, AbCipTransport expected)
|
|
{
|
|
using var sp = Build(Config(value));
|
|
var probe = sp.GetServices<IDriverProbe>().OfType<AbCipDriverProbe>().ShouldHaveSingleItem();
|
|
probe.DefaultTransport.ShouldBe(expected);
|
|
Create(sp, NoKey).EffectiveTransport.ShouldBe(expected); // driver and probe agree
|
|
}
|
|
|
|
/// <summary>The probe stays registered exactly once on a fused node (driver path + admin path).</summary>
|
|
[Fact]
|
|
public void The_factory_built_probe_is_still_deduplicated()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddOtOpcUaDriverProbes();
|
|
services.AddOtOpcUaDriverProbes();
|
|
using var sp = services.BuildServiceProvider();
|
|
sp.GetServices<IDriverProbe>().OfType<AbCipDriverProbe>().Count().ShouldBe(1);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Bogus")]
|
|
[InlineData("1")]
|
|
[InlineData("7")]
|
|
[InlineData("Native,Libplctag")]
|
|
public void A_bad_value_fails_validation_with_a_named_message(string value)
|
|
{
|
|
var result = new AbCipDriverDefaultsOptionsValidator().Validate(
|
|
null, new AbCipDriverDefaultsOptions { DefaultTransport = value });
|
|
result.Failed.ShouldBeTrue();
|
|
result.Failures.ShouldContain(f => f.Contains("Drivers:AbCip:DefaultTransport") && f.Contains(value));
|
|
|
|
using var sp = Build(Config(value));
|
|
Should.Throw<OptionsValidationException>(
|
|
() => sp.GetRequiredService<IOptions<AbCipDriverDefaultsOptions>>().Value)
|
|
.Message.ShouldContain("Drivers:AbCip:DefaultTransport");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Native")]
|
|
[InlineData("libplctag")]
|
|
[InlineData(null)]
|
|
[InlineData(" ")]
|
|
public void A_good_or_blank_value_validates(string? value) =>
|
|
new AbCipDriverDefaultsOptionsValidator()
|
|
.Validate(null, new AbCipDriverDefaultsOptions { DefaultTransport = value })
|
|
.Succeeded.ShouldBeTrue();
|
|
|
|
private sealed class StubSecretResolver : ISecretResolver
|
|
{
|
|
public Task<string?> GetAsync(SecretName name, CancellationToken ct) => Task.FromResult<string?>(null);
|
|
}
|
|
}
|