From 94e7a4d6af0146ff571e96b1fd896f96abb9ed08 Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Thu, 3 Jul 2025 00:02:40 +0200 Subject: [PATCH 1/3] #250 mocked DateTimeStrategy to speedup test - added release notes - added license headers - adjusted Log4NetIntegrationTests to .editorconfig --- src/changelog/3.2.0/.release.xml | 2 +- .../3.2.0/250-broken-rolling-logfiles.xml | 12 + .../Integration/Log4NetIntegrationTests.cs | 246 ++++++++++-------- src/log4net.Tests/Integration/MockDateTime.cs | 37 +++ .../Integration/log4net.no_file_name.config | 42 +-- .../Integration/log4net.roll.config | 14 +- src/log4net.Tests/NdcTest.cs | 14 - src/log4net/Appender/RollingFileAppender.cs | 4 +- src/log4net/Util/Log4NetAssert.cs | 2 +- 9 files changed, 219 insertions(+), 154 deletions(-) create mode 100644 src/changelog/3.2.0/250-broken-rolling-logfiles.xml create mode 100644 src/log4net.Tests/Integration/MockDateTime.cs diff --git a/src/changelog/3.2.0/.release.xml b/src/changelog/3.2.0/.release.xml index aace3b6d..b1a9979b 100644 --- a/src/changelog/3.2.0/.release.xml +++ b/src/changelog/3.2.0/.release.xml @@ -2,5 +2,5 @@ diff --git a/src/changelog/3.2.0/250-broken-rolling-logfiles.xml b/src/changelog/3.2.0/250-broken-rolling-logfiles.xml new file mode 100644 index 00000000..249cfd84 --- /dev/null +++ b/src/changelog/3.2.0/250-broken-rolling-logfiles.xml @@ -0,0 +1,12 @@ + + + + + + + The rolling of logfiles was fixed (reported by @maedula, fixed by @gdziadkiewicz) + + \ No newline at end of file diff --git a/src/log4net.Tests/Integration/Log4NetIntegrationTests.cs b/src/log4net.Tests/Integration/Log4NetIntegrationTests.cs index bf29b381..9725c8eb 100644 --- a/src/log4net.Tests/Integration/Log4NetIntegrationTests.cs +++ b/src/log4net.Tests/Integration/Log4NetIntegrationTests.cs @@ -1,9 +1,29 @@ +#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.IO; using System.Collections.Generic; +using System.IO; using System.Linq; -using log4net; +using log4net.Appender; using log4net.Config; +using log4net.Repository; using NUnit.Framework; namespace log4net.Tests.Integration @@ -13,8 +33,9 @@ namespace log4net.Tests.Integration /// /// Expectations for test log files and directories: /// - /// Test log files may be placed directly in the test directory with names starting with TestLogFilePrefix, or inside directories whose names start with TestLogDirectoryPrefix and may have arbitrary file names. - /// Each test run starts with a clean environment: any files or directories from previous runs matching these prefixes are deleted in SetUp. + /// Test log files may be placed directly in the test directory with names starting with , + /// or inside directories whose names start with and may have arbitrary file names. + /// Each test run starts with a clean environment: any files or directories from previous runs matching these prefixes are deleted in . /// Tests assert the existence, count, and content of log files and directories as part of their validation. /// /// @@ -44,16 +65,16 @@ public void SetUp() [Test] public void Log4Net_WritesLogFile_AndContentIsCorrect() { - var (log, repo) = ArrangeLogger("log4net.integration.basic.config"); - log.Info("Hello integration test"); - log.Error("This is an error"); - repo.Shutdown(); + (ILog log, ILoggerRepository repo) = ArrangeLogger("log4net.integration.basic.config"); + log.Info("Hello integration test"); + log.Error("This is an error"); + repo.Shutdown(); - // Assert: log file exists and contains expected content - string[] logLines = File.ReadAllLines("integrationTestLogFile_integration.log"); - Assert.That(logLines.Length, Is.EqualTo(2)); - Assert.That(logLines[0], Does.Contain("Hello integration test")); - Assert.That(logLines[1], Does.Contain("This is an error")); + // Assert: log file exists and contains expected content + string[] logLines = File.ReadAllLines("integrationTestLogFile_integration.log"); + Assert.That(logLines, Has.Length.EqualTo(2)); + Assert.That(logLines[0], Does.Contain("Hello integration test")); + Assert.That(logLines[1], Does.Contain("This is an error")); } /// @@ -67,21 +88,21 @@ public void Log4Net_WritesLogFile_AndContentIsCorrect() [Test] public void Log4Net_WritesLogFile_AndContentIsCorrectAfterRestart() { - for (int i = 0; i < 10; i++) - { - var (log, repo) = ArrangeLogger("log4net.integration.basic.config"); - log.Info("Hello integration test"); - log.Error("This is an error"); - repo.Shutdown(); - } - // Assert: log file exists and contains expected content - string[] logLines = File.ReadAllLines("integrationTestLogFile_integration.log"); - Assert.That(logLines.Length, Is.EqualTo(20)); - for (int i = 0; i < 10; i++) - { - Assert.That(logLines[i * 2], Does.Contain("Hello integration test")); - Assert.That(logLines[i * 2 + 1], Does.Contain("This is an error")); - } + for (int i = 0; i < 10; i++) + { + (ILog log, ILoggerRepository repo) = ArrangeLogger("log4net.integration.basic.config"); + log.Info("Hello integration test"); + log.Error("This is an error"); + repo.Shutdown(); + } + // Assert: log file exists and contains expected content + string[] logLines = File.ReadAllLines("integrationTestLogFile_integration.log"); + Assert.That(logLines, Has.Length.EqualTo(20)); + for (int i = 0; i < 10; i++) + { + Assert.That(logLines[i * 2], Does.Contain("Hello integration test")); + Assert.That(logLines[i * 2 + 1], Does.Contain("This is an error")); + } } /// @@ -96,18 +117,18 @@ public void Log4Net_WritesLogFile_AndContentIsCorrectAfterRestart() [Test] public void Log4Net_WritesLogFile_WithRollAndNoAppend_AndContentIsCorrectAfterRestart() { - for (int i = 0; i < 20; i++) + for (int i = 0; i < 20; i++) + { + (ILog log, ILoggerRepository repo) = ArrangeLogger("log4net.roll.config"); + for (int j = 0; j < 10; ++j) { - var (log, repo) = ArrangeLogger("log4net.roll.config"); - for (int j = 0; j < 10; ++j) - { - log.Info($"Hello, log4net! {i} {j}"); - } - repo.Shutdown(); + log.Info($"Hello, log4net! {i} {j}"); } - // Assert: log file exists and contains expected content - string[] logFiles = Directory.GetFiles("integrationTestLogDir_roll"); - Assert.That(logFiles.Length, Is.EqualTo(12 + 1)); + repo.Shutdown(); + } + // Assert: log file exists and contains expected content + string[] logFiles = Directory.GetFiles("integrationTestLogDir_roll"); + Assert.That(logFiles, Has.Length.EqualTo(12 + 1)); } /// @@ -122,21 +143,19 @@ public void Log4Net_WritesLogFile_WithRollAndNoAppend_AndContentIsCorrectAfterRe [Test] public void Log4Net_WritesLogFile_WithMaxSizeRoll_Config_Works() { - string logDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "integrationTestLogDir_maxsizeroll"); - if (Directory.Exists(logDir)) Directory.Delete(logDir, true); - Directory.CreateDirectory(logDir); - var (log, repo) = ArrangeLogger("log4net.maxsizeroll.config"); - for (int i = 0; i < 1000; ++i) - { - log.Info($"Log entry {i}"); - } - repo.Shutdown(); - // Assert: rolled files exist - string[] logFiles = Directory.GetFiles(logDir, "*.log"); - Assert.That(logFiles.Length, Is.EqualTo(4)); // 1 current + 3 backups - // Optionally, check that each file is <= 10KB - foreach (var file in logFiles) - Assert.That(new FileInfo(file).Length, Is.LessThanOrEqualTo(10 * 1024 + 100)); + DirectoryInfo logDir = CreateLogDirectory("integrationTestLogDir_maxsizeroll"); + (ILog log, ILoggerRepository repo) = ArrangeLogger("log4net.maxsizeroll.config"); + for (int i = 0; i < 1000; ++i) + { + log.Info($"Log entry {i}"); + } + repo.Shutdown(); + // Assert: rolled files exist + FileInfo[] logFiles = logDir.GetFiles("*.log"); + Assert.That(logFiles, Has.Length.EqualTo(4)); // 1 current + 3 backups + // Check that each file is <= 10KB + foreach (FileInfo file in logFiles) + Assert.That(file.Length, Is.LessThanOrEqualTo(10 * 1024 + 100)); } /// @@ -152,32 +171,35 @@ public void Log4Net_WritesLogFile_WithMaxSizeRoll_Config_Works() [Test] public void Log4Net_WritesLogFile_WithDateAndSizeRoll_Config_Works() { - string logDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "integrationTestLogDir_maxsizerolldate"); - if (Directory.Exists(logDir)) Directory.Delete(logDir, true); - Directory.CreateDirectory(logDir); - var (log, repo) = ArrangeLogger("log4net.maxsizeroll_date.config"); - // Write enough lines to trigger rolling by size and date - for (int i = 1; i < 10000; ++i) - { - log.Debug($"DateRoll entry {i}"); - if (i % 5000 == 0) System.Threading.Thread.Sleep(TimeSpan.FromMinutes(1)); // allow time for date to change if needed - } - repo.Shutdown(); - // Assert: rolled files exist (date+size pattern) - string[] logFiles = Directory.GetFiles(logDir, "*.log"); - Assert.That(logFiles.Length, Is.EqualTo(8)); - // Group files by date part in the filename (yyyy-MM-dd-mm) - var dateGroups = logFiles - .Select(f => Path.GetFileNameWithoutExtension(f)) - .Select(name => name.Split('.').First()) - .GroupBy(date => date) - .ToDictionary(g => g.Key, g => g.Count()); - // Assert that at least one group exists and print group counts - Assert.That(dateGroups.Count, Is.EqualTo(2)); - foreach (var group in dateGroups) + DirectoryInfo logDir = CreateLogDirectory("integrationTestLogDir_maxsizerolldate"); + (ILog log, ILoggerRepository repo) = ArrangeLogger("log4net.maxsizeroll_date.config"); + MockDateTime mockDateTime = new(); + repo.GetAppenders().OfType().First().DateTimeStrategy = mockDateTime; + // Write enough lines to trigger rolling by size and date + for (int i = 1; i < 10000; ++i) + { + log.Debug($"DateRoll entry {i}"); + if (i % 5000 == 0) { - TestContext.Out.WriteLine($"Date group: {group.Key}, file count: {group.Value}"); + mockDateTime.Offset = TimeSpan.FromMinutes(1); // allow time for date to change if needed } + } + repo.Shutdown(); + // Assert: rolled files exist (date+size pattern) + FileInfo[] logFiles = logDir.GetFiles("*.log"); + Assert.That(logFiles, Has.Length.EqualTo(8)); + // Group files by date part in the filename (yyyy-MM-dd-mm) + Dictionary dateGroups = logFiles + .Select(f => Path.GetFileNameWithoutExtension(f.Name)) + .Select(name => name.Split('.').First()) + .GroupBy(date => date) + .ToDictionary(g => g.Key, g => g.Count()); + // Assert that at least one group exists and print group counts + Assert.That(dateGroups, Has.Count.EqualTo(2)); + foreach (KeyValuePair group in dateGroups) + { + TestContext.Out.WriteLine($"Date group: {group.Key}, file count: {group.Value}"); + } } /// @@ -192,20 +214,28 @@ public void Log4Net_WritesLogFile_WithDateAndSizeRoll_Config_Works() [Test] public void Log4Net_ConfigWithoutFileName_CreatesOneFile() { - string logDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "integrationTestLogDir_no_file_name"); - if (Directory.Exists(logDir)) Directory.Delete(logDir, true); - Directory.CreateDirectory(logDir); - var (log, repo) = ArrangeLogger("log4net.no_file_name.config"); - log.Info("Test entry with no file name"); - repo.Shutdown(); - // Assert: exactly one log file was created in the directory - var files = Directory.GetFiles(logDir, "*", SearchOption.AllDirectories); - Assert.That(files.Length, Is.EqualTo(1), "Should create exactly one log file"); - var fileName = Path.GetFileName(files[0]); - TestContext.Out.WriteLine($"Created file: {fileName}"); - // Assert the file name matches the date pattern yyyy-MM-dd.log - string todayPattern = DateTime.Now.ToString("yyyy-MM-dd") + ".log"; - Assert.That(fileName, Is.EqualTo(todayPattern), $"File name should match pattern: {todayPattern}"); + DirectoryInfo logDir = CreateLogDirectory("integrationTestLogDir_no_file_name"); + (ILog log, ILoggerRepository repo) = ArrangeLogger("log4net.no_file_name.config"); + log.Info("Test entry with no file name"); + repo.Shutdown(); + // Assert: exactly one log file was created in the directory + FileInfo[] files = logDir.GetFiles("*", SearchOption.AllDirectories); + Assert.That(files, Has.Length.EqualTo(1), "Should create exactly one log file"); + TestContext.Out.WriteLine($"Created file: {files[0].Name}"); + // Assert the file name matches the date pattern yyyy-MM-dd.log + string todayPattern = DateTime.Now.ToString("yyyy-MM-dd") + ".log"; + Assert.That(files[0].Name, Is.EqualTo(todayPattern), $"File name should match pattern: {todayPattern}"); + } + + private static DirectoryInfo CreateLogDirectory(string name) + { + DirectoryInfo logDir = new(Path.Combine(TestContext.CurrentContext.TestDirectory, name)); + if (logDir.Exists) + { + logDir.Delete(true); + } + logDir.Create(); + return logDir; } private const string TestLogFilePrefix = "integrationTestLogFile"; @@ -213,32 +243,32 @@ public void Log4Net_ConfigWithoutFileName_CreatesOneFile() private static void RemoveDirsFromPreviousRuns() { - List directoriesFromPreviousRuns = - Directory.EnumerateDirectories(TestContext.CurrentContext.TestDirectory) - .Where(dirPath => Path.GetFileName(dirPath).StartsWith(TestLogDirectoryPrefix, StringComparison.InvariantCultureIgnoreCase)) - .ToList(); - directoriesFromPreviousRuns.ForEach(d => Directory.Delete(d, true)); + List directoriesFromPreviousRuns = new DirectoryInfo(TestContext.CurrentContext.TestDirectory) + .EnumerateDirectories() + .Where(directory => directory.Name.StartsWith(TestLogDirectoryPrefix, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + directoriesFromPreviousRuns.ForEach(d => d.Delete(true)); } private static void RemoveFilesFromPreviousRuns() { - List filesFromPreviousRuns = - Directory.EnumerateFiles(TestContext.CurrentContext.TestDirectory) - .Where(filePath => Path.GetFileName(filePath).StartsWith(TestLogFilePrefix, StringComparison.InvariantCultureIgnoreCase)) - .ToList(); - filesFromPreviousRuns.ForEach(File.Delete); + List filesFromPreviousRuns = new DirectoryInfo(TestContext.CurrentContext.TestDirectory) + .EnumerateFiles() + .Where(filePath => filePath.Name.StartsWith(TestLogFilePrefix, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + filesFromPreviousRuns.ForEach(file => file.Delete()); } private static string GetConfigPath(string configFile) => Path.Combine(TestContext.CurrentContext.TestDirectory, "Integration", configFile); - private static (ILog log, log4net.Repository.ILoggerRepository repo) ArrangeLogger(string configFile) + private static (ILog log, ILoggerRepository repo) ArrangeLogger(string configFile) { - string configPath = GetConfigPath(configFile); - var repo = LogManager.CreateRepository(Guid.NewGuid().ToString()); - XmlConfigurator.Configure(repo, new FileInfo(configPath)); - var log = LogManager.GetLogger(repo.Name, "IntegrationTestLogger"); - return (log, repo); + string configPath = GetConfigPath(configFile); + ILoggerRepository repo = LogManager.CreateRepository(Guid.NewGuid().ToString()); + XmlConfigurator.Configure(repo, new FileInfo(configPath)); + ILog log = LogManager.GetLogger(repo.Name, "IntegrationTestLogger"); + return (log, repo); } } -} +} \ No newline at end of file diff --git a/src/log4net.Tests/Integration/MockDateTime.cs b/src/log4net.Tests/Integration/MockDateTime.cs new file mode 100644 index 00000000..b7ee01d5 --- /dev/null +++ b/src/log4net.Tests/Integration/MockDateTime.cs @@ -0,0 +1,37 @@ +#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 log4net.Appender; + +namespace log4net.Tests.Integration; + +/// +/// Mock implementation of +/// +internal class MockDateTime : RollingFileAppender.IDateTime +{ + /// + /// Offset to apply to the current time. + /// + internal TimeSpan Offset { get; set; } + + /// + public DateTime Now => DateTime.Now + Offset; +} \ No newline at end of file diff --git a/src/log4net.Tests/Integration/log4net.no_file_name.config b/src/log4net.Tests/Integration/log4net.no_file_name.config index d0fc0786..79ba9304 100644 --- a/src/log4net.Tests/Integration/log4net.no_file_name.config +++ b/src/log4net.Tests/Integration/log4net.no_file_name.config @@ -1,23 +1,23 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - + + + + + \ No newline at end of file diff --git a/src/log4net.Tests/Integration/log4net.roll.config b/src/log4net.Tests/Integration/log4net.roll.config index 15b6735d..06b4c8ea 100644 --- a/src/log4net.Tests/Integration/log4net.roll.config +++ b/src/log4net.Tests/Integration/log4net.roll.config @@ -1,6 +1,6 @@ - + - + @@ -23,9 +23,9 @@ - - - - - + + + + + \ No newline at end of file diff --git a/src/log4net.Tests/NdcTest.cs b/src/log4net.Tests/NdcTest.cs index ee5b14d3..1b747e3b 100644 --- a/src/log4net.Tests/NdcTest.cs +++ b/src/log4net.Tests/NdcTest.cs @@ -17,20 +17,6 @@ // #endregion -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using log4net.Config; -using log4net.Core; -using log4net.Layout; -using log4net.Layout.Pattern; -using log4net.Repository; -using log4net.Tests.Appender; -using log4net.Util; using NUnit.Framework; namespace log4net.Tests; diff --git a/src/log4net/Appender/RollingFileAppender.cs b/src/log4net/Appender/RollingFileAppender.cs index 55105c9f..5e983df3 100644 --- a/src/log4net/Appender/RollingFileAppender.cs +++ b/src/log4net/Appender/RollingFileAppender.cs @@ -790,7 +790,7 @@ private void InitializeFromOneFile(string baseFile, string curFileName) curFileName = curFileName.ToLowerInvariant(); baseFile = baseFile.ToLowerInvariant(); string dir = string.IsNullOrEmpty(baseFile) ? "" : Path.GetDirectoryName(baseFile); - var baseFileWithoutExtension = Path.Combine(dir ?? "", Path.GetFileNameWithoutExtension(baseFile)); + string baseFileWithoutExtension = Path.Combine(dir ?? "", Path.GetFileNameWithoutExtension(baseFile)); if (curFileName.StartsWith(baseFileWithoutExtension) == false) { @@ -812,7 +812,7 @@ private void InitializeFromOneFile(string baseFile, string curFileName) string suffix = PreserveLogFileNameExtension ? Path.GetExtension(baseFile).ToLowerInvariant() : ""; - var curFileNameWithoutDir = Path.GetFileName(curFileName); + string curFileNameWithoutDir = Path.GetFileName(curFileName); if (!curFileNameWithoutDir.StartsWith(prefix) || !curFileNameWithoutDir.EndsWith(suffix)) { LogLog.Debug(_declaringType, $"Ignoring file [{curFileName}] because it is from a different date period"); diff --git a/src/log4net/Util/Log4NetAssert.cs b/src/log4net/Util/Log4NetAssert.cs index dda240d4..28ea8894 100644 --- a/src/log4net/Util/Log4NetAssert.cs +++ b/src/log4net/Util/Log4NetAssert.cs @@ -1,4 +1,4 @@ -#region Apache License +#region Apache License // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with From 74cdf5377a05d3993a032ffaf73cb52e8434b89e Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Thu, 3 Jul 2025 00:14:21 +0200 Subject: [PATCH 2/3] stabilize RemoteSyslogTest --- src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs b/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs index bc5ea526..2a6c94fb 100644 --- a/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs +++ b/src/log4net.Tests/Appender/RemoteSyslogAppenderTest.cs @@ -18,6 +18,7 @@ #endregion using System.Text; +using System.Threading; using log4net.Appender; using log4net.Appender.Internal; using log4net.Core; @@ -64,11 +65,17 @@ public void RemoteSyslogTest() Domain = "TestDomain", }); appender.DoAppend(loggingEvent); + for (int i = 0; i < 20; i++) + { + if (appender.Mock.Sent.Count == 0) + { + Thread.Sleep(10); + } + } appender.Close(); Assert.That(appender.Mock.ConnectedTo, Is.EqualTo((0, ipAddress, 514))); Assert.That(appender.Mock.Sent, Has.Count.EqualTo(1)); Assert.That(appender.Mock.WasDisposed, Is.True); - Assert.That(appender.Mock.Sent, Has.Count.EqualTo(1)); const string expectedData = @"<14>TestDomain: INFO - Test message"; Assert.That(Encoding.ASCII.GetString(appender.Mock.Sent[0].Datagram), Is.EqualTo(expectedData)); } From 3ba18066115c4f7cb768023f8bb12a4c74c4a3c3 Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Thu, 3 Jul 2025 00:18:31 +0200 Subject: [PATCH 3/3] seal class MockDateTime --- src/log4net.Tests/Integration/MockDateTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/log4net.Tests/Integration/MockDateTime.cs b/src/log4net.Tests/Integration/MockDateTime.cs index b7ee01d5..efa67d7a 100644 --- a/src/log4net.Tests/Integration/MockDateTime.cs +++ b/src/log4net.Tests/Integration/MockDateTime.cs @@ -25,7 +25,7 @@ namespace log4net.Tests.Integration; /// /// Mock implementation of /// -internal class MockDateTime : RollingFileAppender.IDateTime +internal sealed class MockDateTime : RollingFileAppender.IDateTime { /// /// Offset to apply to the current time.