using GitConverter.Lib.Logging;
using GitConverter.TestsApp.Helpers;
namespace GitConverter.TestsApp.Logging
{
///
/// Integration tests for file-based logging using the Log.SetFile / Log.Enable APIs.
///
///
/// - These tests write to temporary files and therefore are slightly slower and non-CPU-bound.
/// - They disable global logging in Dispose to avoid interfering with other tests.
/// - Keep these tests isolated (consider placing in a separate collection to avoid parallel execution with other tests that modify global logging).
///
public class FileLoggingTests : IDisposable
{
private readonly string _tempDir;
public FileLoggingTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "GitConverter.Tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
// restore logging to no-op to avoid interfering with other tests
Log.Disable();
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, true);
}
catch { /* best-effort cleanup */ }
}
// Small helper to allow log4net to flush writes to disk on CI machines
private static void WaitForFileWrite(string path, int attempts = 10, int delayMs = 100)
{
for (int i = 0; i < attempts; i++)
{
if (File.Exists(path)) return;
Thread.Sleep(delayMs);
}
}
///
/// Verifies Log.SetFile + Log.Enable writes logged messages to the configured file.
///
///
/// - Arrange: set file path under a temp directory and enable logging.
/// - Act: write messages using the static Log API.
/// - Assert: the log file exists and contains the messages.
/// - Cleanup: disable logging in the test teardown.
///
[Fact]
public void Log_SetFile_WritesMessagesToFile()
{
var logFile = Path.Combine(_tempDir, "factory.log");
// configure file logger with default log level (All)
Log.SetFile(logFile);
Log.Enable(); // safe to call; SetFile already configured but call to be explicit
// write some messages
Log.Info("Integration test - info message");
Log.Debug("Integration test - debug message");
Log.Warn("Integration test - warn message");
Log.Error("Integration test - error message", null);
// give log4net a moment to flush the file
WaitForFileWrite(logFile);
// assert file created and contains our messages
Assert.True(File.Exists(logFile), "Log file was not created.");
// Use test helper that opens the file with FileShare.ReadWrite and retries if locked.
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
Assert.Contains("Integration test - info message", content);
Assert.Contains("Integration test - debug message", content);
Assert.Contains("Integration test - warn message", content);
Assert.Contains("Integration test - error message", content);
// cleanup
Log.Disable();
}
///
/// Verifies that Log.SetFile with "Info" level filters out DEBUG messages.
///
[Fact]
public void Log_SetFile_WithInfoLevel_FiltersDebugMessages()
{
var logFile = Path.Combine(_tempDir, "info-level.log");
// configure file logger with Info level
Log.SetFile(logFile, "Info");
Log.Enable();
// write messages at different levels
Log.Debug("This DEBUG message should be filtered");
Log.Info("This INFO message should appear");
Log.Warn("This WARN message should appear");
Log.Error("This ERROR message should appear");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
// DEBUG should be filtered out
Assert.DoesNotContain("This DEBUG message should be filtered", content);
// INFO, WARN, ERROR should be present
Assert.Contains("This INFO message should appear", content);
Assert.Contains("This WARN message should appear", content);
Assert.Contains("This ERROR message should appear", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile with "Warn" level filters out DEBUG and INFO messages.
///
[Fact]
public void Log_SetFile_WithWarnLevel_FiltersDebugAndInfo()
{
var logFile = Path.Combine(_tempDir, "warn-level.log");
// configure file logger with Warn level
Log.SetFile(logFile, "Warn");
Log.Enable();
Log.Debug("DEBUG should be filtered");
Log.Info("INFO should be filtered");
Log.Warn("WARN should appear");
Log.Error("ERROR should appear");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
// DEBUG and INFO should be filtered
Assert.DoesNotContain("DEBUG should be filtered", content);
Assert.DoesNotContain("INFO should be filtered", content);
// WARN and ERROR should be present
Assert.Contains("WARN should appear", content);
Assert.Contains("ERROR should appear", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile with "Error" level filters out DEBUG, INFO, and WARN messages.
///
[Fact]
public void Log_SetFile_WithErrorLevel_FiltersDebugInfoWarn()
{
var logFile = Path.Combine(_tempDir, "error-level.log");
// configure file logger with Error level
Log.SetFile(logFile, "Error");
Log.Enable();
Log.Debug("DEBUG should be filtered");
Log.Info("INFO should be filtered");
Log.Warn("WARN should be filtered");
Log.Error("ERROR should appear");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
// DEBUG, INFO, WARN should be filtered
Assert.DoesNotContain("DEBUG should be filtered", content);
Assert.DoesNotContain("INFO should be filtered", content);
Assert.DoesNotContain("WARN should be filtered", content);
// ERROR should be present
Assert.Contains("ERROR should appear", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile with "Off" level produces no log output.
///
[Fact]
public void Log_SetFile_WithOffLevel_ProducesNoOutput()
{
var logFile = Path.Combine(_tempDir, "off-level.log");
// configure file logger with Off level
Log.SetFile(logFile, "Off");
Log.Enable();
Log.Debug("DEBUG should not appear");
Log.Info("INFO should not appear");
Log.Warn("WARN should not appear");
Log.Error("ERROR should not appear");
WaitForFileWrite(logFile);
// File may be created but should be empty or contain only headers
if (File.Exists(logFile))
{
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
// No log messages should be present
Assert.DoesNotContain("DEBUG should not appear", content);
Assert.DoesNotContain("INFO should not appear", content);
Assert.DoesNotContain("WARN should not appear", content);
Assert.DoesNotContain("ERROR should not appear", content);
}
Log.Disable();
}
///
/// Verifies that Log.SetFile with "Debug" level logs all messages (equivalent to "All").
///
[Fact]
public void Log_SetFile_WithDebugLevel_LogsAllMessages()
{
var logFile = Path.Combine(_tempDir, "debug-level.log");
// configure file logger with Debug level
Log.SetFile(logFile, "Debug");
Log.Enable();
Log.Debug("DEBUG message");
Log.Info("INFO message");
Log.Warn("WARN message");
Log.Error("ERROR message");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
// All messages should be present
Assert.Contains("DEBUG message", content);
Assert.Contains("INFO message", content);
Assert.Contains("WARN message", content);
Assert.Contains("ERROR message", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile with invalid log level defaults to "All".
///
[Fact]
public void Log_SetFile_WithInvalidLevel_DefaultsToAll()
{
var logFile = Path.Combine(_tempDir, "invalid-level.log");
// configure file logger with invalid level
Log.SetFile(logFile, "InvalidLevel");
Log.Enable();
Log.Debug("DEBUG should appear with fallback");
Log.Info("INFO should appear with fallback");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
// Should default to ALL, so DEBUG and INFO should be present
Assert.Contains("DEBUG should appear with fallback", content);
Assert.Contains("INFO should appear with fallback", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile with case-insensitive log level works correctly.
///
[Fact]
public void Log_SetFile_WithCaseInsensitiveLevel_Works()
{
var logFile = Path.Combine(_tempDir, "case-insensitive.log");
// configure file logger with mixed-case level
Log.SetFile(logFile, "iNfO");
Log.Enable();
Log.Debug("DEBUG should be filtered");
Log.Info("INFO should appear");
Log.Warn("WARN should appear");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
Assert.DoesNotContain("DEBUG should be filtered", content);
Assert.Contains("INFO should appear", content);
Assert.Contains("WARN should appear", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile creates UTF-8 encoded file supporting international characters.
///
[Fact]
public void Log_SetFile_SupportsUtf8Encoding()
{
var logFile = Path.Combine(_tempDir, "utf8-test.log");
Log.SetFile(logFile);
Log.Enable();
// Log messages with international characters and emoji
Log.Info("Test with Hebrew: שלום");
Log.Info("Test with Arabic: مرحبا");
Log.Info("Test with Chinese: 你好");
Log.Info("Test with emoji: ✅ 🚀 ❌");
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
// Read file with UTF-8 encoding
var content = File.ReadAllText(logFile, System.Text.Encoding.UTF8);
Assert.Contains("שלום", content);
Assert.Contains("مرحبا", content);
Assert.Contains("你好", content);
Assert.Contains("✅", content);
Assert.Contains("🚀", content);
Assert.Contains("❌", content);
Log.Disable();
}
///
/// Verifies that Log.SetFile with exception logging writes stack traces.
///
[Fact]
public void Log_SetFile_LogsExceptionsWithStackTrace()
{
var logFile = Path.Combine(_tempDir, "exception-test.log");
Log.SetFile(logFile, "Error");
Log.Enable();
try
{
throw new InvalidOperationException("Test exception for logging");
}
catch (Exception ex)
{
Log.Error("An error occurred during test", ex);
}
WaitForFileWrite(logFile);
Assert.True(File.Exists(logFile), "Log file was not created.");
var content = FileTestHelpers.ReadAllTextAllowLocked(logFile);
Assert.Contains("An error occurred during test", content);
Assert.Contains("System.InvalidOperationException", content);
Assert.Contains("Test exception for logging", content);
// Stack trace should be present
Assert.Contains("at ", content); // Stack trace frames start with "at "
Log.Disable();
}
///
/// Verifies that multiple calls to Log.SetFile reset configuration properly.
///
[Fact]
public void Log_SetFile_CalledMultipleTimes_ResetsConfiguration()
{
var logFile1 = Path.Combine(_tempDir, "first.log");
var logFile2 = Path.Combine(_tempDir, "second.log");
// First configuration
Log.SetFile(logFile1, "Info");
Log.Enable();
Log.Info("Message to first log");
WaitForFileWrite(logFile1);
Assert.True(File.Exists(logFile1));
// Second configuration (should reset)
Log.SetFile(logFile2, "Warn");
Log.Enable();
Log.Info("INFO should be filtered in second log");
Log.Warn("Message to second log");
WaitForFileWrite(logFile2);
Assert.True(File.Exists(logFile2));
var content1 = FileTestHelpers.ReadAllTextAllowLocked(logFile1);
var content2 = FileTestHelpers.ReadAllTextAllowLocked(logFile2);
// First log should contain INFO
Assert.Contains("Message to first log", content1);
// Second log should filter INFO but contain WARN
Assert.DoesNotContain("INFO should be filtered in second log", content2);
Assert.Contains("Message to second log", content2);
Log.Disable();
}
}
}