diff --git a/build/Version.props b/build/Version.props index acae893..726b569 100644 --- a/build/Version.props +++ b/build/Version.props @@ -1,5 +1,5 @@ - 5.2.1 + 5.3.0 \ No newline at end of file diff --git a/src/Utils/Walterlv.Collections/Threading/AsyncQueue.cs b/src/Utils/Walterlv.Collections/Threading/AsyncQueue.cs index ae5673c..2e3ef24 100644 --- a/src/Utils/Walterlv.Collections/Threading/AsyncQueue.cs +++ b/src/Utils/Walterlv.Collections/Threading/AsyncQueue.cs @@ -23,6 +23,12 @@ public AsyncQueue() _queue = new ConcurrentQueue(); } + /// + /// 获取此刻队列中剩余元素的个数。 + /// 请注意:因为线程安全问题,此值获取后值即过时,所以获取此值的代码需要自行处理线程安全。 + /// + public int Count => _queue.Count; + /// /// 入队。 /// diff --git a/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.AsyncQueue.cs b/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.AsyncQueue.cs index 264c857..f643680 100644 --- a/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.AsyncQueue.cs +++ b/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.AsyncQueue.cs @@ -18,6 +18,8 @@ public AsyncQueue() _queue = new ConcurrentQueue(); } + public int Count => _queue.Count; + public void Enqueue(T item) { _queue.Enqueue(item); @@ -40,7 +42,6 @@ public async Task DequeueAsync(CancellationToken cancellationToken = default) while (true) { await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); - if (_queue.TryDequeue(out var item)) { return item; diff --git a/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.cs b/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.cs index 893edbd..2b17a69 100644 --- a/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.cs +++ b/src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace Walterlv.Logging.Core @@ -11,6 +12,9 @@ public abstract partial class AsyncOutputLogger : ILogger { private readonly AsyncQueue _queue; private bool _isInitialized; + private CancellationTokenSource _waitForEmptyCancellationTokenSource = new CancellationTokenSource(); + private TaskCompletionSource? _waitForEmptyTaskCompletionSource; + private object _waitForEmptyLocker = new object(); /// /// 创建 Markdown 格式的日志记录实例。 @@ -108,7 +112,32 @@ private async void StartLogging() { while (true) { - var context = await _queue.DequeueAsync().ConfigureAwait(false); + LogContext context; + + try + { + context = await _queue.DequeueAsync(_waitForEmptyCancellationTokenSource.Token).ConfigureAwait(false); + await Write(context).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + while (_queue.Count > 0) + { + context = await _queue.DequeueAsync().ConfigureAwait(false); + await Write(context).ConfigureAwait(false); + } + } + + if (_queue.Count == 0) + { + _waitForEmptyCancellationTokenSource.Dispose(); + _waitForEmptyCancellationTokenSource = new CancellationTokenSource(); + _waitForEmptyTaskCompletionSource?.SetResult(null); + } + } + + async Task Write(LogContext context) + { if (!_isInitialized) { _isInitialized = true; @@ -128,5 +157,36 @@ private async void StartLogging() /// /// 包含一条日志的所有上下文信息。 protected abstract void OnLogReceived(in LogContext context); + + /// + /// 如果派生类需要等待当前尚未完成日志输出的日志全部完成输出,则调用此方法。 + /// 但请注意:因为并发问题,如果等待期间还有新写入的日志,那么也会一并等待。 + /// + /// 可等待对象。 + protected async Task WaitFlushingAsync() + { + if (_waitForEmptyTaskCompletionSource is null) + { + lock (_waitForEmptyLocker) + { + if (_waitForEmptyTaskCompletionSource is null) + { + _waitForEmptyTaskCompletionSource = new TaskCompletionSource(); + _waitForEmptyCancellationTokenSource.Cancel(); + } + else if (_waitForEmptyTaskCompletionSource.Task.IsCompleted) + { + return; + } + } + } + + await _waitForEmptyTaskCompletionSource.Task.ConfigureAwait(false); + + lock (_waitForEmptyLocker) + { + _waitForEmptyTaskCompletionSource = null; + } + } } } diff --git a/src/Utils/Walterlv.Logger/IO/TextFileLogger.cs b/src/Utils/Walterlv.Logger/IO/TextFileLogger.cs index 856008e..c8de182 100644 --- a/src/Utils/Walterlv.Logger/IO/TextFileLogger.cs +++ b/src/Utils/Walterlv.Logger/IO/TextFileLogger.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; -using System.Text; +using System.Runtime.ExceptionServices; +using System.Threading; using System.Threading.Tasks; using Walterlv.Logging.Core; @@ -16,18 +21,22 @@ public class TextFileLogger : AsyncOutputLogger, IDisposable private readonly string _lineEnd; private readonly FileInfo _infoLogFile; private readonly FileInfo _errorLogFile; - private readonly bool _shouldAppendInfo; - private readonly bool _shouldAppendError; - private StreamWriter? _infoWriter; - private StreamWriter? _errorWriter; + private Mutex? _infoMutex; + private Mutex? _errorMutex; + private object _disposeLocker = new object(); + private bool _isDisposed; + + /// + /// 针对文件的拦截器。第一个参数是文件,第二个参数是此文件所对应的日志等级,只有两种值:。 + /// + private Action? _fileInterceptor; /// /// 创建文本文件记录日志的 的新实例。 /// /// 日志文件。 - /// 如果你希望每次创建同文件的新实例时追加到原来日志的末尾,则设为 true;如果希望覆盖之前的日志,则设为 false。 /// 行尾符号。默认是 \n,如果你愿意,也可以改为 \r\n 或者 \r。 - public TextFileLogger(FileInfo logFile, bool append = false, string lineEnd = "\n") + public TextFileLogger(FileInfo logFile, string lineEnd = "\n") { if (logFile is null) { @@ -37,8 +46,6 @@ public TextFileLogger(FileInfo logFile, bool append = false, string lineEnd = "\ _infoLogFile = logFile; _errorLogFile = logFile; _lineEnd = VerifyLineEnd(lineEnd); - _shouldAppendInfo = append; - _shouldAppendError = append; } /// @@ -47,11 +54,8 @@ public TextFileLogger(FileInfo logFile, bool append = false, string lineEnd = "\ /// /// 信息和警告的日志文件。 /// 错误日志文件。 - /// 如果你希望每次创建同文件的新实例时追加到原来日志的末尾,则设为 true;如果希望覆盖之前的日志,则设为 false。 - /// 如果你希望每次创建同文件的新实例时追加到原来日志的末尾,则设为 true;如果希望覆盖之前的日志,则设为 false。 /// 行尾符号。默认是 \n,如果你愿意,也可以改为 \r\n 或者 \r。 - public TextFileLogger(FileInfo infoLogFile, FileInfo errorLogFile, - bool shouldAppendInfo = false, bool shouldAppendError = true, string lineEnd = "\n") + public TextFileLogger(FileInfo infoLogFile, FileInfo errorLogFile, string lineEnd = "\n") { if (infoLogFile is null) { @@ -67,22 +71,52 @@ public TextFileLogger(FileInfo infoLogFile, FileInfo errorLogFile, var areSameFile = string.Equals(infoLogFile.FullName, errorLogFile.FullName, StringComparison.OrdinalIgnoreCase); _infoLogFile = infoLogFile; _errorLogFile = areSameFile ? infoLogFile : errorLogFile; - _shouldAppendInfo = shouldAppendInfo; - _shouldAppendError = shouldAppendError; + } + + /// + /// 请求在写入首条日志前针对日志文件执行一些代码。代码可能在任一线程中执行,但确保不会并发。 + /// + /// + /// 针对某文件的拦截器。 + /// 第一个参数是文件,第二个参数是此文件所对应的日志等级,只有两种值,对应此日志文件中能记录信息的最严重范围: + /// - 表示此日志最严重只记到警告。 + /// - 表示此日志最严重记到崩溃。 + /// + internal void AddInitializeInterceptor(Action fileInterceptor) + { + if (_infoMutex != null) + { + throw new InvalidOperationException("已经有日志开始输出了,不可再继续配置日志行为。"); + } + + _fileInterceptor += fileInterceptor ?? throw new ArgumentNullException(nameof(fileInterceptor)); } /// - protected override async Task OnInitializedAsync() + protected override Task OnInitializedAsync() { if (_isDisposed) { - return; + return Task.FromResult(null); + } + + // 初始化文件写入安全区。 + var areSameFile = _errorLogFile == _infoLogFile; + _infoMutex = CreateMutex(_infoLogFile); + _errorMutex = areSameFile ? _infoMutex : CreateMutex(_errorLogFile); + + // 初始化文件。 + CriticalInvoke(_infoMutex, _fileInterceptor, interceptor => interceptor?.Invoke(_infoLogFile, LogLevel.Warning)); + if (!areSameFile) + { + CriticalInvoke(_errorMutex, _fileInterceptor, interceptor => interceptor?.Invoke(_errorLogFile, LogLevel.Fatal)); } - _infoWriter = await CreateWriterAsync(_infoLogFile, _shouldAppendInfo).ConfigureAwait(false); - _errorWriter = _errorLogFile == _infoLogFile - ? _infoWriter - : await CreateWriterAsync(_errorLogFile, _shouldAppendError).ConfigureAwait(false); + return Task.FromResult(null); + + static Mutex CreateMutex(FileInfo file) => new Mutex( + false, + Path.GetFullPath(file.FullName).ToLower(CultureInfo.InvariantCulture).Replace(Path.DirectorySeparatorChar, '_')); } /// @@ -93,18 +127,20 @@ protected sealed override void OnLogReceived(in LogContext context) return; } - var areSameFile = _infoWriter == _errorWriter; + var infoMutex = _infoMutex!; + var errorMutex = _errorMutex!; + var areSameFile = _errorLogFile == _infoLogFile; if (!areSameFile && context.CurrentLevel <= LogLevel.Error) { // 写入日志的主要部分。 - _infoWriter?.WriteLine(BuildLogText(in context, containsExtraInfo: false, _lineEnd)); + CriticalWrite(infoMutex, _infoLogFile, BuildLogText(in context, containsExtraInfo: false, _lineEnd)); // 写入日志的扩展部分。 - _errorWriter?.WriteLine(BuildLogText(in context, context.ExtraInfo != null, _lineEnd)); + CriticalWrite(errorMutex, _errorLogFile, BuildLogText(in context, context.ExtraInfo != null, _lineEnd)); } else { - _infoWriter?.WriteLine(BuildLogText(in context, context.ExtraInfo != null, _lineEnd)); + CriticalWrite(infoMutex, _infoLogFile, BuildLogText(in context, context.ExtraInfo != null, _lineEnd)); } } @@ -130,57 +166,6 @@ protected virtual string BuildLogText(in LogContext context, bool containsExtraI : $@"[{time}][{member}] {text}{lineEnd}{extraInfo}"; } - /// - /// 创建写入到日志的流。 - /// - /// 日志文件。 - /// 是追加到文件还是直接覆盖文件。 - /// 可等待的实例。 - private async Task CreateWriterAsync(FileInfo file, bool append) - { - var directory = file.Directory; - if (directory != null && !Directory.Exists(directory.FullName)) - { - directory.Create(); - } - - for (var i = 0; i < 10; i++) - { - if (_isDisposed) - { - return null; - } - - try - { - var fileStream = File.Open( - file.FullName, - append ? FileMode.Append : FileMode.Create, - FileAccess.Write, - FileShare.Read); - return new StreamWriter(fileStream, Encoding.UTF8) - { - AutoFlush = true, - NewLine = _lineEnd, - }; - } - catch (IOException) - { - // 当出现了 IO 错误,通常还有恢复的可能,所以重试。 - await Task.Delay(1000).ConfigureAwait(false); - continue; - } - catch (Exception) - { - // 当出现了其他错误,恢复的可能性比较低,所以重试更少次数,更长时间。 - await Task.Delay(5000).ConfigureAwait(false); - i++; - continue; - } - } - return null; - } - /// /// 检查字符串是否是行尾符号,如果不是则抛出异常。 /// @@ -196,24 +181,130 @@ protected virtual string BuildLogText(in LogContext context, bool containsExtraI _ => throw new ArgumentException("虽然你可以指定行尾符号,但也只能是 \\n、\\r 或者 \\r\\n。", nameof(lineEnd)) }; - private bool _isDisposed = false; - + /// + /// 派生类重写此方法以回收非托管资源。注意如果重写了此方法,必须在重写方法中调用基类方法。 + /// + /// 如果主动释放资源,请传入 true;如果被动释放资源(析构函数),请传入 false。 protected virtual void Dispose(bool disposing) { - if (!_isDisposed) + if (_isDisposed) + { + return; + } + + lock (_disposeLocker) { if (disposing) { - (_infoWriter?.BaseStream as FileStream)?.Dispose(); - (_errorWriter?.BaseStream as FileStream)?.Dispose(); + try + { + WaitFlushingAsync().Wait(); + _infoMutex?.Dispose(); + _errorMutex?.Dispose(); + } + finally + { + _isDisposed = true; + } } - - _isDisposed = true; } } + /// + /// 将日志最后的缓冲写完后关闭日志记录,然后回收所有资源。 + /// public void Dispose() => Dispose(true); + /// + /// 将日志最后的缓冲写完后关闭日志记录,然后回收所有资源。 + /// public void Close() => Dispose(true); + + private static void CriticalWrite(Mutex mutex, FileInfo file, params string[] texts) + { + try + { + mutex.WaitOne(); + } + catch (AbandonedMutexException ex) + { + // 发现有进程意外退出后,遗留了锁。 + // 此时已经拿到了锁。 + // 参见:https://blog.walterlv.com/post/mutex-in-dotnet.html + texts = new string[] { $"Unexpected lock on this log file is detected. Abandoned index is {ex.MutexIndex}." }.Concat(texts).ToArray(); + } + + try + { + File.AppendAllLines(file.FullName, texts); + } + finally + { + mutex.ReleaseMutex(); + } + } + + private static void CriticalInvoke(Mutex mutex, T? action, Action invoker) where T : MulticastDelegate + { + try + { + mutex.WaitOne(); + } + catch (AbandonedMutexException ex) + { + // 发现有进程意外退出后,遗留了锁。 + // 此时已经拿到了锁。 + // 参见:https://blog.walterlv.com/post/mutex-in-dotnet.html + Debug.WriteLine($"Unexpected lock on this log file is detected. Abandoned index is {ex.MutexIndex}."); + } + + try + { + if (action != null) + { + var exceptions = new List(); + foreach (var a in action.GetInvocationList().Cast()) + { + try + { + invoker(a); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + if (exceptions.Count == 1) + { + ExceptionDispatchInfo.Capture(exceptions[0]).Throw(); + } + else if (exceptions.Count > 1) + { + throw new AggregateException(exceptions); + } + } + } + finally + { + mutex.ReleaseMutex(); + } + } + + /// + /// 不再支持。 + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("不再使用 append 参数决定日志是否保留,请使用 new TextFileLogger().WithWholeFileOverride() 替代。")] + public TextFileLogger(FileInfo logFile, bool append, string lineEnd = "\n") + : this(logFile, lineEnd) => this.WithWholeFileOverride(append); + + /// + /// 不再支持。 + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("不再使用 append 参数决定日志是否保留,请使用 new TextFileLogger().WithWholeFileOverride() 替代。")] + public TextFileLogger(FileInfo infoLogFile, FileInfo errorLogFile, + bool shouldAppendInfo, bool shouldAppendError, string lineEnd = "\n") + : this(infoLogFile, errorLogFile, lineEnd) => this.WithWholeFileOverride(shouldAppendInfo, shouldAppendError); } } diff --git a/src/Utils/Walterlv.Logger/IO/TextFileLoggerExtensions.cs b/src/Utils/Walterlv.Logger/IO/TextFileLoggerExtensions.cs new file mode 100644 index 0000000..e0a0498 --- /dev/null +++ b/src/Utils/Walterlv.Logger/IO/TextFileLoggerExtensions.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Linq; + +namespace Walterlv.Logging.IO +{ + /// + /// 为 和其子类提供扩展方法。 + /// + public static class TextFileLoggerExtensions + { + /// + /// 在开始向日志文件中写入日志的时候,检查日志文件是否过大。如果过大(字节),则清空日志。 + /// + /// 日志实例。 + /// 大于此大小(字节)时清空日志。 + public static void WithMaxFileSize(this TextFileLogger logger, long maxFileSize) + { + if (maxFileSize <= 0) + { + throw new ArgumentException("日志文件限制的大小必须是正整数。", nameof(maxFileSize)); + } + + logger.AddInitializeInterceptor((file, _) => + { + file = new FileInfo(file.FullName); + if (file.Exists && file.Length > maxFileSize) + { + File.WriteAllText(file.FullName, ""); + } + }); + } + + /// + /// 在开始向日志文件中写入日志的时候,检查日志文件行数是否过多。如果过多,则清空前面的行,保留最后的 行。 + /// + /// 日志实例。 + /// 大于此行数时清空日志的前面行。 + /// 清空后应该保留行数。默认为完全不保留。 + public static void WithMaxLineCount(this TextFileLogger logger, int maxLineCount, int newLineCountAfterLimitReached = 0) + { + if (maxLineCount <= 0) + { + throw new ArgumentException("日志文件限制的行数必须是正整数。", nameof(maxLineCount)); + } + + if (newLineCountAfterLimitReached <= 0) + { + throw new ArgumentException("日志文件清空后的行数必须是正整数。", nameof(newLineCountAfterLimitReached)); + } + + if (newLineCountAfterLimitReached > maxLineCount) + { + throw new ArgumentException("日志文件清空后的行数不能大于最大限制行数。", nameof(newLineCountAfterLimitReached)); + } + + logger.AddInitializeInterceptor((file, _) => + { + var lines = File.ReadAllLines(file.FullName); + if (file.Exists && lines.Length > maxLineCount) + { + if (newLineCountAfterLimitReached == 0) + { + File.WriteAllText(file.FullName, ""); + } + else + { + File.WriteAllLines(file.FullName, lines.Skip(lines.Length - newLineCountAfterLimitReached)); + } + } + }); + } + + /// + /// 在开始向日志文件中写入日志的时候,是否覆盖曾经在文件内写入过的日志。 + /// + /// 日志实例。 + /// 如果需要覆盖,请设置为 true。 + public static void WithWholeFileOverride(this TextFileLogger logger, bool @override = true) + { + if (@override) + { + logger.AddInitializeInterceptor((file, _) => File.Delete(file.FullName)); + } + } + + /// + /// 在开始向日志文件中写入日志的时候,是否覆盖曾经在文件内写入过的日志。 + /// + /// 日志实例。 + /// 如果你希望首次写入信息日志时覆盖原来日志的整个文件,则设为 true;如果希望保留之前的日志而追加,则设为 false。 + /// 如果你希望首次写入错误日志时覆盖原来日志的整个文件,则设为 true;如果希望保留之前的日志而追加,则设为 false。 + public static void WithWholeFileOverride(this TextFileLogger logger, bool overrideForInfo, bool overrideForError) + { + if (overrideForInfo || overrideForError) + { + logger.AddInitializeInterceptor((file, level) => + { + if (File.Exists(file.FullName)) + { + if (level == LogLevel.Fatal) + { + if (overrideForError) + { + File.WriteAllText(file.FullName, ""); + } + } + else if (level == LogLevel.Warning) + { + if (overrideForInfo) + { + File.WriteAllText(file.FullName, ""); + } + } + } + }); + } + } + } +} diff --git a/src/Utils/Walterlv.Logger/Markdown/MarkdownLogger.cs b/src/Utils/Walterlv.Logger/Markdown/MarkdownLogger.cs index ebebc89..44ab597 100644 --- a/src/Utils/Walterlv.Logger/Markdown/MarkdownLogger.cs +++ b/src/Utils/Walterlv.Logger/Markdown/MarkdownLogger.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Globalization; using System.IO; @@ -16,10 +17,9 @@ public sealed class MarkdownLogger : TextFileLogger /// 创建 Markdown 格式的日志记录实例。 /// /// 日志文件。如果你希望有 Markdown 的语法高亮,建议指定后缀为 .md。 - /// 如果你希望每次创建同文件的新实例时追加到原来日志的末尾,则设为 true;如果希望覆盖之前的日志,则设为 false。 /// 行尾符号。默认是 \n,如果你愿意,也可以改为 \r\n 或者 \r。 - public MarkdownLogger(FileInfo logFile, bool append = false, string lineEnd = "\n") - : base(logFile, append, lineEnd) + public MarkdownLogger(FileInfo logFile, string lineEnd = "\n") + : base(logFile, lineEnd) { } @@ -29,12 +29,9 @@ public MarkdownLogger(FileInfo logFile, bool append = false, string lineEnd = "\ /// /// 信息和警告的日志文件。如果你希望有 Markdown 的语法高亮,建议指定后缀为 .md。 /// 错误日志文件。如果你希望有 Markdown 的语法高亮,建议指定后缀为 .md。 - /// 如果你希望每次创建同文件的新实例时追加到原来日志的末尾,则设为 true;如果希望覆盖之前的日志,则设为 false。 - /// 如果你希望每次创建同文件的新实例时追加到原来日志的末尾,则设为 true;如果希望覆盖之前的日志,则设为 false。 /// 行尾符号。默认是 \n,如果你愿意,也可以改为 \r\n 或者 \r。 - public MarkdownLogger(FileInfo infoLogFile, FileInfo errorLogFile, - bool shouldAppendInfo = false, bool shouldAppendError = false, string lineEnd = "\n") - : base(infoLogFile, errorLogFile, shouldAppendInfo, shouldAppendError, lineEnd) + public MarkdownLogger(FileInfo infoLogFile, FileInfo errorLogFile, string lineEnd = "\n") + : base(infoLogFile, errorLogFile, lineEnd) { } @@ -63,5 +60,26 @@ protected override string BuildLogText(in LogContext context, bool containsExtra ? $@"[{time}][{member}] {text}" : $@"[{time}][{member}] {text}{lineEnd}{extraInfo}"; } + + /// + /// 不再支持。 + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("不再使用 append 参数决定日志是否保留,请使用 new MarkdownLogger().WithWholeFileOverride() 替代。")] + public MarkdownLogger(FileInfo logFile, bool append = false, string lineEnd = "\n") + : base(logFile, append, lineEnd) + { + } + + /// + /// 不再支持。 + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("不再使用 append 参数决定日志是否保留,请使用 new MarkdownLogger().WithWholeFileOverride() 替代。")] + public MarkdownLogger(FileInfo infoLogFile, FileInfo errorLogFile, + bool shouldAppendInfo, bool shouldAppendError, string lineEnd = "\n") + : base(infoLogFile, errorLogFile, shouldAppendInfo, shouldAppendError, lineEnd) + { + } } } diff --git a/tests/Walterlv.Packages.Tests/Logging/IO/AsyncOutputLoggerTest.cs b/tests/Walterlv.Packages.Tests/Logging/IO/AsyncOutputLoggerTest.cs new file mode 100644 index 0000000..c064212 --- /dev/null +++ b/tests/Walterlv.Packages.Tests/Logging/IO/AsyncOutputLoggerTest.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTest.Extensions.Contracts; +using Walterlv.Logging.Core; + +namespace Walterlv.Tests.Logging.IO +{ + [TestClass] + public class AsyncOutputLoggerTest : AsyncOutputLogger + { + private readonly List _logs = new List(); + + [ContractTestCase] + public void WaitThreadSafe() + { + "关闭是线程安全的:中途加入了等待,那么会全部一起等待。".Test(() => + { + var logger = new AsyncOutputLoggerTest(); + logger.Message("aaaa"); + logger.Message("bbbb"); + + // 必须设置最小线程池数量,否则此单元测试将不能测试到并发。 + // 原理: + // - 假设测试机只有双核,那么最小线程数是 2 + // - 那么一开始,下文的 task1 和 task2 开始执行 + // - 但 task3 尝试执行时,将进入等待,直到超时才会开始执行,而超时时间是 1 秒 + // - 这 1 秒,足以让单元测试的结果不一样 + // - 单元测试不应该引入不确定量,因此我在测三线程的并发,就必须能并发出三个线程 + ThreadPool.GetMinThreads(out var w, out var c); + ThreadPool.SetMinThreads(8, 8); + + var task1 = Task.Run(() => + { + logger.WaitFlushingAsync().Wait(); + Assert.AreEqual(6, logger._logs.Count); + }); + var task2 = Task.Run(() => + { + logger.WaitFlushingAsync().Wait(); + Assert.AreEqual(6, logger._logs.Count); + }); + var task3 = Task.Run(() => + { + Thread.Sleep(100); + logger.Message("cccc"); + logger.WaitFlushingAsync().Wait(); + Assert.AreEqual(6, logger._logs.Count); + }); + + Task.WaitAll(task1, task2, task3); + + ThreadPool.SetMinThreads(w, c); + }); + + "关闭是线程安全的:等待完后新加入的等待,等待独立。".Test(() => + { + var logger = new AsyncOutputLoggerTest(); + logger.Message("aaaa"); + logger.Message("bbbb"); + + var task1 = Task.Run(() => + { + logger.WaitFlushingAsync().Wait(); + Assert.AreEqual(4, logger._logs.Count); + }); + var task2 = Task.Run(() => + { + logger.WaitFlushingAsync().Wait(); + Assert.AreEqual(4, logger._logs.Count); + }); + + Task.WaitAll(task1, task2); + + var task3 = Task.Run(() => + { + logger.Message("cccc"); + logger.WaitFlushingAsync().Wait(); + Assert.AreEqual(6, logger._logs.Count); + }); + + Task.WaitAll(task3); + }); + } + + protected override Task OnInitializedAsync() => Task.CompletedTask; + + protected override void OnLogReceived(in LogContext context) + { + _logs.Add(context.Text); + Thread.Sleep(1000); + _logs.Add(context.Text); + } + } +} diff --git a/tests/Walterlv.Packages.Tests/Logging/IO/TextFileLoggerTests.cs b/tests/Walterlv.Packages.Tests/Logging/IO/TextFileLoggerTests.cs new file mode 100644 index 0000000..099eed1 --- /dev/null +++ b/tests/Walterlv.Packages.Tests/Logging/IO/TextFileLoggerTests.cs @@ -0,0 +1,57 @@ +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTest.Extensions.Contracts; +using Walterlv.Logging.IO; + +namespace Walterlv.Tests.Logging.IO +{ + [TestClass] + public class TextFileLoggerTests + { + [ContractTestCase] + public void ReadWriteShare() + { + "关闭日志写入后,文件确保全部写入完成。".Test(() => + { + const string testFile = "test1.md"; + if (File.Exists(testFile)) + { + File.Delete(testFile); + } + + var aLogger = new TextFileLogger(new FileInfo(testFile)); + + aLogger.Message("aaaa"); + aLogger.Message("bbbb"); + + aLogger.Close(); + + var newLines = File.ReadAllLines(testFile); + Assert.AreEqual(2, newLines.Length); + }); + + "测试多个日志类读写同一个文件,所有内容都没丢。".Test(() => + { + const string testFile = "test2.md"; + if (File.Exists(testFile)) + { + File.Delete(testFile); + } + + var aLogger = new TextFileLogger(new FileInfo(testFile)); + var bLogger = new TextFileLogger(new FileInfo(testFile)); + + aLogger.Message("aaaa"); + bLogger.Message("bbbb"); + aLogger.Message("cccc"); + bLogger.Message("dddd"); + + aLogger.Close(); + bLogger.Close(); + + var newLines = File.ReadAllLines(testFile); + Assert.AreEqual(4, newLines.Length); + }); + } + } +} diff --git a/tests/Walterlv.Packages.Tests/Walterlv.Packages.Tests.csproj b/tests/Walterlv.Packages.Tests/Walterlv.Packages.Tests.csproj index 618c7b7..98df6e1 100644 --- a/tests/Walterlv.Packages.Tests/Walterlv.Packages.Tests.csproj +++ b/tests/Walterlv.Packages.Tests/Walterlv.Packages.Tests.csproj @@ -15,6 +15,7 @@ +