fix(ablegacy): review fixes - one family-aware address authority in Contracts, deploy gate + editor check, malformed address -> BadNodeIdUnknown warned once, probe normalised, octal/migration docs (#244 #245 #247)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+68
@@ -249,4 +249,72 @@ public sealed class AbLegacyFamilyAddressingTests
|
||||
ex.Status.ShouldBe(AbStatus.Unsupported);
|
||||
ex.Message.ShouldContain(why, Case.Insensitive);
|
||||
}
|
||||
|
||||
private sealed class CapturingLogger : Microsoft.Extensions.Logging.ILogger<AbLegacyDriver>
|
||||
{
|
||||
public readonly System.Collections.Concurrent.ConcurrentQueue<string> Warnings = new();
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => true;
|
||||
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel level, Microsoft.Extensions.Logging.EventId id,
|
||||
TState state, Exception? ex, Func<TState, Exception?, string> f)
|
||||
{
|
||||
if (level == Microsoft.Extensions.Logging.LogLevel.Warning) Warnings.Enqueue(f(state, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A PLC-5 address that no longer parses (the pre-octal-era decimal 8/9 digit) is a config error: BadNodeIdUnknown on
|
||||
/// read and write, no runtime created, health untouched, and ONE warning per tag however often it is polled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Malformed_address_is_BadNodeIdUnknown_warned_once_and_never_throws()
|
||||
{
|
||||
var factory = new FakeAbLegacyTagFactory();
|
||||
var logger = new CapturingLogger();
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Plc5)],
|
||||
RawTags = AbLegacyRawTags.Entries(
|
||||
new AbLegacyTagDefinition("Bad", "ab://10.0.0.5/1,0", "I:018", AbLegacyDataType.Int, Writable: true),
|
||||
new AbLegacyTagDefinition("Good", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int)),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, "drv-1", factory, logger);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 };
|
||||
|
||||
var first = await drv.ReadAsync(["Bad", "Good"], CancellationToken.None);
|
||||
var second = await drv.ReadAsync(["Bad"], CancellationToken.None);
|
||||
var write = await drv.WriteAsync([new WriteRequest("Bad", 1)], CancellationToken.None);
|
||||
|
||||
first[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
second[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
write[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
first[1].StatusCode.ShouldBe(AbLegacyStatusMapper.Good); // positive control: the refusal is per tag
|
||||
factory.Tags.Keys.ShouldNotContain(k => k.StartsWith("I:"));
|
||||
logger.Warnings.Count(w => w.Contains("not valid for the device's PLC family") && w.Contains("I:018")).ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>The connectivity-probe address is normalised like tag paths (PLC-5 octal I/O handed over decimal).</summary>
|
||||
[Fact]
|
||||
public async Task Probe_address_is_normalised_like_tag_paths()
|
||||
{
|
||||
var factory = new FakeAbLegacyTagFactory();
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Plc5)],
|
||||
Probe = new AbLegacyProbeOptions
|
||||
{
|
||||
Enabled = true, Interval = TimeSpan.FromMilliseconds(50), Timeout = TimeSpan.FromMilliseconds(200),
|
||||
ProbeAddress = "I:017",
|
||||
},
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||
while (!factory.Tags.ContainsKey("I:15") && DateTime.UtcNow < deadline) await Task.Delay(20);
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
|
||||
factory.Tags.ContainsKey("I:15").ShouldBeTrue();
|
||||
factory.Tags.ContainsKey("I:017").ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>#244/#245/#247: one address authority (AbLegacyAddress.Validate) behind the editor and the deploy gate.</summary>
|
||||
public sealed class AbLegacyFamilyAddressGateTests
|
||||
{
|
||||
private static string Cfg(string address) => "{\"address\":\"" + address + "\",\"dataType\":\"Int\"}";
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"PlcFamily\":\"Plc5\"}", "I:018", true)] // octal 8 on PLC-5
|
||||
[InlineData("{\"PlcFamily\":\"Plc5\"}", "I:017/07", false)]
|
||||
[InlineData("{\"PlcFamily\":\"Slc500\"}", "I:018", false)] // control: decimal on SLC
|
||||
[InlineData("{\"plcFamily\":\"plc5\"}", "I:018", true)] // key + value case-insensitive
|
||||
[InlineData("{\"PlcFamily\":\"Slc500\"}", "RTC:0.HR", true)] // function file off MicroLogix
|
||||
[InlineData("{\"PlcFamily\":\"MicroLogix\"}", "RTC:0.HR", false)]
|
||||
[InlineData(null, "RTC:0.HR", true)] // absent family = runtime default SLC 500
|
||||
[InlineData("{\"PlcFamily\":\"Bogus\"}", "I:018", false)] // unknown family: never stricter than the runtime
|
||||
[InlineData("{\"PlcFamily\":\"Slc500\"}", "Q9:0", true)]
|
||||
public void Gate_checks_the_address_against_the_device_family(string? deviceConfig, string address, bool warns)
|
||||
{
|
||||
var w = EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg(address), deviceConfig);
|
||||
(w.Count > 0).ShouldBe(warns, string.Join("; ", w));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate_warns_about_indirect_addresses_which_never_read()
|
||||
{
|
||||
EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg("N7:[N7:0]"), "{\"PlcFamily\":\"Slc500\"}")
|
||||
.ShouldContain(x => x.Contains("indirect"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("N7:0", null)]
|
||||
[InlineData("I:018", null)] // family unknown to the editor: another family accepts it
|
||||
[InlineData("RTC:0.HR", null)] // MicroLogix accepts it
|
||||
[InlineData("Q9:0", "any PLC family")]
|
||||
public void Editor_validates_only_the_family_independent_shape(string address, string? error)
|
||||
{
|
||||
var result = AbLegacyTagConfigModel.FromJson(Cfg(address)).Validate();
|
||||
if (error is null) result.ShouldBeNull(); else result!.ShouldContain(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Editor_is_never_stricter_than_the_gate()
|
||||
{
|
||||
const string address = "Q9:0";
|
||||
AbLegacyTagConfigModel.FromJson(Cfg(address)).Validate().ShouldNotBeNull();
|
||||
EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg(address), null).ShouldNotBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -346,7 +346,7 @@ public sealed class RawTagConfigGateTests
|
||||
public void A_browse_committed_config_passes_the_gate(string driverType)
|
||||
{
|
||||
var tagConfig = RawBrowseCommitMapper.BuildTagConfig(
|
||||
driverType, "SomeAddress", BrowseFieldsFor(driverType));
|
||||
driverType, driverType == DriverTypeNames.AbLegacy ? "N7:0" : "SomeAddress", BrowseFieldsFor(driverType));
|
||||
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldBeNull(
|
||||
$"browse-commit for {driverType} must not author a config the import refuses");
|
||||
|
||||
Reference in New Issue
Block a user