diff --git a/Chickensoft.Log.Tests/badges/branch_coverage.svg b/Chickensoft.Log.Tests/badges/branch_coverage.svg index e8fe515..9d72145 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:f3b315496ddf65dd7c12ce85fad9f4d3f11a2edb58b7f2a165f253e889ccf43c +oid sha256:37b764ffd43958aa0085ac539e6cc3468d4080e396b65c17aa8596c1b7daac33 size 8494 diff --git a/Chickensoft.Log.Tests/badges/line_coverage.svg b/Chickensoft.Log.Tests/badges/line_coverage.svg index 30afcae..abeef91 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:149940e499ba63e4bfb703a22af2e3b3eef4ffcfe3a0c1a9004de1b8bb2d674e +oid sha256:9a80116b962c254cebf6103707178ebde03a963dd00d9bd3be0ce19161612fa8 size 6267 diff --git a/Chickensoft.Log.Tests/test/src/ConsoleLogTest.cs b/Chickensoft.Log.Tests/test/src/ConsoleLogTest.cs deleted file mode 100644 index c435d08..0000000 --- a/Chickensoft.Log.Tests/test/src/ConsoleLogTest.cs +++ /dev/null @@ -1,285 +0,0 @@ -namespace Chickensoft.Log.Tests; - -using LightMock; -using LightMock.Generator; -using Shouldly; - -public class ConsoleLogTest { - private const string TEST_MSG = "A test message"; - - private static string Format(string msg) { - return $"MockLevel ({nameof(ConsoleLogTest)}): {msg}"; - } - - [Fact] - public void Initializes() { - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object); - log.ShouldBeAssignableTo(); - } - - [Fact] - public void PrintsMessage() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), TEST_MSG)) - .Returns(formattedTestMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Print(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), TEST_MSG), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedTestMsg), - Invoked.Once); - } - - [Fact] - public void PrintsError() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(ConsoleLogTest), TEST_MSG)) - .Returns(formattedTestMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Err(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(ConsoleLogTest), TEST_MSG), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteError(formattedTestMsg), - Invoked.Once); - } - - [Fact] - public void PrintsWarning() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatWarning(nameof(ConsoleLogTest), TEST_MSG)) - .Returns(formattedTestMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Warn(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatWarning(nameof(ConsoleLogTest), TEST_MSG), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteWarning(formattedTestMsg), - Invoked.Once); - } - - [Fact] - public void PrintsException() { - var e = new TestException(TEST_MSG); - var eMsg = e.ToString(); - var formattedExceptionMsg = Format("Exception:"); - var formattedException = Format(eMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(ConsoleLogTest), "Exception:")) - .Returns(formattedExceptionMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(ConsoleLogTest), eMsg)) - .Returns(formattedException); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Print(e); - - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(ConsoleLogTest), "Exception:"), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(ConsoleLogTest), eMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteError(formattedExceptionMsg), - Invoked.Once); - mockWriter.Assert(writer => - writer.WriteError(formattedException), - Invoked.Once); - } - - [Fact] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", - "CA1859:Change type of variable for performance", - Justification = "Need ILog type to test interface method")] - public void PrintsExceptionWithMessage() { - var contextMsg = "Context message"; - var exceptionMsg = "Exception:"; - var e = new TestException(TEST_MSG); - var eStr = e.ToString(); - var formattedContextMsg = Format(contextMsg); - var formattedExceptionMsg = Format(exceptionMsg); - var formattedException = Format(eStr); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(ConsoleLogTest), contextMsg)) - .Returns(formattedContextMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(ConsoleLogTest), exceptionMsg)) - .Returns(formattedExceptionMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(ConsoleLogTest), eStr)) - .Returns(formattedException); - - var mockWriter = new Mock(); - var log = (ILog)new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Print(e, contextMsg); - - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(ConsoleLogTest), contextMsg), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(ConsoleLogTest), exceptionMsg), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(ConsoleLogTest), eStr), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteError(formattedContextMsg), - Invoked.Once); - mockWriter.Assert(writer => - writer.WriteError(formattedExceptionMsg), - Invoked.Once); - mockWriter.Assert(writer => - writer.WriteError(formattedException), - Invoked.Once); - } - - [Fact] - public void PrintsStackTrace() { - var expectedStackTraceMsg = "ClassName.MethodName in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace("File.cs", "ClassName", "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } - - [Fact] - public void PrintsStackTraceWithoutFile() { - var expectedStackTraceMsg = "ClassName.MethodName in **(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace(null, "ClassName", "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } - - [Fact] - public void PrintsStackTraceWithoutClass() { - var expectedStackTraceMsg = "UnknownClass.MethodName in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace("File.cs", null, "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } - - [Fact] - public void PrintsStackTraceWithoutMethod() { - // unknown method is also unknown class - var expectedStackTraceMsg = "UnknownClass.UnknownMethod in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new ConsoleLog(nameof(ConsoleLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace("File.cs", "ClassName", null); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(ConsoleLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } -} diff --git a/Chickensoft.Log.Tests/test/src/FileLogTest.cs b/Chickensoft.Log.Tests/test/src/LogTest.cs similarity index 68% rename from Chickensoft.Log.Tests/test/src/FileLogTest.cs rename to Chickensoft.Log.Tests/test/src/LogTest.cs index 7b90258..ee2d295 100644 --- a/Chickensoft.Log.Tests/test/src/FileLogTest.cs +++ b/Chickensoft.Log.Tests/test/src/LogTest.cs @@ -1,22 +1,22 @@ namespace Chickensoft.Log.Tests; -using Chickensoft.Log; using LightMock; using LightMock.Generator; using Shouldly; -public class FileLogTest { +public class LogTest { private const string TEST_MSG = "A test message"; private static string Format(string msg) { - return $"MockLevel ({nameof(FileLogTest)}): {msg}"; + return $"MockLevel ({nameof(LogTest)}): {msg}"; } [Fact] public void Initializes() { - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object); + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]); log.ShouldBeAssignableTo(); + log.Name.ShouldBe(nameof(LogTest)); } [Fact] @@ -24,17 +24,17 @@ public void PrintsMessage() { var formattedTestMsg = Format(TEST_MSG); var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(FileLogTest), TEST_MSG)) + formatter.FormatMessage(nameof(LogTest), TEST_MSG)) .Returns(formattedTestMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; log.Print(TEST_MSG); mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(FileLogTest), TEST_MSG), + formatter.FormatMessage(nameof(LogTest), TEST_MSG), Invoked.Once); mockWriter.Assert(writer => @@ -47,17 +47,17 @@ public void PrintsError() { var formattedTestMsg = Format(TEST_MSG); var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(FileLogTest), TEST_MSG)) + formatter.FormatError(nameof(LogTest), TEST_MSG)) .Returns(formattedTestMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; log.Err(TEST_MSG); mockFormatter.Assert(formatter => - formatter.FormatError(nameof(FileLogTest), TEST_MSG), + formatter.FormatError(nameof(LogTest), TEST_MSG), Invoked.Once); mockWriter.Assert(writer => @@ -70,17 +70,17 @@ public void PrintsWarning() { var formattedTestMsg = Format(TEST_MSG); var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatWarning(nameof(FileLogTest), TEST_MSG)) + formatter.FormatWarning(nameof(LogTest), TEST_MSG)) .Returns(formattedTestMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; log.Warn(TEST_MSG); mockFormatter.Assert(formatter => - formatter.FormatWarning(nameof(FileLogTest), TEST_MSG), + formatter.FormatWarning(nameof(LogTest), TEST_MSG), Invoked.Once); mockWriter.Assert(writer => @@ -97,23 +97,23 @@ public void PrintsException() { var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(FileLogTest), "Exception:")) + formatter.FormatError(nameof(LogTest), "Exception:")) .Returns(formattedExceptionMsg); mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(FileLogTest), eMsg)) + formatter.FormatError(nameof(LogTest), eMsg)) .Returns(formattedException); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; log.Print(e); mockFormatter.Assert(formatter => - formatter.FormatError(nameof(FileLogTest), "Exception:"), + formatter.FormatError(nameof(LogTest), "Exception:"), Invoked.Once); mockFormatter.Assert(formatter => - formatter.FormatError(nameof(FileLogTest), eMsg), + formatter.FormatError(nameof(LogTest), eMsg), Invoked.Once); mockWriter.Assert(writer => @@ -139,29 +139,29 @@ public void PrintsExceptionWithMessage() { var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(FileLogTest), contextMsg)) + formatter.FormatError(nameof(LogTest), contextMsg)) .Returns(formattedContextMsg); mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(FileLogTest), exceptionMsg)) + formatter.FormatError(nameof(LogTest), exceptionMsg)) .Returns(formattedExceptionMsg); mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(FileLogTest), eStr)) + formatter.FormatError(nameof(LogTest), eStr)) .Returns(formattedException); - var mockWriter = new Mock(); - var log = (ILog)new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = (ILog)new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; log.Print(e, contextMsg); mockFormatter.Assert(formatter => - formatter.FormatError(nameof(FileLogTest), contextMsg), + formatter.FormatError(nameof(LogTest), contextMsg), Invoked.Once); mockFormatter.Assert(formatter => - formatter.FormatError(nameof(FileLogTest), exceptionMsg), + formatter.FormatError(nameof(LogTest), exceptionMsg), Invoked.Once); mockFormatter.Assert(formatter => - formatter.FormatError(nameof(FileLogTest), eStr), + formatter.FormatError(nameof(LogTest), eStr), Invoked.Once); mockWriter.Assert(writer => @@ -182,18 +182,18 @@ public void PrintsStackTrace() { var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg)) + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg)) .Returns(formattedStackTraceMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; var st = new FakeStackTrace("File.cs", "ClassName", "MethodName"); log.Print(st); mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg), + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg), Invoked.Once); mockWriter.Assert(writer => @@ -209,18 +209,18 @@ public void PrintsStackTraceWithoutFile() { var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg)) + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg)) .Returns(formattedStackTraceMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; var st = new FakeStackTrace(null, "ClassName", "MethodName"); log.Print(st); mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg), + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg), Invoked.Once); mockWriter.Assert(writer => @@ -236,18 +236,18 @@ public void PrintsStackTraceWithoutClass() { var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg)) + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg)) .Returns(formattedStackTraceMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; var st = new FakeStackTrace("File.cs", null, "MethodName"); log.Print(st); mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg), + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg), Invoked.Once); mockWriter.Assert(writer => @@ -264,18 +264,18 @@ public void PrintsStackTraceWithoutMethod() { var mockFormatter = new Mock(); mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg)) + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg)) .Returns(formattedStackTraceMsg); - var mockWriter = new Mock(); - var log = new FileLog(nameof(FileLogTest), mockWriter.Object) { + var mockWriter = new Mock(); + var log = new Log(nameof(LogTest), [mockWriter.Object]) { Formatter = mockFormatter.Object }; var st = new FakeStackTrace("File.cs", "ClassName", null); log.Print(st); mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(FileLogTest), expectedStackTraceMsg), + formatter.FormatMessage(nameof(LogTest), expectedStackTraceMsg), Invoked.Once); mockWriter.Assert(writer => diff --git a/Chickensoft.Log.Tests/test/src/MultiLogTest.cs b/Chickensoft.Log.Tests/test/src/MultiLogTest.cs deleted file mode 100644 index 716b4f6..0000000 --- a/Chickensoft.Log.Tests/test/src/MultiLogTest.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace Chickensoft.Log.Tests; - -using System.Collections.Generic; -using System.Linq; -using LightMock; -using LightMock.Generator; -using Shouldly; - -[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", - "IDE0090:new expression can be simplified", - Justification = "Need an explicit 'new Mock' for LightMock")] -public class MultiLogTest { - private readonly string _testMsg = "A test message"; - - [Fact] - public void Initializes() { - var log = new MultiLog(); - log.ShouldBeAssignableTo(); - } - - [Fact] - public void PrintsMessage() { - var mockLogs = new List> { new Mock(), new Mock() }; - var logs = (from ml in mockLogs select ml.Object).ToList(); - var log = new MultiLog(logs); - log.Print(_testMsg); - foreach (var ml in mockLogs) { - ml.Assert(log => log.Print(_testMsg), Invoked.Once); - } - } - - [Fact] - public void PrintsError() { - var mockLogs = new List> { new Mock(), new Mock() }; - var logs = (from ml in mockLogs select ml.Object).ToList(); - var log = new MultiLog(logs); - log.Err(_testMsg); - foreach (var ml in mockLogs) { - ml.Assert(log => log.Err(_testMsg), Invoked.Once); - } - } - - [Fact] - public void PrintsWarning() { - var mockLogs = new List> { new Mock(), new Mock() }; - var logs = (from ml in mockLogs select ml.Object).ToList(); - var log = new MultiLog(logs); - log.Warn(_testMsg); - foreach (var ml in mockLogs) { - ml.Assert(log => log.Warn(_testMsg), Invoked.Once); - } - } - - [Fact] - public void PrintsException() { - var mockLogs = new List> { new Mock(), new Mock() }; - var logs = (from ml in mockLogs select ml.Object).ToList(); - var log = new MultiLog(logs); - var e = new TestException(_testMsg); - log.Print(e); - foreach (var ml in mockLogs) { - ml.Assert(log => log.Print(e), Invoked.Once); - } - } - - [Fact] - public void PrintsStackTrace() { - var mockLogs = new List> { new Mock(), new Mock() }; - var logs = (from ml in mockLogs select ml.Object).ToList(); - var log = new MultiLog(logs); - var st = new FakeStackTrace("File.cs", "ClassName", "MethodName"); - log.Print(st); - foreach (var ml in mockLogs) { - ml.Assert(log => log.Print(st), Invoked.Once); - } - } -} diff --git a/Chickensoft.Log.Tests/test/src/TestLogTest.cs b/Chickensoft.Log.Tests/test/src/TestLogTest.cs deleted file mode 100644 index a7a4441..0000000 --- a/Chickensoft.Log.Tests/test/src/TestLogTest.cs +++ /dev/null @@ -1,133 +0,0 @@ -namespace Chickensoft.Log.Tests; - -using LightMock; -using LightMock.Generator; -using Shouldly; - -public class TestLogTest { - private const string TEST_MSG = "A test message"; - - private static string Format(string msg) { - return $"MockLevel (Test): {msg}"; - } - - [Fact] - public void Initializes() { - var log = new TestLog(); - log.ShouldBeAssignableTo(); - } - - [Fact] - public void PrintsMessage() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - var log = new TestLog() { - Formatter = mockFormatter.Object - }; - mockFormatter.Arrange(formatter => - formatter.FormatMessage(log.Name, TEST_MSG)) - .Returns(formattedTestMsg); - log.Print(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(log.Name, TEST_MSG), - Invoked.Once); - - log.LoggedMessages.Count.ShouldBe(1); - log.LoggedMessages[0].ShouldBe(formattedTestMsg); - } - - [Fact] - public void PrintsError() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - var log = new TestLog() { - Formatter = mockFormatter.Object - }; - mockFormatter.Arrange(formatter => - formatter.FormatError(log.Name, TEST_MSG)) - .Returns(formattedTestMsg); - log.Err(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatError(log.Name, TEST_MSG), - Invoked.Once); - - log.LoggedMessages.Count.ShouldBe(1); - log.LoggedMessages[0].ShouldBe(formattedTestMsg); - } - - [Fact] - public void PrintsWarning() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - var log = new TestLog() { - Formatter = mockFormatter.Object - }; - mockFormatter.Arrange(formatter => - formatter.FormatWarning(log.Name, TEST_MSG)) - .Returns(formattedTestMsg); - log.Warn(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatWarning(log.Name, TEST_MSG), - Invoked.Once); - - log.LoggedMessages.Count.ShouldBe(1); - log.LoggedMessages[0].ShouldBe(formattedTestMsg); - } - - [Fact] - public void PrintsException() { - var e = new TestException(TEST_MSG); - var eMsg = e.ToString(); - var formattedExceptionMsg = Format("Exception:"); - var formattedException = Format(eMsg); - - var mockFormatter = new Mock(); - var log = new TestLog() { - Formatter = mockFormatter.Object - }; - mockFormatter.Arrange(formatter => - formatter.FormatError(log.Name, "Exception:")) - .Returns(formattedExceptionMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(log.Name, eMsg)) - .Returns(formattedException); - log.Print(e); - - mockFormatter.Assert(formatter => - formatter.FormatError(log.Name, "Exception:"), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(log.Name, eMsg), - Invoked.Once); - - log.LoggedMessages.Count.ShouldBe(2); - log.LoggedMessages[0].ShouldBe(formattedExceptionMsg); - log.LoggedMessages[1].ShouldBe(formattedException); - } - - [Fact] - public void PrintsStackTrace() { - var expectedStackTraceMsg = "ClassName.MethodName in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - var log = new TestLog() { - Formatter = mockFormatter.Object - }; - mockFormatter.Arrange(formatter => - formatter.FormatMessage(log.Name, expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - var st = new FakeStackTrace("File.cs", "ClassName", "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(log.Name, expectedStackTraceMsg), - Invoked.Once); - - log.LoggedMessages.Count.ShouldBe(1); - log.LoggedMessages[0].ShouldBe(formattedStackTraceMsg); - } -} diff --git a/Chickensoft.Log.Tests/test/src/TestWriterTest.cs b/Chickensoft.Log.Tests/test/src/TestWriterTest.cs new file mode 100644 index 0000000..4ddcc9a --- /dev/null +++ b/Chickensoft.Log.Tests/test/src/TestWriterTest.cs @@ -0,0 +1,57 @@ +namespace Chickensoft.Log.Tests; + +using System.Collections.Generic; +using Shouldly; + +public class TestWriterTest { + private const string TEST_MSG1 = "A test message 1"; + private const string TEST_MSG2 = "A test message 2"; + + [Fact] + public void WriteMessageStoresMessage() { + var writer = new TestWriter(); + writer.WriteMessage(TEST_MSG1); + writer.WriteMessage(TEST_MSG2); + writer.LoggedMessages + .ShouldBeEquivalentTo(new List { TEST_MSG1, TEST_MSG2 }); + writer.LoggedWarnings.ShouldBeEmpty(); + writer.LoggedErrors.ShouldBeEmpty(); + } + + [Fact] + public void WriteWarningStoresWarning() { + var writer = new TestWriter(); + writer.WriteWarning(TEST_MSG1); + writer.WriteWarning(TEST_MSG2); + writer.LoggedMessages.ShouldBeEmpty(); + writer.LoggedWarnings + .ShouldBeEquivalentTo(new List { TEST_MSG1, TEST_MSG2 }); + writer.LoggedErrors.ShouldBeEmpty(); + } + + [Fact] + public void WriteErrorStoresMessage() { + var writer = new TestWriter(); + writer.WriteError(TEST_MSG1); + writer.WriteError(TEST_MSG2); + writer.LoggedMessages.ShouldBeEmpty(); + writer.LoggedWarnings.ShouldBeEmpty(); + writer.LoggedErrors + .ShouldBeEquivalentTo(new List { TEST_MSG1, TEST_MSG2 }); + } + + [Fact] + public void ResetClearsAllStored() { + var writer = new TestWriter(); + writer.WriteMessage(TEST_MSG1); + writer.WriteMessage(TEST_MSG2); + writer.WriteWarning(TEST_MSG1); + writer.WriteWarning(TEST_MSG2); + writer.WriteError(TEST_MSG1); + writer.WriteError(TEST_MSG2); + writer.Reset(); + writer.LoggedMessages.ShouldBeEmpty(); + writer.LoggedWarnings.ShouldBeEmpty(); + writer.LoggedErrors.ShouldBeEmpty(); + } +} diff --git a/Chickensoft.Log.Tests/test/src/TraceLogTest.cs b/Chickensoft.Log.Tests/test/src/TraceLogTest.cs deleted file mode 100644 index 9ee627e..0000000 --- a/Chickensoft.Log.Tests/test/src/TraceLogTest.cs +++ /dev/null @@ -1,285 +0,0 @@ -namespace Chickensoft.Log.Tests; - -using LightMock; -using LightMock.Generator; -using Shouldly; - -public class TraceLogTest { - private const string TEST_MSG = "A test message"; - - private static string Format(string msg) { - return $"MockLevel ({nameof(FileLogTest)}): {msg}"; - } - - [Fact] - public void Initializes() { - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object); - log.ShouldBeAssignableTo(); - } - - [Fact] - public void PrintsMessage() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(TraceLogTest), TEST_MSG)) - .Returns(formattedTestMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Print(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(TraceLogTest), TEST_MSG), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedTestMsg), - Invoked.Once); - } - - [Fact] - public void PrintsError() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(TraceLogTest), TEST_MSG)) - .Returns(formattedTestMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Err(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(TraceLogTest), TEST_MSG), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteError(formattedTestMsg), - Invoked.Once); - } - - [Fact] - public void PrintsWarning() { - var formattedTestMsg = Format(TEST_MSG); - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatWarning(nameof(TraceLogTest), TEST_MSG)) - .Returns(formattedTestMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Warn(TEST_MSG); - - mockFormatter.Assert(formatter => - formatter.FormatWarning(nameof(TraceLogTest), TEST_MSG), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteWarning(formattedTestMsg), - Invoked.Once); - } - - [Fact] - public void PrintsException() { - var e = new TestException(TEST_MSG); - var eMsg = e.ToString(); - var formattedExceptionMsg = Format("Exception:"); - var formattedException = Format(eMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(TraceLogTest), "Exception:")) - .Returns(formattedExceptionMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(TraceLogTest), eMsg)) - .Returns(formattedException); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Print(e); - - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(TraceLogTest), "Exception:"), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(TraceLogTest), eMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteError(formattedExceptionMsg), - Invoked.Once); - mockWriter.Assert(writer => - writer.WriteError(formattedException), - Invoked.Once); - } - - [Fact] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", - "CA1859:Change type of variable for performance", - Justification = "Need ILog type to test interface method")] - public void PrintsExceptionWithMessage() { - var contextMsg = "Context message"; - var exceptionMsg = "Exception:"; - var e = new TestException(TEST_MSG); - var eStr = e.ToString(); - var formattedContextMsg = Format(contextMsg); - var formattedExceptionMsg = Format(exceptionMsg); - var formattedException = Format(eStr); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(TraceLogTest), contextMsg)) - .Returns(formattedContextMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(TraceLogTest), exceptionMsg)) - .Returns(formattedExceptionMsg); - mockFormatter.Arrange(formatter => - formatter.FormatError(nameof(TraceLogTest), eStr)) - .Returns(formattedException); - - var mockWriter = new Mock(); - var log = (ILog)new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - log.Print(e, contextMsg); - - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(TraceLogTest), contextMsg), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(TraceLogTest), exceptionMsg), - Invoked.Once); - mockFormatter.Assert(formatter => - formatter.FormatError(nameof(TraceLogTest), eStr), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteError(formattedContextMsg), - Invoked.Once); - mockWriter.Assert(writer => - writer.WriteError(formattedExceptionMsg), - Invoked.Once); - mockWriter.Assert(writer => - writer.WriteError(formattedException), - Invoked.Once); - } - - [Fact] - public void PrintsStackTrace() { - var expectedStackTraceMsg = "ClassName.MethodName in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace("File.cs", "ClassName", "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } - - [Fact] - public void PrintsStackTraceWithoutFile() { - var expectedStackTraceMsg = "ClassName.MethodName in **(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace(null, "ClassName", "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } - - [Fact] - public void PrintsStackTraceWithoutClass() { - var expectedStackTraceMsg = "UnknownClass.MethodName in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace("File.cs", null, "MethodName"); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } - - [Fact] - public void PrintsStackTraceWithoutMethod() { - // unknown method is also unknown class - var expectedStackTraceMsg = "UnknownClass.UnknownMethod in File.cs(1,2)"; - var formattedStackTraceMsg = Format(expectedStackTraceMsg); - - var mockFormatter = new Mock(); - mockFormatter.Arrange(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg)) - .Returns(formattedStackTraceMsg); - - var mockWriter = new Mock(); - var log = new TraceLog(nameof(TraceLogTest), mockWriter.Object) { - Formatter = mockFormatter.Object - }; - var st = new FakeStackTrace("File.cs", "ClassName", null); - log.Print(st); - - mockFormatter.Assert(formatter => - formatter.FormatMessage(nameof(TraceLogTest), expectedStackTraceMsg), - Invoked.Once); - - mockWriter.Assert(writer => - writer.WriteMessage(formattedStackTraceMsg), - Invoked.Once - ); - } -} diff --git a/Chickensoft.Log/src/ConsoleWriter.cs b/Chickensoft.Log/src/ConsoleWriter.cs new file mode 100644 index 0000000..cc7e15c --- /dev/null +++ b/Chickensoft.Log/src/ConsoleWriter.cs @@ -0,0 +1,43 @@ +namespace Chickensoft.Log; + +using System; + +/// +/// An that directs output of an +/// to the console. +/// +public sealed class ConsoleWriter : ILogWriter { + private static readonly ConsoleWriter _instance = new(); + + /// + /// Returns a reference to the singleton instance of ConsoleWriter. + /// + public static ConsoleWriter Instance() { + return _instance; + } + + private readonly object _writingLock = new(); + + private ConsoleWriter() { } + + /// + public void WriteError(string message) { + lock (_writingLock) { + Console.Error.WriteLine(message); + } + } + + /// + public void WriteMessage(string message) { + lock (_writingLock) { + Console.WriteLine(message); + } + } + + /// + public void WriteWarning(string message) { + lock (_writingLock) { + Console.WriteLine(message); + } + } +} diff --git a/Chickensoft.Log/src/FileLog.cs b/Chickensoft.Log/src/FileLog.cs deleted file mode 100644 index d3a83d8..0000000 --- a/Chickensoft.Log/src/FileLog.cs +++ /dev/null @@ -1,212 +0,0 @@ -namespace Chickensoft.Log; - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; - -/// -/// An that writes to a file. -/// -public sealed class FileLog : ILog { - /// - /// An implementing output for . - /// - public interface IWriter : ILogWriter; - - /// - /// An that directs output of a - /// to a particular file. - /// - public sealed class Writer : IWriter { - // Implemented as a pseudo-singleton to enforce one truncation per file per - // execution - private static readonly Dictionary _instances = []; - - /// - /// The default file name that will be used when creating a - /// if no filename is specified. - /// - /// - /// This default may be changed. If it is changed after a default - /// has already been created, any future calls to - /// will return a different - /// outputting to the new default, but previously-created instances will not - /// be changed and will continue outputting to the original default file. - /// - public static string DefaultFileName { get; set; } = "output.log"; - - /// - /// Obtains a that directs output to the given filename. - /// - /// - /// The filename to which output should be directed when using the returned - /// . - /// - /// - /// A outputting to a file at - /// . - /// - /// - /// If a outputting to - /// already exists, a reference to the same will be - /// returned. If not, a new will be created. When a new - /// is created, if the file at - /// already exists, it will erased; if not, it - /// will be created. - /// - public static Writer Instance(string fileName) { - if (_instances.TryGetValue(fileName, out var writer)) { - return writer; - } - writer = new Writer(fileName); - _instances[fileName] = writer; - return writer; - } - - /// - /// Obtains a that directs output to the current - /// . - /// - /// - /// A outputting to a file at - /// . - /// - /// - /// - public static Writer Instance() { - return Instance(DefaultFileName); - } - - /// - /// The path of the filename this Writer is writing to. - /// - public string FileName { get; } - - private Writer(string fileName) { - FileName = fileName; - // Clear the file - using var sw = new StreamWriter(FileName); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", - "IDE0063:Use simple 'using' statement", - Justification = "Prefer block, to explicitly delineate scope")] - private void WriteLine(string message) { - using (var sw = File.AppendText(FileName)) { - sw.WriteLine(message); - } - } - - /// - public void WriteMessage(string message) { - WriteLine(message); - } - - /// - public void WriteWarning(string message) { - WriteLine(message); - } - - /// - public void WriteError(string message) { - WriteLine(message); - } - } - - private readonly IWriter _writer; - - /// - public string Name { get; } - - /// - /// The formatter that will be used to format messages before writing them - /// to the file. Defaults to an instance of . - /// - public ILogFormatter Formatter { get; set; } = new LogFormatter(); - - /// - /// Create a using the given name and default - /// file name (). - /// - /// - /// The name associated with this log. Will be included in messages directed - /// through this log (see ). - /// A common value is nameof(EncapsulatingClass). - /// - public FileLog(string name) { - _writer = Writer.Instance(); - Name = name; - } - - /// - /// Create a using the given name, - /// outputting to the given file name. - /// - /// - /// The name associated with this log. Will be included in messages directed - /// through this log (see ). - /// A common value is nameof(EncapsulatingClass). - /// - /// - /// The path for the file where logs should be written. - /// - public FileLog(string name, string fileName) { - _writer = Writer.Instance(fileName); - Name = name; - } - - /// - /// Create a using the given name and - /// to output. Useful for testing. - /// - /// - /// The name associated with this log. Will be included in messages directed - /// through this log (see ). - /// A common value is nameof(EncapsulatingClass). - /// - /// - /// The writer to use for outputting log messages. - /// - public FileLog(string name, IWriter writer) { - _writer = writer; - Name = name; - } - - /// - public void Err(string message) { - _writer.WriteError(Formatter.FormatError(Name, message)); - } - - /// - public void Print(string message) { - _writer.WriteMessage(Formatter.FormatMessage(Name, message)); - } - - /// - public void Print(StackTrace stackTrace) { - foreach (var frame in stackTrace.GetFrames()) { - var fileName = frame.GetFileName() ?? "**"; - var lineNumber = frame.GetFileLineNumber(); - var colNumber = frame.GetFileColumnNumber(); - var method = frame.GetMethod(); - var className = method?.DeclaringType?.Name ?? "UnknownClass"; - var methodName = method?.Name ?? "UnknownMethod"; - Print( - $"{className}.{methodName} in " + - $"{fileName}({lineNumber},{colNumber})" - ); - } - } - - /// - public void Print(Exception e) { - Err("Exception:"); - Err(e.ToString()); - } - - /// - public void Warn(string message) { - _writer.WriteWarning(Formatter.FormatWarning(Name, message)); - } -} diff --git a/Chickensoft.Log/src/FileWriter.cs b/Chickensoft.Log/src/FileWriter.cs new file mode 100644 index 0000000..4d49ce3 --- /dev/null +++ b/Chickensoft.Log/src/FileWriter.cs @@ -0,0 +1,127 @@ +namespace Chickensoft.Log; + +using System.Collections.Generic; +using System.IO; + +/// +/// An that directs output of an +/// to a file. +/// +public sealed class FileWriter : ILogWriter { + // 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 file name that will be used when creating a + /// if no filename is specified. + /// + /// + /// This default may be changed. If it is changed after a default + /// has already been created, any future calls to + /// will return a different + /// outputting to the new default, but previously-created instances will not + /// be changed and will continue outputting to the original default file. + /// + public static string DefaultFileName { + get { + lock (_singletonLock) { + return _defaultFileName; + } + } + set { + lock (_singletonLock) { + _defaultFileName = value; + } + } + } + + /// + /// Obtains a FileWriter that directs output to the given filename. + /// + /// + /// The filename to which output should be directed when using the returned + /// writer. + /// + /// + /// A writer outputting to a file at . + /// + /// + /// If a outputting to + /// already exists, a reference to the same writer will be + /// returned. If not, a new writer will be created. When a + /// new is created, if the file at + /// already exists, it will erased; if not, it + /// will be created. + /// + public static FileWriter Instance(string fileName) { + lock (_singletonLock) { + if (_instances.TryGetValue(fileName, out var writer)) { + return writer; + } + writer = new FileWriter(fileName); + _instances[fileName] = writer; + return writer; + } + } + + /// + /// Obtains a that directs output to the current + /// . + /// + /// + /// A outputting to a file at + /// . + /// + /// + /// + public static FileWriter Instance() { + lock (_singletonLock) { + return Instance(DefaultFileName); + } + } + + private readonly object _writingLock = new(); + + /// + /// The path of the filename this Writer is writing to. + /// + public string FileName { get; } + + private FileWriter(string fileName) { + FileName = fileName; + lock (_writingLock) { + // Clear the file + using var sw = new StreamWriter(FileName); + } + } + + [System.Diagnostics.CodeAnalysis.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); + } + } + } + + /// + public void WriteMessage(string message) { + WriteLine(message); + } + + /// + public void WriteWarning(string message) { + WriteLine(message); + } + + /// + public void WriteError(string message) { + WriteLine(message); + } +} diff --git a/Chickensoft.Log/src/ILog.cs b/Chickensoft.Log/src/ILog.cs index 7b64bf9..05c6435 100644 --- a/Chickensoft.Log/src/ILog.cs +++ b/Chickensoft.Log/src/ILog.cs @@ -1,5 +1,7 @@ namespace Chickensoft.Log; + using System; +using System.Collections.Generic; using System.Diagnostics; /// @@ -14,6 +16,11 @@ public interface ILog { /// public string Name { get; } + /// + /// The writers that will receive messages for this log. + /// + public IList Writers { get; } + /// /// Prints the specified message to the log. /// diff --git a/Chickensoft.Log/src/ConsoleLog.cs b/Chickensoft.Log/src/Log.cs similarity index 56% rename from Chickensoft.Log/src/ConsoleLog.cs rename to Chickensoft.Log/src/Log.cs index 1c91d0a..7789f92 100644 --- a/Chickensoft.Log/src/ConsoleLog.cs +++ b/Chickensoft.Log/src/Log.cs @@ -1,43 +1,19 @@ namespace Chickensoft.Log; using System; +using System.Collections.Generic; using System.Diagnostics; /// -/// An that writes to standard output and error. +/// The standard implementation of . /// -public class ConsoleLog : ILog { - /// - /// An implementing output for . - /// - public interface IWriter : ILogWriter; - - /// - /// An that directs output of a - /// to the console. - /// - public class Writer : IWriter { - /// - public void WriteError(string message) { - Console.Error.WriteLine(message); - } - - /// - public void WriteMessage(string message) { - Console.WriteLine(message); - } - - /// - public void WriteWarning(string message) { - Console.WriteLine(message); - } - } - - private readonly IWriter _writer; - +public sealed class Log : ILog { /// public string Name { get; } + /// + public IList Writers { get; } = []; + /// /// The formatter that will be used to format messages before writing them /// to the console. Defaults to an instance of . @@ -45,43 +21,37 @@ public void WriteWarning(string message) { public ILogFormatter Formatter { get; set; } = new LogFormatter(); /// - /// Create a logger using the given name and standard out/err. + /// Initialize an empty Log (i.e., with no writers). /// /// /// The name associated with this log. Will be included in messages directed /// through this log (see ). /// A common value is nameof(EncapsulatingClass). /// - public ConsoleLog(string name) { + public Log(string name) { Name = name; - _writer = new Writer(); } /// - /// Create a logger using the given name and the provided - /// for output. Useful for testing. + /// Initialize a Log that will use the provided writers. /// /// /// The name associated with this log. Will be included in messages directed /// through this log (see ). /// A common value is nameof(EncapsulatingClass). /// - /// - /// The writer to use for outputting log messages. - /// - public ConsoleLog(string name, IWriter writer) { + /// Writers this log will use to write messages. + public Log(string name, IList writers) { Name = name; - _writer = writer; - } - - /// - public void Err(string message) { - _writer.WriteError(Formatter.FormatError(Name, message)); + Writers = [.. writers]; } /// public void Print(string message) { - _writer.WriteMessage(Formatter.FormatMessage(Name, message)); + var formatted = Formatter.FormatMessage(Name, message); + foreach (var writer in Writers) { + writer.WriteMessage(formatted); + } } /// @@ -108,6 +78,17 @@ public void Print(Exception e) { /// public void Warn(string message) { - _writer.WriteWarning(Formatter.FormatWarning(Name, message)); + var formatted = Formatter.FormatWarning(Name, message); + foreach (var writer in Writers) { + writer.WriteWarning(formatted); + } + } + + /// + public void Err(string message) { + var formatted = Formatter.FormatError(Name, message); + foreach (var writer in Writers) { + writer.WriteError(formatted); + } } } diff --git a/Chickensoft.Log/src/MultiLog.cs b/Chickensoft.Log/src/MultiLog.cs deleted file mode 100644 index 7f74c36..0000000 --- a/Chickensoft.Log/src/MultiLog.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Chickensoft.Log; - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -/// -/// An that writes to multiple other ILogs. Provides a -/// convenient way to log to multiple outputs (e.g., to -/// and to a file). -/// -public sealed class MultiLog : ILog { - /// - /// The name associated with this log. Is always empty; output will use the - /// names associated with respective logs in . - /// - public string Name { get; } = string.Empty; - - /// - /// The logs to which this log will write all messages. - /// - public IList Logs { get; } = []; - - /// - /// Initialize an empty MultiLog (i.e., with no logs to write to). - /// - /// - public MultiLog() { - } - - /// - /// Initialize a MultiLog that will write to the provided logs. - /// - /// Logs to which this MultiLog will write. - public MultiLog(IList logs) { - Logs = [.. logs]; - } - - /// - public void Print(string message) { - foreach (var log in Logs) { - log.Print(message); - } - } - - /// - public void Print(StackTrace stackTrace) { - foreach (var log in Logs) { - log.Print(stackTrace); - } - } - - /// - public void Print(Exception e) { - foreach (var log in Logs) { - log.Print(e); - } - } - - /// - public void Warn(string message) { - foreach (var log in Logs) { - log.Warn(message); - } - } - - /// - public void Err(string message) { - foreach (var log in Logs) { - log.Err(message); - } - } -} diff --git a/Chickensoft.Log/src/TestLog.cs b/Chickensoft.Log/src/TestLog.cs deleted file mode 100644 index 7d72948..0000000 --- a/Chickensoft.Log/src/TestLog.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Chickensoft.Log; - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -/// -/// An that accumulates messages written to it so users can -/// test their logging code without mocking ILog. -/// -public class TestLog : ILog { - /// - /// The name associated with this log. Always has the value "Test". - /// - public string Name { get; } = "Test"; - - /// - /// The formatter that will be used to format messages before writing them - /// to the console. Defaults to an instance of . - /// - public ILogFormatter Formatter { get; set; } = new LogFormatter(); - - /// - /// Contains all logged messages as separate elements. - /// - public IList LoggedMessages { get; set; } = []; - - /// - public void Err(string message) { - LoggedMessages.Add(Formatter.FormatError(Name, message)); - } - - /// - public void Print(string message) { - LoggedMessages.Add(Formatter.FormatMessage(Name, message)); - } - - /// - public void Print(StackTrace stackTrace) { - foreach (var frame in stackTrace.GetFrames()) { - var fileName = frame.GetFileName() ?? "**"; - var lineNumber = frame.GetFileLineNumber(); - var colNumber = frame.GetFileColumnNumber(); - var method = frame.GetMethod(); - var className = method?.DeclaringType?.Name ?? "UnknownClass"; - var methodName = method?.Name ?? "UnknownMethod"; - Print( - $"{className}.{methodName} in " + - $"{fileName}({lineNumber},{colNumber})" - ); - } - } - - /// - public void Print(Exception e) { - Err("Exception:"); - Err(e.ToString()); - } - - /// - public void Warn(string message) { - LoggedMessages.Add(Formatter.FormatWarning(Name, message)); - } -} diff --git a/Chickensoft.Log/src/TestWriter.cs b/Chickensoft.Log/src/TestWriter.cs new file mode 100644 index 0000000..d5c3a3e --- /dev/null +++ b/Chickensoft.Log/src/TestWriter.cs @@ -0,0 +1,47 @@ +namespace Chickensoft.Log; + +using System.Collections.Generic; + +/// +/// An that stores logged messages and does not write +/// them to any destination. Useful for testing code that uses +/// . +/// +public sealed class TestWriter : ILogWriter { + /// + /// Contains all regular logged messages as separate elements. + /// + public IList LoggedMessages { get; set; } = []; + /// + /// Contains all logged warning messages as separate elements. + /// + public IList LoggedWarnings { get; set; } = []; + /// + /// Contains all logged error messages as separate elements. + /// + public IList LoggedErrors { get; set; } = []; + + /// + /// Clears all stored messages, of every level. + /// + public void Reset() { + LoggedMessages.Clear(); + LoggedWarnings.Clear(); + LoggedErrors.Clear(); + } + + /// + public void WriteError(string message) { + LoggedErrors.Add(message); + } + + /// + public void WriteMessage(string message) { + LoggedMessages.Add(message); + } + + /// + public void WriteWarning(string message) { + LoggedWarnings.Add(message); + } +} diff --git a/Chickensoft.Log/src/TraceLog.cs b/Chickensoft.Log/src/TraceLog.cs deleted file mode 100644 index 2e34c88..0000000 --- a/Chickensoft.Log/src/TraceLog.cs +++ /dev/null @@ -1,120 +0,0 @@ -namespace Chickensoft.Log; -using System; -using System.Diagnostics; - -/// -/// A log that uses to output log messages. Useful for -/// observing output in Visual Studio's Output window while debugging. -/// -/// -/// To enable output in the VS Output window, -/// add a to the list of Trace listeners -/// before any logging (i.e., near your entry point): -/// -/// Trace.Listeners.Add(new DefaultTraceListener()); -/// -/// -public sealed class TraceLog : ILog { - /// - /// An implementing output for . - /// - public interface IWriter : ILogWriter; - - /// - /// An that directs output of a - /// to . - /// - public sealed class Writer : IWriter { - /// - public void WriteMessage(string message) { - Trace.WriteLine(message); - } - - /// - public void WriteError(string message) { - Trace.TraceError(message); - } - - /// - public void WriteWarning(string message) { - Trace.TraceWarning(message); - } - } - - private readonly IWriter _writer; - - /// - public string Name { get; } - - /// - /// The formatter that will be used to format messages before writing them - /// to . Defaults to an instance of . - /// - public ILogFormatter Formatter { get; set; } = new LogFormatter(); - - /// - /// Create a trace log with the given name. - /// - /// - /// The name associated with this log. Will be included in messages directed - /// through this log (see ). - /// A common value is nameof(EncapsulatingClass). - /// - public TraceLog(string name) { - Name = name; - _writer = new Writer(); - } - - /// - /// Create a trace log with the given name and writer. Useful for testing. - /// - /// - /// The name associated with this log. Will be included in messages directed - /// through this log (see ). - /// A common value is nameof(EncapsulatingClass). - /// - /// - /// The writer to use for outputting log messages. - /// - public TraceLog(string name, IWriter writer) { - Name = name; - _writer = writer; - } - - /// - public void Err(string message) { - _writer.WriteError(Formatter.FormatError(Name, message)); - } - - /// - public void Print(string message) { - _writer.WriteMessage(Formatter.FormatMessage(Name, message)); - } - - /// - public void Print(StackTrace stackTrace) { - foreach (var frame in stackTrace.GetFrames()) { - var fileName = frame.GetFileName() ?? "**"; - var lineNumber = frame.GetFileLineNumber(); - var colNumber = frame.GetFileColumnNumber(); - var method = frame.GetMethod(); - var className = method?.DeclaringType?.Name ?? "UnknownClass"; - var methodName = method?.Name ?? "UnknownMethod"; - Print( - $"{className}.{methodName} in " + - $"{fileName}({lineNumber},{colNumber})" - ); - } - } - - /// - public void Print(Exception e) { - Err("Exception:"); - Err(e.ToString()); - } - - /// - public void Warn(string message) { - _writer.WriteWarning(Formatter.FormatWarning(Name, message)); - } -} diff --git a/Chickensoft.Log/src/TraceWriter.cs b/Chickensoft.Log/src/TraceWriter.cs new file mode 100644 index 0000000..01f975e --- /dev/null +++ b/Chickensoft.Log/src/TraceWriter.cs @@ -0,0 +1,41 @@ +namespace Chickensoft.Log; + +using System.Diagnostics; + +/// +/// An that directs output of an +/// to . +/// +public sealed class TraceWriter : ILogWriter { + private static readonly TraceWriter _instance = new(); + + /// + /// Returns a reference to the singleton instance of TraceWriter. + /// + public static TraceWriter Instance() { + return _instance; + } + + private readonly object _writingLock = new(); + + /// + public void WriteMessage(string message) { + lock (_writingLock) { + Trace.WriteLine(message); + } + } + + /// + public void WriteError(string message) { + lock (_writingLock) { + Trace.TraceError(message); + } + } + + /// + public void WriteWarning(string message) { + lock (_writingLock) { + Trace.TraceWarning(message); + } + } +} diff --git a/README.md b/README.md index cf3b8dd..d3d3486 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # Chickensoft.Log -[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] [![Read the docs][read-the-docs-badge]][docs] ![line coverage][line-coverage] ![branch coverage][branch-coverage] +[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] +[![Discord][discord-badge]][discord] +[![Read the docs][read-the-docs-badge]][docs] +![line coverage][line-coverage] +![branch coverage][branch-coverage] -Opinionated, simple logging interface and implementations for C# applications and libraries. Forms the basis for [Log.Godot][log-godot]. +Opinionated logging interface and implementations for C# applications +and libraries. --- @@ -21,64 +26,101 @@ Install the latest version of the [Chickensoft.Log] package from nuget: ``` -## 📜 Usage +## 🪵 Usage -### Setup +### Setting up a Log + +In Chickensoft.Log, messages are logged through the `ILog` interface. Each `ILog` +has a name (often the name of the class using the `ILog`) and a list of +`ILogWriter`s that are responsible for directing log messages to their respective +outputs. The package includes a standard implementation of `ILog`, named `Log`. +(More on writers below.) + +Example of creating an `ILog`: ```csharp public class MyClass { - // Create a log that outputs messages to stdout/stderr, prefixed with the name of the class. - private ILog _log = new ConsoleLog(nameof(MyClass)); + // Create a log, with the name of MyClass, that outputs messages to stdout/stderr + private ILog _log = new Log(nameof(MyClass), [ConsoleWriter.Instance()]); } ``` ### Logging +To log messages, you use `ILog`'s methods `Print()`, `Warn()`, and `Err()`. + +Example: + ```csharp -public void MyMethod() +public class MyClass { - _log.Print("A log message"); // Outputs "Info (MyClass): A log message" - _log.Warn("A warning message"); // Outputs "Warn (MyClass): A warning message" - _log.Err("An error occurred"); // Outputs "Error (MyClass): An error occurred" - try - { - SomethingThatThrows(); - } - catch (Exception e) + public void MyMethod() { - _log.Print(e); // Outputs the value of e.ToString(), prefixed by a line labeling it as an exception - // ... + // Outputs "Info (MyClass): A log message" + _log.Print("A log message"); + // Outputs "Warn (MyClass): A warning message" + _log.Warn("A warning message"); + // Outputs "Error (MyClass): An error occurred" + _log.Err("An error occurred"); + + try + { + SomethingThatThrows(); + } + catch (Exception e) + { + // Outputs the value of e.ToString(), prefixed by a line labeling it an exception, + // as an error + _log.Print(e); + } + + // Outputs the current stack trace as a standard log message + _log.Print("An event occurred at:"); + _log.Print(new System.Diagnostics.StackTrace()); } } ``` +> [!TIP] +> Some writers may have separate channels for warnings and errors, while others +> may not. For instance, the `TraceWriter` has separate channels for regular log +> messages, warnings, and errors. The `FileWriter` has only one channel, to the +> file it's writing to. + ### Formatting -Optionally, when constructing a log, you can provide an `ILogFormatter` that the log will use to format the components of each log message (the log's name, the level of the message, and the message itself). +Optionally, when constructing a log, you can provide an `ILogFormatter` that the +log will use to format the components of each log message. (Those components are +the log's name, the level of the message, and the message itself.) ```csharp public class MyClass { - private ILog _log = new ConsoleLog(nameof(MyClass)) + private ILog _log = new Log(nameof(MyClass), [ConsoleWriter()]) { Formatter = new MyFormatter() }; } ``` -By default, logs included with the package will use a standard `LogFormatter` class implementing `ILogFormatter`. +By default, logs included with the package will use a standard `LogFormatter` +class implementing `ILogFormatter`. -Messages are formatted with one of three level labels, depending which log method you call. By default, the included `LogFormatter` uses the labels `"Info"`, `"Warn"`, and `"Error"`. You can change these labels for an individual `LogFormatter`: +Messages are formatted with one of three level labels, depending which log method +you call. By default, the included `LogFormatter` uses the labels `"Info"`, +`"Warn"`, and `"Error"`. You can change these labels for an individual `LogFormatter`: ```csharp -var formatter = new LogFormatter(); -formatter.MessagePrefix = "INFO"; -formatter.WarningPrefix = "WARN"; -formatter.ErrorPrefix = "ERROR"; +var formatter = new LogFormatter() +{ + MessagePrefix = "INFO"; + WarningPrefix = "WARN"; + ErrorPrefix = "ERROR"; +}; ``` -You can also change the default values for these labels: +You can also change the default values of these labels for all `LogFormatter`s: ```csharp LogFormatter.DefaultMessagePrefix = "INFO"; @@ -87,71 +129,95 @@ LogFormatter.DefaultErrorPrefix = "ERROR"; ``` > [!WARNING] -> Changing the default values for the level labels will affect newly-created `LogFormatter`s, but will not affect ones that already exist. +> Changing the default values of the level labels will affect newly-created +> `LogFormatter`s, but will not affect ones that already exist. + +## ✒️ Writer Types -## 🪵 Log Types +`Log` accepts a list of writers to which the log will direct formatted messages. +The writers are responsible for handling the output of the messages. The Log +package provides three operational writer types implementing the `ILogWriter` +interface: -The Log package provides four operational log types implementing the `ILog` interface: +* `ConsoleWriter`: Outputs log messages to stdout and stderr. +* `TraceWriter`: Outputs log messages to .NET's `Trace` system. This is useful +for seeing log output in Visual Studio's "Output" tab while debugging. +* `FileWriter`: Outputs log messages to file. By default, `FileWriter`s will +write to a file called "output.log" in the working directory, but you can either +configure a different default, or configure individual `FileWriter`s to write to +particular files on creation. -* `ConsoleLog`: Outputs log messages to stdout/stderr. -* `TraceLog`: Outputs log messages to .NET's `Trace` system. This is useful for seeing log output in Visual Studio's "Output" tab while debugging. -* `FileLog`: Outputs log messages to file. All `FileLog`s will write to a file called "output.log" in the working directory by default, but you can either configure a different default, or configure individual `FileLog`s to write to particular files on creation. -* `MultiLog`: Delegates log messages to multiple other logs, allowing you to log the same message to, e.g., stdout/stderr and to file with one method call. +To provide thread safety such that only one message may be written at a time, +`ConsoleWriter` and `TraceWriter` are implemented as singletons that may be +obtained using the static method `Instance()`. `FileWriter` is implemented as a +pseudo-singleton with a single instance per file name; see below for details. -The package provides one additional, non-operational log type, `TestLog`, which may be useful for testing your code without mocking `ILog`. +The package provides one additional writer type, `TestWriter`, which may be +useful for testing your code without mocking `ILog` (see below). -### Using `FileLog` +### Using `FileWriter` -Create a log that outputs messages to the default file name `"output.log"`: +`FileWriter` provides a static `Instance()` method that returns one unique +writer per file name. + +You can obtain a reference to a writer that outputs messages to the default file +name `"output.log"`: ```csharp public class MyClass { - private ILog _log = new FileLog(nameof(MyClass)); + private ILog _log = new Log(nameof(MyClass), [FileWriter.Instance()]); } ``` --- -Create a log that outputs messages to a custom file name: +You can create a writer that outputs messages to a custom file name: ```csharp public class MyClass { - private ILog _log = new FileLog(nameof(MyClass), "CustomFileName.log"); + private ILog _log = new Log(nameof(MyClass), [FileWriter.Instance("CustomFileName.log")]; } ``` --- -Change the default file name for `FileLog`s: +You can change the default file name for `FileWriter`s: ```csharp public class Entry { public static void Main() { - // Change the default file name for FileLog before any logs are created - FileLog.Writer.DefaultFileName = "MyFileName.log"; + // Change the default file name for FileWriter before any writers are created + FileWriter.DefaultFileName = "MyFileName.log"; + // ... } } public class MyClass { - private ILog _log = new FileLog(nameof(MyClass)); + // Create a FileWriter that writes to the new default name + private ILog _log = new Log(nameof(MyClass), [FileWriter.Instance()]); } ``` > [!WARNING] -> Changing the default value for the log file name will affect newly-created `FileLog`s, but will not affect ones that already exist. +> Changing the default value for the log file name will affect newly-created +> `FileLog`s, but will not affect ones that already exist. -### Using `TestLog` +### Using `TestWriter` -When testing code that uses an `ILog`, it may be cumbersome to mock `ILog`'s methods. In that case, you may prefer to use the provided `TestLog` type, which accumulates log messages for testing: +When testing code that uses an `ILog`, it may be cumbersome to mock `ILog`'s +methods. In that case, you may prefer to use the provided `TestWriter` type, +which accumulates log messages for testing: ```csharp +// Class under test public class MyClass { - public ILog Log { get; set; } = new ConsoleLog(); + public ILog Log { get; set; } = new Log(nameof(MyClass), [new ConsoleWriter()]); + // Method that logs some information; we want to test the logged messages public void MyMethod() { Log.Print("A normal log message"); @@ -164,19 +230,21 @@ public class MyClassTest [Fact] public void MyMethodLogs() { - var obj = new MyClass() { Log = new TestLog() }; + // set up an instance of MyClass, but with a TestWriter instead of a ConsoleWriter + var testWriter = new TestWriter(); + var obj = new MyClass() { Log = new Log(nameof(MyClass), [testWriter]) }; obj.MyMethod(); - obj.Log.LoggedMessages.Count.ShouldBe(2); - obj.Log.LoggedMessages[0].ShouldBe("Info (Test): A normal log message"); - obj.Log.LoggedMessages[1].ShouldBe("Error (Test): An error message"); + // use TestWriter to test the logging behavior of MyClass + testWriter.LoggedMessages + .ShouldBeEquivalentTo(new List + { + "Info (MyClass): A normal log message", + "Error (MyClass): An error message" + }); } } ``` -## ✋ Intentional Limitations - -The Log package does not provide thread safety. If you are using the Log package in a multithreaded environment, please be sure to employ thread-safe access to your log objects (especially if using multiple `FileLog`s). - ## 💁 Getting Help *Having issues?* We'll be happy to help you in the [Chickensoft Discord server][discord].