chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
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>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user