test(logging): 添加日志系统单元测试

- 为 AsyncLogAppender 添加完整功能测试,包括异步写入、缓冲区管理、并发处理等场景
- 为 CachedLoggerFactory 添加缓存机制测试,验证相同名称和级别的日志记录器重用
- 为 CompositeFilter 添加过滤器组合测试,验证多个过滤器的逻辑组合功能
- 为 CompositeLogger 添加复合日志记录器测试,验证多追加器写入和级别过滤功能
- 为 ConsoleAppender 添加控制台追加器测试,验证格式化输出和过滤器支持
- 为 DefaultLogFormatter 添加默认格式化器测试,验证基本格式化和异常处理功能
- 为 FileAppender 添加文件追加器测试,验证文件写入、目录创建和追加模式功能
- 为 JsonLogFormatter 添加 JSON 格式化器测试,验证 JSON 输出和属性序列化功能
- 为 LogContext 添加日志上下文测试,验证属性推送和作用域管理功能
This commit is contained in:
GeWuYou 2026-02-26 17:31:11 +08:00 committed by gewuyou
parent 1ba771e13a
commit abdf4cc690
14 changed files with 2105 additions and 0 deletions

View File

@ -0,0 +1,219 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.appenders;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 AsyncLogAppender 的功能和行为
/// </summary>
[TestFixture]
public class AsyncLogAppenderTests
{
[Test]
public void Constructor_WithNullInnerAppender_ShouldThrowArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new AsyncLogAppender(null!));
}
[Test]
public void Constructor_WithInvalidBufferSize_ShouldThrowArgumentException()
{
var innerAppender = new TestAppender();
Assert.Throws<ArgumentException>(() => new AsyncLogAppender(innerAppender, bufferSize: 0));
Assert.Throws<ArgumentException>(() => 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<ObjectDisposedException>(() => 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<LogEntry> 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()
{
}
}
}

View File

@ -0,0 +1,95 @@
using System.IO;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 CachedLoggerFactory 的功能和行为
/// </summary>
[TestFixture]
public class CachedLoggerFactoryTests
{
[Test]
public void Constructor_WithNullInnerFactory_ShouldThrowArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => 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);
}
}
}

View File

@ -0,0 +1,102 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.filters;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 CompositeFilter 的功能和行为
/// </summary>
[TestFixture]
public class CompositeFilterTests
{
[Test]
public void Constructor_WithNullFilters_ShouldThrowArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new CompositeFilter(null!));
}
[Test]
public void Constructor_WithEmptyFilters_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new CompositeFilter(Array.Empty<ILogFilter>()));
}
[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
}
}

View File

@ -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;
/// <summary>
/// 测试 CompositeLogger 的功能和行为
/// </summary>
[TestFixture]
public class CompositeLoggerTests
{
[Test]
public void Constructor_WithNullAppenders_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new CompositeLogger("Test", LogLevel.Info, null!));
}
[Test]
public void Constructor_WithEmptyAppenders_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new CompositeLogger("Test", LogLevel.Info, Array.Empty<ILogAppender>()));
}
[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()
{
}
}
}

View File

@ -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;
/// <summary>
/// 测试 ConsoleAppender 的功能和行为
/// </summary>
[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<ArgumentNullException>(() => 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}"));
}
}
}

View File

