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));
}
}