From 5b0116bcfe30deeede975cb6289a701c636d3044 Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Thu, 15 Aug 2024 16:51:56 +0200 Subject: [PATCH 1/4] #168 wrap encoding in ColoredConsoleAppender for skipping preamble --- .../log4net-611-lib/DerivedAppender.cs | 13 +- .../log4net-611-main/Program.cs | 26 +- .../log4net-672/Program.cs | 64 +- .../log4net-673/Program.cs | 63 +- .../log4net-681/Program.cs | 37 +- .../Util/EncodingWithoutPreambleTest.cs | 57 ++ .../Appender/ColoredConsoleAppender.cs | 809 +++++++++--------- src/log4net/Util/EncodingWithoutPreamble.cs | 121 +++ 8 files changed, 674 insertions(+), 516 deletions(-) create mode 100644 src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs create mode 100644 src/log4net/Util/EncodingWithoutPreamble.cs diff --git a/src/integration-testing/log4net-611-lib/DerivedAppender.cs b/src/integration-testing/log4net-611-lib/DerivedAppender.cs index 2b1faebd..20fa3f16 100644 --- a/src/integration-testing/log4net-611-lib/DerivedAppender.cs +++ b/src/integration-testing/log4net-611-lib/DerivedAppender.cs @@ -1,14 +1,13 @@ using log4net.Appender; using log4net.Core; -using log4net.Layout; namespace log4net_611_lib; -public class DerivedAppender: RollingFileAppender +public class DerivedAppender : RollingFileAppender { - protected override void Append(LoggingEvent loggingEvent) - { - loggingEvent.Properties["appender-class-name"] = nameof(DerivedAppender); - base.Append(loggingEvent); - } + protected override void Append(LoggingEvent loggingEvent) + { + loggingEvent.Properties["appender-class-name"] = nameof(DerivedAppender); + base.Append(loggingEvent); + } } \ No newline at end of file diff --git a/src/integration-testing/log4net-611-main/Program.cs b/src/integration-testing/log4net-611-main/Program.cs index 36f09762..bc316376 100644 --- a/src/integration-testing/log4net-611-main/Program.cs +++ b/src/integration-testing/log4net-611-main/Program.cs @@ -1,7 +1,6 @@ using System.Reflection; using log4net; using log4net.Config; -using log4net_611_lib; var appPath = new Uri(Assembly.GetExecutingAssembly().Location).LocalPath; var appFolder = Path.GetDirectoryName(appPath); @@ -9,7 +8,7 @@ Assembly.LoadFile("log4net-611-lib.dll"); if (appFolder is null) { - throw new InvalidOperationException("Can't find myself"); + throw new InvalidOperationException("Can't find myself"); } Assembly.LoadFile(Path.Combine(appFolder, "log4net-611-lib.dll")); @@ -17,36 +16,33 @@ var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } if (Directory.Exists("Logs")) { - Console.WriteLine("Clearing out old logs..."); - foreach (var file in Directory.EnumerateFiles("Logs")) - { - File.Delete(file); - } + Console.WriteLine("Clearing out old logs..."); + foreach (var file in Directory.EnumerateFiles("Logs")) + { + File.Delete(file); + } } var info = new FileInfo(configFile); var logRepo = LogManager.GetRepository(Assembly.GetExecutingAssembly()); -XmlConfigurator.ConfigureAndWatch( - logRepo, - info -); +XmlConfigurator.ConfigureAndWatch(logRepo, info); var logger = LogManager.GetLogger(typeof(Program)); Console.WriteLine("logging..."); for (var i = 0; i < 10; i++) { - logger.Info($"test log {i}"); + logger.Info($"test log {i}"); } foreach (var file in Directory.EnumerateFiles("Logs")) { - Console.WriteLine($"log file: {file}"); - Console.WriteLine(File.ReadAllText(file)); + Console.WriteLine($"log file: {file}"); + Console.WriteLine(File.ReadAllText(file)); } \ No newline at end of file diff --git a/src/integration-testing/log4net-672/Program.cs b/src/integration-testing/log4net-672/Program.cs index 79f47611..720c3774 100644 --- a/src/integration-testing/log4net-672/Program.cs +++ b/src/integration-testing/log4net-672/Program.cs @@ -14,43 +14,41 @@ var appFolder = Path.GetDirectoryName(appPath); if (appFolder is null) { - throw new InvalidOperationException( - $"Can't determine app folder for {appPath}" - ); + throw new InvalidOperationException($"Can't determine app folder for {appPath}"); } var logFolder = Path.Combine(appFolder, "Logs"); if (Directory.Exists(logFolder)) { - Directory.Delete(logFolder, recursive: true); + Directory.Delete(logFolder, recursive: true); } var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } var logCount = 10; var identifiers = new List(); for (var i = 0; i < 10; i++) { - var identifier = Guid.NewGuid(); - identifiers.Add(identifier); - var logged = LogWith(identifier, logCount); - if (logged != logCount) - { - Die($"Missing logs immediately for '{identifier}' - found {logged}/{logCount}", MISSING_LOGS); - } + var identifier = Guid.NewGuid(); + identifiers.Add(identifier); + var logged = LogWith(identifier, logCount); + if (logged != logCount) + { + Die($"Missing logs immediately for '{identifier}' - found {logged}/{logCount}", MISSING_LOGS); + } } foreach (var identifier in identifiers) { - var logged = CountIdentifierInLogs(identifier); - if (logged != logCount) - { - Die($"Logs have been overwritten for '{identifier}' - found {logged}/{logCount}", OVERWRITTEN_LOGS); - } + var logged = CountIdentifierInLogs(identifier); + if (logged != logCount) + { + Die($"Logs have been overwritten for '{identifier}' - found {logged}/{logCount}", OVERWRITTEN_LOGS); + } } Console.WriteLine("All good: LOG4NET-672 is resolved"); @@ -58,34 +56,34 @@ void Die(string message, int exitCode) { - Console.Error.WriteLine(message); - Environment.Exit(exitCode); + Console.Error.WriteLine(message); + Environment.Exit(exitCode); } int CountIdentifierInLogs(Guid id) { - return Directory.EnumerateFiles("Logs").Select( - filePath => CountIdentifierInFile(id, filePath) - ).Sum(); + return Directory.EnumerateFiles("Logs").Select( + filePath => CountIdentifierInFile(id, filePath) + ).Sum(); } int CountIdentifierInFile(Guid id, string filePath) { - var contents = File.ReadAllLines(filePath); - return contents.Count(line => line.Contains(id.ToString())); + var contents = File.ReadAllLines(filePath); + return contents.Count(line => line.Contains(id.ToString())); } int LogWith(Guid identifier, int howManyLogs) { - var info = new FileInfo(configFile); - XmlConfigurator.Configure(info); - var logger = LogManager.GetLogger("main"); + var info = new FileInfo(configFile); + XmlConfigurator.Configure(info); + var logger = LogManager.GetLogger("main"); - for (var i = 0; i < howManyLogs; i++) - { - logger.Info($"test log {i} [{identifier}]"); - } + for (var i = 0; i < howManyLogs; i++) + { + logger.Info($"test log {i} [{identifier}]"); + } - LogManager.Flush(int.MaxValue); - return CountIdentifierInLogs(identifier); + LogManager.Flush(int.MaxValue); + return CountIdentifierInLogs(identifier); } \ No newline at end of file diff --git a/src/integration-testing/log4net-673/Program.cs b/src/integration-testing/log4net-673/Program.cs index fb910898..a3d50414 100644 --- a/src/integration-testing/log4net-673/Program.cs +++ b/src/integration-testing/log4net-673/Program.cs @@ -10,68 +10,65 @@ // force loading the assembly, otherwise the appender type isn't found later if (appFolder is null) { - throw new InvalidOperationException("Can't find myself"); + throw new InvalidOperationException("Can't find myself"); } var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } if (Directory.Exists("log")) { - Console.WriteLine("Clearing out old logs..."); - foreach (var file in Directory.EnumerateFiles("log")) - { - File.Delete(file); - } + Console.WriteLine("Clearing out old logs..."); + foreach (var file in Directory.EnumerateFiles("log")) + { + File.Delete(file); + } } var info = new FileInfo(configFile); var logRepo = LogManager.GetRepository(Assembly.GetExecutingAssembly()); -XmlConfigurator.ConfigureAndWatch( - logRepo, - info -); +XmlConfigurator.ConfigureAndWatch(logRepo, info); var logger = LogManager.GetLogger(typeof(Program)); Console.WriteLine("logging..."); for (var i = 0; i < 10; i++) { - logger.Info($"test log {i}"); - logger.Error($"error log {i}"); - logger.Warn($"warning log {i}"); + logger.Info($"test log {i}"); + logger.Error($"error log {i}"); + logger.Warn($"warning log {i}"); } foreach (var file in Directory.EnumerateFiles("log")) { - Console.WriteLine($"log file: {file}"); - TryDumpFile(file); + Console.WriteLine($"log file: {file}"); + TryDumpFile(file); } -void TryDumpFile(string at) +static void TryDumpFile(string at) { - if (!File.Exists(at)) + if (!File.Exists(at)) + { + Console.WriteLine($"File not found: {at}"); + return; + } + + for (var i = 0; i < 10; i++) + { + try { - Console.WriteLine($"File not found: {at}"); - return; + Console.WriteLine(File.ReadAllText(at)); + return; } - - for (var i = 0; i < 10; i++) + catch { - try - { - Console.WriteLine(File.ReadAllText(at)); - return; - } - catch - { - Thread.Sleep(100); - } + Thread.Sleep(100); } + } - Console.WriteLine($"Unable to read file at {at}"); -} + Console.WriteLine($"Unable to read file at {at}"); +} \ No newline at end of file diff --git a/src/integration-testing/log4net-681/Program.cs b/src/integration-testing/log4net-681/Program.cs index 20d6bc12..fac4e47a 100644 --- a/src/integration-testing/log4net-681/Program.cs +++ b/src/integration-testing/log4net-681/Program.cs @@ -7,30 +7,27 @@ if (appFolder is null) { - throw new InvalidOperationException("Can't find myself"); + throw new InvalidOperationException("Can't find myself"); } var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } if (Directory.Exists("Logs")) { - Console.WriteLine("Clearing out old logs..."); - foreach (var file in Directory.EnumerateFiles("Logs")) - { - File.Delete(file); - } + Console.WriteLine("Clearing out old logs..."); + foreach (var file in Directory.EnumerateFiles("Logs")) + { + File.Delete(file); + } } var info = new FileInfo(configFile); var logRepo = LogManager.GetRepository(Assembly.GetExecutingAssembly()); -XmlConfigurator.ConfigureAndWatch( - logRepo, - info -); +XmlConfigurator.ConfigureAndWatch(logRepo, info); var logger = LogManager.GetLogger(typeof(Program)); @@ -38,25 +35,25 @@ var threads = new List(); for (var i = 0; i < 128; i++) { - var thread = new Thread(LogABit); - thread.Start(); - threads.Add(thread); + var thread = new Thread(LogABit); + thread.Start(); + threads.Add(thread); } foreach (var t in threads) { - t.Join(); + t.Join(); } foreach (var file in Directory.EnumerateFiles("Logs")) { - Console.WriteLine($"found log file: {file}"); + Console.WriteLine($"found log file: {file}"); } void LogABit() { - for (var i = 0; i < 100; i++) - { - logger.Info($"test log {i}"); - } + for (var i = 0; i < 100; i++) + { + logger.Info($"test log {i}"); + } } \ No newline at end of file diff --git a/src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs b/src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs new file mode 100644 index 00000000..adcd0e4e --- /dev/null +++ b/src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs @@ -0,0 +1,57 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using log4net.Util; +using NUnit.Framework; + +namespace log4net.Tests.Util; + +/// +/// Tests for +/// +[TestFixture] +public sealed class EncodingWithoutPreambleTest +{ + /// + /// Tests the wrapping functionality + /// + [Test] + public void WrappedTest() + { + Encoding wrapped = Encoding.UTF8; + Type encodingType = typeof(LogLog).Assembly.GetType("log4net.Util.EncodingWithoutPreamble", true)!; + Encoding target = (Encoding)encodingType + .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) + .First() + .Invoke(new[] { wrapped }); + Assert.IsTrue(target.Equals(wrapped)); + const string text = "Hallöchen!"; + byte[] bytes = wrapped.GetBytes(text); + Assert.AreEqual(bytes, target.GetBytes(text)); + Assert.AreEqual(wrapped.GetString(bytes), target.GetString(bytes)); + CollectionAssert.AreEqual(new byte[] { 0xEF, 0xBB, 0xBF }, wrapped.GetPreamble()); + CollectionAssert.AreEqual(Array.Empty(), target.GetPreamble()); + } +} \ No newline at end of file diff --git a/src/log4net/Appender/ColoredConsoleAppender.cs b/src/log4net/Appender/ColoredConsoleAppender.cs index c5c43915..e4c208c3 100644 --- a/src/log4net/Appender/ColoredConsoleAppender.cs +++ b/src/log4net/Appender/ColoredConsoleAppender.cs @@ -20,471 +20,464 @@ using System; using System.IO; using System.Runtime.InteropServices; - +using System.Text; using log4net.Core; -using log4net.Layout; using log4net.Util; -namespace log4net.Appender +namespace log4net.Appender; + +/// +/// Appends logging events to the console. +/// +/// +/// +/// ColoredConsoleAppender appends log events to the standard output stream +/// or the error output stream using a layout specified by the +/// user. It also allows the color of a specific type of message to be set. +/// +/// +/// By default, all output is written to the console's standard output stream. +/// The property can be set to direct the output to the +/// error stream. +/// +/// +/// NOTE: This appender writes directly to the application's attached console +/// not to the System.Console.Out or System.Console.Error TextWriter. +/// The System.Console.Out and System.Console.Error streams can be +/// programmatically redirected (for example NUnit does this to capture program output). +/// This appender will ignore these redirections because it needs to use Win32 +/// API calls to colorize the output. To respect these redirections the +/// must be used. +/// +/// +/// When configuring the colored console appender, mapping should be +/// specified to map a logging level to a color. For example: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// The Level is the standard log4net logging level and ForeColor and BackColor can be any +/// combination of the following values: +/// +/// Blue +/// Green +/// Red +/// White +/// Yellow +/// Purple +/// Cyan +/// HighIntensity +/// +/// +/// +/// Rick Hobbs +/// Nicko Cadell +public class ColoredConsoleAppender : AppenderSkeleton { /// - /// Appends logging events to the console. + /// The enum of possible color values for use with the color mapping method /// /// /// - /// ColoredConsoleAppender appends log events to the standard output stream - /// or the error output stream using a layout specified by the - /// user. It also allows the color of a specific type of message to be set. - /// - /// - /// By default, all output is written to the console's standard output stream. - /// The property can be set to direct the output to the - /// error stream. - /// - /// - /// NOTE: This appender writes directly to the application's attached console - /// not to the System.Console.Out or System.Console.Error TextWriter. - /// The System.Console.Out and System.Console.Error streams can be - /// programmatically redirected (for example NUnit does this to capture program output). - /// This appender will ignore these redirections because it needs to use Win32 - /// API calls to colorize the output. To respect these redirections the - /// must be used. - /// - /// - /// When configuring the colored console appender, mapping should be - /// specified to map a logging level to a color. For example: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// The Level is the standard log4net logging level and ForeColor and BackColor can be any - /// combination of the following values: - /// - /// Blue - /// Green - /// Red - /// White - /// Yellow - /// Purple - /// Cyan - /// HighIntensity - /// + /// The following flags can be combined to form the colors. /// /// - /// Rick Hobbs - /// Nicko Cadell - public class ColoredConsoleAppender : AppenderSkeleton + /// + [Flags] + public enum Colors { /// - /// The enum of possible color values for use with the color mapping method + /// color is blue /// - /// - /// - /// The following flags can be combined to form the colors. - /// - /// - /// - [Flags] - public enum Colors - { - /// - /// color is blue - /// - Blue = 0x0001, - - /// - /// color is green - /// - Green = 0x0002, - - /// - /// color is red - /// - Red = 0x0004, - - /// - /// color is white - /// - White = Blue | Green | Red, - - /// - /// color is yellow - /// - Yellow = Red | Green, - - /// - /// color is purple - /// - Purple = Red | Blue, - - /// - /// color is cyan - /// - Cyan = Green | Blue, - - /// - /// color is intensified - /// - HighIntensity = 0x0008, - } + Blue = 0x0001, /// - /// Initializes a new instance of the class. + /// color is green /// - /// - /// The instance of the class is set up to write - /// to the standard output stream. - /// - public ColoredConsoleAppender() - { - } + Green = 0x0002, /// - /// Target is the value of the console output stream. - /// This is either "Console.Out" or "Console.Error". + /// color is red /// - /// - /// Target is the value of the console output stream. - /// This is either "Console.Out" or "Console.Error". - /// - /// - /// - /// Target is the value of the console output stream. - /// This is either "Console.Out" or "Console.Error". - /// - /// - public virtual string Target - { - get => m_writeToErrorStream ? ConsoleError : ConsoleOut; - set => m_writeToErrorStream = StringComparer.OrdinalIgnoreCase.Equals(ConsoleError, value.Trim()); - } + Red = 0x0004, /// - /// Add a mapping of level to color - done by the config file + /// color is white /// - /// The mapping to add - /// - /// - /// Add a mapping to this appender. - /// Each mapping defines the foreground and background colors - /// for a level. - /// - /// - public void AddMapping(LevelColors mapping) - { - m_levelMapping.Add(mapping); - } + White = Blue | Green | Red, /// - /// This method is called by the method. + /// color is yellow /// - /// The event to log. - /// - /// - /// Writes the event to the console. - /// - /// - /// The format of the output will depend on the appender's layout. - /// - /// - [System.Security.SecuritySafeCritical] - [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] - protected override void Append(LoggingEvent loggingEvent) - { - if (m_consoleOutputWriter is not null) - { - IntPtr consoleHandle = GetStdHandle(m_writeToErrorStream ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE); - - // Default to white on black - ushort colorInfo = (ushort)Colors.White; - - // see if there is a specified lookup - if (m_levelMapping.Lookup(loggingEvent.Level) is LevelColors levelColors) - { - colorInfo = levelColors.CombinedColor; - } - - // Render the event to a string - string strLoggingMessage = RenderLoggingEvent(loggingEvent); - - // get the current console color - to restore later - GetConsoleScreenBufferInfo(consoleHandle, out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); - - // set the console colors - SetConsoleTextAttribute(consoleHandle, colorInfo); - - // Using WriteConsoleW seems to be unreliable. - // If a large buffer is written, say 15,000 chars - // Followed by a larger buffer, say 20,000 chars - // then WriteConsoleW will fail, last error 8 - // 'Not enough storage is available to process this command.' - // - // Although the documentation states that the buffer must - // be less that 64KB (i.e. 32,000 WCHARs) the longest string - // that I can write out a the first call to WriteConsoleW - // is only 30,704 chars. - // - // Unlike the WriteFile API the WriteConsoleW method does not - // seem to be able to partially write out from the input buffer. - // It does have a lpNumberOfCharsWritten parameter, but this is - // either the length of the input buffer if any output was written, - // or 0 when an error occurs. - // - // All results above were observed on Windows XP SP1 running - // .NET runtime 1.1 SP1. - // - // Old call to WriteConsoleW: - // - // WriteConsoleW( - // consoleHandle, - // strLoggingMessage, - // (UInt32)strLoggingMessage.Length, - // out (UInt32)ignoreWrittenCount, - // IntPtr.Zero); - // - // Instead of calling WriteConsoleW we use WriteFile which - // handles large buffers correctly. Because WriteFile does not - // handle the codepage conversion as WriteConsoleW does we - // need to use a System.IO.StreamWriter with the appropriate - // Encoding. The WriteFile calls are wrapped up in the - // System.IO.__ConsoleStream internal class obtained through - // the System.Console.OpenStandardOutput method. - // - // See the ActivateOptions method below for the code that - // retrieves and wraps the stream. - - - // The windows console uses ScrollConsoleScreenBuffer internally to - // scroll the console buffer when the display buffer of the console - // has been used up. ScrollConsoleScreenBuffer fills the area uncovered - // by moving the current content with the background color - // currently specified on the console. This means that it fills the - // whole line in front of the cursor position with the current - // background color. - // This causes an issue when writing out text with a non default - // background color. For example; We write a message with a Blue - // background color and the scrollable area of the console is full. - // When we write the newline at the end of the message the console - // needs to scroll the buffer to make space available for the new line. - // The ScrollConsoleScreenBuffer internals will fill the newly created - // space with the current background color: Blue. - // We then change the console color back to default (White text on a - // Black background). We write some text to the console, the text is - // written correctly in White with a Black background, however the - // remainder of the line still has a Blue background. - // - // This causes a disjointed appearance to the output where the background - // colors change. - // - // This can be remedied by restoring the console colors before causing - // the buffer to scroll, i.e. before writing the last newline. This does - // assume that the rendered message will end with a newline. - // - // Therefore we identify a trailing newline in the message and don't - // write this to the output, then we restore the console color and write - // a newline. Note that we must AutoFlush before we restore the console - // color otherwise we will have no effect. - // - // There will still be a slight artefact for the last line of the message - // will have the background extended to the end of the line, however this - // is unlikely to cause any user issues. - // - // Note that none of the above is visible while the console buffer is scrollable - // within the console window viewport, the effects only arise when the actual - // buffer is full and needs to be scrolled. - - char[] messageCharArray = strLoggingMessage.ToCharArray(); - int arrayLength = messageCharArray.Length; - bool appendNewline = false; - - // Trim off last newline, if it exists - if (arrayLength > 1 && messageCharArray[arrayLength - 2] == '\r' && messageCharArray[arrayLength - 1] == '\n') - { - arrayLength -= 2; - appendNewline = true; - } - - // Write to the output stream - m_consoleOutputWriter.Write(messageCharArray, 0, arrayLength); - - // Restore the console back to its previous color scheme - SetConsoleTextAttribute(consoleHandle, bufferInfo.wAttributes); - - if (appendNewline) - { - // Write the newline, after changing the color scheme - m_consoleOutputWriter.Write(s_windowsNewline, 0, 2); - } - } - } + Yellow = Red | Green, - private static readonly char[] s_windowsNewline = { '\r', '\n' }; + /// + /// color is purple + /// + Purple = Red | Blue, /// - /// This appender requires a to be set. + /// color is cyan /// - protected override bool RequiresLayout => true; + Cyan = Green | Blue, /// - /// Initializes the options for this appender. + /// color is intensified /// - [System.Security.SecuritySafeCritical] - [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] - public override void ActivateOptions() + HighIntensity = 0x0008, + } + + private static readonly char[] windowsNewline = { '\r', '\n' }; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The instance of the class is set up to write + /// to the standard output stream. + /// + public ColoredConsoleAppender() + { } + + /// + /// Target is the value of the console output stream. + /// This is either "Console.Out" or "Console.Error". + /// + /// + /// Target is the value of the console output stream. + /// This is either "Console.Out" or "Console.Error". + /// + /// + /// + /// Target is the value of the console output stream. + /// This is either "Console.Out" or "Console.Error". + /// + /// + public virtual string Target + { + get => writeToErrorStream ? ConsoleError : ConsoleOut; + set => writeToErrorStream = StringComparer.OrdinalIgnoreCase.Equals(ConsoleError, value.Trim()); + } + + /// + /// Add a mapping of level to color - done by the config file + /// + /// The mapping to add + /// + /// + /// Add a mapping to this appender. + /// Each mapping defines the foreground and background colors + /// for a level. + /// + /// + public void AddMapping(LevelColors mapping) => levelMapping.Add(mapping); + + /// + /// This method is called by the method. + /// + /// The event to log. + /// + /// + /// Writes the event to the console. + /// + /// + /// The format of the output will depend on the appender's layout. + /// + /// + [System.Security.SecuritySafeCritical] + [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] + protected override void Append(LoggingEvent loggingEvent) + { + if (consoleOutputWriter is not null) { - base.ActivateOptions(); - m_levelMapping.ActivateOptions(); + IntPtr consoleHandle = GetStdHandle(writeToErrorStream ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE); + + // Default to white on black + ushort colorInfo = (ushort)Colors.White; - // Use the Console methods to open a Stream over the console std handle - Stream consoleOutputStream = m_writeToErrorStream ? Console.OpenStandardError() : Console.OpenStandardOutput(); + // see if there is a specified lookup + if (levelMapping.Lookup(loggingEvent.Level) is LevelColors levelColors) + { + colorInfo = levelColors.CombinedColor; + } + + // Render the event to a string + string strLoggingMessage = RenderLoggingEvent(loggingEvent); + + // get the current console color - to restore later + GetConsoleScreenBufferInfo(consoleHandle, out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); + + // set the console colors + SetConsoleTextAttribute(consoleHandle, colorInfo); + + // Using WriteConsoleW seems to be unreliable. + // If a large buffer is written, say 15,000 chars + // Followed by a larger buffer, say 20,000 chars + // then WriteConsoleW will fail, last error 8 + // 'Not enough storage is available to process this command.' + // + // Although the documentation states that the buffer must + // be less that 64KB (i.e. 32,000 WCHARs) the longest string + // that I can write out a the first call to WriteConsoleW + // is only 30,704 chars. + // + // Unlike the WriteFile API the WriteConsoleW method does not + // seem to be able to partially write out from the input buffer. + // It does have a lpNumberOfCharsWritten parameter, but this is + // either the length of the input buffer if any output was written, + // or 0 when an error occurs. + // + // All results above were observed on Windows XP SP1 running + // .NET runtime 1.1 SP1. + // + // Old call to WriteConsoleW: + // + // WriteConsoleW( + // consoleHandle, + // strLoggingMessage, + // (UInt32)strLoggingMessage.Length, + // out (UInt32)ignoreWrittenCount, + // IntPtr.Zero); + // + // Instead of calling WriteConsoleW we use WriteFile which + // handles large buffers correctly. Because WriteFile does not + // handle the codepage conversion as WriteConsoleW does we + // need to use a System.IO.StreamWriter with the appropriate + // Encoding. The WriteFile calls are wrapped up in the + // System.IO.__ConsoleStream internal class obtained through + // the System.Console.OpenStandardOutput method. + // + // See the ActivateOptions method below for the code that + // retrieves and wraps the stream. + + + // The windows console uses ScrollConsoleScreenBuffer internally to + // scroll the console buffer when the display buffer of the console + // has been used up. ScrollConsoleScreenBuffer fills the area uncovered + // by moving the current content with the background color + // currently specified on the console. This means that it fills the + // whole line in front of the cursor position with the current + // background color. + // This causes an issue when writing out text with a non default + // background color. For example; We write a message with a Blue + // background color and the scrollable area of the console is full. + // When we write the newline at the end of the message the console + // needs to scroll the buffer to make space available for the new line. + // The ScrollConsoleScreenBuffer internals will fill the newly created + // space with the current background color: Blue. + // We then change the console color back to default (White text on a + // Black background). We write some text to the console, the text is + // written correctly in White with a Black background, however the + // remainder of the line still has a Blue background. + // + // This causes a disjointed appearance to the output where the background + // colors change. + // + // This can be remedied by restoring the console colors before causing + // the buffer to scroll, i.e. before writing the last newline. This does + // assume that the rendered message will end with a newline. + // + // Therefore we identify a trailing newline in the message and don't + // write this to the output, then we restore the console color and write + // a newline. Note that we must AutoFlush before we restore the console + // color otherwise we will have no effect. + // + // There will still be a slight artefact for the last line of the message + // will have the background extended to the end of the line, however this + // is unlikely to cause any user issues. + // + // Note that none of the above is visible while the console buffer is scrollable + // within the console window viewport, the effects only arise when the actual + // buffer is full and needs to be scrolled. + + char[] messageCharArray = strLoggingMessage.ToCharArray(); + int arrayLength = messageCharArray.Length; + bool appendNewline = false; + + // Trim off last newline, if it exists + if (arrayLength > 1 && messageCharArray[arrayLength - 2] == '\r' && messageCharArray[arrayLength - 1] == '\n') + { + arrayLength -= 2; + appendNewline = true; + } - // Look up the codepage encoding for the console - System.Text.Encoding consoleEncoding = System.Text.Encoding.GetEncoding(GetConsoleOutputCP()); + // Write to the output stream + consoleOutputWriter.Write(messageCharArray, 0, arrayLength); - // Create a writer around the console stream - m_consoleOutputWriter = new StreamWriter(consoleOutputStream, consoleEncoding, 0x100) + // Restore the console back to its previous color scheme + SetConsoleTextAttribute(consoleHandle, bufferInfo.wAttributes); + + if (appendNewline) { - AutoFlush = true - }; - - // SuppressFinalize on m_consoleOutputWriter because all it will do is flush - // and close the file handle. Because we have set AutoFlush the additional flush - // is not required. The console file handle should not be closed, so we don't call - // Dispose, Close or the finalizer. - GC.SuppressFinalize(m_consoleOutputWriter); + // Write the newline, after changing the color scheme + consoleOutputWriter.Write(windowsNewline, 0, 2); + } } + } - /// - /// The to use when writing to the Console - /// standard output stream. - /// - public const string ConsoleOut = "Console.Out"; + /// + /// This appender requires a to be set. + /// + protected override bool RequiresLayout => true; - /// - /// The to use when writing to the Console - /// standard error output stream. - /// - public const string ConsoleError = "Console.Error"; + /// + /// Initializes the options for this appender. + /// + [System.Security.SecuritySafeCritical] + [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] + public override void ActivateOptions() + { + base.ActivateOptions(); + levelMapping.ActivateOptions(); + + // Use the Console methods to open a Stream over the console std handle + Stream consoleOutputStream = writeToErrorStream ? Console.OpenStandardError() : Console.OpenStandardOutput(); + + // Look up the codepage encoding for the console + Encoding consoleEncoding = EncodingWithoutPreamble.Get(Encoding.GetEncoding(GetConsoleOutputCP())); + + // Create a writer around the console stream + consoleOutputWriter = new StreamWriter(consoleOutputStream, consoleEncoding, 0x100) + { + AutoFlush = true + }; + + // SuppressFinalize on m_consoleOutputWriter because all it will do is flush + // and close the file handle. Because we have set AutoFlush the additional flush + // is not required. The console file handle should not be closed, so we don't call + // Dispose, Close or the finalizer. + GC.SuppressFinalize(consoleOutputWriter); + } + + /// + /// The to use when writing to the Console + /// standard output stream. + /// + public const string ConsoleOut = "Console.Out"; + + /// + /// The to use when writing to the Console + /// standard error output stream. + /// + public const string ConsoleError = "Console.Error"; + + /// + /// Flag to write output to the error stream rather than the standard output stream + /// + private bool writeToErrorStream; + /// + /// Mapping from level object to color value + /// + private readonly LevelMapping levelMapping = new(); + + /// + /// The console output stream writer to write to + /// + /// + /// + /// This writer is not thread safe. + /// + /// + private StreamWriter? consoleOutputWriter; + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern int GetConsoleOutputCP(); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern bool SetConsoleTextAttribute( + IntPtr consoleHandle, + ushort attributes); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern bool GetConsoleScreenBufferInfo( + IntPtr consoleHandle, + out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); + + private const uint STD_OUTPUT_HANDLE = unchecked((uint)-11); + private const uint STD_ERROR_HANDLE = unchecked((uint)-12); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern IntPtr GetStdHandle(uint type); + + [StructLayout(LayoutKind.Sequential)] + private struct COORD + { + public ushort x; + public ushort y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SMALL_RECT + { + public ushort Left; + public ushort Top; + public ushort Right; + public ushort Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct CONSOLE_SCREEN_BUFFER_INFO + { + public COORD dwSize; + public COORD dwCursorPosition; + public ushort wAttributes; + public SMALL_RECT srWindow; + public COORD dwMaximumWindowSize; + } + + /// + /// A class to act as a mapping between the level that a logging call is made at and + /// the color it should be displayed as. + /// + /// + /// + /// Defines the mapping between a level and the color it should be displayed in. + /// + /// + public class LevelColors : LevelMappingEntry + { /// - /// Flag to write output to the error stream rather than the standard output stream + /// The mapped foreground color for the specified level /// - private bool m_writeToErrorStream; + public Colors ForeColor { get; set; } /// - /// Mapping from level object to color value + /// The mapped background color for the specified level /// - private readonly LevelMapping m_levelMapping = new(); + public Colors BackColor { get; set; } /// - /// The console output stream writer to write to + /// Initialize the options for the object /// /// /// - /// This writer is not thread safe. + /// Combine the and together. /// /// - private StreamWriter? m_consoleOutputWriter; - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern int GetConsoleOutputCP(); - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern bool SetConsoleTextAttribute( - IntPtr consoleHandle, - ushort attributes); - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern bool GetConsoleScreenBufferInfo( - IntPtr consoleHandle, - out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); - - private const UInt32 STD_OUTPUT_HANDLE = unchecked((UInt32)(-11)); - private const UInt32 STD_ERROR_HANDLE = unchecked((UInt32)(-12)); - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern IntPtr GetStdHandle( - UInt32 type); - - [StructLayout(LayoutKind.Sequential)] - private struct COORD - { - public UInt16 x; - public UInt16 y; - } - - [StructLayout(LayoutKind.Sequential)] - private struct SMALL_RECT - { - public UInt16 Left; - public UInt16 Top; - public UInt16 Right; - public UInt16 Bottom; - } - - [StructLayout(LayoutKind.Sequential)] - private struct CONSOLE_SCREEN_BUFFER_INFO + public override void ActivateOptions() { - public COORD dwSize; - public COORD dwCursorPosition; - public ushort wAttributes; - public SMALL_RECT srWindow; - public COORD dwMaximumWindowSize; + base.ActivateOptions(); + CombinedColor = (ushort)((int)ForeColor + (((int)BackColor) << 4)); } /// - /// A class to act as a mapping between the level that a logging call is made at and - /// the color it should be displayed as. + /// The combined and suitable for + /// setting the console color. /// - /// - /// - /// Defines the mapping between a level and the color it should be displayed in. - /// - /// - public class LevelColors : LevelMappingEntry - { - /// - /// The mapped foreground color for the specified level - /// - public Colors ForeColor { get; set; } - - /// - /// The mapped background color for the specified level - /// - public Colors BackColor { get; set; } - - /// - /// Initialize the options for the object - /// - /// - /// - /// Combine the and together. - /// - /// - public override void ActivateOptions() - { - base.ActivateOptions(); - CombinedColor = (ushort)((int)ForeColor + (((int)BackColor) << 4)); - } - - /// - /// The combined and suitable for - /// setting the console color. - /// - internal ushort CombinedColor { get; private set; } - } + internal ushort CombinedColor { get; private set; } } } \ No newline at end of file diff --git a/src/log4net/Util/EncodingWithoutPreamble.cs b/src/log4net/Util/EncodingWithoutPreamble.cs new file mode 100644 index 00000000..a80cc8fb --- /dev/null +++ b/src/log4net/Util/EncodingWithoutPreamble.cs @@ -0,0 +1,121 @@ +#region Apache License +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +using System.Text; + +namespace log4net.Util; + +/// +/// Wrapper for an +/// +/// acts like the wrapped encoding, but without a preamble +internal sealed class EncodingWithoutPreamble : Encoding +{ + private readonly Encoding wrapped; + + /// + private EncodingWithoutPreamble(Encoding wrapped) => this.wrapped = wrapped.EnsureNotNull(); + + /// + /// wraps the in case it has a preamble + /// + /// Encoding to check + /// encoding without preamble + internal static Encoding Get(Encoding encoding) + => encoding.EnsureNotNull().GetPreamble()?.Length > 0 + ? new EncodingWithoutPreamble(encoding) + : encoding; + + /// + public override string BodyName => wrapped.BodyName; + + /// + public override int CodePage => wrapped.CodePage; + + /// + public override string EncodingName => wrapped.EncodingName; + + /// + public override string HeaderName => wrapped.HeaderName; + + /// + public override bool IsBrowserDisplay => wrapped.IsBrowserDisplay; + + /// + public override bool IsBrowserSave => wrapped.IsBrowserSave; + + /// + public override bool IsMailNewsDisplay => wrapped.IsMailNewsDisplay; + + /// + public override bool IsMailNewsSave => wrapped.IsMailNewsSave; + + /// + public override bool IsSingleByte => wrapped.IsSingleByte; + + /// + public override string WebName => wrapped.WebName; + + /// + public override int WindowsCodePage => wrapped.WindowsCodePage; + + + /// + public override object Clone() => new EncodingWithoutPreamble(wrapped.Clone().EnsureIs()); + + /// + public override int GetByteCount(char[] chars, int index, int count) + => wrapped.GetByteCount(chars, index, count); + + /// + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + => wrapped.GetBytes(chars, charIndex, charCount, bytes, byteIndex); + + /// + public override int GetCharCount(byte[] bytes, int index, int count) + => wrapped.GetCharCount(bytes, index, count); + + /// + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + => wrapped.GetChars(bytes, byteIndex, byteCount, chars, charIndex); + + /// + public override int GetMaxByteCount(int charCount) => wrapped.GetMaxByteCount(charCount); + + /// + public override int GetMaxCharCount(int byteCount) => wrapped.GetMaxCharCount(byteCount); + + /// + public override Decoder GetDecoder() => wrapped.GetDecoder(); + + /// + public override Encoder GetEncoder() => wrapped.GetEncoder(); + + /// + public override bool IsAlwaysNormalized(NormalizationForm form) => wrapped.IsAlwaysNormalized(form); + + /// + public override bool Equals(object value) => wrapped.Equals(value); + + /// + public override int GetHashCode() => wrapped.GetHashCode(); + + /// + public override string ToString() => $"{wrapped}-WithoutPreamble"; +} \ No newline at end of file From 2a7683ca66f3172e1b87ac7da47563452adc805d Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Fri, 16 Aug 2024 09:19:41 +0200 Subject: [PATCH 2/4] #168 added tests for LevelMapping --- src/log4net.Tests/Core/LevelMapTest.cs | 69 ++++---- src/log4net.Tests/Core/LevelMappingTest.cs | 124 ++++++++++++++ src/log4net.Tests/Core/LevelTest.cs | 73 +++++---- src/log4net.Tests/Core/LoggingEventTest.cs | 152 +++++++++--------- .../Appender/AnsiColorTerminalAppender.cs | 26 +-- src/log4net/Core/Level.cs | 32 ++-- src/log4net/Util/LevelMapping.cs | 50 ++---- src/log4net/Util/LevelMappingEntry.cs | 7 +- src/log4net/Util/SystemInfo.cs | 3 - src/log4net/Util/SystemStringFormat.cs | 7 +- 10 files changed, 321 insertions(+), 222 deletions(-) create mode 100644 src/log4net.Tests/Core/LevelMappingTest.cs diff --git a/src/log4net.Tests/Core/LevelMapTest.cs b/src/log4net.Tests/Core/LevelMapTest.cs index 234550d7..1b3e5910 100644 --- a/src/log4net.Tests/Core/LevelMapTest.cs +++ b/src/log4net.Tests/Core/LevelMapTest.cs @@ -21,48 +21,47 @@ using NUnit.Framework; -namespace log4net.Tests.Core +namespace log4net.Tests.Core; + +/// +/// Used for internal unit testing the class. +/// +[TestFixture] +public sealed class LevelMapTest { /// - /// Used for internal unit testing the class. + /// Tests the creation of a and calling its method /// - [TestFixture] - public sealed class LevelMapTest + [Test] + public void LevelMapCreateClear() { - /// - /// Tests the creation of a and calling its method - /// - [Test] - public void LevelMapCreateClear() - { - var map = new LevelMap(); - LevelCollection allLevels = map.AllLevels; - Assert.AreEqual(0, allLevels.Count); - Assert.IsNull(map["nonexistent"]); + var map = new LevelMap(); + LevelCollection allLevels = map.AllLevels; + Assert.AreEqual(0, allLevels.Count); + Assert.IsNull(map["nonexistent"]); - map.Add("level1234", 1234, "displayName"); - allLevels = map.AllLevels; - Assert.AreEqual(1, allLevels.Count); - Assert.AreEqual("level1234", allLevels[0].Name); - Assert.AreEqual("displayName", allLevels[0].DisplayName); - Assert.AreEqual(1234, allLevels[0].Value); - Level? level1234 = map["level1234"]; - Assert.IsNotNull(level1234); - Assert.AreSame(level1234, allLevels[0]); + map.Add("level1234", 1234, "displayName"); + allLevels = map.AllLevels; + Assert.AreEqual(1, allLevels.Count); + Assert.AreEqual("level1234", allLevels[0].Name); + Assert.AreEqual("displayName", allLevels[0].DisplayName); + Assert.AreEqual(1234, allLevels[0].Value); + Level? level1234 = map["level1234"]; + Assert.IsNotNull(level1234); + Assert.AreSame(level1234, allLevels[0]); - Level lookupLevel = map.LookupWithDefault(level1234!); - Assert.AreSame(level1234, lookupLevel); + Level lookupLevel = map.LookupWithDefault(level1234!); + Assert.AreSame(level1234, lookupLevel); - var otherLevel = new Level(5678, "level5678", "display"); - lookupLevel = map.LookupWithDefault(otherLevel); - Assert.AreSame(otherLevel, lookupLevel); - Assert.AreSame(otherLevel, map["LEVEL5678"]); + var otherLevel = new Level(5678, "level5678", "display"); + lookupLevel = map.LookupWithDefault(otherLevel); + Assert.AreSame(otherLevel, lookupLevel); + Assert.AreSame(otherLevel, map["LEVEL5678"]); - map.Clear(); - allLevels = map.AllLevels; - Assert.AreEqual(0, allLevels.Count); - Assert.IsNull(map["level1234"]); - Assert.IsNull(map["LEVEL5678"]); - } + map.Clear(); + allLevels = map.AllLevels; + Assert.AreEqual(0, allLevels.Count); + Assert.IsNull(map["level1234"]); + Assert.IsNull(map["LEVEL5678"]); } } \ No newline at end of file diff --git a/src/log4net.Tests/Core/LevelMappingTest.cs b/src/log4net.Tests/Core/LevelMappingTest.cs new file mode 100644 index 00000000..6a6dec62 --- /dev/null +++ b/src/log4net.Tests/Core/LevelMappingTest.cs @@ -0,0 +1,124 @@ +#region Apache License +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using log4net.Core; +using log4net.Util; +using NUnit.Framework; + +namespace log4net.Tests.Core; + +/// +/// Used for internal unit testing the class. +/// +[TestFixture] +public sealed class LevelMappingTest +{ + /// + private sealed class MappingEntry : LevelMappingEntry + { + /// + internal MappingEntry(Level level) => Level = level; + + /// + public override string ToString() => $"{Level?.Value} - {Level?.Name}"; + } + + /// + /// Tests the sorting of the entries + /// + [Test] + public void SortEntriesTest() + { + MappingEntry[] unsorted = [ + new(Level.Info), + new(Level.Off), + new(Level.Emergency), + new(Level.Error), + new(Level.Alert), + new(Level.All), + new(Level.Critical), + new(Level.Debug), + new(Level.Fatal), + new(Level.Fine), + new(Level.Finer), + new(Level.Finest), + new(Level.Log4Net_Debug), + new(Level.Notice), + new(Level.Severe), + new(Level.Trace), + new(Level.Verbose), + new(Level.Warn) + ]; + LevelMapping mapping = new(); + foreach (MappingEntry entry in unsorted) + { + mapping.Add(entry); + } + + List withoutDuplicates = unsorted.GroupBy(entry => entry.Level!.Value) + .Select(group => group.Last()).ToList(); + + List sorted = (List)typeof(LevelMapping) + .GetMethod("SortEntries", BindingFlags.NonPublic | BindingFlags.Instance)! + .Invoke(mapping, Array.Empty())!; + + CollectionAssert.AreEquivalent(withoutDuplicates, sorted); + CollectionAssert.AreNotEqual(withoutDuplicates, sorted); + + int lowestLevelSeen = int.MaxValue; + foreach (LevelMappingEntry entry in sorted) + { + Assert.IsTrue(lowestLevelSeen >= entry.Level!.Value, entry.Level.Name); + lowestLevelSeen = entry.Level!.Value; + } + Assert.AreEqual(Level.All.Value, lowestLevelSeen); + } + + /// + /// Tests the method + /// + [Test] + public void LookupTest() + { + LevelMapping mapping = new(); + mapping.Add(new MappingEntry(Level.Info)); + mapping.Add(new MappingEntry(Level.Off)); + mapping.Add(new MappingEntry(Level.Emergency)); + mapping.Add(new MappingEntry(Level.Warn)); + + Assert.IsNull(mapping.Lookup(Level.Info)?.Level); + + mapping.ActivateOptions(); + + Assert.AreEqual(Level.Info, mapping.Lookup(Level.Info)?.Level); + Assert.AreEqual(Level.Off, mapping.Lookup(Level.Off)?.Level); + Assert.AreEqual(Level.Emergency, mapping.Lookup(Level.Emergency)?.Level); + Assert.AreEqual(Level.Warn, mapping.Lookup(Level.Warn)?.Level); + Assert.AreEqual(Level.Warn, mapping.Lookup(Level.Error)?.Level); + Assert.IsNull(mapping.Lookup(Level.Fine)?.Level); + Assert.AreEqual(Level.Emergency, mapping.Lookup(Level.Log4Net_Debug)?.Level); + Assert.IsNull(mapping.Lookup(Level.Trace)?.Level); + Assert.AreEqual(Level.Warn, mapping.Lookup(Level.Alert)?.Level); + Assert.IsNull(mapping.Lookup(Level.All)?.Level); + } +} \ No newline at end of file diff --git a/src/log4net.Tests/Core/LevelTest.cs b/src/log4net.Tests/Core/LevelTest.cs index 959ec3e1..c56370d1 100644 --- a/src/log4net.Tests/Core/LevelTest.cs +++ b/src/log4net.Tests/Core/LevelTest.cs @@ -23,50 +23,49 @@ using NUnit.Framework; -namespace log4net.Tests.Core +namespace log4net.Tests.Core; + +/// +/// Used for internal unit testing the class. +/// +[TestFixture] +public sealed class LevelTest { /// - /// Used for internal unit testing the class. + /// Tests the comparison between two s /// - [TestFixture] - public sealed class LevelTest + [Test] + public void LevelCompare() { - /// - /// Tests the comparison between two s - /// - [Test] - public void LevelCompare() - { - Level? nullLevel = null; - int? nullInt = null; - Compare(nullInt, nullInt, nullLevel, nullLevel); - Compare(nullInt, Level.Verbose.Value, nullLevel, Level.Verbose); - Compare(Level.Verbose.Value, nullInt, Level.Verbose, nullLevel); - Compare(Level.Verbose.Value, Level.Verbose.Value, Level.Verbose, Level.Verbose); - Compare(Level.Debug.Value, Level.Verbose.Value, Level.Debug, Level.Verbose); - } + Level? nullLevel = null; + int? nullInt = null; + Compare(nullInt, nullInt, nullLevel, nullLevel); + Compare(nullInt, Level.Verbose.Value, nullLevel, Level.Verbose); + Compare(Level.Verbose.Value, nullInt, Level.Verbose, nullLevel); + Compare(Level.Verbose.Value, Level.Verbose.Value, Level.Verbose, Level.Verbose); + Compare(Level.Debug.Value, Level.Verbose.Value, Level.Debug, Level.Verbose); + } - private static void Compare(int? leftInt, int? rightInt, Level? left, Level? right, - [CallerArgumentExpression(nameof(left))] string leftName = "", - [CallerArgumentExpression(nameof(right))] string rightName = "") + private static void Compare(int? leftInt, int? rightInt, Level? left, Level? right, + [CallerArgumentExpression(nameof(left))] string leftName = "", + [CallerArgumentExpression(nameof(right))] string rightName = "") + { + Assert.AreEqual(leftInt < rightInt, left < right, "{0} < {1}", leftName, rightName); + Assert.AreEqual(leftInt > rightInt, left > right, "{0} > {1}", leftName, rightName); + Assert.AreEqual(leftInt <= rightInt, left <= right, "{0} <= {1}", leftName, rightName); + Assert.AreEqual(leftInt >= rightInt, left >= right, "{0} >= {1}", leftName, rightName); + Assert.AreEqual(leftInt == rightInt, left == right, "{0} == {1}", leftName, rightName); + Assert.AreEqual(leftInt != rightInt, left != right, "{0} != {1}", leftName, rightName); + Assert.AreEqual(leftInt?.Equals(rightInt), left?.Equals(right), "{0}?.Equals({1})", leftName, rightName); + if (leftInt is not null) { - Assert.AreEqual(leftInt < rightInt, left < right, "{0} < {1}", leftName, rightName); - Assert.AreEqual(leftInt > rightInt, left > right, "{0} > {1}", leftName, rightName); - Assert.AreEqual(leftInt <= rightInt, left <= right, "{0} <= {1}", leftName, rightName); - Assert.AreEqual(leftInt >= rightInt, left >= right, "{0} >= {1}", leftName, rightName); - Assert.AreEqual(leftInt == rightInt, left == right, "{0} == {1}", leftName, rightName); - Assert.AreEqual(leftInt != rightInt, left != right, "{0} != {1}", leftName, rightName); - Assert.AreEqual(leftInt?.Equals(rightInt), left?.Equals(right), "{0}?.Equals({1})", leftName, rightName); - if (leftInt is not null) + if (rightInt is not null) + { + Assert.AreEqual(leftInt?.CompareTo(rightInt), left?.CompareTo(right!), "{0}?.CompareTo({1})", leftName, rightName); + } + else { - if (rightInt is not null) - { - Assert.AreEqual(leftInt?.CompareTo(rightInt), left?.CompareTo(right!), "{0}?.CompareTo({1})", leftName, rightName); - } - else - { - Assert.Throws(() => left!.CompareTo(right!)); - } + Assert.Throws(() => left!.CompareTo(right!)); } } } diff --git a/src/log4net.Tests/Core/LoggingEventTest.cs b/src/log4net.Tests/Core/LoggingEventTest.cs index e72840df..d5f535b0 100644 --- a/src/log4net.Tests/Core/LoggingEventTest.cs +++ b/src/log4net.Tests/Core/LoggingEventTest.cs @@ -28,89 +28,89 @@ using System.IO; using System.Runtime.Serialization.Formatters.Binary; -namespace log4net.Tests.Core +namespace log4net.Tests.Core; + +[TestFixture] +public sealed class LoggingEventTest { - [TestFixture] - public class LoggingEventTest - { - DateTime localTime = new DateTime(2000, 7, 1, 0, 0, 0, 0, CultureInfo.InvariantCulture.Calendar, DateTimeKind.Local); + private static readonly DateTime localTime + = new DateTime(2000, 7, 1, 0, 0, 0, 0, CultureInfo.InvariantCulture.Calendar, DateTimeKind.Local); - [Test] - public void SerializeDeserialize_BinaryFormatter() + [Test] + public void SerializeDeserialize_BinaryFormatter() + { + var timestamp = localTime.ToUniversalTime(); + var ev = new LoggingEvent(new LoggingEventData { - var timestamp = localTime.ToUniversalTime(); - var ev = new LoggingEvent(new LoggingEventData - { - LoggerName = "aLogger", - Level = Level.Log4Net_Debug, - Message = "aMessage", - ThreadName = "aThread", - TimeStampUtc = timestamp, - LocationInfo = new LocationInfo(GetType()), - UserName = "aUser", - Identity = "anIdentity", - ExceptionString = "anException", - Domain = "aDomain", - Properties = new PropertiesDictionary { ["foo"] = "bar" }, - }); + LoggerName = "aLogger", + Level = Level.Log4Net_Debug, + Message = "aMessage", + ThreadName = "aThread", + TimeStampUtc = timestamp, + LocationInfo = new LocationInfo(GetType()), + UserName = "aUser", + Identity = "anIdentity", + ExceptionString = "anException", + Domain = "aDomain", + Properties = new PropertiesDictionary { ["foo"] = "bar" }, + }); - var formatter = new BinaryFormatter(); - using var stream = new MemoryStream(); - formatter.Serialize(stream, ev); - stream.Position = 0; - var ev2 = (LoggingEvent)formatter.Deserialize(stream); + var formatter = new BinaryFormatter(); + using var stream = new MemoryStream(); + formatter.Serialize(stream, ev); + stream.Position = 0; + var ev2 = (LoggingEvent)formatter.Deserialize(stream); - Assert.AreEqual("aLogger", ev2.LoggerName); - Assert.AreEqual(Level.Log4Net_Debug, ev2.Level); - Assert.IsNull(ev2.MessageObject); - Assert.AreEqual("aMessage", ev2.RenderedMessage); - Assert.AreEqual("aThread", ev2.ThreadName); - Assert.AreEqual(timestamp, ev2.TimeStampUtc); - Assert.IsNotNull(ev2.LocationInfo); - Assert.AreEqual("System.RuntimeMethodHandle", ev2.LocationInfo!.ClassName); - Assert.AreEqual("InvokeMethod", ev2.LocationInfo!.MethodName); - Assert.IsNull(ev2.LocationInfo!.FileName); - Assert.AreEqual("0", ev2.LocationInfo!.LineNumber); - Assert.AreEqual("aUser", ev2.UserName); - Assert.AreEqual("anIdentity", ev2.Identity); - Assert.IsNull(ev2.ExceptionObject); - Assert.AreEqual("anException", ev2.GetExceptionString()); - Assert.AreEqual("aDomain", ev2.Domain); - Assert.AreEqual(1, ev.Properties.Count); - Assert.AreEqual("bar", ev2.Properties["foo"]); - } + Assert.AreEqual("aLogger", ev2.LoggerName); + Assert.AreEqual(Level.Log4Net_Debug, ev2.Level); + Assert.IsNull(ev2.MessageObject); + Assert.AreEqual("aMessage", ev2.RenderedMessage); + Assert.AreEqual("aThread", ev2.ThreadName); + Assert.AreEqual(timestamp, ev2.TimeStampUtc); + Assert.IsNotNull(ev2.LocationInfo); + Assert.AreEqual("System.RuntimeMethodHandle", ev2.LocationInfo!.ClassName); + Assert.AreEqual("InvokeMethod", ev2.LocationInfo!.MethodName); + Assert.IsNull(ev2.LocationInfo!.FileName); + Assert.AreEqual("0", ev2.LocationInfo!.LineNumber); + Assert.AreEqual("aUser", ev2.UserName); + Assert.AreEqual("anIdentity", ev2.Identity); + Assert.IsNull(ev2.ExceptionObject); + Assert.AreEqual("anException", ev2.GetExceptionString()); + Assert.AreEqual("aDomain", ev2.Domain); + Assert.AreEqual(1, ev.Properties.Count); + Assert.AreEqual("bar", ev2.Properties["foo"]); + } - /// - /// Loads and validates the cached serialized v2 event data from the log4net2-SerializeEvent directory. - /// - [Test] - public void DeserializeV2() - { - const string datPath = @"..\..\..\..\integration-testing\log4net2-SerializeEvent\SerializeV2Event.dat"; - using var stream = File.OpenRead(datPath); - var formatter = new BinaryFormatter(); - LoggingEvent ev = (LoggingEvent)formatter.Deserialize(stream); - Assert.IsNotNull(ev); + /// + /// Loads and validates the cached serialized v2 event data from the log4net2-SerializeEvent directory. + /// + [Test] + public void DeserializeV2() + { + const string datPath = @"..\..\..\..\integration-testing\log4net2-SerializeEvent\SerializeV2Event.dat"; + using var stream = File.OpenRead(datPath); + var formatter = new BinaryFormatter(); + LoggingEvent ev = (LoggingEvent)formatter.Deserialize(stream); + Assert.IsNotNull(ev); - Assert.AreEqual("aLogger", ev!.LoggerName); - Assert.AreEqual(Level.Log4Net_Debug, ev.Level); - Assert.IsNull(ev.MessageObject); - Assert.AreEqual("aMessage", ev.RenderedMessage); - Assert.AreEqual("aThread", ev.ThreadName); - Assert.IsNotNull(ev.LocationInfo); - Assert.AreEqual("?", ev.LocationInfo!.ClassName); - Assert.AreEqual("?", ev.LocationInfo!.MethodName); - Assert.AreEqual("?", ev.LocationInfo!.FileName); - Assert.AreEqual("?", ev.LocationInfo!.LineNumber); - Assert.AreEqual("aUser", ev.UserName); - Assert.AreEqual("anIdentity", ev.Identity); - Assert.IsNull(ev.ExceptionObject); - Assert.AreEqual("anException", ev.GetExceptionString()); - Assert.AreEqual("aDomain", ev.Domain); - Assert.AreEqual(1, ev.Properties.Count); - Assert.AreEqual("bar", ev.Properties["foo"]); - Assert.AreEqual(localTime.ToUniversalTime(), ev.TimeStampUtc); - } + Assert.AreEqual("aLogger", ev!.LoggerName); + Assert.AreEqual(Level.Log4Net_Debug, ev.Level); + Assert.IsNull(ev.MessageObject); + Assert.AreEqual("aMessage", ev.RenderedMessage); + Assert.AreEqual("aThread", ev.ThreadName); + Assert.IsNotNull(ev.LocationInfo); + Assert.AreEqual("?", ev.LocationInfo!.ClassName); + Assert.AreEqual("?", ev.LocationInfo!.MethodName); + Assert.AreEqual("?", ev.LocationInfo!.FileName); + Assert.AreEqual("?", ev.LocationInfo!.LineNumber); + Assert.AreEqual("aUser", ev.UserName); + Assert.AreEqual("anIdentity", ev.Identity); + Assert.IsNull(ev.ExceptionObject); + Assert.AreEqual("anException", ev.GetExceptionString()); + Assert.AreEqual("aDomain", ev.Domain); + Assert.AreEqual(1, ev.Properties.Count); + Assert.AreEqual("bar", ev.Properties["foo"]); + Assert.AreEqual(localTime.ToUniversalTime(), ev.TimeStampUtc); } } #endif // NET462_OR_GREATER \ No newline at end of file diff --git a/src/log4net/Appender/AnsiColorTerminalAppender.cs b/src/log4net/Appender/AnsiColorTerminalAppender.cs index f1d602ab..831be2b7 100644 --- a/src/log4net/Appender/AnsiColorTerminalAppender.cs +++ b/src/log4net/Appender/AnsiColorTerminalAppender.cs @@ -215,18 +215,18 @@ public AnsiColorTerminalAppender() /// public virtual string Target { - get => m_writeToErrorStream ? ConsoleError : ConsoleOut; + get => writeToErrorStream ? ConsoleError : ConsoleOut; set { string trimmedTargetName = value.Trim(); if (SystemInfo.EqualsIgnoringCase(ConsoleError, trimmedTargetName)) { - m_writeToErrorStream = true; + writeToErrorStream = true; } else { - m_writeToErrorStream = false; + writeToErrorStream = false; } } } @@ -237,7 +237,7 @@ public virtual string Target /// The mapping to add public void AddMapping(LevelColors mapping) { - m_levelMapping.Add(mapping); + levelMapping.Add(mapping); } /// @@ -257,7 +257,7 @@ protected override void Append(LoggingEvent loggingEvent) string loggingMessage = RenderLoggingEvent(loggingEvent); // see if there is a specified lookup. - if (m_levelMapping.Lookup(loggingEvent.Level) is LevelColors levelColors) + if (levelMapping.Lookup(loggingEvent.Level) is LevelColors levelColors) { // Prepend the Ansi Color code loggingMessage = levelColors.CombinedColor + loggingMessage; @@ -294,7 +294,7 @@ protected override void Append(LoggingEvent loggingEvent) } } - if (m_writeToErrorStream) + if (writeToErrorStream) { // Write to the error stream Console.Error.Write(loggingMessage); @@ -318,17 +318,17 @@ protected override void Append(LoggingEvent loggingEvent) public override void ActivateOptions() { base.ActivateOptions(); - m_levelMapping.ActivateOptions(); + levelMapping.ActivateOptions(); } /// - /// The to use when writing to the Console + /// The to use when writing to the Console /// standard output stream. /// public const string ConsoleOut = "Console.Out"; /// - /// The to use when writing to the Console + /// The to use when writing to the Console /// standard error output stream. /// public const string ConsoleError = "Console.Error"; @@ -336,12 +336,12 @@ public override void ActivateOptions() /// /// Flag to write output to the error stream rather than the standard output stream /// - private bool m_writeToErrorStream; + private bool writeToErrorStream; /// /// Mapping from level object to color value /// - private readonly LevelMapping m_levelMapping = new(); + private readonly LevelMapping levelMapping = new(); /// /// Ansi code to reset terminal @@ -393,7 +393,7 @@ public override void ActivateOptions() { base.ActivateOptions(); - StringBuilder buf = new StringBuilder(); + StringBuilder buf = new(); // Reset any existing codes buf.Append("\x1b[0;"); @@ -449,4 +449,4 @@ public override void ActivateOptions() internal string CombinedColor { get; private set; } = string.Empty; } } -} +} \ No newline at end of file diff --git a/src/log4net/Core/Level.cs b/src/log4net/Core/Level.cs index 4e852be5..e15078b6 100644 --- a/src/log4net/Core/Level.cs +++ b/src/log4net/Core/Level.cs @@ -383,96 +383,96 @@ public static int Compare(Level? l, Level? r) /// The level designates very severe error events. /// System unusable, emergencies. /// - public static readonly Level Log4Net_Debug = new(120000, "log4net:DEBUG"); + public static readonly Level Log4Net_Debug = new(120_000, "log4net:DEBUG"); /// /// The level designates very severe error events. /// System unusable, emergencies. /// - public static readonly Level Emergency = new(120000, "EMERGENCY"); + public static readonly Level Emergency = new(120_000, "EMERGENCY"); /// /// The level designates very severe error events /// that will presumably lead the application to abort. /// - public static readonly Level Fatal = new(110000, "FATAL"); + public static readonly Level Fatal = new(110_000, "FATAL"); /// /// The level designates very severe error events. /// Take immediate action, alerts. /// - public static readonly Level Alert = new(100000, "ALERT"); + public static readonly Level Alert = new(100_000, "ALERT"); /// /// The level designates very severe error events. /// Critical condition, critical. /// - public static readonly Level Critical = new(90000, "CRITICAL"); + public static readonly Level Critical = new(90_000, "CRITICAL"); /// /// The level designates very severe error events. /// - public static readonly Level Severe = new(80000, "SEVERE"); + public static readonly Level Severe = new(80_000, "SEVERE"); /// /// The level designates error events that might /// still allow the application to continue running. /// - public static readonly Level Error = new(70000, "ERROR"); + public static readonly Level Error = new(70_000, "ERROR"); /// /// The level designates potentially harmful /// situations. /// - public static readonly Level Warn = new(60000, "WARN"); + public static readonly Level Warn = new(60_000, "WARN"); /// /// The level designates informational messages /// that highlight the progress of the application at the highest level. /// - public static readonly Level Notice = new(50000, "NOTICE"); + public static readonly Level Notice = new(50_000, "NOTICE"); /// /// The level designates informational messages that /// highlight the progress of the application at coarse-grained level. /// - public static readonly Level Info = new(40000, "INFO"); + public static readonly Level Info = new(40_000, "INFO"); /// /// The level designates fine-grained informational /// events that are most useful to debug an application. /// - public static readonly Level Debug = new(30000, "DEBUG"); + public static readonly Level Debug = new(30_000, "DEBUG"); /// /// The level designates fine-grained informational /// events that are most useful to debug an application. /// - public static readonly Level Fine = new(30000, "FINE"); + public static readonly Level Fine = new(30_000, "FINE"); /// /// The level designates fine-grained informational /// events that are most useful to debug an application. /// - public static readonly Level Trace = new(20000, "TRACE"); + public static readonly Level Trace = new(20_000, "TRACE"); /// /// The level designates fine-grained informational /// events that are most useful to debug an application. /// - public static readonly Level Finer = new(20000, "FINER"); + public static readonly Level Finer = new(20_000, "FINER"); /// /// The level designates fine-grained informational /// events that are most useful to debug an application. /// - public static readonly Level Verbose = new(10000, "VERBOSE"); + public static readonly Level Verbose = new(10_000, "VERBOSE"); /// /// The level designates fine-grained informational /// events that are most useful to debug an application. /// - public static readonly Level Finest = new(10000, "FINEST"); + public static readonly Level Finest = new(10_000, "FINEST"); /// /// The level designates the lowest level possible. diff --git a/src/log4net/Util/LevelMapping.cs b/src/log4net/Util/LevelMapping.cs index 92e5f37a..6d3aa709 100644 --- a/src/log4net/Util/LevelMapping.cs +++ b/src/log4net/Util/LevelMapping.cs @@ -17,8 +17,8 @@ // #endregion -using System; using System.Collections.Generic; +using System.Linq; using log4net.Core; namespace log4net.Util @@ -30,6 +30,9 @@ namespace log4net.Util /// Nicko Cadell public sealed class LevelMapping : IOptionHandler { + private readonly Dictionary entries = new(); + private List? sortedEntries; + /// /// Add a to this mapping /// @@ -45,7 +48,7 @@ public void Add(LevelMappingEntry entry) { if (entry.Level is not null) { - m_entriesMap[entry.Level] = entry; + entries[entry.Level] = entry; } } @@ -55,22 +58,19 @@ public void Add(LevelMappingEntry entry) /// specified. /// /// the level to look up. - /// The for the level or null if no mapping found + /// The for the level or if no mapping found public LevelMappingEntry? Lookup(Level? level) { - if (level is null) + if (level is null || sortedEntries is null) { return null; } - if (m_entries is not null) + foreach (LevelMappingEntry entry in sortedEntries) { - foreach (LevelMappingEntry entry in m_entries) + if (level >= entry.Level) { - if (entry.Level is not null && level >= entry.Level) - { - return entry; - } + return entry; } } return null; @@ -80,33 +80,15 @@ public void Add(LevelMappingEntry entry) /// Initialize options /// /// - /// - /// Caches the sorted list of in an array - /// + /// Caches the sorted list of /// public void ActivateOptions() { - Level[] sortKeys = new Level[m_entriesMap.Count]; - LevelMappingEntry[] sortValues = new LevelMappingEntry[m_entriesMap.Count]; - - m_entriesMap.Keys.CopyTo(sortKeys, 0); - m_entriesMap.Values.CopyTo(sortValues, 0); - - // Sort in level order - Array.Sort(sortKeys, sortValues, 0, sortKeys.Length, null); - - // Reverse list so that highest level is first - Array.Reverse(sortValues, 0, sortValues.Length); - - foreach (LevelMappingEntry entry in sortValues) - { - entry.ActivateOptions(); - } - - m_entries = sortValues; + sortedEntries = SortEntries(); + sortedEntries.ForEach(entry => entry.ActivateOptions()); } - private readonly Dictionary m_entriesMap = new(); - private LevelMappingEntry[]? m_entries; + private List SortEntries() + => entries.OrderByDescending(entry => entry.Key).Select(entry => entry.Value).ToList(); } -} +} \ No newline at end of file diff --git a/src/log4net/Util/LevelMappingEntry.cs b/src/log4net/Util/LevelMappingEntry.cs index b46cd43e..30ad337c 100644 --- a/src/log4net/Util/LevelMappingEntry.cs +++ b/src/log4net/Util/LevelMappingEntry.cs @@ -17,6 +17,7 @@ // #endregion +using System.Diagnostics; using log4net.Core; namespace log4net.Util @@ -26,14 +27,14 @@ namespace log4net.Util /// object. /// /// Nicko Cadell + [DebuggerDisplay("{Level}")] public abstract class LevelMappingEntry : IOptionHandler { /// /// Default protected constructor /// protected LevelMappingEntry() - { - } + { } /// /// Gets or sets the level that is the key for this mapping. @@ -45,7 +46,7 @@ protected LevelMappingEntry() /// /// /// - /// Should be overridden by any classes that need to initialise based on their options + /// Should be overridden by any classes that need to initialize based on their options /// /// public virtual void ActivateOptions() diff --git a/src/log4net/Util/SystemInfo.cs b/src/log4net/Util/SystemInfo.cs index 75608474..e04dc184 100644 --- a/src/log4net/Util/SystemInfo.cs +++ b/src/log4net/Util/SystemInfo.cs @@ -572,7 +572,6 @@ public static NotSupportedException CreateReadOnlyCollectionNotModifiableExcepti /// public static bool TryParse(string s, out int val) { - // Initialise out param val = 0; try @@ -605,7 +604,6 @@ public static bool TryParse(string s, out int val) /// public static bool TryParse(string s, out long val) { - // Initialise out param val = 0; try @@ -638,7 +636,6 @@ public static bool TryParse(string s, out long val) /// public static bool TryParse(string s, out short val) { - // Initialise out param val = 0; try diff --git a/src/log4net/Util/SystemStringFormat.cs b/src/log4net/Util/SystemStringFormat.cs index 9de9e087..36ba331b 100644 --- a/src/log4net/Util/SystemStringFormat.cs +++ b/src/log4net/Util/SystemStringFormat.cs @@ -41,7 +41,7 @@ public sealed class SystemStringFormat public object?[]? Args { get; set; } /// - /// Initialise the + /// Constructor /// /// An that supplies culture-specific formatting information. /// A containing zero or more format items. @@ -57,10 +57,7 @@ public SystemStringFormat(IFormatProvider? provider, string format, params objec /// Format the string and arguments /// /// the formatted string - public override string? ToString() - { - return StringFormat(m_provider, Format, Args); - } + public override string? ToString() => StringFormat(m_provider, Format, Args); /// /// Replaces the format item in a specified with the text equivalent From af6f59f3b5f14b0da1ca7cf36a51de6b06331035 Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Fri, 16 Aug 2024 12:47:32 +0200 Subject: [PATCH 3/4] #169 onlyFixPartialEventData -> fix = Partial in config examples --- src/site/xdoc/release/config-examples.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/xdoc/release/config-examples.xml b/src/site/xdoc/release/config-examples.xml index 8b0928f6..40f9de33 100644 --- a/src/site/xdoc/release/config-examples.xml +++ b/src/site/xdoc/release/config-examples.xml @@ -788,7 +788,7 @@ CREATE TABLE Log ( - + ]]> From 62aa1e819096b9063fda76172ca5c356478be3de Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Sun, 18 Aug 2024 13:43:16 +0200 Subject: [PATCH 4/4] #168 added release notes --- src/site/xdoc/release/release-notes.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/site/xdoc/release/release-notes.xml b/src/site/xdoc/release/release-notes.xml index 30efb9d1..1441b258 100644 --- a/src/site/xdoc/release/release-notes.xml +++ b/src/site/xdoc/release/release-notes.xml @@ -183,6 +183,9 @@ limitations under the License.
  • System.NullReferenceException when comparing with a null Level (by @freeandnil)
  • +
  • + ColoredConsoleAppender writes UTF-8 preamble to the console on initialization (reported by @RoboBurned, fixed by @freeandnil) +