using Serilog; using Serilog.Core; using Serilog.Events; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Scripting; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests; /// /// Exercises the factory that creates per-script Serilog loggers with the /// ScriptName structured property pre-bound. The property is what lets /// Admin UI filter the scripts-*.log sink by which tag/alarm emitted each event. /// [Trait("Category", "Unit")] public sealed class ScriptLoggerFactoryTests { /// Capturing sink that collects every emitted LogEvent for assertion. private sealed class CapturingSink : ILogEventSink { public List Events { get; } = []; public void Emit(LogEvent logEvent) => Events.Add(logEvent); } [Fact] public void Create_sets_ScriptName_structured_property() { var sink = new CapturingSink(); var root = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); var factory = new ScriptLoggerFactory(root); var logger = factory.Create("LineRate"); logger.Information("hello"); sink.Events.Count.ShouldBe(1); var ev = sink.Events[0]; ev.Properties.ShouldContainKey(ScriptLoggerFactory.ScriptNameProperty); ((ScalarValue)ev.Properties[ScriptLoggerFactory.ScriptNameProperty]).Value.ShouldBe("LineRate"); } [Fact] public void Each_script_gets_its_own_property_value() { var sink = new CapturingSink(); var root = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); var factory = new ScriptLoggerFactory(root); factory.Create("Alarm_A").Information("event A"); factory.Create("Tag_B").Warning("event B"); factory.Create("Alarm_A").Error("event A again"); sink.Events.Count.ShouldBe(3); ((ScalarValue)sink.Events[0].Properties[ScriptLoggerFactory.ScriptNameProperty]).Value.ShouldBe("Alarm_A"); ((ScalarValue)sink.Events[1].Properties[ScriptLoggerFactory.ScriptNameProperty]).Value.ShouldBe("Tag_B"); ((ScalarValue)sink.Events[2].Properties[ScriptLoggerFactory.ScriptNameProperty]).Value.ShouldBe("Alarm_A"); } [Fact] public void Error_level_event_preserves_level_and_exception() { var sink = new CapturingSink(); var root = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); var factory = new ScriptLoggerFactory(root); factory.Create("Test").Error(new InvalidOperationException("boom"), "script failed"); sink.Events[0].Level.ShouldBe(LogEventLevel.Error); sink.Events[0].Exception.ShouldBeOfType(); } [Fact] public void Null_root_rejected() { Should.Throw(() => new ScriptLoggerFactory(null!)); } [Fact] public void Empty_script_name_rejected() { var root = new LoggerConfiguration().CreateLogger(); var factory = new ScriptLoggerFactory(root); Should.Throw(() => factory.Create("")); Should.Throw(() => factory.Create(" ")); Should.Throw(() => factory.Create(null!)); } [Fact] public void ScriptNameProperty_constant_is_stable() { // Stability is an external contract — the Admin UI's log filter references // this exact string. If it changes, the filter breaks silently. ScriptLoggerFactory.ScriptNameProperty.ShouldBe("ScriptName"); } }