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