From abdf4cc6909d6b83a92c132a9ee83701fa896034 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 26 Feb 2026 17:31:11 +0800
Subject: [PATCH] =?UTF-8?q?test(logging):=20=E6=B7=BB=E5=8A=A0=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E7=B3=BB=E7=BB=9F=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为 AsyncLogAppender 添加完整功能测试,包括异步写入、缓冲区管理、并发处理等场景
- 为 CachedLoggerFactory 添加缓存机制测试,验证相同名称和级别的日志记录器重用
- 为 CompositeFilter 添加过滤器组合测试,验证多个过滤器的逻辑组合功能
- 为 CompositeLogger 添加复合日志记录器测试,验证多追加器写入和级别过滤功能
- 为 ConsoleAppender 添加控制台追加器测试,验证格式化输出和过滤器支持
- 为 DefaultLogFormatter 添加默认格式化器测试,验证基本格式化和异常处理功能
- 为 FileAppender 添加文件追加器测试,验证文件写入、目录创建和追加模式功能
- 为 JsonLogFormatter 添加 JSON 格式化器测试,验证 JSON 输出和属性序列化功能
- 为 LogContext 添加日志上下文测试,验证属性推送和作用域管理功能
---
.../logging/AsyncLogAppenderTests.cs | 219 +++++++++++++
.../logging/CachedLoggerFactoryTests.cs | 95 ++++++
.../logging/CompositeFilterTests.cs | 102 ++++++
.../logging/CompositeLoggerTests.cs | 182 +++++++++++
.../logging/ConsoleAppenderTests.cs | 109 +++++++
.../logging/DefaultLogFormatterTests.cs | 117 +++++++
.../logging/FileAppenderTests.cs | 172 ++++++++++
.../logging/JsonLogFormatterTests.cs | 153 +++++++++
.../logging/LogContextTests.cs | 204 ++++++++++++
.../logging/LogEntryTests.cs | 145 +++++++++
.../logging/LogLevelFilterTests.cs | 59 ++++
.../logging/LoggingConfigurationTests.cs | 299 ++++++++++++++++++
.../logging/NamespaceFilterTests.cs | 85 +++++
.../logging/RollingFileAppenderTests.cs | 164 ++++++++++
14 files changed, 2105 insertions(+)
create mode 100644 GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs
create mode 100644 GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs
create mode 100644 GFramework.Core.Tests/logging/CompositeFilterTests.cs
create mode 100644 GFramework.Core.Tests/logging/CompositeLoggerTests.cs
create mode 100644 GFramework.Core.Tests/logging/ConsoleAppenderTests.cs
create mode 100644 GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs
create mode 100644 GFramework.Core.Tests/logging/FileAppenderTests.cs
create mode 100644 GFramework.Core.Tests/logging/JsonLogFormatterTests.cs
create mode 100644 GFramework.Core.Tests/logging/LogContextTests.cs
create mode 100644 GFramework.Core.Tests/logging/LogEntryTests.cs
create mode 100644 GFramework.Core.Tests/logging/LogLevelFilterTests.cs
create mode 100644 GFramework.Core.Tests/logging/LoggingConfigurationTests.cs
create mode 100644 GFramework.Core.Tests/logging/NamespaceFilterTests.cs
create mode 100644 GFramework.Core.Tests/logging/RollingFileAppenderTests.cs
diff --git a/GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs b/GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs
new file mode 100644
index 0000000..dab71c7
--- /dev/null
+++ b/GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs
@@ -0,0 +1,219 @@
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging.appenders;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 AsyncLogAppender 的功能和行为
+///
+[TestFixture]
+public class AsyncLogAppenderTests
+{
+ [Test]
+ public void Constructor_WithNullInnerAppender_ShouldThrowArgumentNullException()
+ {
+ Assert.Throws(() => new AsyncLogAppender(null!));
+ }
+
+ [Test]
+ public void Constructor_WithInvalidBufferSize_ShouldThrowArgumentException()
+ {
+ var innerAppender = new TestAppender();
+ Assert.Throws(() => new AsyncLogAppender(innerAppender, bufferSize: 0));
+ Assert.Throws(() => new AsyncLogAppender(innerAppender, bufferSize: -1));
+ }
+
+ [Test]
+ public void Append_ShouldNotBlock()
+ {
+ var innerAppender = new SlowAppender(delayMs: 100);
+ using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
+
+ var startTime = DateTime.Now;
+ for (int i = 0; i < 10; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+
+ var elapsed = (DateTime.Now - startTime).TotalMilliseconds;
+
+ // 异步写入应该非常快(< 100ms),不应该等待内部 Appender
+ Assert.That(elapsed, Is.LessThan(100));
+ }
+
+ [Test]
+ public void Append_ShouldEventuallyWriteToInnerAppender()
+ {
+ var innerAppender = new TestAppender();
+ using (var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000))
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+
+ asyncAppender.Flush();
+ }
+
+ Assert.That(innerAppender.Entries.Count, Is.EqualTo(10));
+ }
+
+ [Test]
+ public void Flush_ShouldWaitForAllEntriesToBeProcessed()
+ {
+ var innerAppender = new TestAppender();
+ using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
+
+ for (int i = 0; i < 100; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+
+ asyncAppender.Flush();
+
+ Assert.That(innerAppender.Entries.Count, Is.EqualTo(100));
+ }
+
+ [Test]
+ public void Dispose_ShouldProcessRemainingEntries()
+ {
+ var innerAppender = new TestAppender();
+ using (var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000))
+ {
+ for (int i = 0; i < 50; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+ } // Dispose 会等待所有日志处理完成
+
+ Assert.That(innerAppender.Entries.Count, Is.EqualTo(50));
+ }
+
+ [Test]
+ public void Append_AfterDispose_ShouldThrowObjectDisposedException()
+ {
+ var innerAppender = new TestAppender();
+ var asyncAppender = new AsyncLogAppender(innerAppender);
+ asyncAppender.Dispose();
+
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test", null, null);
+
+ Assert.Throws(() => asyncAppender.Append(entry));
+ }
+
+ [Test]
+ public void PendingCount_ShouldReflectQueuedEntries()
+ {
+ var innerAppender = new SlowAppender(delayMs: 50);
+ using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
+
+ for (int i = 0; i < 10; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+
+ // 应该有一些待处理的条目
+ Assert.That(asyncAppender.PendingCount, Is.GreaterThanOrEqualTo(0));
+ }
+
+ [Test]
+ public async Task Append_FromMultipleThreads_ShouldHandleConcurrency()
+ {
+ var innerAppender = new TestAppender();
+ using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10000);
+
+ var tasks = new Task[10];
+ for (int t = 0; t < 10; t++)
+ {
+ int threadId = t;
+ tasks[t] = Task.Run(() =>
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger",
+ $"Thread {threadId} Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+ });
+ }
+
+ await Task.WhenAll(tasks);
+ asyncAppender.Flush();
+
+ Assert.That(innerAppender.Entries.Count, Is.EqualTo(1000));
+ }
+
+ [Test]
+ public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
+ {
+ var innerAppender = new ThrowingAppender();
+ using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
+
+ // 即使内部 Appender 抛出异常,也不应该影响调用线程
+ Assert.DoesNotThrow(() =>
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ asyncAppender.Append(entry);
+ }
+ });
+
+ Thread.Sleep(100); // 等待后台处理
+ }
+
+ // 辅助测试类
+ private class TestAppender : ILogAppender
+ {
+ public List Entries { get; } = new();
+
+ public void Append(LogEntry entry)
+ {
+ lock (Entries)
+ {
+ Entries.Add(entry);
+ }
+ }
+
+ public void Flush()
+ {
+ }
+ }
+
+ private class SlowAppender : ILogAppender
+ {
+ private readonly int _delayMs;
+
+ public SlowAppender(int delayMs)
+ {
+ _delayMs = delayMs;
+ }
+
+ public void Append(LogEntry entry)
+ {
+ Thread.Sleep(_delayMs);
+ }
+
+ public void Flush()
+ {
+ }
+ }
+
+ private class ThrowingAppender : ILogAppender
+ {
+ public void Append(LogEntry entry)
+ {
+ throw new InvalidOperationException("Test exception");
+ }
+
+ public void Flush()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs b/GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs
new file mode 100644
index 0000000..18a6bac
--- /dev/null
+++ b/GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs
@@ -0,0 +1,95 @@
+using System.IO;
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 CachedLoggerFactory 的功能和行为
+///
+[TestFixture]
+public class CachedLoggerFactoryTests
+{
+ [Test]
+ public void Constructor_WithNullInnerFactory_ShouldThrowArgumentNullException()
+ {
+ Assert.Throws(() => new CachedLoggerFactory(null!));
+ }
+
+ [Test]
+ public void GetLogger_WithSameNameAndLevel_ShouldReturnSameInstance()
+ {
+ var innerFactory = new ConsoleLoggerFactory();
+ var cachedFactory = new CachedLoggerFactory(innerFactory);
+
+ var logger1 = cachedFactory.GetLogger("TestLogger", LogLevel.Info);
+ var logger2 = cachedFactory.GetLogger("TestLogger", LogLevel.Info);
+
+ Assert.That(logger1, Is.SameAs(logger2));
+ }
+
+ [Test]
+ public void GetLogger_WithDifferentNames_ShouldReturnDifferentInstances()
+ {
+ var innerFactory = new ConsoleLoggerFactory();
+ var cachedFactory = new CachedLoggerFactory(innerFactory);
+
+ var logger1 = cachedFactory.GetLogger("Logger1", LogLevel.Info);
+ var logger2 = cachedFactory.GetLogger("Logger2", LogLevel.Info);
+
+ Assert.That(logger1, Is.Not.SameAs(logger2));
+ }
+
+ [Test]
+ public void GetLogger_WithDifferentLevels_ShouldReturnDifferentInstances()
+ {
+ var innerFactory = new ConsoleLoggerFactory();
+ var cachedFactory = new CachedLoggerFactory(innerFactory);
+
+ var logger1 = cachedFactory.GetLogger("TestLogger", LogLevel.Info);
+ var logger2 = cachedFactory.GetLogger("TestLogger", LogLevel.Debug);
+
+ Assert.That(logger1, Is.Not.SameAs(logger2));
+ }
+
+ [Test]
+ public void GetLogger_MultipleCalls_ShouldOnlyCreateOnce()
+ {
+ var trackingFactory = new TrackingLoggerFactory();
+ var cachedFactory = new CachedLoggerFactory(trackingFactory);
+
+ cachedFactory.GetLogger("TestLogger", LogLevel.Info);
+ cachedFactory.GetLogger("TestLogger", LogLevel.Info);
+ cachedFactory.GetLogger("TestLogger", LogLevel.Info);
+
+ Assert.That(trackingFactory.CreateCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void GetLogger_WithMultipleNamesAndLevels_ShouldCacheCorrectly()
+ {
+ var trackingFactory = new TrackingLoggerFactory();
+ var cachedFactory = new CachedLoggerFactory(trackingFactory);
+
+ cachedFactory.GetLogger("Logger1", LogLevel.Info);
+ cachedFactory.GetLogger("Logger1", LogLevel.Debug);
+ cachedFactory.GetLogger("Logger2", LogLevel.Info);
+ cachedFactory.GetLogger("Logger1", LogLevel.Info); // 缓存命中
+ cachedFactory.GetLogger("Logger2", LogLevel.Info); // 缓存命中
+
+ Assert.That(trackingFactory.CreateCount, Is.EqualTo(3));
+ }
+
+ // 辅助测试类
+ private class TrackingLoggerFactory : ILoggerFactory
+ {
+ public int CreateCount { get; private set; }
+
+ public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
+ {
+ CreateCount++;
+ return new ConsoleLogger(name, minLevel, new StringWriter(), false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/CompositeFilterTests.cs b/GFramework.Core.Tests/logging/CompositeFilterTests.cs
new file mode 100644
index 0000000..a14462f
--- /dev/null
+++ b/GFramework.Core.Tests/logging/CompositeFilterTests.cs
@@ -0,0 +1,102 @@
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging.filters;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 CompositeFilter 的功能和行为
+///
+[TestFixture]
+public class CompositeFilterTests
+{
+ [Test]
+ public void Constructor_WithNullFilters_ShouldThrowArgumentNullException()
+ {
+ Assert.Throws(() => new CompositeFilter(null!));
+ }
+
+ [Test]
+ public void Constructor_WithEmptyFilters_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => new CompositeFilter(Array.Empty()));
+ }
+
+ [Test]
+ public void ShouldLog_WithAllFiltersReturningTrue_ShouldReturnTrue()
+ {
+ var filter1 = new LogLevelFilter(LogLevel.Info);
+ var filter2 = new NamespaceFilter("GFramework");
+ var compositeFilter = new CompositeFilter(filter1, filter2);
+
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null);
+
+ Assert.That(compositeFilter.ShouldLog(entry), Is.True);
+ }
+
+ [Test]
+ public void ShouldLog_WithOneFilterReturningFalse_ShouldReturnFalse()
+ {
+ var filter1 = new LogLevelFilter(LogLevel.Warning); // 要求 Warning 以上
+ var filter2 = new NamespaceFilter("GFramework");
+ var compositeFilter = new CompositeFilter(filter1, filter2);
+
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null);
+
+ Assert.That(compositeFilter.ShouldLog(entry), Is.False);
+ }
+
+ [Test]
+ public void ShouldLog_WithAllFiltersReturningFalse_ShouldReturnFalse()
+ {
+ var filter1 = new LogLevelFilter(LogLevel.Warning);
+ var filter2 = new NamespaceFilter("MyApp");
+ var compositeFilter = new CompositeFilter(filter1, filter2);
+
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null);
+
+ Assert.That(compositeFilter.ShouldLog(entry), Is.False);
+ }
+
+ [Test]
+ public void ShouldLog_WithMultipleFilters_ShouldApplyAndLogic()
+ {
+ var levelFilter = new LogLevelFilter(LogLevel.Info);
+ var namespaceFilter = new NamespaceFilter("GFramework");
+ var compositeFilter = new CompositeFilter(levelFilter, namespaceFilter);
+
+ // 满足所有条件
+ var entry1 = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null);
+ Assert.That(compositeFilter.ShouldLog(entry1), Is.True);
+
+ // 级别不满足
+ var entry2 = new LogEntry(DateTime.Now, LogLevel.Debug, "GFramework.Core", "Test", null, null);
+ Assert.That(compositeFilter.ShouldLog(entry2), Is.False);
+
+ // 命名空间不满足
+ var entry3 = new LogEntry(DateTime.Now, LogLevel.Info, "OtherNamespace", "Test", null, null);
+ Assert.That(compositeFilter.ShouldLog(entry3), Is.False);
+
+ // 都不满足
+ var entry4 = new LogEntry(DateTime.Now, LogLevel.Debug, "OtherNamespace", "Test", null, null);
+ Assert.That(compositeFilter.ShouldLog(entry4), Is.False);
+ }
+
+ [Test]
+ public void ShouldLog_WithNestedCompositeFilters_ShouldWork()
+ {
+ var filter1 = new LogLevelFilter(LogLevel.Info);
+ var filter2 = new NamespaceFilter("GFramework");
+ var innerComposite = new CompositeFilter(filter1, filter2);
+
+ var filter3 = new LogLevelFilter(LogLevel.Warning);
+ var outerComposite = new CompositeFilter(innerComposite, filter3);
+
+ // 需要同时满足:Info 以上 AND GFramework 命名空间 AND Warning 以上
+ var entry1 = new LogEntry(DateTime.Now, LogLevel.Warning, "GFramework.Core", "Test", null, null);
+ Assert.That(outerComposite.ShouldLog(entry1), Is.True);
+
+ var entry2 = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null);
+ Assert.That(outerComposite.ShouldLog(entry2), Is.False); // 不满足 Warning
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/CompositeLoggerTests.cs b/GFramework.Core.Tests/logging/CompositeLoggerTests.cs
new file mode 100644
index 0000000..cbf355c
--- /dev/null
+++ b/GFramework.Core.Tests/logging/CompositeLoggerTests.cs
@@ -0,0 +1,182 @@
+using System.IO;
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging;
+using GFramework.Core.logging.appenders;
+using GFramework.Core.logging.formatters;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 CompositeLogger 的功能和行为
+///
+[TestFixture]
+public class CompositeLoggerTests
+{
+ [Test]
+ public void Constructor_WithNullAppenders_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => new CompositeLogger("Test", LogLevel.Info, null!));
+ }
+
+ [Test]
+ public void Constructor_WithEmptyAppenders_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => new CompositeLogger("Test", LogLevel.Info, Array.Empty()));
+ }
+
+ [Test]
+ public void Write_ShouldWriteToAllAppenders()
+ {
+ var writer1 = new StringWriter();
+ var writer2 = new StringWriter();
+ var appender1 = new ConsoleAppender(new DefaultLogFormatter(), writer1, useColors: false);
+ var appender2 = new ConsoleAppender(new DefaultLogFormatter(), writer2, useColors: false);
+
+ using var logger = new CompositeLogger("TestLogger", LogLevel.Info, appender1, appender2);
+
+ logger.Info("Test message");
+
+ var output1 = writer1.ToString();
+ var output2 = writer2.ToString();
+
+ Assert.That(output1, Does.Contain("Test message"));
+ Assert.That(output2, Does.Contain("Test message"));
+
+ writer1.Dispose();
+ writer2.Dispose();
+ }
+
+ [Test]
+ public void Log_WithStructuredProperties_ShouldWriteToAllAppenders()
+ {
+ var writer1 = new StringWriter();
+ var writer2 = new StringWriter();
+ var appender1 = new ConsoleAppender(new DefaultLogFormatter(), writer1, useColors: false);
+ var appender2 = new ConsoleAppender(new DefaultLogFormatter(), writer2, useColors: false);
+
+ using var logger = new CompositeLogger("TestLogger", LogLevel.Info, appender1, appender2);
+
+ logger.Log(LogLevel.Info, "User action", ("UserId", 12345), ("Action", "Login"));
+
+ var output1 = writer1.ToString();
+ var output2 = writer2.ToString();
+
+ Assert.That(output1, Does.Contain("User action"));
+ Assert.That(output1, Does.Contain("UserId=12345"));
+ Assert.That(output2, Does.Contain("User action"));
+ Assert.That(output2, Does.Contain("UserId=12345"));
+
+ writer1.Dispose();
+ writer2.Dispose();
+ }
+
+ [Test]
+ public void Log_WithException_ShouldWriteToAllAppenders()
+ {
+ var writer1 = new StringWriter();
+ var writer2 = new StringWriter();
+ var appender1 = new ConsoleAppender(new DefaultLogFormatter(), writer1, useColors: false);
+ var appender2 = new ConsoleAppender(new DefaultLogFormatter(), writer2, useColors: false);
+
+ using var logger = new CompositeLogger("TestLogger", LogLevel.Info, appender1, appender2);
+
+ var exception = new InvalidOperationException("Test exception");
+ logger.Log(LogLevel.Error, "Error occurred", exception, ("ErrorCode", 500));
+
+ var output1 = writer1.ToString();
+ var output2 = writer2.ToString();
+
+ Assert.That(output1, Does.Contain("Error occurred"));
+ Assert.That(output1, Does.Contain("InvalidOperationException"));
+ Assert.That(output2, Does.Contain("Error occurred"));
+ Assert.That(output2, Does.Contain("InvalidOperationException"));
+
+ writer1.Dispose();
+ writer2.Dispose();
+ }
+
+ [Test]
+ public void Flush_ShouldFlushAllAppenders()
+ {
+ var testAppender1 = new TestFlushAppender();
+ var testAppender2 = new TestFlushAppender();
+
+ using var logger = new CompositeLogger("TestLogger", LogLevel.Info, testAppender1, testAppender2);
+
+ logger.Info("Test message");
+ logger.Flush();
+
+ Assert.That(testAppender1.FlushCalled, Is.True);
+ Assert.That(testAppender2.FlushCalled, Is.True);
+ }
+
+ [Test]
+ public void Dispose_ShouldDisposeAllAppenders()
+ {
+ var testAppender1 = new TestDisposableAppender();
+ var testAppender2 = new TestDisposableAppender();
+
+ var logger = new CompositeLogger("TestLogger", LogLevel.Info, testAppender1, testAppender2);
+ logger.Dispose();
+
+ Assert.That(testAppender1.DisposeCalled, Is.True);
+ Assert.That(testAppender2.DisposeCalled, Is.True);
+ }
+
+ [Test]
+ public void Write_WithLevelFiltering_ShouldRespectMinLevel()
+ {
+ var writer = new StringWriter();
+ var appender = new ConsoleAppender(new DefaultLogFormatter(), writer, useColors: false);
+
+ using var logger = new CompositeLogger("TestLogger", LogLevel.Warning, appender);
+
+ logger.Debug("Debug message");
+ logger.Info("Info message");
+ logger.Warn("Warning message");
+ logger.Error("Error message");
+
+ var output = writer.ToString();
+
+ Assert.That(output, Does.Not.Contain("Debug message"));
+ Assert.That(output, Does.Not.Contain("Info message"));
+ Assert.That(output, Does.Contain("Warning message"));
+ Assert.That(output, Does.Contain("Error message"));
+
+ writer.Dispose();
+ }
+
+ // 辅助测试类
+ private class TestFlushAppender : ILogAppender
+ {
+ public bool FlushCalled { get; private set; }
+
+ public void Append(LogEntry entry)
+ {
+ }
+
+ public void Flush()
+ {
+ FlushCalled = true;
+ }
+ }
+
+ private class TestDisposableAppender : ILogAppender, IDisposable
+ {
+ public bool DisposeCalled { get; private set; }
+
+ public void Dispose()
+ {
+ DisposeCalled = true;
+ }
+
+ public void Append(LogEntry entry)
+ {
+ }
+
+ public void Flush()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/ConsoleAppenderTests.cs b/GFramework.Core.Tests/logging/ConsoleAppenderTests.cs
new file mode 100644
index 0000000..9375562
--- /dev/null
+++ b/GFramework.Core.Tests/logging/ConsoleAppenderTests.cs
@@ -0,0 +1,109 @@
+using System.IO;
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging.appenders;
+using GFramework.Core.logging.filters;
+using GFramework.Core.logging.formatters;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 ConsoleAppender 的功能和行为
+///
+[TestFixture]
+public class ConsoleAppenderTests
+{
+ [SetUp]
+ public void SetUp()
+ {
+ _stringWriter = new StringWriter();
+ _appender = new ConsoleAppender(new DefaultLogFormatter(), _stringWriter, useColors: false);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ _appender?.Dispose();
+ _stringWriter?.Dispose();
+ }
+
+ private StringWriter _stringWriter = null!;
+ private ConsoleAppender _appender = null!;
+
+ [Test]
+ public void Constructor_WithNullFormatter_ShouldThrowArgumentNullException()
+ {
+ Assert.Throws(() => new ConsoleAppender(null!));
+ }
+
+ [Test]
+ public void Append_ShouldWriteToWriter()
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ _appender.Append(entry);
+
+ var output = _stringWriter.ToString();
+ Assert.That(output, Does.Contain("Test message"));
+ Assert.That(output, Does.Contain("INFO"));
+ }
+
+ [Test]
+ public void Append_WithFilter_ShouldRespectFilter()
+ {
+ var filter = new LogLevelFilter(LogLevel.Warning);
+ var appender = new ConsoleAppender(new DefaultLogFormatter(), _stringWriter, useColors: false, filter: filter);
+
+ var infoEntry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Info message", null, null);
+ var warningEntry = new LogEntry(DateTime.Now, LogLevel.Warning, "TestLogger", "Warning message", null, null);
+
+ appender.Append(infoEntry);
+ appender.Append(warningEntry);
+
+ var output = _stringWriter.ToString();
+ Assert.That(output, Does.Not.Contain("Info message"));
+ Assert.That(output, Does.Contain("Warning message"));
+
+ appender.Dispose();
+ }
+
+ [Test]
+ public void Flush_ShouldFlushWriter()
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ _appender.Append(entry);
+ _appender.Flush();
+
+ var output = _stringWriter.ToString();
+ Assert.That(output, Does.Contain("Test message"));
+ }
+
+ [Test]
+ public void Dispose_ShouldFlushWriter()
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ _appender.Append(entry);
+ _appender.Dispose();
+
+ var output = _stringWriter.ToString();
+ Assert.That(output, Does.Contain("Test message"));
+ }
+
+ [Test]
+ public void Append_MultipleEntries_ShouldWriteAll()
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ _appender.Append(entry);
+ }
+
+ var output = _stringWriter.ToString();
+ for (int i = 0; i < 10; i++)
+ {
+ Assert.That(output, Does.Contain($"Message {i}"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs b/GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs
new file mode 100644
index 0000000..f997c1c
--- /dev/null
+++ b/GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs
@@ -0,0 +1,117 @@
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging.formatters;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 DefaultLogFormatter 的功能和行为
+///
+[TestFixture]
+public class DefaultLogFormatterTests
+{
+ [SetUp]
+ public void SetUp()
+ {
+ _formatter = new DefaultLogFormatter();
+ }
+
+ private DefaultLogFormatter _formatter = null!;
+
+ [Test]
+ public void Format_WithBasicEntry_ShouldFormatCorrectly()
+ {
+ var timestamp = new DateTime(2026, 2, 26, 10, 30, 45, 123);
+ var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain("[2026-02-26 10:30:45.123]"));
+ Assert.That(result, Does.Contain("INFO"));
+ Assert.That(result, Does.Contain("[TestLogger]"));
+ Assert.That(result, Does.Contain("Test message"));
+ }
+
+ [Test]
+ public void Format_WithException_ShouldIncludeException()
+ {
+ var exception = new InvalidOperationException("Test exception");
+ var entry = new LogEntry(DateTime.Now, LogLevel.Error, "TestLogger", "Error occurred", exception, null);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain("Error occurred"));
+ Assert.That(result, Does.Contain("InvalidOperationException"));
+ Assert.That(result, Does.Contain("Test exception"));
+ }
+
+ [Test]
+ public void Format_WithProperties_ShouldIncludeProperties()
+ {
+ var properties = new Dictionary
+ {
+ ["UserId"] = 12345,
+ ["UserName"] = "TestUser"
+ };
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "User action", null, properties);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain("User action"));
+ Assert.That(result, Does.Contain("|"));
+ Assert.That(result, Does.Contain("UserId=12345"));
+ Assert.That(result, Does.Contain("UserName=TestUser"));
+ }
+
+ [Test]
+ public void Format_WithNullProperty_ShouldHandleNull()
+ {
+ var properties = new Dictionary
+ {
+ ["Key1"] = null
+ };
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain("Key1="));
+ }
+
+ [Test]
+ public void Format_WithAllLogLevels_ShouldFormatCorrectly()
+ {
+ var levels = new[]
+ { LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Fatal };
+ var expectedStrings = new[] { "TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" };
+
+ for (int i = 0; i < levels.Length; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, levels[i], "TestLogger", "Test", null, null);
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain(expectedStrings[i]));
+ }
+ }
+
+ [Test]
+ public void Format_WithLongMessage_ShouldNotTruncate()
+ {
+ var longMessage = new string('A', 1000);
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", longMessage, null, null);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain(longMessage));
+ }
+
+ [Test]
+ public void Format_WithSpecialCharacters_ShouldPreserveCharacters()
+ {
+ var message = "Test\nNew\tLine\r\nSpecial: <>&\"'";
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", message, null, null);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(result, Does.Contain(message));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/FileAppenderTests.cs b/GFramework.Core.Tests/logging/FileAppenderTests.cs
new file mode 100644
index 0000000..9c2cd24
--- /dev/null
+++ b/GFramework.Core.Tests/logging/FileAppenderTests.cs
@@ -0,0 +1,172 @@
+using System.IO;
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging.appenders;
+using GFramework.Core.logging.formatters;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 FileAppender 的功能和行为
+///
+[TestFixture]
+public class FileAppenderTests
+{
+ [SetUp]
+ public void SetUp()
+ {
+ _testFilePath = Path.Combine(Path.GetTempPath(), $"test_log_{Guid.NewGuid()}.log");
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (File.Exists(_testFilePath))
+ {
+ try
+ {
+ File.Delete(_testFilePath);
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ private string _testFilePath = null!;
+
+ [Test]
+ public void Constructor_WithNullFilePath_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => new FileAppender(null!));
+ }
+
+ [Test]
+ public void Constructor_WithEmptyFilePath_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => new FileAppender(""));
+ }
+
+ [Test]
+ public void Constructor_ShouldCreateDirectoryIfNotExists()
+ {
+ var dirPath = Path.Combine(Path.GetTempPath(), $"test_dir_{Guid.NewGuid()}");
+ var filePath = Path.Combine(dirPath, "test.log");
+
+ try
+ {
+ using var appender = new FileAppender(filePath);
+
+ Assert.That(Directory.Exists(dirPath), Is.True);
+ }
+ finally
+ {
+ if (Directory.Exists(dirPath))
+ {
+ Directory.Delete(dirPath, true);
+ }
+ }
+ }
+
+ [Test]
+ public void Append_ShouldWriteToFile()
+ {
+ using (var appender = new FileAppender(_testFilePath))
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+ appender.Append(entry);
+ appender.Flush();
+ }
+
+ var content = File.ReadAllText(_testFilePath);
+ Assert.That(content, Does.Contain("Test message"));
+ Assert.That(content, Does.Contain("INFO"));
+ }
+
+ [Test]
+ public void Append_MultipleEntries_ShouldWriteAll()
+ {
+ using (var appender = new FileAppender(_testFilePath))
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
+ appender.Append(entry);
+ }
+
+ appender.Flush();
+ }
+
+ var lines = File.ReadAllLines(_testFilePath);
+ Assert.That(lines.Length, Is.EqualTo(10));
+ for (int i = 0; i < 10; i++)
+ {
+ Assert.That(lines[i], Does.Contain($"Message {i}"));
+ }
+ }
+
+ [Test]
+ public void Append_WithJsonFormatter_ShouldWriteJson()
+ {
+ using (var appender = new FileAppender(_testFilePath, new JsonLogFormatter()))
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+ appender.Append(entry);
+ appender.Flush();
+ }
+
+ var content = File.ReadAllText(_testFilePath);
+ Assert.That(content, Does.Contain("\"message\""));
+ Assert.That(content, Does.Contain("\"level\""));
+ }
+
+ [Test]
+ public void Append_AfterDispose_ShouldThrowObjectDisposedException()
+ {
+ var appender = new FileAppender(_testFilePath);
+ appender.Dispose();
+
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ Assert.Throws(() => appender.Append(entry));
+ }
+
+ [Test]
+ public void Append_WithAppendMode_ShouldAppendToExistingFile()
+ {
+ // 第一次写入
+ using (var appender1 = new FileAppender(_testFilePath))
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "First message", null, null);
+ appender1.Append(entry);
+ appender1.Flush();
+ }
+
+ // 第二次写入
+ using (var appender2 = new FileAppender(_testFilePath))
+ {
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Second message", null, null);
+ appender2.Append(entry);
+ appender2.Flush();
+ }
+
+ var lines = File.ReadAllLines(_testFilePath);
+ Assert.That(lines.Length, Is.EqualTo(2));
+ Assert.That(lines[0], Does.Contain("First message"));
+ Assert.That(lines[1], Does.Contain("Second message"));
+ }
+
+ [Test]
+ public void Flush_ShouldEnsureDataWritten()
+ {
+ using var appender = new FileAppender(_testFilePath);
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ appender.Append(entry);
+ appender.Flush();
+
+ // 立即读取文件应该能看到内容
+ var content = File.ReadAllText(_testFilePath);
+ Assert.That(content, Does.Contain("Test message"));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs b/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs
new file mode 100644
index 0000000..2cf8ffb
--- /dev/null
+++ b/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs
@@ -0,0 +1,153 @@
+using System.Text.Json;
+using GFramework.Core.Abstractions.logging;
+using GFramework.Core.logging.formatters;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 JsonLogFormatter 的功能和行为
+///
+[TestFixture]
+public class JsonLogFormatterTests
+{
+ [SetUp]
+ public void SetUp()
+ {
+ _formatter = new JsonLogFormatter();
+ }
+
+ private JsonLogFormatter _formatter = null!;
+
+ [Test]
+ public void Format_WithBasicEntry_ShouldProduceValidJson()
+ {
+ var timestamp = new DateTime(2026, 2, 26, 10, 30, 45, 123, DateTimeKind.Utc);
+ var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, null);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(() => JsonDocument.Parse(result), Throws.Nothing);
+
+ var doc = JsonDocument.Parse(result);
+ Assert.That(doc.RootElement.GetProperty("level").GetString(), Is.EqualTo("INFO"));
+ Assert.That(doc.RootElement.GetProperty("logger").GetString(), Is.EqualTo("TestLogger"));
+ Assert.That(doc.RootElement.GetProperty("message").GetString(), Is.EqualTo("Test message"));
+ }
+
+ [Test]
+ public void Format_WithException_ShouldIncludeExceptionDetails()
+ {
+ var exception = new InvalidOperationException("Test exception");
+ var entry = new LogEntry(DateTime.Now, LogLevel.Error, "TestLogger", "Error occurred", exception, null);
+
+ var result = _formatter.Format(entry);
+
+ var doc = JsonDocument.Parse(result);
+ var exceptionObj = doc.RootElement.GetProperty("exception");
+
+ Assert.That(exceptionObj.GetProperty("type").GetString(), Does.Contain("InvalidOperationException"));
+ Assert.That(exceptionObj.GetProperty("message").GetString(), Is.EqualTo("Test exception"));
+ Assert.That(exceptionObj.TryGetProperty("stackTrace", out _), Is.True);
+ }
+
+ [Test]
+ public void Format_WithProperties_ShouldIncludePropertiesObject()
+ {
+ var properties = new Dictionary
+ {
+ ["UserId"] = 12345,
+ ["UserName"] = "TestUser",
+ ["IsActive"] = true
+ };
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "User action", null, properties);
+
+ var result = _formatter.Format(entry);
+
+ var doc = JsonDocument.Parse(result);
+ var propsObj = doc.RootElement.GetProperty("properties");
+
+ Assert.That(propsObj.GetProperty("userId").GetInt32(), Is.EqualTo(12345));
+ Assert.That(propsObj.GetProperty("userName").GetString(), Is.EqualTo("TestUser"));
+ Assert.That(propsObj.GetProperty("isActive").GetBoolean(), Is.True);
+ }
+
+ [Test]
+ public void Format_WithNullProperty_ShouldHandleNull()
+ {
+ var properties = new Dictionary
+ {
+ ["Key1"] = null
+ };
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties);
+
+ var result = _formatter.Format(entry);
+
+ var doc = JsonDocument.Parse(result);
+ var propsObj = doc.RootElement.GetProperty("properties");
+
+ Assert.That(propsObj.GetProperty("key1").ValueKind, Is.EqualTo(JsonValueKind.Null));
+ }
+
+ [Test]
+ public void Format_WithAllLogLevels_ShouldFormatCorrectly()
+ {
+ var levels = new[]
+ { LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Fatal };
+ var expectedStrings = new[] { "TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" };
+
+ for (int i = 0; i < levels.Length; i++)
+ {
+ var entry = new LogEntry(DateTime.Now, levels[i], "TestLogger", "Test", null, null);
+ var result = _formatter.Format(entry);
+
+ var doc = JsonDocument.Parse(result);
+ Assert.That(doc.RootElement.GetProperty("level").GetString(), Is.EqualTo(expectedStrings[i]));
+ }
+ }
+
+ [Test]
+ public void Format_WithSpecialCharacters_ShouldEscapeCorrectly()
+ {
+ var message = "Test \"quoted\" and \n newline";
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", message, null, null);
+
+ var result = _formatter.Format(entry);
+
+ var doc = JsonDocument.Parse(result);
+ Assert.That(doc.RootElement.GetProperty("message").GetString(), Is.EqualTo(message));
+ }
+
+ [Test]
+ public void Format_ShouldUseIso8601Timestamp()
+ {
+ var timestamp = new DateTime(2026, 2, 26, 10, 30, 45, 123, DateTimeKind.Utc);
+ var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test", null, null);
+
+ var result = _formatter.Format(entry);
+
+ var doc = JsonDocument.Parse(result);
+ var timestampStr = doc.RootElement.GetProperty("timestamp").GetString();
+
+ Assert.That(timestampStr, Does.Contain("2026-02-26"));
+ Assert.That(timestampStr, Does.Contain("T"));
+ }
+
+ [Test]
+ public void Format_WithComplexProperties_ShouldSerializeCorrectly()
+ {
+ var properties = new Dictionary
+ {
+ ["Number"] = 123,
+ ["String"] = "test",
+ ["Boolean"] = true,
+ ["Null"] = null,
+ ["Array"] = new[] { 1, 2, 3 }
+ };
+ var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test", null, properties);
+
+ var result = _formatter.Format(entry);
+
+ Assert.That(() => JsonDocument.Parse(result), Throws.Nothing);
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/logging/LogContextTests.cs b/GFramework.Core.Tests/logging/LogContextTests.cs
new file mode 100644
index 0000000..27402dc
--- /dev/null
+++ b/GFramework.Core.Tests/logging/LogContextTests.cs
@@ -0,0 +1,204 @@
+using GFramework.Core.Abstractions.logging;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.logging;
+
+///
+/// 测试 LogContext 的功能和行为
+///
+[TestFixture]
+public class LogContextTests
+{
+ [SetUp]
+ public void SetUp()
+ {
+ LogContext.Clear();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ LogContext.Clear();
+ }
+
+ [Test]
+ public void Current_WhenEmpty_ShouldReturnEmptyDictionary()
+ {
+ var current = LogContext.Current;
+
+ Assert.That(current, Is.Not.Null);
+ Assert.That(current.Count, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void Push_ShouldAddPropertyToContext()
+ {
+ using (LogContext.Push("Key1", "Value1"))
+ {
+ var current = LogContext.Current;
+
+ Assert.That(current.Count, Is.EqualTo(1));
+ Assert.That(current["Key1"], Is.EqualTo("Value1"));
+ }
+ }
+
+ [Test]
+ public void Push_WithMultipleProperties_ShouldAddAllProperties()
+ {
+ using (LogContext.PushProperties(("Key1", "Value1"), ("Key2", 123)))
+ {
+ var current = LogContext.Current;
+
+ Assert.That(current.Count, Is.EqualTo(2));
+ Assert.That(current["Key1"], Is.EqualTo("Value1"));
+ Assert.That(current["Key2"], Is.EqualTo(123));
+ }
+ }
+
+ [Test]
+ public void Push_WithNestedContext_ShouldMergeProperties()
+ {
+ using (LogContext.Push("Key1", "Value1"))
+ {
+ using (LogContext.Push("Key2", "Value2"))
+ {
+ var current = LogContext.Current;
+
+ Assert.That(current.Count, Is.EqualTo(2));
+ Assert.That(current["Key1"], Is.EqualTo("Value1"));
+ Assert.That(current["Key2"], Is.EqualTo("Value2"));
+ }
+ }
+ }
+
+ [Test]
+ public void Push_WithSameKey_ShouldOverrideValue()
+ {
+ using (LogContext.Push("Key1", "Value1"))
+ {
+ using (LogContext.Push("Key1", "Value2"))
+ {
+ var current = LogContext.Current;
+
+ Assert.That(current.Count, Is.EqualTo(1));
+ Assert.That(current["Key1"], Is.EqualTo("Value2"));
+ }
+
+ // 释放后应该恢复原值
+ var restored = LogContext.Current;
+ Assert.That(restored["Key1"], Is.EqualTo("Value1"));
+ }
+ }
+
+ [Test]
+ public void Dispose_ShouldRestorePreviousValue()
+ {
+ using (LogContext.Push("Key1", "Value1"))
+ {
+ Assert.That(LogContext.Current["Key1"], Is.EqualTo("Value1"));
+ }
+
+ // 释放后应该清空
+ Assert.That(LogContext.Current.Count, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void Clear_ShouldRemoveAllProperties()
+ {
+ using (LogContext.Push("Key1", "Value1"))
+ {
+ using (LogContext.Push("Key2", "Value2"))
+ {
+ LogContext.Clear();
+
+ Assert.That(LogContext.Current.Count, Is.EqualTo(0));
+ }
+ }
+ }
+
+ [Test]
+ public void Push_WithNullKey_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => LogContext.Push(null!, "Value"));
+ }
+
+ [Test]
+ public void Push_WithEmptyKey_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => LogContext.Push("", "Value"));
+ }
+
+ [Test]
+ public void Push_WithWhitespaceKey_ShouldThrowArgumentException()
+ {
+ Assert.Throws(() => LogContext.Push(" ", "Value"));
+ }
+
+ [Test]
+ public void Push_WithNullValue_ShouldWork()
+ {
+ using (LogContext.Push("Key1", null))
+ {
+ var current = LogContext.Current;
+
+ Assert.That(current.Count, Is.EqualTo(1));
+ Assert.That(current["Key1"], Is.Null);
+ }
+ }
+
+ [Test]
+ public async Task Push_InAsyncContext_ShouldIsolateAcrossThreads()
+ {
+ var task1Values = new List