@ -0,0 +1,117 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.formatters;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 DefaultLogFormatter 的功能和行为
/// </summary>
[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<string, object?>
{
["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<string, object?>
{
["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));
}
}

View File

@ -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;
/// <summary>
/// 测试 FileAppender 的功能和行为
/// </summary>
[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<ArgumentException>(() => new FileAppender(null!));
}
[Test]
public void Constructor_WithEmptyFilePath_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => 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<ObjectDisposedException>(() => 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"));
}
}

View File

@ -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;
/// <summary>
/// 测试 JsonLogFormatter 的功能和行为
/// </summary>
[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<string, object?>
{
["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<string, object?>
{
["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<string, object?>
{
["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);
}
}

View File

@ -0,0 +1,204 @@
using GFramework.Core.Abstractions.logging;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 LogContext 的功能和行为
/// </summary>
[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<ArgumentException>(() => LogContext.Push(null!, "Value"));
}
[Test]
public void Push_WithEmptyKey_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => LogContext.Push("", "Value"));
}
[Test]
public void Push_WithWhitespaceKey_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => 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<object?>();
var task2Values = new List<object?>();
var task1 = Task.Run(() =>
{
using (LogContext.Push("TaskId", "Task1"))
{
task1Values.Add(LogContext.Current["TaskId"]);
Task.Delay(50).Wait();
task1Values.Add(LogContext.Current["TaskId"]);
}
});
var task2 = Task.Run(() =>
{
using (LogContext.Push("TaskId", "Task2"))
{
task2Values.Add(LogContext.Current["TaskId"]);
Task.Delay(50).Wait();
task2Values.Add(LogContext.Current["TaskId"]);
}
});
await Task.WhenAll(task1, task2);
Assert.That(task1Values, Has.All.EqualTo("Task1"));
Assert.That(task2Values, Has.All.EqualTo("Task2"));
}
[Test]
public void Push_WithComplexObject_ShouldStoreReference()
{
var obj = new { Name = "Test", Value = 123 };
using (LogContext.Push("Object", obj))
{
var current = LogContext.Current;
Assert.That(current["Object"], Is.SameAs(obj));
}
}
[Test]
public void Push_MultipleDispose_ShouldBeIdempotent()
{
var disposable = LogContext.Push("Key1", "Value1");
disposable.Dispose();
disposable.Dispose(); // 第二次调用不应该抛出异常
Assert.That(LogContext.Current.Count, Is.EqualTo(0));
}
}

View File

@ -0,0 +1,145 @@
using GFramework.Core.Abstractions.logging;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 LogEntry 的功能和行为
/// </summary>
[TestFixture]
public class LogEntryTests
{
[Test]
public void Constructor_WithAllParameters_ShouldCreateEntry()
{
var timestamp = DateTime.Now;
var properties = new Dictionary<string, object?> { ["Key1"] = "Value1" };
var exception = new InvalidOperationException("Test");
var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", exception, properties);
Assert.That(entry.Timestamp, Is.EqualTo(timestamp));
Assert.That(entry.Level, Is.EqualTo(LogLevel.Info));
Assert.That(entry.LoggerName, Is.EqualTo("TestLogger"));
Assert.That(entry.Message, Is.EqualTo("Test message"));
Assert.That(entry.Exception, Is.SameAs(exception));
Assert.That(entry.Properties, Is.SameAs(properties));
}
[Test]
public void Constructor_WithNullException_ShouldWork()
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
Assert.That(entry.Exception, Is.Null);
}
[Test]
public void Constructor_WithNullProperties_ShouldWork()
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
Assert.That(entry.Properties, Is.Null);
}
[Test]
public void GetAllProperties_WithNoProperties_ShouldReturnContextProperties()
{
LogContext.Clear();
using (LogContext.Push("ContextKey", "ContextValue"))
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
var allProps = entry.GetAllProperties();
Assert.That(allProps.Count, Is.EqualTo(1));
Assert.That(allProps["ContextKey"], Is.EqualTo("ContextValue"));
}
LogContext.Clear();
}
[Test]
public void GetAllProperties_WithProperties_ShouldReturnOnlyProperties()
{
LogContext.Clear();
var properties = new Dictionary<string, object?> { ["PropKey"] = "PropValue" };
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
Assert.That(allProps.Count, Is.EqualTo(1));
Assert.That(allProps["PropKey"], Is.EqualTo("PropValue"));
}
[Test]
public void GetAllProperties_WithBothPropertiesAndContext_ShouldMerge()
{
LogContext.Clear();
using (LogContext.Push("ContextKey", "ContextValue"))
{
var properties = new Dictionary<string, object?> { ["PropKey"] = "PropValue" };
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
Assert.That(allProps.Count, Is.EqualTo(2));
Assert.That(allProps["ContextKey"], Is.EqualTo("ContextValue"));
Assert.That(allProps["PropKey"], Is.EqualTo("PropValue"));
}
LogContext.Clear();
}
[Test]
public void GetAllProperties_WithConflictingKeys_ShouldPreferEntryProperties()
{
LogContext.Clear();
using (LogContext.Push("Key1", "ContextValue"))
{
var properties = new Dictionary<string, object?> { ["Key1"] = "PropValue" };
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
Assert.That(allProps.Count, Is.EqualTo(1));
Assert.That(allProps["Key1"], Is.EqualTo("PropValue")); // 日志属性优先
}
LogContext.Clear();
}
[Test]
public void GetAllProperties_WithEmptyPropertiesAndEmptyContext_ShouldReturnEmpty()
{
LogContext.Clear();
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
var allProps = entry.GetAllProperties();
Assert.That(allProps.Count, Is.EqualTo(0));
}
[Test]
public void RecordEquality_WithSameValues_ShouldBeEqual()
{
var timestamp = DateTime.Now;
var properties = new Dictionary<string, object?> { ["Key1"] = "Value1" };
var entry1 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, properties);
var entry2 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, properties);
Assert.That(entry1, Is.EqualTo(entry2));
}
[Test]
public void RecordEquality_WithDifferentValues_ShouldNotBeEqual()
{
var timestamp = DateTime.Now;
var entry1 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message 1", null, null);
var entry2 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message 2", null, null);
Assert.That(entry1, Is.Not.EqualTo(entry2));
}
}

