3a312ffdd1
v2-ci / unit-tests (push) Has been cancelled
A notifier folder read one event source, its NodeId identifier (a RawPath or an EquipmentId). AVEVA stores a Galaxy object's A&E history under the object's tag_name (Source_Object), so AVEVA's own alarm and event history reached no production folder. Its lower-case property keys were also missed, so every row read Severity 1 with no ConditionName. Sources. A folder's event history now reads its own source plus the Galaxy object of every native condition that fires on it live: those parented under it and those WireAlarmNotifiers wired to it. Live and history use the same membership. The object comes from the condition's Galaxy alarm reference, its raw tag's attributeRef, up to the first '.': - The composer reads it with the driver's own parser (GalaxyAttributeRef, shared with GalaxyDriver), only for a GalaxyMxGateway tag with an alarm (RawTagPlan.NativeAlarmReference, part of plan equality). - The applier binds it after materialising the condition (IOpcUaAddressSpaceSink.BindNativeAlarmHistory, forwarded by the deferred sink). A rebuild or the condition's removal forgets it. Each object is read with the #882 rule: its own events and <Object>.<Attribute> rows, never a prefix sibling. Fan-out. historiangw's ReadEvents filter is a single predicate, so there is one request per source; the wire contract is unchanged. EventSourceFanOut merges the reads as one source: - order is (time, source position, arrival), reversed for a reverse read; ties are never re-sorted by EventId, so the boundary skip stays stable; - duplicates by EventId are dropped and the merge is cut to the cap; - a truncated source bounds the page at the time it stopped. Paging, continuation points (bound to the whole source set), the walked-back reverse read and the tie-cluster over-fetch are unchanged. A folder with one source is read exactly as before. ACL. Every source is read inside the folder's own HistoryRead, after AuthorizeHistoryRead (#826). A denied folder reads nothing and fails closed. Mapping (EventMapper, property names now case-insensitive): - Severity: an AVEVA row's priority goes through GalaxyAlarmPriority, the table live Galaxy alarms use (MxAccessSeverityMapper now delegates to it), so history matches live. Otherwise severity (OtOpcUa's own rows, which AVEVA stores lower-cased), clamped. AVEVA's own 1-4 severity is ignored. The table ranks a higher priority as more severe, which inverts AVEVA's convention: filed as #887. - An AVEVA row whose source_conditionvariable names a native condition that fires on the folder takes that condition's SourceNode and live ConditionName. Otherwise ConditionName is alarmname or the condition variable's attribute. - alarm_id/alarmid map to AlarmId. Tests: - GalaxyEquipmentEventHistoryWireTests (4, real SDK session, production Gateway backend, AVEVA-shaped fake gateway): equipment EqA backed by objects Oven_007 + OvenDoor_07. Covers object and attribute events from both objects, the prefix sibling excluded, cross-object and in-object ties, forward paging at 1-4 a page, reverse whole and paged, SourceNode/ConditionName/Severity, the raw device folder, ACL deny with no backend call plus a granted positive control, and a rebuild forgetting the bindings. - EventSourceFanOutTests (8), EventMapper AVEVA cases (11), GalaxyAttributeRefTests (16), a composer test and an applier ordering test. - Mutation-checked, 17 mutants, all killed. Covered: parent-only membership, no scope, a case-sensitive condition match, no condition rewrite, no truncation bound, EventId tie order, no sort, no cap, no stop at the bound, unreversed source order, no dedupe, no '.' boundary, priority ignored, case-sensitive keys, no applier bind, no deferred forward, and a composer that ignores the driver type. The forget-on-rematerialise guard survives as belt-and-braces: the rebuild clear covers it. - Live, read-only, no WinRM, vd03 HistorianGateway ea9f21c: equipment folder EQ-886-ZipsenLeft (objects DevWinPlatform + Left_001). It read 0 events before and 92 now, equal to the unfiltered read's rows for both objects. 1/7-event pages and reverse whole/5-event pages agree, and 73 rows resolve to their live condition. diff_runs vs the #882 run: 221/226 identical. The 5 that differ are expected: DevWinPlatform Severity 1 -> 750/1000, and #881 capabilities. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
41 lines
1.7 KiB
C#
41 lines
1.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
|
|
|
/// <summary>Gitea #886 — the Galaxy reference parser the driver and the composer share, and the object it names.</summary>
|
|
public sealed class GalaxyAttributeRefTests
|
|
{
|
|
[Theory]
|
|
[InlineData("""{"attributeRef":"Oven_007.TempHiAlarm"}""", "Oven_007.TempHiAlarm")]
|
|
[InlineData("""{"AttributeRef":"Oven_007.Temp","alarm":{"alarmType":"LimitAlarm"}}""", "Oven_007.Temp")]
|
|
[InlineData("""{"attributeRef":" "}""", null)]
|
|
[InlineData("""{"attributeRef":7}""", null)]
|
|
[InlineData("""{"FullName":"Oven_007.Temp"}""", null)]
|
|
[InlineData("""not json""", null)]
|
|
[InlineData("", null)]
|
|
[InlineData(null, null)]
|
|
public void Reads_the_attributeRef(string? tagConfig, string? expected) =>
|
|
GalaxyAttributeRef.TryRead(tagConfig).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void Only_a_galaxy_tag_names_an_aveva_alarm_reference()
|
|
{
|
|
const string config = """{"attributeRef":"Oven_007.TempHiAlarm"}""";
|
|
GalaxyAttributeRef.AlarmReferenceOf(DriverTypeNames.Galaxy, config).ShouldBe("Oven_007.TempHiAlarm");
|
|
GalaxyAttributeRef.AlarmReferenceOf(DriverTypeNames.Modbus, config).ShouldBeNull();
|
|
GalaxyAttributeRef.AlarmReferenceOf(null, config).ShouldBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Oven_007.TempHiAlarm", "Oven_007")]
|
|
[InlineData("Oven_007.Temp.HiHi", "Oven_007")]
|
|
[InlineData("Oven_007", "Oven_007")]
|
|
[InlineData(" Oven_007.Temp ", "Oven_007")]
|
|
[InlineData(".Temp", null)]
|
|
[InlineData("", null)]
|
|
[InlineData(null, null)]
|
|
public void The_object_is_the_reference_up_to_its_first_dot(string? reference, string? expected) =>
|
|
GalaxyAttributeRef.ObjectNameOf(reference).ShouldBe(expected);
|
|
}
|