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;
/// Review R05:6 — a throwing subscriber used to be
/// swallowed with no diagnostic: a lost alarm event with no trace. It is now logged at Warning.
[Trait("Category", "Unit")]
public sealed class ScriptedAlarmSourceLoggingTests
{
private sealed class CapturingSink : ILogEventSink
{
public List 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(), 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);
}
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);
}
}