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>
156 lines
5.9 KiB
C#
156 lines
5.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Verifies the sink that mirrors script Error+ events to the main log at Warning
|
|
/// level. Ensures script noise (Debug/Info/Warning) doesn't reach the main log
|
|
/// while genuine script failures DO surface there so operators see them without
|
|
/// watching a separate log file.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ScriptLogCompanionSinkTests
|
|
{
|
|
private sealed class CapturingSink : ILogEventSink
|
|
{
|
|
public List<LogEvent> Events { get; } = [];
|
|
public void Emit(LogEvent logEvent) => Events.Add(logEvent);
|
|
}
|
|
|
|
private static (ILogger script, CapturingSink scriptSink, CapturingSink mainSink) BuildPipeline()
|
|
{
|
|
// Main logger captures companion forwards.
|
|
var mainSink = new CapturingSink();
|
|
var mainLogger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose().WriteTo.Sink(mainSink).CreateLogger();
|
|
|
|
// Script logger fans out to scripts file (here: capture sink) + the companion sink.
|
|
var scriptSink = new CapturingSink();
|
|
var scriptLogger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Sink(scriptSink)
|
|
.WriteTo.Sink(new ScriptLogCompanionSink(mainLogger))
|
|
.CreateLogger();
|
|
|
|
return (scriptLogger, scriptSink, mainSink);
|
|
}
|
|
|
|
[Fact]
|
|
public void Info_event_lands_in_scripts_sink_but_not_in_main()
|
|
{
|
|
var (script, scriptSink, mainSink) = BuildPipeline();
|
|
script.ForContext(ScriptLoggerFactory.ScriptNameProperty, "Test").Information("just info");
|
|
|
|
scriptSink.Events.Count.ShouldBe(1);
|
|
mainSink.Events.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Warning_event_lands_in_scripts_sink_but_not_in_main()
|
|
{
|
|
var (script, scriptSink, mainSink) = BuildPipeline();
|
|
script.ForContext(ScriptLoggerFactory.ScriptNameProperty, "Test").Warning("just a warning");
|
|
|
|
scriptSink.Events.Count.ShouldBe(1);
|
|
mainSink.Events.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Error_event_mirrored_to_main_at_Warning_level()
|
|
{
|
|
var (script, scriptSink, mainSink) = BuildPipeline();
|
|
script.ForContext(ScriptLoggerFactory.ScriptNameProperty, "MyAlarm")
|
|
.Error("condition script failed");
|
|
|
|
scriptSink.Events[0].Level.ShouldBe(LogEventLevel.Error);
|
|
mainSink.Events.Count.ShouldBe(1);
|
|
mainSink.Events[0].Level.ShouldBe(LogEventLevel.Warning, "Error+ is downgraded to Warning in the main log");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mirrored_event_includes_ScriptName_and_original_level()
|
|
{
|
|
var (script, _, mainSink) = BuildPipeline();
|
|
script.ForContext(ScriptLoggerFactory.ScriptNameProperty, "HighTemp")
|
|
.Error("temp exceeded limit");
|
|
|
|
var forwarded = mainSink.Events[0];
|
|
forwarded.Properties.ShouldContainKey("ScriptName");
|
|
((ScalarValue)forwarded.Properties["ScriptName"]).Value.ShouldBe("HighTemp");
|
|
forwarded.Properties.ShouldContainKey("OriginalLevel");
|
|
((ScalarValue)forwarded.Properties["OriginalLevel"]).Value.ShouldBe(LogEventLevel.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mirrored_event_preserves_exception_for_main_log_stack_trace()
|
|
{
|
|
var (script, _, mainSink) = BuildPipeline();
|
|
var ex = new InvalidOperationException("user code threw");
|
|
script.ForContext(ScriptLoggerFactory.ScriptNameProperty, "BadScript").Error(ex, "boom");
|
|
|
|
mainSink.Events.Count.ShouldBe(1);
|
|
mainSink.Events[0].Exception.ShouldBeSameAs(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fatal_event_mirrored_just_like_Error()
|
|
{
|
|
var (script, _, mainSink) = BuildPipeline();
|
|
script.ForContext(ScriptLoggerFactory.ScriptNameProperty, "Fatal_Script").Fatal("catastrophic");
|
|
mainSink.Events.Count.ShouldBe(1);
|
|
mainSink.Events[0].Level.ShouldBe(LogEventLevel.Warning);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_ScriptName_property_falls_back_to_unknown()
|
|
{
|
|
var (_, _, mainSink) = BuildPipeline();
|
|
// Log without the ScriptName property to simulate a direct root-logger call
|
|
// that bypassed the factory (defensive — shouldn't normally happen).
|
|
var mainLogger = new LoggerConfiguration().CreateLogger();
|
|
var companion = new ScriptLogCompanionSink(Log.Logger);
|
|
|
|
// Build an event manually so we can omit the property.
|
|
var ev = new LogEvent(
|
|
timestamp: DateTimeOffset.UtcNow,
|
|
level: LogEventLevel.Error,
|
|
exception: null,
|
|
messageTemplate: new Serilog.Parsing.MessageTemplateParser().Parse("naked error"),
|
|
properties: []);
|
|
// Direct test: sink should not throw + message should be well-formed.
|
|
Should.NotThrow(() => companion.Emit(ev));
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_main_logger_rejected()
|
|
{
|
|
Should.Throw<ArgumentNullException>(() => new ScriptLogCompanionSink(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_mirror_threshold_applied()
|
|
{
|
|
// Caller can raise the mirror threshold to Fatal if they want only
|
|
// catastrophic events in the main log.
|
|
var mainSink = new CapturingSink();
|
|
var mainLogger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose().WriteTo.Sink(mainSink).CreateLogger();
|
|
|
|
var scriptLogger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Sink(new ScriptLogCompanionSink(mainLogger, LogEventLevel.Fatal))
|
|
.CreateLogger();
|
|
|
|
scriptLogger.ForContext(ScriptLoggerFactory.ScriptNameProperty, "X").Error("error");
|
|
mainSink.Events.Count.ShouldBe(0, "Error below configured Fatal threshold — not mirrored");
|
|
|
|
scriptLogger.ForContext(ScriptLoggerFactory.ScriptNameProperty, "X").Fatal("fatal");
|
|
mainSink.Events.Count.ShouldBe(1);
|
|
}
|
|
}
|