using System.Reflection; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; /// /// Guards every driver's hard-coded OPC UA status-code constant against /// — the pinned SDK is the oracle, and a constant whose /// name disagrees with its value fails here. /// Why the drivers hard-code these at all. The driver layer is deliberately SDK-free: a /// driver assembly must not drag in the OPC UA stack just to spell a status code, so each one declares /// the handful it needs as bare uint literals. That is a sound layering decision with one /// failure mode — nothing checks the literal against the name — and it is exactly the failure mode /// Gitea #497 found in six places across four drivers. /// Discovery is reflection-driven, not a hand-copied list (the same convention /// uses): the test scans every /// ZB.MOM.WW.OtOpcUa.Driver.*.dll deployed to its output directory for const uint fields /// whose name reads as a status code, so a brand-new driver assembly referenced by this project is /// covered automatically, with no edit here. /// An inline literal is invisible to this guard. Reflection can only see a named /// constant, so StatusCode: 0x800B0000u, // BadTimeout written at a call site is unguarded — /// which is how GalaxyDriver shipped BadServiceUnsupported under a BadTimeout /// comment. Hoist a status literal into a named constant rather than writing it at the call site. /// public sealed class StatusCodeParityTests { /// /// Name prefixes that mark a uint constant as an OPC UA status code. These are the three /// Part 4 severity categories, so they cover the whole space by construction — a status constant /// that does not start with one of them is not a status constant. /// private static readonly string[] StatusPrefixes = ["Good", "Uncertain", "Bad"]; /// /// Field-name prefixes stripped before the name is looked up on . /// Drivers vary the spelling (SampleMapper.StatusBadNoData vs /// SqlStatusCodes.BadTimeout); the SDK member is BadNoData either way. /// private static readonly string[] NamePrefixesToStrip = ["Status"]; /// Every Opc.Ua.StatusCodes member, by name — the oracle this test checks against. private static readonly IReadOnlyDictionary SdkStatusCodes = typeof(Opc.Ua.StatusCodes) .GetFields(BindingFlags.Public | BindingFlags.Static) .Where(f => f.IsLiteral && f.FieldType == typeof(uint)) .ToDictionary(f => f.Name, f => (uint)f.GetRawConstantValue()!, StringComparer.Ordinal); /// /// Every status-shaped const uint declared anywhere in the deployed driver assemblies, as /// (assembly, declaring type, field name, SDK member name, declared value). /// /// /// Non-public types and fields are included on purpose — SampleMapper.StatusBadNoData is a /// private const on an internal class, and it carried one of the #497 defects. Access /// modifiers say nothing about whether a value reaches an OPC UA client. /// public static TheoryData DeclaredStatusConstants() { var data = new TheoryData(); var binDir = Path.GetDirectoryName(typeof(StatusCodeParityTests).Assembly.Location)!; foreach (var dll in Directory.GetFiles(binDir, "ZB.MOM.WW.OtOpcUa.Driver.*.dll").OrderBy(p => p, StringComparer.Ordinal)) { Assembly asm; try { asm = Assembly.LoadFrom(dll); } catch { continue; } Type?[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } foreach (var type in types) { if (type is null) continue; var fields = type.GetFields( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly); foreach (var field in fields) { if (!field.IsLiteral || field.IsInitOnly || field.FieldType != typeof(uint)) continue; var sdkName = StripKnownPrefix(field.Name); if (!StatusPrefixes.Any(p => sdkName.StartsWith(p, StringComparison.Ordinal))) continue; data.Add( asm.GetName().Name!, type.FullName ?? type.Name, field.Name, sdkName, (uint)field.GetRawConstantValue()!); } } } return data; } /// Removes a leading Status-style prefix so the remainder can be looked up on the SDK type. private static string StripKnownPrefix(string fieldName) { foreach (var prefix in NamePrefixesToStrip) { if (fieldName.Length > prefix.Length && fieldName.StartsWith(prefix, StringComparison.Ordinal)) return fieldName[prefix.Length..]; } return fieldName; } /// /// A driver's status constant must carry the value the SDK gives the code it is named after. /// /// The declaring driver assembly (failure-message context). /// The declaring type's full name (failure-message context). /// The constant's name as declared. /// The member the name resolves to. /// The value the driver declares. [Theory] [MemberData(nameof(DeclaredStatusConstants))] public void Driver_status_constant_matches_the_pinned_SDK( string assembly, string type, string field, string sdkName, uint declared) { SdkStatusCodes.ShouldContainKey(sdkName, $"{type}.{field} (in {assembly}) reads as an OPC UA status code, but '{sdkName}' is not a member " + "of Opc.Ua.StatusCodes. Either the constant is misnamed, or it is not a status code and should " + "not start with Good/Uncertain/Bad."); var expected = SdkStatusCodes[sdkName]; declared.ShouldBe(expected, $"{type}.{field} (in {assembly}) is declared 0x{declared:X8}, but Opc.Ua.StatusCodes.{sdkName} " + $"is 0x{expected:X8}" + (SdkStatusCodes.FirstOrDefault(kv => kv.Value == declared) is { Key: { } actual } && actual.Length > 0 ? $" — 0x{declared:X8} is actually {actual}, which is what OPC UA clients would branch on." : ". The declared value is not any OPC UA status code.")); } /// /// Discovery must find constants in more than one driver assembly. A zero (or near-zero) here means /// the bin scan broke or the drivers stopped declaring these by convention — either way the theory /// above would pass vacuously, which is the one outcome a guard test must never do quietly. /// [Fact] public void Discovery_finds_status_constants_across_multiple_driver_assemblies() { var found = DeclaredStatusConstants(); found.Count.ShouldBeGreaterThan(20, "the driver bin scan found almost no status constants — the assemblies did not deploy to bin, " + "or the naming convention changed"); var assemblies = found .Select(row => row.Data.Item1) .Distinct(StringComparer.Ordinal) .ToArray(); assemblies.Length.ShouldBeGreaterThan(3, "status constants were found in fewer than four driver assemblies: " + string.Join(", ", assemblies)); } }