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); } }