using AssertionHelper;
using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using Serilog;
using System;
using System.IO;
using System.Linq;
namespace SerilogConfigurationTests;
///
/// Builds a real Serilog logger from the config Libation generates and writes until it rolls. Asserting on the
/// JSON alone would pass just as happily with a misspelled argument name, which Serilog ignores in silence -
/// and silently not rolling is the bug being fixed.
///
[TestClass]
[DoNotParallelize]
public class LogRolloverTests
{
private string tempDir = string.Empty;
private ILogger originalLogger = Serilog.Log.Logger;
[TestInitialize]
public void Initialize()
{
originalLogger = Serilog.Log.Logger;
tempDir = Path.Combine(Path.GetTempPath(), $"libation-log-rollover-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
}
[TestCleanup]
public void Cleanup()
{
Serilog.Log.CloseAndFlush();
Serilog.Log.Logger = originalLogger;
Configuration.RestoreSingletonInstance();
try
{
Directory.Delete(tempDir, recursive: true);
}
catch (IOException)
{
// A leftover temp directory is not worth failing a test over.
}
}
/// The pre-13.7.9 default: a monthly rolling interval and nothing about size.
private JObject LegacySerilogConfig(long? fileSizeLimitBytes = null)
{
var args = new JObject
{
["path"] = Path.Combine(tempDir, "Log.log"),
["rollingInterval"] = "Month",
["outputTemplate"] = "{Message:lj}{NewLine}"
};
if (fileSizeLimitBytes is not null)
args["fileSizeLimitBytes"] = fileSizeLimitBytes;
return new JObject
{
["MinimumLevel"] = "Information",
["WriteTo"] = new JArray { new JObject { ["Name"] = "File", ["Args"] = args } }
};
}
private string[] LogFiles() => [.. Directory.GetFiles(tempDir, "Log*.log").Order()];
[TestMethod]
public void An_existing_config_is_migrated_and_then_actually_rolls_on_size()
{
var config = Configuration.CreateMockInstance();
// A small limit so the test writes kilobytes rather than tens of megabytes. Libation's own default
// value is asserted in SerilogConfigurationTests; what matters here is that Serilog obeys it.
config.SetNonString(LegacySerilogConfig(fileSizeLimitBytes: 4096), "Serilog");
config.EnsureSerilogConfig();
config.ConfigureLogging();
var line = new string('x', 512);
for (var i = 0; i < 40; i++)
Serilog.Log.Logger.Information(line);
Serilog.Log.CloseAndFlush();
var files = LogFiles();
Assert.IsTrue(files.Length > 1, $"expected the log to roll, found {files.Length} file(s)");
foreach (var file in files)
Assert.IsTrue(new FileInfo(file).Length < 8192, $"{Path.GetFileName(file)} grew past the size limit");
}
[TestMethod]
public void The_generated_default_rolls_on_size_too()
{
var config = Configuration.CreateMockInstance();
config.EnsureSerilogConfig();
var args = (JObject)((JObject)config.GetObject("Serilog")!).SelectToken("$.WriteTo[0].Args")!;
// Serilog ignores an argument it does not recognise, so the names have to match the sink exactly.
args["fileSizeLimitBytes"] = 4096;
args["path"] = Path.Combine(tempDir, "Log.log");
args["outputTemplate"] = "{Message:lj}{NewLine}";
config.SetNonString((JObject)config.GetObject("Serilog")!, "Serilog");
config.ConfigureLogging();
var line = new string('x', 512);
for (var i = 0; i < 40; i++)
Serilog.Log.Logger.Information(line);
Serilog.Log.CloseAndFlush();
var files = LogFiles();
Assert.IsTrue(files.Length > 1, $"expected the log to roll, found {files.Length} file(s)");
}
[TestMethod]
public void Without_size_rolling_a_single_file_grows_unbounded()
{
// The behaviour being fixed, pinned so a future change to the defaults cannot quietly restore it.
var config = Configuration.CreateMockInstance();
var serilog = LegacySerilogConfig(fileSizeLimitBytes: 4096);
var args = (JObject)serilog.SelectToken("$.WriteTo[0].Args")!;
args["outputTemplate"] = "{Message:lj}{NewLine}";
args["rollOnFileSizeLimit"] = false;
config.SetNonString(serilog, "Serilog");
config.EnsureSerilogConfig();
config.ConfigureLogging();
var line = new string('x', 512);
for (var i = 0; i < 40; i++)
Serilog.Log.Logger.Information(line);
Serilog.Log.CloseAndFlush();
// One file, and Serilog stopped writing at the limit rather than starting a new one.
LogFiles().Length.Should().Be(1);
}
[TestMethod]
public void Old_log_files_beyond_the_retained_count_are_deleted()
{
var config = Configuration.CreateMockInstance();
var serilog = LegacySerilogConfig(fileSizeLimitBytes: 4096);
var args = (JObject)serilog.SelectToken("$.WriteTo[0].Args")!;
args["outputTemplate"] = "{Message:lj}{NewLine}";
args["retainedFileCountLimit"] = 2;
config.SetNonString(serilog, "Serilog");
config.EnsureSerilogConfig();
config.ConfigureLogging();
var line = new string('x', 512);
for (var i = 0; i < 100; i++)
Serilog.Log.Logger.Information(line);
Serilog.Log.CloseAndFlush();
LogFiles().Length.Should().Be(2);
}
}