Activates the Phase 7 engines in production. Loads Script + VirtualTag + ScriptedAlarm rows from the bootstrapped generation, wires the engines through the Phase7EngineComposer kernel (#243), starts the DriverSubscriptionBridge feed (#244), and late-binds the resulting IReadable sources to OpcUaApplicationHost before OPC UA server start. ## Phase7Composer (Server.Phase7) Singleton orchestrator. PrepareAsync loads the three Phase 7 row sets in one DB scope, builds CachedTagUpstreamSource, calls Phase7EngineComposer.Compose, constructs DriverSubscriptionBridge with one DriverFeed per registered ISubscribable driver (path-to-fullRef map built from EquipmentNamespaceContent via MapPathsToFullRefs), starts the bridge. DisposeAsync tears down in the right order: bridge first (no more events fired into the cache), then engines (cascades + timers stop), then any disposable sink. MapPathsToFullRefs: deterministic path convention is /{areaName}/{lineName}/{equipmentName}/{tagName} matching exactly what EquipmentNodeWalker emits into the OPC UA browse tree, so script literals against the operator-visible UNS tree work without translation. Tags missing EquipmentId or pointing at unknown Equipment are skipped silently (Galaxy SystemPlatform-style tags + dangling references handled). ## OpcUaApplicationHost.SetPhase7Sources New late-bind setter. Throws InvalidOperationException if called after StartAsync because OtOpcUaServer + DriverNodeManagers capture the field values at construction; mutation post-start would silently fail. ## OpcUaServerService After bootstrap loads the current generation, calls phase7Composer.PrepareAsync + applicationHost.SetPhase7Sources before applicationHost.StartAsync. StopAsync disposes Phase7Composer first so the bridge stops feeding the cache before the OPC UA server tears down its node managers (avoids in-flight cascades surfacing as noisy shutdown warnings). ## Program.cs Registers IAlarmHistorianSink as NullAlarmHistorianSink.Instance (task #247 swaps in the real Galaxy.Host-writer-backed SqliteStoreAndForwardSink), Serilog root logger, and Phase7Composer singleton. ## Tests — 5 new Phase7ComposerMappingTests = 34 Phase 7 tests total Maps tag → walker UNS path, skips null EquipmentId, skips unknown Equipment reference, multiple tags under same equipment map distinctly, empty content yields empty map. Pure functions; no DI/DB needed. The real PrepareAsync DB query path can't be exercised without SQL Server in the test environment — it's exercised by the live E2E smoke (task #240) which unblocks once #247 lands. ## Phase 7 production wiring chain status - ✅ #243 composition kernel - ✅ #245 scripted-alarm IReadable adapter - ✅ #244 driver bridge - ✅ #246 this — Program.cs wire-in - 🟡 #247 — Galaxy.Host SqliteStoreAndForwardSink writer adapter (replaces NullSink) - 🟡 #240 — live E2E smoke (unblocks once #247 lands)
94 lines
3.4 KiB
C#
94 lines
3.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server.Tests.Phase7;
|
|
|
|
/// <summary>
|
|
/// Task #246 — covers the deterministic mapping inside <see cref="Phase7Composer"/>
|
|
/// that turns <see cref="EquipmentNamespaceContent"/> into the path → fullRef map
|
|
/// <see cref="DriverFeed.PathToFullRef"/> consumes. Pure function; no DI / DB needed.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class Phase7ComposerMappingTests
|
|
{
|
|
private static UnsArea Area(string id, string name) =>
|
|
new() { UnsAreaId = id, ClusterId = "c", Name = name, GenerationId = 1 };
|
|
|
|
private static UnsLine Line(string id, string areaId, string name) =>
|
|
new() { UnsLineId = id, UnsAreaId = areaId, Name = name, GenerationId = 1 };
|
|
|
|
private static Equipment Eq(string id, string lineId, string name) => new()
|
|
{
|
|
EquipmentRowId = Guid.NewGuid(), GenerationId = 1, EquipmentId = id,
|
|
EquipmentUuid = Guid.NewGuid(), DriverInstanceId = "drv",
|
|
UnsLineId = lineId, Name = name, MachineCode = "m",
|
|
};
|
|
|
|
private static Tag T(string id, string name, string fullRef, string equipmentId) => new()
|
|
{
|
|
TagRowId = Guid.NewGuid(), GenerationId = 1, TagId = id,
|
|
DriverInstanceId = "drv", EquipmentId = equipmentId,
|
|
Name = name, DataType = "Float32",
|
|
AccessLevel = TagAccessLevel.Read, TagConfig = fullRef,
|
|
};
|
|
|
|
[Fact]
|
|
public void Maps_tag_to_UNS_path_walker_emits()
|
|
{
|
|
var content = new EquipmentNamespaceContent(
|
|
Areas: [Area("a1", "warsaw")],
|
|
Lines: [Line("l1", "a1", "oven-line")],
|
|
Equipment: [Eq("e1", "l1", "oven-3")],
|
|
Tags: [T("t1", "Temp", "DR.Temp", "e1")]);
|
|
|
|
var map = Phase7Composer.MapPathsToFullRefs(content);
|
|
|
|
map.ShouldContainKeyAndValue("/warsaw/oven-line/oven-3/Temp", "DR.Temp");
|
|
}
|
|
|
|
[Fact]
|
|
public void Skips_tag_with_null_EquipmentId()
|
|
{
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("a1", "warsaw")], [Line("l1", "a1", "ol")], [Eq("e1", "l1", "ov")],
|
|
[T("t1", "Bare", "DR.Bare", null!)]); // SystemPlatform-style orphan
|
|
|
|
Phase7Composer.MapPathsToFullRefs(content).ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Skips_tag_pointing_at_unknown_Equipment()
|
|
{
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("a1", "warsaw")], [Line("l1", "a1", "ol")], [Eq("e1", "l1", "ov")],
|
|
[T("t1", "Lost", "DR.Lost", "e-missing")]);
|
|
|
|
Phase7Composer.MapPathsToFullRefs(content).ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Maps_multiple_tags_under_same_equipment_distinctly()
|
|
{
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("a1", "site")], [Line("l1", "a1", "line1")], [Eq("e1", "l1", "cell")],
|
|
[T("t1", "Temp", "DR.T", "e1"), T("t2", "Pressure", "DR.P", "e1")]);
|
|
|
|
var map = Phase7Composer.MapPathsToFullRefs(content);
|
|
|
|
map.Count.ShouldBe(2);
|
|
map["/site/line1/cell/Temp"].ShouldBe("DR.T");
|
|
map["/site/line1/cell/Pressure"].ShouldBe("DR.P");
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_content_yields_empty_map()
|
|
{
|
|
Phase7Composer.MapPathsToFullRefs(new EquipmentNamespaceContent([], [], [], []))
|
|
.ShouldBeEmpty();
|
|
}
|
|
}
|