using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Parity guards for the two remaining per-driver dispatch maps (deferment.md G-5, G-6). /// Both were already testable structures — unlike the two Razor @switch blocks that needed /// extracting first — so nothing but the tests was missing. /// [Trait("Category", "Unit")] public sealed class DriverDispatchMapParityTests { /// /// Driver types with a typed tag editor but no CSV map, so import/export silently degrades them to /// the raw TagConfigJson fallback. Empty — every editor-backed driver now has typed columns. /// Deliberately expressed as "has a typed editor ⇒ has typed CSV columns" rather than /// "every driver type", because a driver with no typed editor has nothing to project into columns. /// Tying the two maps together is what makes this catch the next omission. /// [Fact] public void Every_driver_with_a_typed_tag_editor_has_typed_csv_columns() { var missing = DriverTypeNames.All .Where(t => TagConfigEditorMap.Resolve(t) is not null && CsvColumnMap.Resolve(t) is null) .OrderBy(t => t, StringComparer.Ordinal) .ToList(); missing.ShouldBeEmpty( $"these driver types have a typed tag editor but no CsvColumnMap entry, so CSV import/export " + $"drops them to the raw TagConfigJson fallback: {string.Join(", ", missing)}"); } /// A CSV map for a driver type that no longer exists is a stale entry. [Fact] public void Every_csv_map_targets_a_declared_driver_type() { var declared = DriverTypeNames.All.ToHashSet(StringComparer.OrdinalIgnoreCase); var stale = CsvColumnMap.All .Select(m => m.DriverType) .Where(t => !declared.Contains(t)) .OrderBy(t => t, StringComparer.Ordinal) .ToList(); stale.ShouldBeEmpty($"CsvColumnMap has entries for undeclared driver types: {string.Join(", ", stale)}"); } /// /// Every driver type either produces a TYPED blob from browse-commit, or is on the documented /// non-browsable list. The generic {"address": …} fallback opens EMPTY in a typed editor and /// blanks the field on save — the MTConnect branch was added for exactly that, and Sql had the same /// bug unnoticed (deferment.md G-6) because the fallback's comment claimed "browsable drivers /// are all handled above", which stopped being true the moment SqlDriverBrowser registered. /// Asserted in both directions on purpose. A one-way check would let a newly-browsable /// driver be quietly added to the exclusion list instead of getting a branch. Requiring the two sets /// to match exactly means a driver gaining or losing a browser forces this list to be revisited. /// [Fact] public void Browse_commit_falls_back_to_the_generic_address_key_for_exactly_the_non_browsable_drivers() { // Address fields as a browser supplies them; a driver whose browser states none must still produce // something its typed editor can read from the leaf name alone. var addressFields = new Dictionary(StringComparer.Ordinal) { ["schema"] = "dbo", ["table"] = "Readings", ["columnName"] = "Speed", }; var fellBack = DriverTypeNames.All .Where(t => RawBrowseCommitMapper.BuildTagConfig(t, "Speed", addressFields) == """{"address":"Speed"}""") .OrderBy(t => t, StringComparer.Ordinal) .ToList(); fellBack.ShouldBe( NonBrowsableDriverTypes.OrderBy(t => t, StringComparer.Ordinal).ToList(), $"the set of driver types committing to the generic \"address\" key drifted. Extra entries are " + $"browsable drivers whose typed editor will open EMPTY and blank the field on save; missing " + $"entries gained a typed branch and should leave this list. Got: {string.Join(", ", fellBack)}"); } /// /// Driver types with no browser, for which the generic address key is the correct commit. /// /// Modbus — register addresses are numeric coordinates, not a browsable namespace. /// Calculation — a pseudo-driver over other tags' RawPaths; there is no device. /// /// Adding a type here is a claim that it cannot be browsed. If it can, give it a branch instead. /// private static readonly string[] NonBrowsableDriverTypes = [ DriverTypeNames.Modbus, DriverTypeNames.Calculation, ]; }