feat: port session 04 — Logging, Signals & Services

- NatsLogger.cs: INatsLogger interface (Noticef/Warnf/Fatalf/Errorf/Debugf/Tracef),
  ServerLogging state class with atomic debug/trace flags, rate-limited logging
  (RateLimitWarnf/RateLimitDebugf), error variants (Errors/Errorc/Errorsc),
  MicrosoftLoggerAdapter bridging to ILogger
- SignalHandler.cs: ProcessSignal (Unix kill via Process), CommandToUnixSignal mapping
  (Stop→SIGKILL, Quit→SIGINT, Reopen→SIGUSR1, Reload→SIGHUP), ResolvePids via pgrep,
  SetProcessName, Run/IsWindowsService stubs for non-Windows
- 11 tests (6 logger, 5 signal/service)
- WASM/Windows signal stubs already n/a
- All 141 tests pass (140 unit + 1 integration)
- DB: features 368/3673 complete, tests 155/3257 complete (9.6% overall)
This commit is contained in:
Joseph Doherty
2026-02-26 11:54:25 -05:00
parent f08fc5d6a7
commit b8f2f66d45
7 changed files with 581 additions and 9 deletions

View File

@@ -0,0 +1,131 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
/// <summary>
/// Tests for NatsLogger / ServerLogging — mirrors tests from server/log_test.go.
/// </summary>
public class NatsLoggerTests
{
private sealed class TestLogger : INatsLogger
{
public List<string> Messages { get; } = [];
public void Noticef(string format, params object[] args) => Messages.Add($"[INF] {string.Format(format, args)}");
public void Warnf(string format, params object[] args) => Messages.Add($"[WRN] {string.Format(format, args)}");
public void Fatalf(string format, params object[] args) => Messages.Add($"[FTL] {string.Format(format, args)}");
public void Errorf(string format, params object[] args) => Messages.Add($"[ERR] {string.Format(format, args)}");
public void Debugf(string format, params object[] args) => Messages.Add($"[DBG] {string.Format(format, args)}");
public void Tracef(string format, params object[] args) => Messages.Add($"[TRC] {string.Format(format, args)}");
}
/// <summary>
/// Mirrors TestSetLogger — verify logger assignment and atomic flags.
/// </summary>
[Fact] // T:2017
public void SetLogger_ShouldSetLoggerAndFlags()
{
var logging = new ServerLogging();
var testLog = new TestLogger();
logging.SetLoggerV2(testLog, true, true, false);
logging.IsDebug.ShouldBeTrue();
logging.IsTrace.ShouldBeTrue();
logging.IsTraceSysAcc.ShouldBeFalse();
logging.GetLogger().ShouldBe(testLog);
}
/// <summary>
/// Verify all log methods produce output when flags enabled.
/// </summary>
[Fact] // T:2017 (continuation)
public void AllLogMethods_ShouldProduceOutput()
{
var logging = new ServerLogging();
var testLog = new TestLogger();
logging.SetLoggerV2(testLog, true, true, false);
logging.Noticef("notice {0}", "test");
logging.Errorf("error {0}", "test");
logging.Warnf("warn {0}", "test");
logging.Fatalf("fatal {0}", "test");
logging.Debugf("debug {0}", "test");
logging.Tracef("trace {0}", "test");
testLog.Messages.Count.ShouldBe(6);
testLog.Messages[0].ShouldContain("[INF]");
testLog.Messages[1].ShouldContain("[ERR]");
testLog.Messages[4].ShouldContain("[DBG]");
testLog.Messages[5].ShouldContain("[TRC]");
}
/// <summary>
/// Debug/Trace should not produce output when flags disabled.
/// </summary>
[Fact] // T:2017 (continuation)
public void DebugTrace_ShouldBeNoOpWhenDisabled()
{
var logging = new ServerLogging();
var testLog = new TestLogger();
logging.SetLoggerV2(testLog, false, false, false);
logging.Debugf("debug");
logging.Tracef("trace");
testLog.Messages.ShouldBeEmpty();
}
/// <summary>
/// Verify null logger does not throw.
/// </summary>
[Fact]
public void NullLogger_ShouldNotThrow()
{
var logging = new ServerLogging();
Should.NotThrow(() => logging.Noticef("test"));
Should.NotThrow(() => logging.Errorf("test"));
Should.NotThrow(() => logging.Debugf("test"));
}
/// <summary>
/// Verify rate-limited logging suppresses duplicate messages.
/// </summary>
[Fact] // T:2017 (RateLimitWarnf behavior)
public void RateLimitWarnf_ShouldSuppressDuplicates()
{
var logging = new ServerLogging();
var testLog = new TestLogger();
logging.SetLoggerV2(testLog, false, false, false);
logging.RateLimitWarnf("duplicate message");
logging.RateLimitWarnf("duplicate message");
logging.RateLimitWarnf("different message");
// Should only log 2 unique messages, not 3.
testLog.Messages.Count.ShouldBe(2);
}
/// <summary>
/// Verify Errors/Errorc/Errorsc convenience methods.
/// </summary>
[Fact]
public void ErrorVariants_ShouldFormatCorrectly()
{
var logging = new ServerLogging();
var testLog = new TestLogger();
logging.SetLoggerV2(testLog, false, false, false);
logging.Errors("client", new Exception("conn reset"));
logging.Errorc("TLS", new Exception("cert expired"));
logging.Errorsc("route", "cluster", new Exception("timeout"));
testLog.Messages.Count.ShouldBe(3);
testLog.Messages[0].ShouldContain("client - conn reset");
testLog.Messages[1].ShouldContain("TLS: cert expired");
testLog.Messages[2].ShouldContain("route - cluster: timeout");
}
}

