1d7a4b7a53
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
60 lines
2.6 KiB
C#
60 lines
2.6 KiB
C#
using Serilog;
|
|
using Serilog.Core;
|
|
using Serilog.Events;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests;
|
|
|
|
/// <summary>Review R05:6 — a throwing <see cref="ScriptedAlarmSource.OnAlarmEvent"/> subscriber used to be
|
|
/// swallowed with no diagnostic: a lost alarm event with no trace. It is now logged at Warning.</summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ScriptedAlarmSourceLoggingTests
|
|
{
|
|
private sealed class CapturingSink : ILogEventSink
|
|
{
|
|
public List<LogEvent> Events { get; } = [];
|
|
public void Emit(LogEvent logEvent) { lock (Events) Events.Add(logEvent); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_throwing_subscriber_is_logged_at_Warning_and_does_not_crash_the_adapter()
|
|
{
|
|
var sinkCapture = new CapturingSink();
|
|
var logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sinkCapture).CreateLogger();
|
|
var up = new FakeUpstream();
|
|
up.Set("Temp", 50);
|
|
var engine = new ScriptedAlarmEngine(up, new InMemoryAlarmStateStore(), new ScriptLoggerFactory(logger), logger);
|
|
await engine.LoadAsync(
|
|
[new ScriptedAlarmDefinition("Plant/Line1::HighTemp", "Plant/Line1", "HighTemp", AlarmKind.LimitAlarm,
|
|
AlarmSeverity.High, "Temp {Temp}C", """return (int)ctx.GetTag("Temp").Value > 100;""")],
|
|
TestContext.Current.CancellationToken);
|
|
using var _e = engine;
|
|
using var source = new ScriptedAlarmSource(engine, logger);
|
|
await source.SubscribeAlarmsAsync(Array.Empty<string>(), TestContext.Current.CancellationToken);
|
|
source.OnAlarmEvent += (_, _) => throw new InvalidOperationException("subscriber broke");
|
|
|
|
up.Push("Temp", 150);
|
|
|
|
var deadline = DateTime.UtcNow.AddSeconds(10);
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
lock (sinkCapture.Events)
|
|
{
|
|
if (sinkCapture.Events.Any(e => e.Level == LogEventLevel.Warning && e.Exception?.Message == "subscriber broke"))
|
|
break;
|
|
}
|
|
await Task.Delay(25, TestContext.Current.CancellationToken);
|
|
}
|
|
|
|
LogEvent warning;
|
|
lock (sinkCapture.Events)
|
|
warning = sinkCapture.Events.Single(e => e.Level == LogEventLevel.Warning && e.Exception?.Message == "subscriber broke");
|
|
warning.RenderMessage().ShouldContain("Plant/Line1::HighTemp");
|
|
// The adapter survived: the engine's state still moved.
|
|
engine.GetState("Plant/Line1::HighTemp")!.Active.ShouldBe(AlarmActiveState.Active);
|
|
}
|
|
}
|