View File

@ -0,0 +1,59 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.filters;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 LogLevelFilter 的功能和行为
/// </summary>
[TestFixture]
public class LogLevelFilterTests
{
[Test]
public void ShouldLog_WithLevelAboveMinimum_ShouldReturnTrue()
{
var filter = new LogLevelFilter(LogLevel.Info);
var entry = new LogEntry(DateTime.Now, LogLevel.Warning, "TestLogger", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.True);
}
[Test]
public void ShouldLog_WithLevelEqualToMinimum_ShouldReturnTrue()
{
var filter = new LogLevelFilter(LogLevel.Info);
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.True);
}
[Test]
public void ShouldLog_WithLevelBelowMinimum_ShouldReturnFalse()
{
var filter = new LogLevelFilter(LogLevel.Info);
var entry = new LogEntry(DateTime.Now, LogLevel.Debug, "TestLogger", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.False);
}
[Test]
public void ShouldLog_WithAllLevels_ShouldWorkCorrectly()
{
var filter = new LogLevelFilter(LogLevel.Warning);
var traceEntry = new LogEntry(DateTime.Now, LogLevel.Trace, "TestLogger", "Test", null, null);
var debugEntry = new LogEntry(DateTime.Now, LogLevel.Debug, "TestLogger", "Test", null, null);
var infoEntry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test", null, null);
var warningEntry = new LogEntry(DateTime.Now, LogLevel.Warning, "TestLogger", "Test", null, null);
var errorEntry = new LogEntry(DateTime.Now, LogLevel.Error, "TestLogger", "Test", null, null);
var fatalEntry = new LogEntry(DateTime.Now, LogLevel.Fatal, "TestLogger", "Test", null, null);
Assert.That(filter.ShouldLog(traceEntry), Is.False);
Assert.That(filter.ShouldLog(debugEntry), Is.False);
Assert.That(filter.ShouldLog(infoEntry), Is.False);
Assert.That(filter.ShouldLog(warningEntry), Is.True);
Assert.That(filter.ShouldLog(errorEntry), Is.True);
Assert.That(filter.ShouldLog(fatalEntry), Is.True);
}
}

View File

