mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
test(logging): 添加日志系统单元测试
- 为 AsyncLogAppender 添加完整功能测试,包括异步写入、缓冲区管理、并发处理等场景 - 为 CachedLoggerFactory 添加缓存机制测试,验证相同名称和级别的日志记录器重用 - 为 CompositeFilter 添加过滤器组合测试,验证多个过滤器的逻辑组合功能 - 为 CompositeLogger 添加复合日志记录器测试,验证多追加器写入和级别过滤功能 - 为 ConsoleAppender 添加控制台追加器测试,验证格式化输出和过滤器支持 - 为 DefaultLogFormatter 添加默认格式化器测试,验证基本格式化和异常处理功能 - 为 FileAppender 添加文件追加器测试,验证文件写入、目录创建和追加模式功能 - 为 JsonLogFormatter 添加 JSON 格式化器测试,验证 JSON 输出和属性序列化功能 - 为 LogContext 添加日志上下文测试,验证属性推送和作用域管理功能
This commit is contained in:
parent
1ba771e13a
commit
abdf4cc690
219
GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs
Normal file
219
GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
95
GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs
Normal file
95
GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
GFramework.Core.Tests/logging/CompositeFilterTests.cs
Normal file
102
GFramework.Core.Tests/logging/CompositeFilterTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
182
GFramework.Core.Tests/logging/CompositeLoggerTests.cs
Normal file
182
GFramework.Core.Tests/logging/CompositeLoggerTests.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
109
GFramework.Core.Tests/logging/ConsoleAppenderTests.cs
Normal file
109
GFramework.Core.Tests/logging/ConsoleAppenderTests.cs
Normal 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}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
117
GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs
Normal file
117
GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
172
GFramework.Core.Tests/logging/FileAppenderTests.cs
Normal file
172
GFramework.Core.Tests/logging/FileAppenderTests.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
153
GFramework.Core.Tests/logging/JsonLogFormatterTests.cs
Normal file
153
GFramework.Core.Tests/logging/JsonLogFormatterTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
204
GFramework.Core.Tests/logging/LogContextTests.cs
Normal file
204
GFramework.Core.Tests/logging/LogContextTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
145
GFramework.Core.Tests/logging/LogEntryTests.cs
Normal file
145
GFramework.Core.Tests/logging/LogEntryTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
59
GFramework.Core.Tests/logging/LogLevelFilterTests.cs
Normal file
59
GFramework.Core.Tests/logging/LogLevelFilterTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
299
GFramework.Core.Tests/logging/LoggingConfigurationTests.cs
Normal file
299
GFramework.Core.Tests/logging/LoggingConfigurationTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
85
GFramework.Core.Tests/logging/NamespaceFilterTests.cs
Normal file
85
GFramework.Core.Tests/logging/NamespaceFilterTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
164
GFramework.Core.Tests/logging/RollingFileAppenderTests.cs
Normal file
164
GFramework.Core.Tests/logging/RollingFileAppenderTests.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user