GFramework/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs
GeWuYou 685897f2de fix(core): 收口 PR267 事件契约遗留问题
- 修复 AsyncLogAppender 接口刷新路径重复触发完成事件,并补充单次通知回归测试
- 补充 Architecture、CoroutineExceptionEventArgs 与阶段协调器的事件契约注释
- 更新 PhaseChanged 迁移文档与 analyzer-warning-reduction recovery 记录
2026-04-21 16:50:56 +08:00

343 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging.Appenders;
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.UtcNow;
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
var elapsed = (DateTime.UtcNow - 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.UtcNow, 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.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
asyncAppender.Flush();
Assert.That(innerAppender.Entries.Count, Is.EqualTo(100));
}
[Test]
public void Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result()
{
var innerAppender = new TestAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
object? observedSender = null;
AsyncLogFlushCompletedEventArgs? observedArgs = null;
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Flush check", null, null));
asyncAppender.OnFlushCompleted += (sender, eventArgs) =>
{
observedSender = sender;
observedArgs = eventArgs;
};
var result = asyncAppender.Flush(TimeSpan.FromSeconds(1));
Assert.Multiple(() =>
{
Assert.That(observedSender, Is.SameAs(asyncAppender));
Assert.That(observedArgs, Is.Not.Null);
Assert.That(observedArgs!.Success, Is.EqualTo(result));
});
}
[Test]
public void ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once()
{
var innerAppender = new TestAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
ILogAppender logAppender = asyncAppender;
var observedResults = new List<bool>();
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Interface flush check", null, null));
asyncAppender.OnFlushCompleted += (_, eventArgs) => observedResults.Add(eventArgs.Success);
logAppender.Flush();
Assert.That(observedResults, Has.Count.EqualTo(1));
Assert.That(observedResults, Has.All.True);
}
[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.UtcNow, 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.UtcNow, 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.UtcNow, 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.UtcNow, 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 reportedExceptions = new List<Exception>();
var innerAppender = new ThrowingAppender();
using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: reportedExceptions.Add);
// 即使内部 Appender 抛出异常,也不应该影响调用线程
Assert.DoesNotThrow(() =>
{
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
});
asyncAppender.Flush();
Assert.That(reportedExceptions, Has.Count.EqualTo(10));
Assert.That(reportedExceptions, Has.All.TypeOf<InvalidOperationException>());
Assert.That(reportedExceptions.Select(static exception => exception.Message),
Has.All.EqualTo("Test exception"));
}
[Test]
public void Append_WhenProcessingErrorHandlerThrows_ShouldStillNotCrash()
{
var innerAppender = new ThrowingAppender();
using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: static _ => throw new InvalidOperationException("Observer failure"));
Assert.DoesNotThrow(() =>
{
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
});
Assert.That(asyncAppender.Flush(), Is.True);
}
[Test]
public void Append_WhenInnerAppenderThrowsOperationCanceledException_ShouldNotReportError()
{
var reportedExceptions = new List<Exception>();
var innerAppender = new CancellationAppender();
using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: reportedExceptions.Add);
Assert.DoesNotThrow(() =>
{
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
});
Assert.That(asyncAppender.Flush(), Is.True);
Assert.That(reportedExceptions, Is.Empty);
}
// 辅助测试类
private class TestAppender : ILogAppender
{
public List<LogEntry> Entries { get; } = new();
public void Append(LogEntry entry)
{
lock (Entries)
{
Entries.Add(entry);
}
}
public void Flush()
{
}
public void Dispose()
{
}
}
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()
{
}
public void Dispose()
{
}
}
private class ThrowingAppender : ILogAppender
{
public void Append(LogEntry entry)
{
throw new InvalidOperationException("Test exception");
}
public void Flush()
{
}
public void Dispose()
{
}
}
private class CancellationAppender : ILogAppender
{
public void Append(LogEntry entry)
{
throw new OperationCanceledException("Simulated cancellation");
}
public void Flush()
{
}
public void Dispose()
{
}
}
}