@ -0,0 +1,299 @@
using System.IO;
using System.Text.Json;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 LoggingConfiguration 和 LoggingConfigurationLoader 的功能和行为
/// </summary>
[TestFixture]
public class LoggingConfigurationTests
{
[Test]
public void LoadFromJsonString_WithValidJson_ShouldDeserialize()
{
var json = @"{
""minLevel"": ""Debug"",
""appenders"": [
{
""type"": ""Console"",
""formatter"": ""Default"",
""useColors"": true
}
],
""loggerLevels"": {
""GFramework.Core"": ""Trace"",
""MyApp"": ""Info""
}
}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
Assert.That(config.MinLevel, Is.EqualTo(LogLevel.Debug));
Assert.That(config.Appenders.Count, Is.EqualTo(1));
Assert.That(config.Appenders[0].Type, Is.EqualTo("Console"));
Assert.That(config.LoggerLevels.Count, Is.EqualTo(2));
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Trace));
}
[Test]
public void LoadFromJsonString_WithInvalidJson_ShouldThrow()
{
var invalidJson = "{ invalid json }";
Assert.Throws<JsonException>(() => LoggingConfigurationLoader.LoadFromJsonString(invalidJson));
}
[Test]
public void CreateFactory_WithConsoleAppender_ShouldCreateFactory()
{
var json = @"{
""minLevel"": ""Info"",
""appenders"": [
{
""type"": ""Console"",
""formatter"": ""Default"",
""useColors"": false
}
]
}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
var factory = LoggingConfigurationLoader.CreateFactory(config);
Assert.That(factory, Is.Not.Null);
var logger = factory.GetLogger("TestLogger");
Assert.That(logger, Is.Not.Null);
Assert.That(logger.Name(), Is.EqualTo("TestLogger"));
}
[Test]
public void CreateFactory_WithFileAppender_ShouldCreateFactory()
{
var testFile = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.log");
try
{
var json = $@"{{
""minLevel"": ""Info"",
""appenders"": [
{{
""type"": ""File"",
""filePath"": ""{testFile.Replace("\\", "\\\\")}"",
""formatter"": ""Json""
}}
]
}}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
var factory = LoggingConfigurationLoader.CreateFactory(config);
var logger = factory.GetLogger("TestLogger");
logger.Info("Test message");
// 验证文件是否创建
Assert.That(File.Exists(testFile), Is.True);
}
finally
{
if (File.Exists(testFile))
{
try
{
File.Delete(testFile);
}
catch
{
}
}
}
}
[Test]
public void CreateFactory_WithLoggerLevels_ShouldApplyCorrectLevels()
{
var json = @"{
""minLevel"": ""Info"",
""appenders"": [
{
""type"": ""Console"",
""formatter"": ""Default""
}
],
""loggerLevels"": {
""GFramework.Core"": ""Trace"",
""MyApp"": ""Warning""
}
}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
var factory = LoggingConfigurationLoader.CreateFactory(config);
var logger1 = factory.GetLogger("GFramework.Core.Test");
var logger2 = factory.GetLogger("MyApp.Controllers");
var logger3 = factory.GetLogger("OtherNamespace");
Assert.That(logger1.IsTraceEnabled(), Is.True);
Assert.That(logger2.IsTraceEnabled(), Is.False);
Assert.That(logger2.IsWarnEnabled(), Is.True);
Assert.That(logger3.IsInfoEnabled(), Is.True);
}
[Test]
public void CreateFactory_WithInvalidAppenderType_ShouldThrowException()
{
var json = @"{
""minLevel"": ""Info"",
""appenders"": [
{
""type"": ""UnsupportedType"",
""formatter"": ""Default""
}
]
}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
Assert.Throws<NotSupportedException>(() => LoggingConfigurationLoader.CreateFactory(config));
}
[Test]
public void CreateFactory_WithLogLevelFilter_ShouldApplyFilter()
{
var testFile = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.log");
try
{
var json = $@"{{
""minLevel"": ""Info"",
""appenders"": [
{{
""type"": ""File"",
""filePath"": ""{testFile.Replace("\\", "\\\\")}"",
""formatter"": ""Default"",
""filter"": {{
""type"": ""LogLevel"",
""minLevel"": ""Warning""
}}
}}
]
}}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
var factory = LoggingConfigurationLoader.CreateFactory(config);
var logger = factory.GetLogger("TestLogger");
logger.Info("Info message");
logger.Warn("Warning message");
// 只有 Warning 应该被写入
var content = File.ReadAllText(testFile);
Assert.That(content, Does.Not.Contain("Info message"));
Assert.That(content, Does.Contain("Warning message"));
}
finally
{
if (File.Exists(testFile))
{
try
{
File.Delete(testFile);
}
catch
{
}
}
}
}
[Test]
public void CreateFactory_WithNamespaceFilter_ShouldApplyFilter()
{
var testFile = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.log");
try
{
var json = $@"{{
""minLevel"": ""Info"",
""appenders"": [
{{
""type"": ""File"",
""filePath"": ""{testFile.Replace("\\", "\\\\")}"",
""formatter"": ""Default"",
""filter"": {{
""type"": ""Namespace"",
""namespaces"": [""GFramework""]
}}
}}
]
}}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
Assert.That(config.Appenders[0].Filter, Is.Not.Null);
Assert.That(config.Appenders[0].Filter.Type, Is.EqualTo("Namespace"));
}
finally
{
if (File.Exists(testFile))
{
try
{
File.Delete(testFile);
}
catch
{
}
}
}
}
[Test]
public void LoadFromJsonString_WithComplexConfiguration_ShouldWork()
{
var json = @"{
""minLevel"": ""Info"",
""appenders"": [
{
""type"": ""Console"",
""formatter"": ""Default"",
""useColors"": true
},
{
""type"": ""File"",
""filePath"": ""logs/app.log"",
""formatter"": ""Json"",
""filter"": {
""type"": ""LogLevel"",
""minLevel"": ""Warning""
}
},
{
""type"": ""RollingFile"",
""filePath"": ""logs/rolling.log"",
""formatter"": ""Default"",
""maxFileSize"": 10485760,
""maxFileCount"": 5
}
],
""loggerLevels"": {
""GFramework.Core"": ""Debug"",
""MyApp.Controllers"": ""Info"",
""MyApp.Services"": ""Warning""
}
}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
Assert.That(config.MinLevel, Is.EqualTo(LogLevel.Info));
Assert.That(config.Appenders.Count, Is.EqualTo(3));
Assert.That(config.Appenders[0].Type, Is.EqualTo("Console"));
Assert.That(config.Appenders[1].Type, Is.EqualTo("File"));
Assert.That(config.Appenders[1].Filter, Is.Not.Null);
Assert.That(config.Appenders[2].Type, Is.EqualTo("RollingFile"));
Assert.That(config.Appenders[2].MaxFileSize, Is.EqualTo(10485760));
Assert.That(config.LoggerLevels.Count, Is.EqualTo(3));
}
}

