Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
6.1 KiB
C#
163 lines
6.1 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Serilog;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
|
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server.Tests.Phase7;
|
|
|
|
/// <summary>
|
|
/// Phase 7 follow-up (task #243) — verifies the composer that maps Config DB
|
|
/// rows to runtime engine definitions + wires up VirtualTagEngine +
|
|
/// ScriptedAlarmEngine + historian routing.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class Phase7EngineComposerTests
|
|
{
|
|
private static Script ScriptRow(string id, string source) => new()
|
|
{
|
|
ScriptRowId = Guid.NewGuid(), GenerationId = 1,
|
|
ScriptId = id, Name = id, SourceCode = source, SourceHash = "h",
|
|
};
|
|
|
|
private static VirtualTag VtRow(string id, string scriptId) => new()
|
|
{
|
|
VirtualTagRowId = Guid.NewGuid(), GenerationId = 1,
|
|
VirtualTagId = id, EquipmentId = "eq-1", Name = id,
|
|
DataType = "Float32", ScriptId = scriptId,
|
|
};
|
|
|
|
private static ScriptedAlarm AlarmRow(string id, string scriptId) => new()
|
|
{
|
|
ScriptedAlarmRowId = Guid.NewGuid(), GenerationId = 1,
|
|
ScriptedAlarmId = id, EquipmentId = "eq-1", Name = id,
|
|
AlarmType = "LimitAlarm", Severity = 500,
|
|
MessageTemplate = "x", PredicateScriptId = scriptId,
|
|
};
|
|
|
|
[Fact]
|
|
public void Compose_empty_rows_returns_Empty_sentinel()
|
|
{
|
|
var result = Phase7EngineComposer.Compose(
|
|
scripts: [],
|
|
virtualTags: [],
|
|
scriptedAlarms: [],
|
|
upstream: new CachedTagUpstreamSource(),
|
|
alarmStateStore: new InMemoryAlarmStateStore(),
|
|
historianSink: NullAlarmHistorianSink.Instance,
|
|
rootScriptLogger: new LoggerConfiguration().CreateLogger(),
|
|
loggerFactory: NullLoggerFactory.Instance);
|
|
|
|
result.ShouldBeSameAs(Phase7ComposedSources.Empty);
|
|
result.VirtualReadable.ShouldBeNull();
|
|
result.ScriptedAlarmReadable.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Compose_VirtualTag_rows_returns_non_null_VirtualReadable()
|
|
{
|
|
var scripts = new[] { ScriptRow("scr-1", "return 1;") };
|
|
var vtags = new[] { VtRow("vt-1", "scr-1") };
|
|
|
|
var result = Phase7EngineComposer.Compose(
|
|
scripts, vtags, [],
|
|
upstream: new CachedTagUpstreamSource(),
|
|
alarmStateStore: new InMemoryAlarmStateStore(),
|
|
historianSink: NullAlarmHistorianSink.Instance,
|
|
rootScriptLogger: new LoggerConfiguration().CreateLogger(),
|
|
loggerFactory: NullLoggerFactory.Instance);
|
|
|
|
result.VirtualReadable.ShouldNotBeNull();
|
|
result.ScriptedAlarmReadable.ShouldBeNull("no alarms configured");
|
|
result.Disposables.Count.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Compose_ScriptedAlarm_rows_returns_non_null_ScriptedAlarmReadable()
|
|
{
|
|
var scripts = new[] { ScriptRow("scr-1", "return false;") };
|
|
var alarms = new[] { AlarmRow("al-1", "scr-1") };
|
|
|
|
var result = Phase7EngineComposer.Compose(
|
|
scripts, [], alarms,
|
|
upstream: new CachedTagUpstreamSource(),
|
|
alarmStateStore: new InMemoryAlarmStateStore(),
|
|
historianSink: NullAlarmHistorianSink.Instance,
|
|
rootScriptLogger: new LoggerConfiguration().CreateLogger(),
|
|
loggerFactory: NullLoggerFactory.Instance);
|
|
|
|
result.ScriptedAlarmReadable.ShouldNotBeNull("task #245 — alarm Active state readable");
|
|
result.VirtualReadable.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Compose_missing_script_reference_throws_with_actionable_message()
|
|
{
|
|
var vtags = new[] { VtRow("vt-1", "scr-missing") };
|
|
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
Phase7EngineComposer.Compose(
|
|
scripts: [],
|
|
vtags, [],
|
|
upstream: new CachedTagUpstreamSource(),
|
|
alarmStateStore: new InMemoryAlarmStateStore(),
|
|
historianSink: NullAlarmHistorianSink.Instance,
|
|
rootScriptLogger: new LoggerConfiguration().CreateLogger(),
|
|
loggerFactory: NullLoggerFactory.Instance))
|
|
.Message.ShouldContain("scr-missing");
|
|
}
|
|
|
|
[Fact]
|
|
public void Compose_disabled_VirtualTag_is_skipped()
|
|
{
|
|
var scripts = new[] { ScriptRow("scr-1", "return 1;") };
|
|
var disabled = VtRow("vt-1", "scr-1");
|
|
disabled.Enabled = false;
|
|
|
|
var defs = Phase7EngineComposer.ProjectVirtualTags(
|
|
new[] { disabled },
|
|
new Dictionary<string, Script> { ["scr-1"] = scripts[0] }).ToList();
|
|
|
|
defs.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProjectVirtualTags_maps_timer_interval_milliseconds_to_TimeSpan()
|
|
{
|
|
var scripts = new[] { ScriptRow("scr-1", "return 1;") };
|
|
var vt = VtRow("vt-1", "scr-1");
|
|
vt.TimerIntervalMs = 2500;
|
|
|
|
var def = Phase7EngineComposer.ProjectVirtualTags(
|
|
new[] { vt },
|
|
new Dictionary<string, Script> { ["scr-1"] = scripts[0] }).Single();
|
|
|
|
def.TimerInterval.ShouldBe(TimeSpan.FromMilliseconds(2500));
|
|
}
|
|
|
|
[Fact]
|
|
public void ProjectScriptedAlarms_maps_Severity_numeric_to_AlarmSeverity_bucket()
|
|
{
|
|
var scripts = new[] { ScriptRow("scr-1", "return true;") };
|
|
|
|
var buckets = new[] { (1, AlarmSeverity.Low), (250, AlarmSeverity.Low),
|
|
(251, AlarmSeverity.Medium), (500, AlarmSeverity.Medium),
|
|
(501, AlarmSeverity.High), (750, AlarmSeverity.High),
|
|
(751, AlarmSeverity.Critical), (1000, AlarmSeverity.Critical) };
|
|
foreach (var (input, expected) in buckets)
|
|
{
|
|
var row = AlarmRow("a1", "scr-1");
|
|
row.Severity = input;
|
|
var def = Phase7EngineComposer.ProjectScriptedAlarms(
|
|
new[] { row },
|
|
new Dictionary<string, Script> { ["scr-1"] = scripts[0] }).Single();
|
|
def.Severity.ShouldBe(expected, $"severity {input} should map to {expected}");
|
|
}
|
|
}
|
|
}
|
|
|