diff --git a/GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs b/GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs new file mode 100644 index 0000000..dab71c7 --- /dev/null +++ b/GFramework.Core.Tests/logging/AsyncLogAppenderTests.cs @@ -0,0 +1,219 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.appenders; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 AsyncLogAppender 的功能和行为 +/// +[TestFixture] +public class AsyncLogAppenderTests +{ + [Test] + public void Constructor_WithNullInnerAppender_ShouldThrowArgumentNullException() + { + Assert.Throws(() => new AsyncLogAppender(null!)); + } + + [Test] + public void Constructor_WithInvalidBufferSize_ShouldThrowArgumentException() + { + var innerAppender = new TestAppender(); + Assert.Throws(() => new AsyncLogAppender(innerAppender, bufferSize: 0)); + Assert.Throws(() => new AsyncLogAppender(innerAppender, bufferSize: -1)); + } + + [Test] + public void Append_ShouldNotBlock() + { + var innerAppender = new SlowAppender(delayMs: 100); + using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000); + + var startTime = DateTime.Now; + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + + var elapsed = (DateTime.Now - startTime).TotalMilliseconds; + + // 异步写入应该非常快(< 100ms),不应该等待内部 Appender + Assert.That(elapsed, Is.LessThan(100)); + } + + [Test] + public void Append_ShouldEventuallyWriteToInnerAppender() + { + var innerAppender = new TestAppender(); + using (var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000)) + { + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + + asyncAppender.Flush(); + } + + Assert.That(innerAppender.Entries.Count, Is.EqualTo(10)); + } + + [Test] + public void Flush_ShouldWaitForAllEntriesToBeProcessed() + { + var innerAppender = new TestAppender(); + using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000); + + for (int i = 0; i < 100; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + + asyncAppender.Flush(); + + Assert.That(innerAppender.Entries.Count, Is.EqualTo(100)); + } + + [Test] + public void Dispose_ShouldProcessRemainingEntries() + { + var innerAppender = new TestAppender(); + using (var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000)) + { + for (int i = 0; i < 50; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + } // Dispose 会等待所有日志处理完成 + + Assert.That(innerAppender.Entries.Count, Is.EqualTo(50)); + } + + [Test] + public void Append_AfterDispose_ShouldThrowObjectDisposedException() + { + var innerAppender = new TestAppender(); + var asyncAppender = new AsyncLogAppender(innerAppender); + asyncAppender.Dispose(); + + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test", null, null); + + Assert.Throws(() => asyncAppender.Append(entry)); + } + + [Test] + public void PendingCount_ShouldReflectQueuedEntries() + { + var innerAppender = new SlowAppender(delayMs: 50); + using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000); + + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + + // 应该有一些待处理的条目 + Assert.That(asyncAppender.PendingCount, Is.GreaterThanOrEqualTo(0)); + } + + [Test] + public async Task Append_FromMultipleThreads_ShouldHandleConcurrency() + { + var innerAppender = new TestAppender(); + using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10000); + + var tasks = new Task[10]; + for (int t = 0; t < 10; t++) + { + int threadId = t; + tasks[t] = Task.Run(() => + { + for (int i = 0; i < 100; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", + $"Thread {threadId} Message {i}", null, null); + asyncAppender.Append(entry); + } + }); + } + + await Task.WhenAll(tasks); + asyncAppender.Flush(); + + Assert.That(innerAppender.Entries.Count, Is.EqualTo(1000)); + } + + [Test] + public void Append_WhenInnerAppenderThrows_ShouldNotCrash() + { + var innerAppender = new ThrowingAppender(); + using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000); + + // 即使内部 Appender 抛出异常,也不应该影响调用线程 + Assert.DoesNotThrow(() => + { + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + }); + + Thread.Sleep(100); // 等待后台处理 + } + + // 辅助测试类 + private class TestAppender : ILogAppender + { + public List Entries { get; } = new(); + + public void Append(LogEntry entry) + { + lock (Entries) + { + Entries.Add(entry); + } + } + + public void Flush() + { + } + } + + private class SlowAppender : ILogAppender + { + private readonly int _delayMs; + + public SlowAppender(int delayMs) + { + _delayMs = delayMs; + } + + public void Append(LogEntry entry) + { + Thread.Sleep(_delayMs); + } + + public void Flush() + { + } + } + + private class ThrowingAppender : ILogAppender + { + public void Append(LogEntry entry) + { + throw new InvalidOperationException("Test exception"); + } + + public void Flush() + { + } + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs b/GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs new file mode 100644 index 0000000..18a6bac --- /dev/null +++ b/GFramework.Core.Tests/logging/CachedLoggerFactoryTests.cs @@ -0,0 +1,95 @@ +using System.IO; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 CachedLoggerFactory 的功能和行为 +/// +[TestFixture] +public class CachedLoggerFactoryTests +{ + [Test] + public void Constructor_WithNullInnerFactory_ShouldThrowArgumentNullException() + { + Assert.Throws(() => new CachedLoggerFactory(null!)); + } + + [Test] + public void GetLogger_WithSameNameAndLevel_ShouldReturnSameInstance() + { + var innerFactory = new ConsoleLoggerFactory(); + var cachedFactory = new CachedLoggerFactory(innerFactory); + + var logger1 = cachedFactory.GetLogger("TestLogger", LogLevel.Info); + var logger2 = cachedFactory.GetLogger("TestLogger", LogLevel.Info); + + Assert.That(logger1, Is.SameAs(logger2)); + } + + [Test] + public void GetLogger_WithDifferentNames_ShouldReturnDifferentInstances() + { + var innerFactory = new ConsoleLoggerFactory(); + var cachedFactory = new CachedLoggerFactory(innerFactory); + + var logger1 = cachedFactory.GetLogger("Logger1", LogLevel.Info); + var logger2 = cachedFactory.GetLogger("Logger2", LogLevel.Info); + + Assert.That(logger1, Is.Not.SameAs(logger2)); + } + + [Test] + public void GetLogger_WithDifferentLevels_ShouldReturnDifferentInstances() + { + var innerFactory = new ConsoleLoggerFactory(); + var cachedFactory = new CachedLoggerFactory(innerFactory); + + var logger1 = cachedFactory.GetLogger("TestLogger", LogLevel.Info); + var logger2 = cachedFactory.GetLogger("TestLogger", LogLevel.Debug); + + Assert.That(logger1, Is.Not.SameAs(logger2)); + } + + [Test] + public void GetLogger_MultipleCalls_ShouldOnlyCreateOnce() + { + var trackingFactory = new TrackingLoggerFactory(); + var cachedFactory = new CachedLoggerFactory(trackingFactory); + + cachedFactory.GetLogger("TestLogger", LogLevel.Info); + cachedFactory.GetLogger("TestLogger", LogLevel.Info); + cachedFactory.GetLogger("TestLogger", LogLevel.Info); + + Assert.That(trackingFactory.CreateCount, Is.EqualTo(1)); + } + + [Test] + public void GetLogger_WithMultipleNamesAndLevels_ShouldCacheCorrectly() + { + var trackingFactory = new TrackingLoggerFactory(); + var cachedFactory = new CachedLoggerFactory(trackingFactory); + + cachedFactory.GetLogger("Logger1", LogLevel.Info); + cachedFactory.GetLogger("Logger1", LogLevel.Debug); + cachedFactory.GetLogger("Logger2", LogLevel.Info); + cachedFactory.GetLogger("Logger1", LogLevel.Info); // 缓存命中 + cachedFactory.GetLogger("Logger2", LogLevel.Info); // 缓存命中 + + Assert.That(trackingFactory.CreateCount, Is.EqualTo(3)); + } + + // 辅助测试类 + private class TrackingLoggerFactory : ILoggerFactory + { + public int CreateCount { get; private set; } + + public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) + { + CreateCount++; + return new ConsoleLogger(name, minLevel, new StringWriter(), false); + } + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/CompositeFilterTests.cs b/GFramework.Core.Tests/logging/CompositeFilterTests.cs new file mode 100644 index 0000000..a14462f --- /dev/null +++ b/GFramework.Core.Tests/logging/CompositeFilterTests.cs @@ -0,0 +1,102 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.filters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 CompositeFilter 的功能和行为 +/// +[TestFixture] +public class CompositeFilterTests +{ + [Test] + public void Constructor_WithNullFilters_ShouldThrowArgumentNullException() + { + Assert.Throws(() => new CompositeFilter(null!)); + } + + [Test] + public void Constructor_WithEmptyFilters_ShouldThrowArgumentException() + { + Assert.Throws(() => new CompositeFilter(Array.Empty())); + } + + [Test] + public void ShouldLog_WithAllFiltersReturningTrue_ShouldReturnTrue() + { + var filter1 = new LogLevelFilter(LogLevel.Info); + var filter2 = new NamespaceFilter("GFramework"); + var compositeFilter = new CompositeFilter(filter1, filter2); + + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null); + + Assert.That(compositeFilter.ShouldLog(entry), Is.True); + } + + [Test] + public void ShouldLog_WithOneFilterReturningFalse_ShouldReturnFalse() + { + var filter1 = new LogLevelFilter(LogLevel.Warning); // 要求 Warning 以上 + var filter2 = new NamespaceFilter("GFramework"); + var compositeFilter = new CompositeFilter(filter1, filter2); + + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null); + + Assert.That(compositeFilter.ShouldLog(entry), Is.False); + } + + [Test] + public void ShouldLog_WithAllFiltersReturningFalse_ShouldReturnFalse() + { + var filter1 = new LogLevelFilter(LogLevel.Warning); + var filter2 = new NamespaceFilter("MyApp"); + var compositeFilter = new CompositeFilter(filter1, filter2); + + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null); + + Assert.That(compositeFilter.ShouldLog(entry), Is.False); + } + + [Test] + public void ShouldLog_WithMultipleFilters_ShouldApplyAndLogic() + { + var levelFilter = new LogLevelFilter(LogLevel.Info); + var namespaceFilter = new NamespaceFilter("GFramework"); + var compositeFilter = new CompositeFilter(levelFilter, namespaceFilter); + + // 满足所有条件 + var entry1 = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null); + Assert.That(compositeFilter.ShouldLog(entry1), Is.True); + + // 级别不满足 + var entry2 = new LogEntry(DateTime.Now, LogLevel.Debug, "GFramework.Core", "Test", null, null); + Assert.That(compositeFilter.ShouldLog(entry2), Is.False); + + // 命名空间不满足 + var entry3 = new LogEntry(DateTime.Now, LogLevel.Info, "OtherNamespace", "Test", null, null); + Assert.That(compositeFilter.ShouldLog(entry3), Is.False); + + // 都不满足 + var entry4 = new LogEntry(DateTime.Now, LogLevel.Debug, "OtherNamespace", "Test", null, null); + Assert.That(compositeFilter.ShouldLog(entry4), Is.False); + } + + [Test] + public void ShouldLog_WithNestedCompositeFilters_ShouldWork() + { + var filter1 = new LogLevelFilter(LogLevel.Info); + var filter2 = new NamespaceFilter("GFramework"); + var innerComposite = new CompositeFilter(filter1, filter2); + + var filter3 = new LogLevelFilter(LogLevel.Warning); + var outerComposite = new CompositeFilter(innerComposite, filter3); + + // 需要同时满足:Info 以上 AND GFramework 命名空间 AND Warning 以上 + var entry1 = new LogEntry(DateTime.Now, LogLevel.Warning, "GFramework.Core", "Test", null, null); + Assert.That(outerComposite.ShouldLog(entry1), Is.True); + + var entry2 = new LogEntry(DateTime.Now, LogLevel.Info, "GFramework.Core", "Test", null, null); + Assert.That(outerComposite.ShouldLog(entry2), Is.False); // 不满足 Warning + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/CompositeLoggerTests.cs b/GFramework.Core.Tests/logging/CompositeLoggerTests.cs new file mode 100644 index 0000000..cbf355c --- /dev/null +++ b/GFramework.Core.Tests/logging/CompositeLoggerTests.cs @@ -0,0 +1,182 @@ +using System.IO; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging; +using GFramework.Core.logging.appenders; +using GFramework.Core.logging.formatters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 CompositeLogger 的功能和行为 +/// +[TestFixture] +public class CompositeLoggerTests +{ + [Test] + public void Constructor_WithNullAppenders_ShouldThrowArgumentException() + { + Assert.Throws(() => new CompositeLogger("Test", LogLevel.Info, null!)); + } + + [Test] + public void Constructor_WithEmptyAppenders_ShouldThrowArgumentException() + { + Assert.Throws(() => new CompositeLogger("Test", LogLevel.Info, Array.Empty())); + } + + [Test] + public void Write_ShouldWriteToAllAppenders() + { + var writer1 = new StringWriter(); + var writer2 = new StringWriter(); + var appender1 = new ConsoleAppender(new DefaultLogFormatter(), writer1, useColors: false); + var appender2 = new ConsoleAppender(new DefaultLogFormatter(), writer2, useColors: false); + + using var logger = new CompositeLogger("TestLogger", LogLevel.Info, appender1, appender2); + + logger.Info("Test message"); + + var output1 = writer1.ToString(); + var output2 = writer2.ToString(); + + Assert.That(output1, Does.Contain("Test message")); + Assert.That(output2, Does.Contain("Test message")); + + writer1.Dispose(); + writer2.Dispose(); + } + + [Test] + public void Log_WithStructuredProperties_ShouldWriteToAllAppenders() + { + var writer1 = new StringWriter(); + var writer2 = new StringWriter(); + var appender1 = new ConsoleAppender(new DefaultLogFormatter(), writer1, useColors: false); + var appender2 = new ConsoleAppender(new DefaultLogFormatter(), writer2, useColors: false); + + using var logger = new CompositeLogger("TestLogger", LogLevel.Info, appender1, appender2); + + logger.Log(LogLevel.Info, "User action", ("UserId", 12345), ("Action", "Login")); + + var output1 = writer1.ToString(); + var output2 = writer2.ToString(); + + Assert.That(output1, Does.Contain("User action")); + Assert.That(output1, Does.Contain("UserId=12345")); + Assert.That(output2, Does.Contain("User action")); + Assert.That(output2, Does.Contain("UserId=12345")); + + writer1.Dispose(); + writer2.Dispose(); + } + + [Test] + public void Log_WithException_ShouldWriteToAllAppenders() + { + var writer1 = new StringWriter(); + var writer2 = new StringWriter(); + var appender1 = new ConsoleAppender(new DefaultLogFormatter(), writer1, useColors: false); + var appender2 = new ConsoleAppender(new DefaultLogFormatter(), writer2, useColors: false); + + using var logger = new CompositeLogger("TestLogger", LogLevel.Info, appender1, appender2); + + var exception = new InvalidOperationException("Test exception"); + logger.Log(LogLevel.Error, "Error occurred", exception, ("ErrorCode", 500)); + + var output1 = writer1.ToString(); + var output2 = writer2.ToString(); + + Assert.That(output1, Does.Contain("Error occurred")); + Assert.That(output1, Does.Contain("InvalidOperationException")); + Assert.That(output2, Does.Contain("Error occurred")); + Assert.That(output2, Does.Contain("InvalidOperationException")); + + writer1.Dispose(); + writer2.Dispose(); + } + + [Test] + public void Flush_ShouldFlushAllAppenders() + { + var testAppender1 = new TestFlushAppender(); + var testAppender2 = new TestFlushAppender(); + + using var logger = new CompositeLogger("TestLogger", LogLevel.Info, testAppender1, testAppender2); + + logger.Info("Test message"); + logger.Flush(); + + Assert.That(testAppender1.FlushCalled, Is.True); + Assert.That(testAppender2.FlushCalled, Is.True); + } + + [Test] + public void Dispose_ShouldDisposeAllAppenders() + { + var testAppender1 = new TestDisposableAppender(); + var testAppender2 = new TestDisposableAppender(); + + var logger = new CompositeLogger("TestLogger", LogLevel.Info, testAppender1, testAppender2); + logger.Dispose(); + + Assert.That(testAppender1.DisposeCalled, Is.True); + Assert.That(testAppender2.DisposeCalled, Is.True); + } + + [Test] + public void Write_WithLevelFiltering_ShouldRespectMinLevel() + { + var writer = new StringWriter(); + var appender = new ConsoleAppender(new DefaultLogFormatter(), writer, useColors: false); + + using var logger = new CompositeLogger("TestLogger", LogLevel.Warning, appender); + + logger.Debug("Debug message"); + logger.Info("Info message"); + logger.Warn("Warning message"); + logger.Error("Error message"); + + var output = writer.ToString(); + + Assert.That(output, Does.Not.Contain("Debug message")); + Assert.That(output, Does.Not.Contain("Info message")); + Assert.That(output, Does.Contain("Warning message")); + Assert.That(output, Does.Contain("Error message")); + + writer.Dispose(); + } + + // 辅助测试类 + private class TestFlushAppender : ILogAppender + { + public bool FlushCalled { get; private set; } + + public void Append(LogEntry entry) + { + } + + public void Flush() + { + FlushCalled = true; + } + } + + private class TestDisposableAppender : ILogAppender, IDisposable + { + public bool DisposeCalled { get; private set; } + + public void Dispose() + { + DisposeCalled = true; + } + + public void Append(LogEntry entry) + { + } + + public void Flush() + { + } + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/ConsoleAppenderTests.cs b/GFramework.Core.Tests/logging/ConsoleAppenderTests.cs new file mode 100644 index 0000000..9375562 --- /dev/null +++ b/GFramework.Core.Tests/logging/ConsoleAppenderTests.cs @@ -0,0 +1,109 @@ +using System.IO; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.appenders; +using GFramework.Core.logging.filters; +using GFramework.Core.logging.formatters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 ConsoleAppender 的功能和行为 +/// +[TestFixture] +public class ConsoleAppenderTests +{ + [SetUp] + public void SetUp() + { + _stringWriter = new StringWriter(); + _appender = new ConsoleAppender(new DefaultLogFormatter(), _stringWriter, useColors: false); + } + + [TearDown] + public void TearDown() + { + _appender?.Dispose(); + _stringWriter?.Dispose(); + } + + private StringWriter _stringWriter = null!; + private ConsoleAppender _appender = null!; + + [Test] + public void Constructor_WithNullFormatter_ShouldThrowArgumentNullException() + { + Assert.Throws(() => new ConsoleAppender(null!)); + } + + [Test] + public void Append_ShouldWriteToWriter() + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + + _appender.Append(entry); + + var output = _stringWriter.ToString(); + Assert.That(output, Does.Contain("Test message")); + Assert.That(output, Does.Contain("INFO")); + } + + [Test] + public void Append_WithFilter_ShouldRespectFilter() + { + var filter = new LogLevelFilter(LogLevel.Warning); + var appender = new ConsoleAppender(new DefaultLogFormatter(), _stringWriter, useColors: false, filter: filter); + + var infoEntry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Info message", null, null); + var warningEntry = new LogEntry(DateTime.Now, LogLevel.Warning, "TestLogger", "Warning message", null, null); + + appender.Append(infoEntry); + appender.Append(warningEntry); + + var output = _stringWriter.ToString(); + Assert.That(output, Does.Not.Contain("Info message")); + Assert.That(output, Does.Contain("Warning message")); + + appender.Dispose(); + } + + [Test] + public void Flush_ShouldFlushWriter() + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + + _appender.Append(entry); + _appender.Flush(); + + var output = _stringWriter.ToString(); + Assert.That(output, Does.Contain("Test message")); + } + + [Test] + public void Dispose_ShouldFlushWriter() + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + + _appender.Append(entry); + _appender.Dispose(); + + var output = _stringWriter.ToString(); + Assert.That(output, Does.Contain("Test message")); + } + + [Test] + public void Append_MultipleEntries_ShouldWriteAll() + { + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + _appender.Append(entry); + } + + var output = _stringWriter.ToString(); + for (int i = 0; i < 10; i++) + { + Assert.That(output, Does.Contain($"Message {i}")); + } + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs b/GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs new file mode 100644 index 0000000..f997c1c --- /dev/null +++ b/GFramework.Core.Tests/logging/DefaultLogFormatterTests.cs @@ -0,0 +1,117 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.formatters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 DefaultLogFormatter 的功能和行为 +/// +[TestFixture] +public class DefaultLogFormatterTests +{ + [SetUp] + public void SetUp() + { + _formatter = new DefaultLogFormatter(); + } + + private DefaultLogFormatter _formatter = null!; + + [Test] + public void Format_WithBasicEntry_ShouldFormatCorrectly() + { + var timestamp = new DateTime(2026, 2, 26, 10, 30, 45, 123); + var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, null); + + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain("[2026-02-26 10:30:45.123]")); + Assert.That(result, Does.Contain("INFO")); + Assert.That(result, Does.Contain("[TestLogger]")); + Assert.That(result, Does.Contain("Test message")); + } + + [Test] + public void Format_WithException_ShouldIncludeException() + { + var exception = new InvalidOperationException("Test exception"); + var entry = new LogEntry(DateTime.Now, LogLevel.Error, "TestLogger", "Error occurred", exception, null); + + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain("Error occurred")); + Assert.That(result, Does.Contain("InvalidOperationException")); + Assert.That(result, Does.Contain("Test exception")); + } + + [Test] + public void Format_WithProperties_ShouldIncludeProperties() + { + var properties = new Dictionary + { + ["UserId"] = 12345, + ["UserName"] = "TestUser" + }; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "User action", null, properties); + + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain("User action")); + Assert.That(result, Does.Contain("|")); + Assert.That(result, Does.Contain("UserId=12345")); + Assert.That(result, Does.Contain("UserName=TestUser")); + } + + [Test] + public void Format_WithNullProperty_ShouldHandleNull() + { + var properties = new Dictionary + { + ["Key1"] = null + }; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties); + + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain("Key1=")); + } + + [Test] + public void Format_WithAllLogLevels_ShouldFormatCorrectly() + { + var levels = new[] + { LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Fatal }; + var expectedStrings = new[] { "TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" }; + + for (int i = 0; i < levels.Length; i++) + { + var entry = new LogEntry(DateTime.Now, levels[i], "TestLogger", "Test", null, null); + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain(expectedStrings[i])); + } + } + + [Test] + public void Format_WithLongMessage_ShouldNotTruncate() + { + var longMessage = new string('A', 1000); + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", longMessage, null, null); + + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain(longMessage)); + } + + [Test] + public void Format_WithSpecialCharacters_ShouldPreserveCharacters() + { + var message = "Test\nNew\tLine\r\nSpecial: <>&\"'"; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", message, null, null); + + var result = _formatter.Format(entry); + + Assert.That(result, Does.Contain(message)); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/FileAppenderTests.cs b/GFramework.Core.Tests/logging/FileAppenderTests.cs new file mode 100644 index 0000000..9c2cd24 --- /dev/null +++ b/GFramework.Core.Tests/logging/FileAppenderTests.cs @@ -0,0 +1,172 @@ +using System.IO; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.appenders; +using GFramework.Core.logging.formatters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 FileAppender 的功能和行为 +/// +[TestFixture] +public class FileAppenderTests +{ + [SetUp] + public void SetUp() + { + _testFilePath = Path.Combine(Path.GetTempPath(), $"test_log_{Guid.NewGuid()}.log"); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(_testFilePath)) + { + try + { + File.Delete(_testFilePath); + } + catch + { + } + } + } + + private string _testFilePath = null!; + + [Test] + public void Constructor_WithNullFilePath_ShouldThrowArgumentException() + { + Assert.Throws(() => new FileAppender(null!)); + } + + [Test] + public void Constructor_WithEmptyFilePath_ShouldThrowArgumentException() + { + Assert.Throws(() => new FileAppender("")); + } + + [Test] + public void Constructor_ShouldCreateDirectoryIfNotExists() + { + var dirPath = Path.Combine(Path.GetTempPath(), $"test_dir_{Guid.NewGuid()}"); + var filePath = Path.Combine(dirPath, "test.log"); + + try + { + using var appender = new FileAppender(filePath); + + Assert.That(Directory.Exists(dirPath), Is.True); + } + finally + { + if (Directory.Exists(dirPath)) + { + Directory.Delete(dirPath, true); + } + } + } + + [Test] + public void Append_ShouldWriteToFile() + { + using (var appender = new FileAppender(_testFilePath)) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + appender.Append(entry); + appender.Flush(); + } + + var content = File.ReadAllText(_testFilePath); + Assert.That(content, Does.Contain("Test message")); + Assert.That(content, Does.Contain("INFO")); + } + + [Test] + public void Append_MultipleEntries_ShouldWriteAll() + { + using (var appender = new FileAppender(_testFilePath)) + { + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + appender.Append(entry); + } + + appender.Flush(); + } + + var lines = File.ReadAllLines(_testFilePath); + Assert.That(lines.Length, Is.EqualTo(10)); + for (int i = 0; i < 10; i++) + { + Assert.That(lines[i], Does.Contain($"Message {i}")); + } + } + + [Test] + public void Append_WithJsonFormatter_ShouldWriteJson() + { + using (var appender = new FileAppender(_testFilePath, new JsonLogFormatter())) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + appender.Append(entry); + appender.Flush(); + } + + var content = File.ReadAllText(_testFilePath); + Assert.That(content, Does.Contain("\"message\"")); + Assert.That(content, Does.Contain("\"level\"")); + } + + [Test] + public void Append_AfterDispose_ShouldThrowObjectDisposedException() + { + var appender = new FileAppender(_testFilePath); + appender.Dispose(); + + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + + Assert.Throws(() => appender.Append(entry)); + } + + [Test] + public void Append_WithAppendMode_ShouldAppendToExistingFile() + { + // 第一次写入 + using (var appender1 = new FileAppender(_testFilePath)) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "First message", null, null); + appender1.Append(entry); + appender1.Flush(); + } + + // 第二次写入 + using (var appender2 = new FileAppender(_testFilePath)) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Second message", null, null); + appender2.Append(entry); + appender2.Flush(); + } + + var lines = File.ReadAllLines(_testFilePath); + Assert.That(lines.Length, Is.EqualTo(2)); + Assert.That(lines[0], Does.Contain("First message")); + Assert.That(lines[1], Does.Contain("Second message")); + } + + [Test] + public void Flush_ShouldEnsureDataWritten() + { + using var appender = new FileAppender(_testFilePath); + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + + appender.Append(entry); + appender.Flush(); + + // 立即读取文件应该能看到内容 + var content = File.ReadAllText(_testFilePath); + Assert.That(content, Does.Contain("Test message")); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs b/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs new file mode 100644 index 0000000..2cf8ffb --- /dev/null +++ b/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs @@ -0,0 +1,153 @@ +using System.Text.Json; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.formatters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 JsonLogFormatter 的功能和行为 +/// +[TestFixture] +public class JsonLogFormatterTests +{ + [SetUp] + public void SetUp() + { + _formatter = new JsonLogFormatter(); + } + + private JsonLogFormatter _formatter = null!; + + [Test] + public void Format_WithBasicEntry_ShouldProduceValidJson() + { + var timestamp = new DateTime(2026, 2, 26, 10, 30, 45, 123, DateTimeKind.Utc); + var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, null); + + var result = _formatter.Format(entry); + + Assert.That(() => JsonDocument.Parse(result), Throws.Nothing); + + var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.GetProperty("level").GetString(), Is.EqualTo("INFO")); + Assert.That(doc.RootElement.GetProperty("logger").GetString(), Is.EqualTo("TestLogger")); + Assert.That(doc.RootElement.GetProperty("message").GetString(), Is.EqualTo("Test message")); + } + + [Test] + public void Format_WithException_ShouldIncludeExceptionDetails() + { + var exception = new InvalidOperationException("Test exception"); + var entry = new LogEntry(DateTime.Now, LogLevel.Error, "TestLogger", "Error occurred", exception, null); + + var result = _formatter.Format(entry); + + var doc = JsonDocument.Parse(result); + var exceptionObj = doc.RootElement.GetProperty("exception"); + + Assert.That(exceptionObj.GetProperty("type").GetString(), Does.Contain("InvalidOperationException")); + Assert.That(exceptionObj.GetProperty("message").GetString(), Is.EqualTo("Test exception")); + Assert.That(exceptionObj.TryGetProperty("stackTrace", out _), Is.True); + } + + [Test] + public void Format_WithProperties_ShouldIncludePropertiesObject() + { + var properties = new Dictionary + { + ["UserId"] = 12345, + ["UserName"] = "TestUser", + ["IsActive"] = true + }; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "User action", null, properties); + + var result = _formatter.Format(entry); + + var doc = JsonDocument.Parse(result); + var propsObj = doc.RootElement.GetProperty("properties"); + + Assert.That(propsObj.GetProperty("userId").GetInt32(), Is.EqualTo(12345)); + Assert.That(propsObj.GetProperty("userName").GetString(), Is.EqualTo("TestUser")); + Assert.That(propsObj.GetProperty("isActive").GetBoolean(), Is.True); + } + + [Test] + public void Format_WithNullProperty_ShouldHandleNull() + { + var properties = new Dictionary + { + ["Key1"] = null + }; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties); + + var result = _formatter.Format(entry); + + var doc = JsonDocument.Parse(result); + var propsObj = doc.RootElement.GetProperty("properties"); + + Assert.That(propsObj.GetProperty("key1").ValueKind, Is.EqualTo(JsonValueKind.Null)); + } + + [Test] + public void Format_WithAllLogLevels_ShouldFormatCorrectly() + { + var levels = new[] + { LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error, LogLevel.Fatal }; + var expectedStrings = new[] { "TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" }; + + for (int i = 0; i < levels.Length; i++) + { + var entry = new LogEntry(DateTime.Now, levels[i], "TestLogger", "Test", null, null); + var result = _formatter.Format(entry); + + var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.GetProperty("level").GetString(), Is.EqualTo(expectedStrings[i])); + } + } + + [Test] + public void Format_WithSpecialCharacters_ShouldEscapeCorrectly() + { + var message = "Test \"quoted\" and \n newline"; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", message, null, null); + + var result = _formatter.Format(entry); + + var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.GetProperty("message").GetString(), Is.EqualTo(message)); + } + + [Test] + public void Format_ShouldUseIso8601Timestamp() + { + var timestamp = new DateTime(2026, 2, 26, 10, 30, 45, 123, DateTimeKind.Utc); + var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test", null, null); + + var result = _formatter.Format(entry); + + var doc = JsonDocument.Parse(result); + var timestampStr = doc.RootElement.GetProperty("timestamp").GetString(); + + Assert.That(timestampStr, Does.Contain("2026-02-26")); + Assert.That(timestampStr, Does.Contain("T")); + } + + [Test] + public void Format_WithComplexProperties_ShouldSerializeCorrectly() + { + var properties = new Dictionary + { + ["Number"] = 123, + ["String"] = "test", + ["Boolean"] = true, + ["Null"] = null, + ["Array"] = new[] { 1, 2, 3 } + }; + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test", null, properties); + + var result = _formatter.Format(entry); + + Assert.That(() => JsonDocument.Parse(result), Throws.Nothing); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/LogContextTests.cs b/GFramework.Core.Tests/logging/LogContextTests.cs new file mode 100644 index 0000000..27402dc --- /dev/null +++ b/GFramework.Core.Tests/logging/LogContextTests.cs @@ -0,0 +1,204 @@ +using GFramework.Core.Abstractions.logging; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 LogContext 的功能和行为 +/// +[TestFixture] +public class LogContextTests +{ + [SetUp] + public void SetUp() + { + LogContext.Clear(); + } + + [TearDown] + public void TearDown() + { + LogContext.Clear(); + } + + [Test] + public void Current_WhenEmpty_ShouldReturnEmptyDictionary() + { + var current = LogContext.Current; + + Assert.That(current, Is.Not.Null); + Assert.That(current.Count, Is.EqualTo(0)); + } + + [Test] + public void Push_ShouldAddPropertyToContext() + { + using (LogContext.Push("Key1", "Value1")) + { + var current = LogContext.Current; + + Assert.That(current.Count, Is.EqualTo(1)); + Assert.That(current["Key1"], Is.EqualTo("Value1")); + } + } + + [Test] + public void Push_WithMultipleProperties_ShouldAddAllProperties() + { + using (LogContext.PushProperties(("Key1", "Value1"), ("Key2", 123))) + { + var current = LogContext.Current; + + Assert.That(current.Count, Is.EqualTo(2)); + Assert.That(current["Key1"], Is.EqualTo("Value1")); + Assert.That(current["Key2"], Is.EqualTo(123)); + } + } + + [Test] + public void Push_WithNestedContext_ShouldMergeProperties() + { + using (LogContext.Push("Key1", "Value1")) + { + using (LogContext.Push("Key2", "Value2")) + { + var current = LogContext.Current; + + Assert.That(current.Count, Is.EqualTo(2)); + Assert.That(current["Key1"], Is.EqualTo("Value1")); + Assert.That(current["Key2"], Is.EqualTo("Value2")); + } + } + } + + [Test] + public void Push_WithSameKey_ShouldOverrideValue() + { + using (LogContext.Push("Key1", "Value1")) + { + using (LogContext.Push("Key1", "Value2")) + { + var current = LogContext.Current; + + Assert.That(current.Count, Is.EqualTo(1)); + Assert.That(current["Key1"], Is.EqualTo("Value2")); + } + + // 释放后应该恢复原值 + var restored = LogContext.Current; + Assert.That(restored["Key1"], Is.EqualTo("Value1")); + } + } + + [Test] + public void Dispose_ShouldRestorePreviousValue() + { + using (LogContext.Push("Key1", "Value1")) + { + Assert.That(LogContext.Current["Key1"], Is.EqualTo("Value1")); + } + + // 释放后应该清空 + Assert.That(LogContext.Current.Count, Is.EqualTo(0)); + } + + [Test] + public void Clear_ShouldRemoveAllProperties() + { + using (LogContext.Push("Key1", "Value1")) + { + using (LogContext.Push("Key2", "Value2")) + { + LogContext.Clear(); + + Assert.That(LogContext.Current.Count, Is.EqualTo(0)); + } + } + } + + [Test] + public void Push_WithNullKey_ShouldThrowArgumentException() + { + Assert.Throws(() => LogContext.Push(null!, "Value")); + } + + [Test] + public void Push_WithEmptyKey_ShouldThrowArgumentException() + { + Assert.Throws(() => LogContext.Push("", "Value")); + } + + [Test] + public void Push_WithWhitespaceKey_ShouldThrowArgumentException() + { + Assert.Throws(() => LogContext.Push(" ", "Value")); + } + + [Test] + public void Push_WithNullValue_ShouldWork() + { + using (LogContext.Push("Key1", null)) + { + var current = LogContext.Current; + + Assert.That(current.Count, Is.EqualTo(1)); + Assert.That(current["Key1"], Is.Null); + } + } + + [Test] + public async Task Push_InAsyncContext_ShouldIsolateAcrossThreads() + { + var task1Values = new List(); + var task2Values = new List(); + + 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)); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/LogEntryTests.cs b/GFramework.Core.Tests/logging/LogEntryTests.cs new file mode 100644 index 0000000..2e51b93 --- /dev/null +++ b/GFramework.Core.Tests/logging/LogEntryTests.cs @@ -0,0 +1,145 @@ +using GFramework.Core.Abstractions.logging; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 LogEntry 的功能和行为 +/// +[TestFixture] +public class LogEntryTests +{ + [Test] + public void Constructor_WithAllParameters_ShouldCreateEntry() + { + var timestamp = DateTime.Now; + var properties = new Dictionary { ["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 { ["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 { ["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 { ["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 { ["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)); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/LogLevelFilterTests.cs b/GFramework.Core.Tests/logging/LogLevelFilterTests.cs new file mode 100644 index 0000000..fcc86b7 --- /dev/null +++ b/GFramework.Core.Tests/logging/LogLevelFilterTests.cs @@ -0,0 +1,59 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.filters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 LogLevelFilter 的功能和行为 +/// +[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); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs b/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs new file mode 100644 index 0000000..69afccf --- /dev/null +++ b/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs @@ -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; + +/// +/// 测试 LoggingConfiguration 和 LoggingConfigurationLoader 的功能和行为 +/// +[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(() => 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(() => 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)); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/NamespaceFilterTests.cs b/GFramework.Core.Tests/logging/NamespaceFilterTests.cs new file mode 100644 index 0000000..3e7dd4f --- /dev/null +++ b/GFramework.Core.Tests/logging/NamespaceFilterTests.cs @@ -0,0 +1,85 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.filters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +/// +/// 测试 NamespaceFilter 的功能和行为 +/// +[TestFixture] +public class NamespaceFilterTests +{ + [Test] + public void Constructor_WithNullNamespaces_ShouldThrowArgumentNullException() + { + Assert.Throws(() => new NamespaceFilter(null!)); + } + + [Test] + public void Constructor_WithEmptyNamespaces_ShouldThrowArgumentException() + { + Assert.Throws(() => new NamespaceFilter(Array.Empty())); + } + + [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); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs b/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs new file mode 100644 index 0000000..8fdab20 --- /dev/null +++ b/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs @@ -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; + +/// +/// 测试 RollingFileAppender 的功能和行为 +/// +[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(() => new RollingFileAppender(_testFilePath, maxFileSize: 0)); + Assert.Throws(() => new RollingFileAppender(_testFilePath, maxFileSize: -1)); + } + + [Test] + public void Constructor_WithInvalidMaxFileCount_ShouldThrowArgumentException() + { + Assert.Throws(() => new RollingFileAppender(_testFilePath, maxFileCount: 0)); + Assert.Throws(() => 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(() => 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")); + } +} \ No newline at end of file