View File

@ -0,0 +1,85 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.filters;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 NamespaceFilter 的功能和行为
/// </summary>
[TestFixture]
public class NamespaceFilterTests
{
[Test]
public void Constructor_WithNullNamespaces_ShouldThrowArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new NamespaceFilter(null!));
}
[Test]
public void Constructor_WithEmptyNamespaces_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new NamespaceFilter(Array.Empty<string>()));
}
[Test]
public void ShouldLog_WithMatchingNamespace_ShouldReturnTrue()
{
var filter = new NamespaceFilter("GFramework.Core", "MyApp");
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core.Logging", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.True);
}
[Test]
public void ShouldLog_WithNonMatchingNamespace_ShouldReturnFalse()
{
var filter = new NamespaceFilter("GFramework.Core", "MyApp");
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "OtherNamespace", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.False);
}
[Test]
public void ShouldLog_WithExactMatch_ShouldReturnTrue()
{
var filter = new NamespaceFilter("GFramework.Core");
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.True);
}
[Test]
public void ShouldLog_WithPrefixMatch_ShouldReturnTrue()
{
var filter = new NamespaceFilter("GFramework");
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core.Logging", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.True);
}
[Test]
public void ShouldLog_IsCaseInsensitive()
{
var filter = new NamespaceFilter("gframework.core");
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core.Logging", "Test", null, null);
Assert.That(filter.ShouldLog(entry), Is.True);
}
[Test]
public void ShouldLog_WithMultipleNamespaces_ShouldMatchAny()
{
var filter = new NamespaceFilter("GFramework.Core", "MyApp.Services", "ThirdParty");
var entry1 = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core.Logging", "Test", null, null);
var entry2 = new LogEntry(DateTime.Now, LogLevel.Info, "MyApp.Services.UserService", "Test", null, null);
var entry3 = new LogEntry(DateTime.Now, LogLevel.Info, "ThirdParty.Library", "Test", null, null);
var entry4 = new LogEntry(DateTime.Now, LogLevel.Info, "OtherNamespace", "Test", null, null);
Assert.That(filter.ShouldLog(entry1), Is.True);
Assert.That(filter.ShouldLog(entry2), Is.True);
Assert.That(filter.ShouldLog(entry3), Is.True);
Assert.That(filter.ShouldLog(entry4), Is.False);
}
}

View File

