diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs new file mode 100644 index 00000000..2e166a21 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs @@ -0,0 +1,68 @@ +namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +/// +/// Single source of truth for the driver-type identifier strings the runtime +/// dispatches on. Each constant's value is the exact DriverTypeName +/// that the corresponding driver factory registers under in the process +/// DriverFactoryRegistry — the string the deploy pipeline stores in +/// DriverInstance.DriverType and every dispatch map keys by. +/// +/// +/// +/// Historically several dispatch maps hand-authored these strings and drifted from +/// the authoritative factory names (e.g. "TwinCat" vs the real +/// "TwinCAT", "Focas" vs "FOCAS"). Consumers should reference +/// these constants instead of literals so the drift can never recur. +/// +/// +/// The reflection guard test +/// (DriverTypeNamesGuardTests) asserts bidirectional parity between these +/// constants and the set the driver factories actually register, so a rename on +/// either side breaks the build's test gate. Only constants for +/// currently-registered factories belong here — a not-yet-registered driver +/// (e.g. the Calculation driver landing in a later Batch 2 package) adds its own +/// constant when its factory is wired in. +/// +/// +public static class DriverTypeNames +{ + /// Modbus TCP / RTU-over-TCP driver. + public const string Modbus = "Modbus"; + + /// Siemens S7 driver. + public const string S7 = "S7"; + + /// Allen-Bradley CIP (ControlLogix / CompactLogix) driver. + public const string AbCip = "AbCip"; + + /// Allen-Bradley legacy (PCCC / DF1-over-TCP) driver. + public const string AbLegacy = "AbLegacy"; + + /// Beckhoff TwinCAT ADS driver. + public const string TwinCAT = "TwinCAT"; + + /// FANUC FOCAS CNC driver. + public const string FOCAS = "FOCAS"; + + /// OPC UA client (upstream-server) driver. + public const string OpcUaClient = "OpcUaClient"; + + /// AVEVA System Platform (Wonderware) Galaxy driver, via the mxaccessgw gateway. + public const string Galaxy = "GalaxyMxGateway"; + + /// + /// Every driver-type string declared above, for callers that need to enumerate + /// the full set (e.g. validation of an authored DriverType). + /// + public static IReadOnlyCollection All { get; } = + [ + Modbus, + S7, + AbCip, + AbLegacy, + TwinCAT, + FOCAS, + OpcUaClient, + Galaxy, + ]; +} diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs new file mode 100644 index 00000000..d5b12aaf --- /dev/null +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs @@ -0,0 +1,97 @@ +using System.Reflection; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Core.Hosting; + +namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; + +/// +/// Guards against drift from the driver factories that are the +/// authority for these strings. The test builds the real registered-factory set the way +/// production does — it drives each driver assembly's *DriverFactoryExtensions.Register +/// entry point into a live , exactly as the Host's +/// DriverFactoryBootstrap.Register does — then asserts bidirectional parity with the +/// constants. If a factory renames its DriverTypeName (or a constant is added/removed +/// without a matching factory), this fails, forcing the constant and the factory back in sync. +/// +public sealed class DriverTypeNamesGuardTests +{ + /// + /// Build the authoritative registered-factory set by invoking every driver assembly's + /// registration extension against a fresh registry — the same set of calls, in the same + /// spirit, as DriverFactoryBootstrap.Register on a driver-role node. + /// + private static IReadOnlyCollection RegisteredDriverTypeNames() + { + var registry = new DriverFactoryRegistry(); + + // loggerFactory is optional on every extension; the factory func is never invoked here, + // so null is fine — we only need the type-name key each Register call installs. + Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry); + Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory: null); + Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry); + Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, loggerFactory: null); + Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory: null); + Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory: null); + Driver.S7.S7DriverFactoryExtensions.Register(registry); + Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry); + + return registry.RegisteredTypes; + } + + /// Reflect every public const string declared on . + private static IReadOnlyCollection DeclaredConstantValues() => + typeof(DriverTypeNames) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string)) + .Select(f => (string)f.GetRawConstantValue()!) + .ToArray(); + + [Fact] + public void Every_registered_factory_has_a_matching_constant() + { + var constants = DeclaredConstantValues(); + + foreach (var registered in RegisteredDriverTypeNames()) + { + constants.ShouldContain(registered, + $"driver factory registers '{registered}' but no DriverTypeNames constant has that value — " + + "add/rename the constant so dispatch maps can reference it instead of a drifting literal"); + } + } + + [Fact] + public void Every_constant_matches_a_registered_factory() + { + var registered = RegisteredDriverTypeNames(); + + foreach (var constant in DeclaredConstantValues()) + { + registered.ShouldContain(constant, + $"DriverTypeNames has '{constant}' but no registered driver factory uses it — " + + "remove the constant or wire the factory (do not add constants for unregistered drivers)"); + } + } + + [Fact] + public void Constants_and_registered_factories_are_exactly_the_same_set() + { + DeclaredConstantValues() + .OrderBy(s => s, StringComparer.Ordinal) + .ShouldBe( + RegisteredDriverTypeNames().OrderBy(s => s, StringComparer.Ordinal), + "DriverTypeNames must be exactly the set of registered driver-type strings — no drift, no extras, no gaps"); + } + + [Fact] + public void All_collection_equals_the_declared_constants() + { + // Keeps the convenience `All` collection honest against the individual consts. + DriverTypeNames.All + .OrderBy(s => s, StringComparer.Ordinal) + .ShouldBe( + DeclaredConstantValues().OrderBy(s => s, StringComparer.Ordinal), + "DriverTypeNames.All must list every declared constant exactly once"); + } +} diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj index daf8a0f2..f11c7282 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj @@ -21,6 +21,18 @@ + + + + + + + + + +