diff --git a/docs/drivers/README.md b/docs/drivers/README.md index 0a06ffff..9982505f 100644 --- a/docs/drivers/README.md +++ b/docs/drivers/README.md @@ -43,11 +43,13 @@ Reading the Tags column: | Cell | Meaning | |---|---| -| `—` | the driver cannot report skips. **Not** the same as reporting none. | -| `12 tags` | the driver reports, and skipped nothing. | +| `—` | **no answer.** Either the driver cannot report skips, or it never got as far as building a tag table (it faulted during initialise). Deliberately *not* the same as reporting none. | +| `12 tags` | the driver built a table, and skipped nothing. | | `20 / 22 skipped` (amber) | partly working — some tags poll, some do not. | | `22 / 22 skipped` (red) | polling **nothing**. The state most likely to look healthy, because it has no failing reads to report. | +That first row is a distinction the live rig forced. A pre-fix build rendered `0 tags` for two drivers that had faulted during initialise — each with two authored tags — because a never-built ledger returned a zero report. A zero report is a *positive claim* ("this driver skipped nothing"), and making it on behalf of a driver that never looked is the same quiet inaccuracy the column exists to remove. `GetSkippedTags()` now returns null until the first committed build. + Three properties worth knowing: - **A skip never changes driver state.** Latching non-Healthy for a config problem would satisfy both halves of the [#527](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/527) comms-loss conjunction — *90 s without a successful read AND non-Healthy* — and a driver whose tags were all skipped has no successful reads by construction, so it would reconnect-loop forever against a device that is perfectly fine. The signal is additive to the state, not a change of it. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISkippedTagReporter.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISkippedTagReporter.cs index dec8a980..eead5914 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISkippedTagReporter.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISkippedTagReporter.cs @@ -44,8 +44,15 @@ public interface ISkippedTagReporter /// Must be a pure in-memory read with no I/O — it is called on the health-publish path, is /// deliberately NOT routed through the resilience pipeline, and must never block. /// - /// The current report; never null. An all-zero report means "nothing skipped". - SkippedTagReport GetSkippedTags(); + /// + /// The report from the driver's most recent tag-table build, or when it has + /// not built one yet — a driver that faulted during initialise has no answer, which is not + /// the same as the answer "nothing skipped". Returning a zero report there would state that a + /// driver with a dozen authored tags has none, which is the class of quiet inaccuracy this whole + /// capability exists to remove. (Found on the docker-dev rig, where two faulted drivers each + /// rendered "0 tags" against two authored ones.) + /// + SkippedTagReport? GetSkippedTags(); } /// One authored tag a driver could not build into its poll table, and why. @@ -71,9 +78,6 @@ public sealed record SkippedTag(string RawPath, string Reason); /// public sealed record SkippedTagReport(int AuthoredCount, int SkippedCount, IReadOnlyList Sample) { - /// A driver that has authored nothing and skipped nothing. - public static readonly SkippedTagReport Empty = new(0, 0, []); - /// Whether any authored tag was skipped — i.e. whether this is worth an operator's attention. public bool HasSkips => SkippedCount > 0; @@ -113,7 +117,9 @@ public sealed class SkippedTagLedger public const int MaxSample = 20; private readonly object _gate = new(); - private SkippedTagReport _visible = SkippedTagReport.Empty; + + /// Null until the first — "no answer yet", never a zero report. + private SkippedTagReport? _visible; private int _stagedAuthored; private int _stagedSkipped; @@ -168,8 +174,9 @@ public sealed class SkippedTagLedger } /// The current published snapshot. - /// The last committed report, or before the first commit. - public SkippedTagReport Snapshot() + /// The last committed report, or before the first commit — a driver + /// that has not built a tag table has no answer, and a zero report would be a false one. + public SkippedTagReport? Snapshot() { lock (_gate) return _visible; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs index 3ee99b70..2d6c6d06 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -31,7 +31,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); private AbCipDriverOptions _options; private readonly string _driverInstanceId; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs index ea28176b..3d6aed9b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs @@ -17,7 +17,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); /// Mutable so can adopt a re-parsed config on reinitialize /// (#516). Only ever written on the init path, before any reader/session uses it. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriver.cs index 92f0cc14..3c1d9f2f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriver.cs @@ -42,7 +42,7 @@ public sealed class CalculationDriver : IDriver, IDependencyConsumer, ISubscriba private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); // OPC UA status codes surfaced by the calc driver. private const uint StatusGood = 0u; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs index 0dc16118..d06529be 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs @@ -27,7 +27,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); /// Mutable so can adopt a re-parsed config (#516). Written only /// on the init path, together with , before either is used. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs index b1366ea2..153160c6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs @@ -45,7 +45,7 @@ public sealed class GalaxyDriver private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); private readonly string _driverInstanceId; private readonly GalaxyDriverOptions _options; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs index bf313b20..6302aa9f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs @@ -79,7 +79,7 @@ public sealed class MTConnectDriver private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); /// /// The Agent could not be reached, answered unusably, or blew the per-call deadline. Distinct diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs index 9f1153f6..45d358f5 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs @@ -25,7 +25,7 @@ public sealed class ModbusDriver private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); // ---- instance fields (grouped at top for auditability) ---- diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/MqttDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/MqttDriver.cs index f1fee36c..ab4d3758 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/MqttDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/MqttDriver.cs @@ -70,7 +70,7 @@ public sealed class MqttDriver private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); // ---- Write status codes (Gitea #508) ---- // diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs index 28639053..80ddbd8e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs @@ -37,7 +37,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); /// /// Recomputes the skip report from the freshly-built map. Derived by re-asking the map to resolve diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs index 89c7a6c8..04ebf8ae 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs @@ -36,7 +36,7 @@ public sealed class S7Driver private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); private readonly string _driverInstanceId; private readonly ILogger _logger; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs index 2058d4cb..bdaa1702 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs @@ -46,7 +46,7 @@ public sealed class SqlDriver private IReadOnlyList _blobSkips = []; /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); /// /// Republishes the skip ledger as the union of the blob-level skips and the catalog gate's diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs index 210cd29a..1775959f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs @@ -17,7 +17,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery private readonly SkippedTagLedger _skippedTags = new(); /// - public SkippedTagReport GetSkippedTags() => _skippedTags.Snapshot(); + public SkippedTagReport? GetSkippedTags() => _skippedTags.Snapshot(); // Mutable so ReinitializeAsync can apply a new config generation. The constructor seeds // it; InitializeAsync re-parses driverConfigJson over the top of it. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Hosts.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Hosts.razor index 0957c267..80ca1f26 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Hosts.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Hosts.razor @@ -234,8 +234,11 @@ else Healthy, Errors/5 min stays 0, Last error stays empty, and Last read keeps advancing off the tags that DID map. On the Makino in #540 that read as a perfectly healthy driver ignoring 20 of its 22 - tags. "—" means the driver cannot report skips, which is distinct - from reporting none. *@ + tags. "—" means NO ANSWER — either the driver cannot report skips, + or it faulted before building a tag table — which is distinct from + the positive claim "this driver skipped nothing". A pre-fix build + rendered "0 tags" for two faulted drivers on the docker-dev rig + that had two authored tags apiece. *@ @if (d.SkippedTags is null) { — diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/HostsDriverView.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/HostsDriverView.cs index 0cadfb89..62e752d5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/HostsDriverView.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/HostsDriverView.cs @@ -44,9 +44,10 @@ public sealed record HostsDriverInstanceInfo(string DriverInstanceId, string Clu /// Per-host connectivity for a multi-device driver, ordered by host name; null /// when the driver reports no per-host detail. Lets one unreachable device show even while the driver /// row itself is Healthy. -/// Authored tags the driver could not build into its poll table; null when the -/// driver cannot report them. Lets "ignoring 20 of 22 tags" show even while every other column on the -/// row — state, last read, error count, last error — reads perfectly healthy (Gitea #541). +/// Authored tags the driver could not build into its poll table; null when there +/// is no answer — the driver cannot report skips, or it faulted before building a tag table. Lets +/// "ignoring 20 of 22 tags" show even while every other column on the row — state, last read, error +/// count, last error — reads perfectly healthy (Gitea #541). public sealed record HostsDriverRow( string DriverInstanceId, string? Name, string? DriverType, string State, DateTime? LastSuccessfulReadUtc, string? LastError, int ErrorCount5Min, DateTime PublishedUtc, diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/SkippedTagLedgerTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/SkippedTagLedgerTests.cs index eb19b7d9..63a20cde 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/SkippedTagLedgerTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/SkippedTagLedgerTests.cs @@ -9,17 +9,45 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; /// public sealed class SkippedTagLedgerTests { + /// + /// A ledger that has never built reports NO ANSWER, not a clean bill of health. Caught on + /// the docker-dev rig: two drivers that faulted during initialise each rendered "0 tags" on + /// /hosts against two authored tags apiece. A zero report is a positive claim — "this driver + /// skipped nothing" — and making it on behalf of a driver that never got as far as looking is the + /// same species of quiet inaccuracy this capability exists to remove. + /// [Fact] - public void A_fresh_ledger_reports_nothing() - { - var report = new SkippedTagLedger().Snapshot(); + public void A_ledger_that_has_never_built_reports_no_answer_rather_than_a_clean_one() + => new SkippedTagLedger().Snapshot().ShouldBeNull(); - report.AuthoredCount.ShouldBe(0); + /// An empty deployment that DID build reports a zero report — distinct from the above. + [Fact] + public void A_committed_empty_build_reports_zero_rather_than_no_answer() + { + var ledger = new SkippedTagLedger(); + ledger.BeginRebuild(0); + ledger.Commit(); + + var report = ledger.Snapshot(); + report.ShouldNotBeNull(); + report!.AuthoredCount.ShouldBe(0); report.SkippedCount.ShouldBe(0); - report.Sample.ShouldBeEmpty(); report.HasSkips.ShouldBeFalse(); } + /// + /// An in-flight first build is still "no answer" — the staging state must not leak as a report. + /// + [Fact] + public void A_first_build_still_in_progress_reports_no_answer() + { + var ledger = new SkippedTagLedger(); + ledger.BeginRebuild(10); + ledger.Skip("a", "r"); + + ledger.Snapshot().ShouldBeNull(); + } + [Fact] public void A_committed_build_reports_its_skips_with_the_authored_denominator() { @@ -30,7 +58,7 @@ public sealed class SkippedTagLedgerTests ledger.Skip("Line1/Makino/Identity/Version", "invalid Address"); ledger.Commit(); - var report = ledger.Snapshot(); + var report = ledger.Snapshot()!; report.AuthoredCount.ShouldBe(22); report.SkippedCount.ShouldBe(2); report.Sample.Select(s => s.RawPath) @@ -54,7 +82,7 @@ public sealed class SkippedTagLedgerTests ledger.Skip("c", "r"); ledger.Commit(); - ledger.Snapshot().SkippedEverything.ShouldBeTrue(); + ledger.Snapshot()!.SkippedEverything.ShouldBeTrue(); } /// @@ -70,7 +98,7 @@ public sealed class SkippedTagLedgerTests ledger.BeginRebuild(0); ledger.Commit(); - ledger.Snapshot().SkippedEverything.ShouldBeFalse(); + ledger.Snapshot()!.SkippedEverything.ShouldBeFalse(); } /// @@ -86,7 +114,7 @@ public sealed class SkippedTagLedgerTests for (var i = 0; i < 500; i++) ledger.Skip($"tag-{i}", "TagConfig did not map to a definition"); ledger.Commit(); - var report = ledger.Snapshot(); + var report = ledger.Snapshot()!; report.SkippedCount.ShouldBe(500); report.Sample.Count.ShouldBe(SkippedTagLedger.MaxSample); report.Sample[0].RawPath.ShouldBe("tag-0", "the sample keeps the FIRST skips, in encounter order"); @@ -108,13 +136,13 @@ public sealed class SkippedTagLedgerTests ledger.BeginRebuild(10); ledger.Skip("new-1", "r"); - var midRebuild = ledger.Snapshot(); + var midRebuild = ledger.Snapshot()!; midRebuild.SkippedCount.ShouldBe(1); midRebuild.Sample.Single().RawPath.ShouldBe("old"); ledger.Skip("new-2", "r"); ledger.Commit(); - ledger.Snapshot().Sample.Select(s => s.RawPath).ShouldBe(["new-1", "new-2"]); + ledger.Snapshot()!.Sample.Select(s => s.RawPath).ShouldBe(["new-1", "new-2"]); } /// @@ -133,7 +161,7 @@ public sealed class SkippedTagLedgerTests ledger.Skip("would-have-been-reported", "invalid Address"); // …driver throws here; no Commit. - ledger.Snapshot().Sample.Single().RawPath.ShouldBe("known-bad"); + ledger.Snapshot()!.Sample.Single().RawPath.ShouldBe("known-bad"); } /// @@ -151,7 +179,7 @@ public sealed class SkippedTagLedgerTests ledger.BeginRebuild(2); ledger.Commit(); - var report = ledger.Snapshot(); + var report = ledger.Snapshot()!; report.HasSkips.ShouldBeFalse(); report.SkippedCount.ShouldBe(0); report.AuthoredCount.ShouldBe(2); @@ -167,7 +195,7 @@ public sealed class SkippedTagLedgerTests var ledger = new SkippedTagLedger(); Should.NotThrow(() => ledger.Skip("orphan", "r")); - ledger.Snapshot().SkippedCount.ShouldBe(0); + ledger.Snapshot().ShouldBeNull("a stray Skip must not conjure a report out of nothing"); } /// @@ -197,7 +225,7 @@ public sealed class SkippedTagLedgerTests { while (!writer.IsCompleted && !cts.IsCancellationRequested) { - var r = ledger.Snapshot(); + var r = ledger.Snapshot()!; // Every committed snapshot is internally consistent: the sample never exceeds either the // cap or the true count it is drawn from. r.Sample.Count.ShouldBeLessThanOrEqualTo(SkippedTagLedger.MaxSample); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasSkippedTagReportingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasSkippedTagReportingTests.cs index 35919234..75474ae9 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasSkippedTagReportingTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasSkippedTagReportingTests.cs @@ -42,7 +42,7 @@ public sealed class FocasSkippedTagReportingTests await drv.InitializeAsync("{}", CancellationToken.None); - var report = drv.GetSkippedTags(); + var report = drv.GetSkippedTags()!; report.AuthoredCount.ShouldBe(2); report.SkippedCount.ShouldBe(0); report.HasSkips.ShouldBeFalse(); @@ -75,7 +75,7 @@ public sealed class FocasSkippedTagReportingTests drv.GetHealth().LastError.ShouldBeNull(); // …and the fact that contradicts it. - var report = drv.GetSkippedTags(); + var report = drv.GetSkippedTags()!; report.AuthoredCount.ShouldBe(22); report.SkippedCount.ShouldBe(20); report.SkippedEverything.ShouldBeFalse("two tags do poll — this driver is partly working"); @@ -97,7 +97,7 @@ public sealed class FocasSkippedTagReportingTests await drv.InitializeAsync("{}", CancellationToken.None); - var report = drv.GetSkippedTags(); + var report = drv.GetSkippedTags()!; report.SkippedCount.ShouldBe(25); report.Sample.Count.ShouldBe(SkippedTagLedger.MaxSample); report.SkippedEverything.ShouldBeTrue("this driver polls nothing at all"); @@ -116,11 +116,11 @@ public sealed class FocasSkippedTagReportingTests new FocasTagDefinition("raw/Good", Host, "MACRO:500", FocasDataType.Float64)); await drv.InitializeAsync("{}", CancellationToken.None); - drv.GetSkippedTags().SkippedCount.ShouldBe(1); + drv.GetSkippedTags()!.SkippedCount.ShouldBe(1); await drv.InitializeAsync("{}", CancellationToken.None); - var report = drv.GetSkippedTags(); + var report = drv.GetSkippedTags()!; report.SkippedCount.ShouldBe(1, "the same one bad tag — not two"); report.AuthoredCount.ShouldBe(2); } @@ -145,7 +145,7 @@ public sealed class FocasSkippedTagReportingTests }; var drv = new FocasDriver(good, "drv-1", new FakeFocasClientFactory()); await drv.InitializeAsync("{}", CancellationToken.None); - drv.GetSkippedTags().Sample.Single().RawPath.ShouldBe("raw/Bad"); + drv.GetSkippedTags()!.Sample.Single().RawPath.ShouldBe("raw/Bad"); // A second driver over an artifact whose tag names a device that is not configured: the loop skips // the first entry, then throws on the second. @@ -164,7 +164,9 @@ public sealed class FocasSkippedTagReportingTests await Should.ThrowAsync( () => drv2.InitializeAsync("{}", CancellationToken.None)); - drv2.GetSkippedTags().SkippedCount.ShouldBe(0, - "an abandoned build must not publish; a partial report would read as 'we skip fewer tags now'"); + // NO ANSWER, not "zero skipped": this driver's only build threw, so it never reached a verdict. + // A zero report here would be the same false reassurance the docker-dev rig showed for two + // faulted drivers, and a PARTIAL one would read as "we skip fewer tags now". + drv2.GetSkippedTags().ShouldBeNull(); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorSkippedTagTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorSkippedTagTests.cs index 8491de2d..090aa352 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorSkippedTagTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorSkippedTagTests.cs @@ -248,7 +248,7 @@ public sealed class DriverInstanceActorSkippedTagTests : RuntimeActorTestBase } /// - public SkippedTagReport GetSkippedTags() + public SkippedTagReport? GetSkippedTags() { if (ThrowOnGetSkippedTags) throw new InvalidOperationException("reporter is broken"); return _ledger.Snapshot();