diff --git a/Chickensoft.Log.Tests/badges/branch_coverage.svg b/Chickensoft.Log.Tests/badges/branch_coverage.svg index 6d254a4..3e33f55 100644 --- a/Chickensoft.Log.Tests/badges/branch_coverage.svg +++ b/Chickensoft.Log.Tests/badges/branch_coverage.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cc0038d4cd36e082c8439910b3bd6c2174cb4ff1823447470f6bf83bfa9260a -size 8494 +oid sha256:cdb54e7d0e2a5fedad521ab5d54f07c99c6c69cfd1e4139c265bec981e95e9f8 +size 8492 diff --git a/Chickensoft.Log.Tests/badges/line_coverage.svg b/Chickensoft.Log.Tests/badges/line_coverage.svg index 038ad83..385b4d2 100644 --- a/Chickensoft.Log.Tests/badges/line_coverage.svg +++ b/Chickensoft.Log.Tests/badges/line_coverage.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35d909779d17f25a28b74063115500faed330c2b4793a28c6895faabf7d1f0b -size 6267 +oid sha256:d6985d865f33cb4b037730f65881e1f1d7d099a6ba22c4c5021f1656eef609f0 +size 6265 diff --git a/Chickensoft.Log.Tests/coverage.sh b/Chickensoft.Log.Tests/coverage.sh old mode 100644 new mode 100755 diff --git a/Chickensoft.Log.Tests/test/src/FileWriterTest.cs b/Chickensoft.Log.Tests/test/src/FileWriterTest.cs new file mode 100644 index 0000000..4768035 --- /dev/null +++ b/Chickensoft.Log.Tests/test/src/FileWriterTest.cs @@ -0,0 +1,115 @@ +namespace Chickensoft.Log.Tests; + +using System; +using System.IO; +using Shouldly; + +public class FileWriterStreamTester : IDisposable { + private bool _isDisposed; + private readonly MemoryStream _memoryStream; + + public FileWriterStreamTester( + string filename = FileWriter.DEFAULT_FILE_NAME + ) { + _memoryStream = new MemoryStream(); + + FileWriter.AppendText = fileName => new StreamWriter( + _memoryStream, + System.Text.Encoding.UTF8, + bufferSize: 1024, + leaveOpen: true + ); + } + + public string GetString() { + _memoryStream.Position = 0; + using var reader = new StreamReader(_memoryStream); + var result = reader.ReadToEnd(); + return result; + } + + public void Dispose() { + if (_isDisposed) { return; } + + GC.SuppressFinalize(this); + _isDisposed = true; + + FileWriter.AppendText = FileWriter.AppendTextDefault; + _memoryStream.Dispose(); + } +} + +public class FileWriterTest { + [Fact] + public void DefaultFileName() { + FileWriter.DefaultFileName.ShouldBe(FileWriter.DEFAULT_FILE_NAME); + + var filename = "test.log"; + FileWriter.DefaultFileName = filename; + FileWriter.DefaultFileName.ShouldBe(filename); + + FileWriter.DefaultFileName = FileWriter.DEFAULT_FILE_NAME; + } + + [Fact] + public void DefaultInstance() { + var writer = FileWriter.Instance(); + writer.ShouldNotBeNull(); + writer.ShouldBeOfType(); + } + + [Fact] + public void NewInstance() { + var filename = "test.log"; + var writer = FileWriter.Instance(filename); + writer.ShouldNotBeNull(); + writer.ShouldBeOfType(); + } + + [Fact] + public void ReusesInstanceAndRemoves() { + var filename = "test.log"; + var writer1 = FileWriter.Instance(filename); + var writer2 = FileWriter.Instance(filename); + writer1.ShouldBeSameAs(writer2); + + FileWriter.Remove(filename).ShouldBeSameAs(writer1); + FileWriter.Remove(filename).ShouldBeNull(); + } + + [Fact] + public void WriteMessage() { + using var tester = new FileWriterStreamTester(); + + var writer = FileWriter.Instance(); + var value = "test message"; + + writer.WriteMessage(value); + + tester.GetString().ShouldBe(value + Environment.NewLine); + } + + [Fact] + public void WriteWarning() { + using var tester = new FileWriterStreamTester(); + + var writer = FileWriter.Instance(); + var value = "test message"; + + writer.WriteWarning(value); + + tester.GetString().ShouldBe(value + Environment.NewLine); + } + + [Fact] + public void WriteError() { + using var tester = new FileWriterStreamTester(); + + var writer = FileWriter.Instance(); + var value = "test message"; + + writer.WriteError(value); + + tester.GetString().ShouldBe(value + Environment.NewLine); + } +} diff --git a/Chickensoft.Log.Tests/test/src/LogTest.cs b/Chickensoft.Log.Tests/test/src/LogTest.cs index 3b28f6e..b142941 100644 --- a/Chickensoft.Log.Tests/test/src/LogTest.cs +++ b/Chickensoft.Log.Tests/test/src/LogTest.cs @@ -237,4 +237,19 @@ public void PrintsStackTraceWithMessage() { Invoked.Once ); } + + [Fact] + public void AddsAndRemovesWriters() { + var writerA = new Mock(); + var writerB = new Mock(); + var log = new Log(nameof(LogTest), [writerA.Object]); + + log.AddWriter(writerB.Object); + + log._writers.ShouldBe([writerA.Object, writerB.Object]); + + log.RemoveWriter(writerA.Object); + + log._writers.ShouldBe([writerB.Object]); + } } diff --git a/Chickensoft.Log/src/Assembly.cs b/Chickensoft.Log/src/Assembly.cs new file mode 100644 index 0000000..7b04436 --- /dev/null +++ b/Chickensoft.Log/src/Assembly.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Chickensoft.Log.Tests")] diff --git a/Chickensoft.Log/src/FileWriter.cs b/Chickensoft.Log/src/FileWriter.cs index 298e187..d4c2bb1 100644 --- a/Chickensoft.Log/src/FileWriter.cs +++ b/Chickensoft.Log/src/FileWriter.cs @@ -1,22 +1,32 @@ namespace Chickensoft.Log; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; /// /// An that directs output of an /// to a file. /// -[ExcludeFromCodeCoverage(Justification = "File output is untestable")] public sealed class FileWriter : ILogWriter { + internal delegate StreamWriter AppendTextDelegate(string text); + internal static AppendTextDelegate AppendTextDefault { get; } = + File.AppendText; + internal static AppendTextDelegate AppendText { get; set; } = + AppendTextDefault; + // protect static members from simultaneous thread access private static readonly object _singletonLock = new(); // Implemented as a pseudo-singleton to enforce one truncation per file per // execution private static readonly Dictionary _instances = []; - private static string _defaultFileName = "output.log"; + /// The default filename for logs. + public const string DEFAULT_FILE_NAME = "output.log"; + +#pragma warning disable IDE0032 // Use auto property + private static string _defaultFileName = DEFAULT_FILE_NAME; +#pragma warning restore IDE0032 // Use auto property + /// /// The default file name that will be used when creating a /// if no filename is specified. Defaults to @@ -87,6 +97,24 @@ public static FileWriter Instance() { } } + /// + /// Remove a that had previously been created. + /// While not necessary, this can free up resources if writing to many + /// different log files. + /// + /// Filename for the log. + /// The file writer, if one existed for the given filename. + /// Otherwise, just null. + public static FileWriter? Remove(string fileName) { + lock (_singletonLock) { + if (_instances.TryGetValue(fileName, out var writer)) { + _instances.Remove(fileName); + return writer; + } + } + return null; + } + private readonly object _writingLock = new(); /// @@ -102,14 +130,10 @@ private FileWriter(string fileName) { } } - [SuppressMessage("Style", - "IDE0063:Use simple 'using' statement", - Justification = "Prefer block, to explicitly delineate scope")] private void WriteLine(string message) { lock (_writingLock) { - using (var sw = File.AppendText(FileName)) { - sw.WriteLine(message); - } + using var sw = AppendText(FileName); + sw.WriteLine(message); } } diff --git a/Chickensoft.Log/src/Log.cs b/Chickensoft.Log/src/Log.cs index 9fc3bad..e86bbce 100644 --- a/Chickensoft.Log/src/Log.cs +++ b/Chickensoft.Log/src/Log.cs @@ -13,7 +13,7 @@ public sealed class Log : ILog { /// public string Name { get; } - private readonly List _writers = []; + internal readonly List _writers = []; /// /// The formatter that will be used to format messages before writing them