using System.Reflection; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// Gitea #599 / archreview 03/U11 — a mechanical guard on the Backing*-field parity invariant. /// /// /// /// A non-alarm raw-tag delta over the classifier's surgical whitelist is applied IN PLACE on the raw /// node, and every referencing UNS mirror is repaired only because the same edit makes its /// record unequal — the planner then emits a coincident /// UnsReferenceDelta. That works only while every whitelisted raw-tag field the mirror inherits /// has a counterpart on (same name, or Backing + name). /// Omitting one is silent: the raw node updates, the mirror stays stale, and nothing fails. Until this /// test the invariant was enforced by comments alone. /// /// /// The whitelist is DISCOVERED, not restated: each property is perturbed in /// isolation and asked of . So /// widening the classifier whitelist without mirroring the field fails here, and so does adding a /// Backing* field that names no raw-tag field. /// /// public sealed class UnsReferenceBackingParityTests { /// /// Surgically-eligible raw-tag fields that a UNS mirror deliberately does NOT inherit, each with the /// reason. Anything else the classifier lets through surgically must be mirrored. /// private static readonly IReadOnlyDictionary NotInheritedByMirror = new Dictionary { // Node-inert for a value tag: it only drives native-alarm notifier wiring, and an alarm-bearing delta // never takes the surgical path. The mirror is the referencing side, so it has nothing to inherit. [nameof(RawTagPlan.ReferencingEquipmentPaths)] = "notifier wiring only; the mirror is the referencing side", }; private static RawTagPlan Baseline() => new( TagId: "tag-1", NodeId: "MAIN/modbus/dev/grp/temp", ParentNodeId: "MAIN/modbus/dev/grp", DriverInstanceId: "drv-1", Name: "temp", DataType: "Float", Writable: false, Alarm: null, ReferencingEquipmentPaths: new[] { "Area/Line/Eq" }, IsHistorized: false, HistorianTagname: null, IsArray: false, ArrayLength: null, Realm: AddressSpaceRealm.Raw); /// The raw-tag fields the classifier lets through surgically, found by perturbing each one. private static IReadOnlyList SurgicallyEligibleRawTagFields() { var baseline = Baseline(); var eligible = new List(); foreach (var p in RecordProperties(typeof(RawTagPlan))) { var current = Perturb(baseline, p); if (AddressSpaceChangeClassifier.RawTagDeltaIsSurgicalEligible(new AddressSpacePlan.RawTagDelta(baseline, current))) eligible.Add(p); } return eligible; } /// /// Every raw-tag field the classifier applies surgically is mirrored on the UNS reference record (or is /// explicitly listed as not inherited) — with the same type, so the planner's record equality sees the /// edit. /// [Trait("Category", "Unit")] [Fact] public void Every_surgically_eligible_raw_tag_field_is_mirrored_on_the_UNS_reference_record() { var eligible = SurgicallyEligibleRawTagFields(); // Positive control: the perturbation machinery really does find the whitelist. If it found nothing, // the loop below would pass vacuously. eligible.Select(p => p.Name).ShouldBe( new[] { nameof(RawTagPlan.DataType), nameof(RawTagPlan.Writable), nameof(RawTagPlan.ReferencingEquipmentPaths), nameof(RawTagPlan.IsHistorized), nameof(RawTagPlan.HistorianTagname), nameof(RawTagPlan.IsArray), nameof(RawTagPlan.ArrayLength), nameof(RawTagPlan.ArrayWritable), }, ignoreOrder: true); var mirror = RecordProperties(typeof(UnsReferenceVariable)).ToDictionary(p => p.Name, StringComparer.Ordinal); foreach (var p in eligible) { if (NotInheritedByMirror.ContainsKey(p.Name)) continue; var counterpart = mirror.GetValueOrDefault(p.Name) ?? mirror.GetValueOrDefault("Backing" + p.Name); counterpart.ShouldNotBeNull( $"RawTagPlan.{p.Name} is applied surgically on the raw node, but UnsReferenceVariable has neither " + $"'{p.Name}' nor 'Backing{p.Name}' — a raw-side edit of it would leave every referencing UNS mirror stale."); counterpart.PropertyType.ShouldBe(p.PropertyType, $"UnsReferenceVariable.{counterpart.Name} vs RawTagPlan.{p.Name}"); } } /// Every Backing* field on the UNS reference record names a real raw-tag field of the same /// type — a misspelt or orphaned mirror field repairs nothing. [Trait("Category", "Unit")] [Fact] public void Every_Backing_field_names_a_raw_tag_field_of_the_same_type() { var raw = RecordProperties(typeof(RawTagPlan)).ToDictionary(p => p.Name, StringComparer.Ordinal); var backing = RecordProperties(typeof(UnsReferenceVariable)) .Where(p => p.Name.StartsWith("Backing", StringComparison.Ordinal) && p.Name != nameof(UnsReferenceVariable.BackingRawPath)) .ToList(); backing.ShouldNotBeEmpty(); foreach (var b in backing) { var source = raw.GetValueOrDefault(b.Name["Backing".Length..]); source.ShouldNotBeNull($"UnsReferenceVariable.{b.Name} names no RawTagPlan field"); source.PropertyType.ShouldBe(b.PropertyType, $"UnsReferenceVariable.{b.Name} vs RawTagPlan.{source.Name}"); } } /// The allow-list stays honest: every entry must still be a surgically-eligible raw-tag field. [Trait("Category", "Unit")] [Fact] public void Not_inherited_allow_list_names_only_surgically_eligible_fields() { var eligible = SurgicallyEligibleRawTagFields().Select(p => p.Name).ToHashSet(StringComparer.Ordinal); foreach (var name in NotInheritedByMirror.Keys) eligible.ShouldContain(name); } private static IEnumerable RecordProperties(Type t) => t.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanWrite && p.Name != "EqualityContract"); /// A copy of differing ONLY in . private static RawTagPlan Perturb(RawTagPlan source, PropertyInfo property) { var clone = (RawTagPlan)typeof(RawTagPlan).GetMethod("$")!.Invoke(source, null)!; var value = property.GetValue(source); object? changed = property.PropertyType switch { var t when t == typeof(bool) => !(bool)value!, var t when t == typeof(string) => (string?)value + "-changed", var t when t == typeof(uint?) => ((uint?)value ?? 0u) + 1u, var t when t == typeof(IReadOnlyList) => ((IReadOnlyList)value!).Append("Other/Eq").ToArray(), var t when t == typeof(AddressSpaceRealm) => (AddressSpaceRealm)value! == AddressSpaceRealm.Raw ? AddressSpaceRealm.Uns : AddressSpaceRealm.Raw, var t when t == typeof(EquipmentTagAlarmInfo) => new EquipmentTagAlarmInfo("OffNormalAlarm", 500), var t => throw new InvalidOperationException( $"RawTagPlan.{property.Name} has type {t.Name}, which this guard cannot perturb — add a case so the field is checked."), }; property.SetValue(clone, changed); return clone; } }