View File

@@ -0,0 +1,70 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Runtime.InteropServices;
using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
/// <summary>
/// Tests for SignalHandler — mirrors tests from server/signal_test.go.
/// </summary>
public class SignalHandlerTests
{
/// <summary>
/// Mirrors CommandToSignal mapping tests.
/// </summary>
[Fact] // T:3158
public void CommandToUnixSignal_ShouldMapCorrectly()
{
SignalHandler.CommandToUnixSignal(ServerCommand.Stop).ShouldBe(UnixSignal.SigKill);
SignalHandler.CommandToUnixSignal(ServerCommand.Quit).ShouldBe(UnixSignal.SigInt);
SignalHandler.CommandToUnixSignal(ServerCommand.Reopen).ShouldBe(UnixSignal.SigUsr1);
SignalHandler.CommandToUnixSignal(ServerCommand.Reload).ShouldBe(UnixSignal.SigHup);
}
/// <summary>
/// Mirrors SetProcessName test.
/// </summary>
[Fact] // T:3155
public void SetProcessName_ShouldNotThrow()
{
Should.NotThrow(() => SignalHandler.SetProcessName("test-server"));
}
/// <summary>
/// Verify IsWindowsService returns false on non-Windows.
/// </summary>
[Fact] // T:3149
public void IsWindowsService_ShouldReturnFalse()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return; // Skip on Windows
SignalHandler.IsWindowsService().ShouldBeFalse();
}
/// <summary>
/// Mirrors Run — service.go Run() simply invokes the start function.
/// </summary>
[Fact] // T:3148
public void Run_ShouldInvokeStartAction()
{
var called = false;
SignalHandler.Run(() => called = true);
called.ShouldBeTrue();
}
/// <summary>
/// ProcessSignal with invalid PID expression should return error.
/// </summary>
[Fact] // T:3157
public void ProcessSignal_InvalidPid_ShouldReturnError()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return; // Skip on Windows
var err = SignalHandler.ProcessSignal(ServerCommand.Stop, "not-a-pid");
err.ShouldNotBeNull();
}
}