@ -0,0 +1,164 @@
using System.IO;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.appenders;
using NUnit.Framework;
namespace GFramework.Core.Tests.logging;
/// <summary>
/// 测试 RollingFileAppender 的功能和行为
/// </summary>
[TestFixture]
public class RollingFileAppenderTests
{
[SetUp]
public void SetUp()
{
_testDir = Path.Combine(Path.GetTempPath(), $"rolling_test_{Guid.NewGuid()}");
Directory.CreateDirectory(_testDir);
_testFilePath = Path.Combine(_testDir, "app.log");
}
[TearDown]
public void TearDown()
{
if (Directory.Exists(_testDir))
{
try
{
Directory.Delete(_testDir, true);
}
catch
{
}
}
}
private string _testDir = null!;
private string _testFilePath = null!;
[Test]
public void Constructor_WithInvalidMaxFileSize_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new RollingFileAppender(_testFilePath, maxFileSize: 0));
Assert.Throws<ArgumentException>(() => new RollingFileAppender(_testFilePath, maxFileSize: -1));
}
[Test]
public void Constructor_WithInvalidMaxFileCount_ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new RollingFileAppender(_testFilePath, maxFileCount: 0));
Assert.Throws<ArgumentException>(() => new RollingFileAppender(_testFilePath, maxFileCount: -1));
}
[Test]
public void Append_WhenFileSizeExceedsLimit_ShouldRollFiles()
{
using (var appender = new RollingFileAppender(_testFilePath, maxFileSize: 500, maxFileCount: 3))
{
// 写入足够多的日志触发轮转
for (int i = 0; i < 20; i++)
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger",
$"This is a test message number {i} with some padding to increase size", null, null);
appender.Append(entry);
}
appender.Flush();
}
// 检查是否生成了多个文件
var files = Directory.GetFiles(_testDir, "*.log").OrderBy(f => f).ToArray();
Assert.That(files.Length, Is.GreaterThan(1));
}
[Test]
public void Append_ShouldNotExceedMaxFileCount()
{
const int maxFileCount = 3;
using (var appender = new RollingFileAppender(_testFilePath, maxFileSize: 300, maxFileCount: maxFileCount))
{
// 写入大量日志触发多次轮转
for (int i = 0; i < 50; i++)
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger",
$"This is a test message number {i} with some padding to increase size significantly", null, null);
appender.Append(entry);
}
appender.Flush();
}
var files = Directory.GetFiles(_testDir, "*.log");
Assert.That(files.Length, Is.LessThanOrEqualTo(maxFileCount));
}
[Test]
public void Append_RolledFiles_ShouldHaveCorrectNaming()
{
using (var appender = new RollingFileAppender(_testFilePath, maxFileSize: 400, maxFileCount: 3))
{
for (int i = 0; i < 30; i++)
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger",
$"Test message {i} with padding to trigger rolling", null, null);
appender.Append(entry);
}
appender.Flush();
}
var files = Directory.GetFiles(_testDir, "*.log").Select(Path.GetFileName).OrderBy(f => f).ToArray();
// 应该有 app.log, app.1.log, app.2.log 等
Assert.That(files, Does.Contain("app.log"));
if (files.Length > 1)
{
Assert.That(files.Any(f => f.StartsWith("app.") && f.EndsWith(".log") && f != "app.log"), Is.True);
}
}
[Test]
public void Append_AfterDispose_ShouldThrowObjectDisposedException()
{
var appender = new RollingFileAppender(_testFilePath);
appender.Dispose();
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
Assert.Throws<ObjectDisposedException>(() => appender.Append(entry));
}
[Test]
public void Append_WithSmallMaxFileSize_ShouldRollFrequently()
{
using (var appender = new RollingFileAppender(_testFilePath, maxFileSize: 200, maxFileCount: 5))
{
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger",
"This is a longer message to trigger rolling more frequently", null, null);
appender.Append(entry);
}
appender.Flush();
}
var files = Directory.GetFiles(_testDir, "*.log");
Assert.That(files.Length, Is.GreaterThan(1));
}
[Test]
public void Flush_ShouldEnsureDataWritten()
{
using var appender = new RollingFileAppender(_testFilePath);
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
appender.Append(entry);
appender.Flush();
Assert.That(File.Exists(_testFilePath), Is.True);
var content = File.ReadAllText(_testFilePath);
Assert.That(content, Does.Contain("Test message"));
}
}