Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Ical.Net.Tests/Ical.Net.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
<NoWarn>$(NoWarn);CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8620;CS8714</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NLog" Version="6.0.2" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.2" />
Expand Down
38 changes: 38 additions & 0 deletions Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Copyright ical.net project maintainers and contributors.
// Licensed under the MIT license.
//
#nullable enable
using System;
using Microsoft.Extensions.Logging;

namespace Ical.Net.Tests.Logging.Adapters;

#pragma warning disable CA2254 // template should be a static expression

internal class MicrosoftLoggerAdapter(ILogger msLogger) : Ical.Net.Logging.ILogger
{
public void Log(Ical.Net.Logging.LogLevel level, string messageTemplate, params object[] args)
=> msLogger.Log(ConvertLogLevel(level), messageTemplate, args);

public void Log(Ical.Net.Logging.LogLevel level, Exception exception, string messageTemplate, params object[] args)
=> msLogger.Log(ConvertLogLevel(level), exception, messageTemplate, args);

public bool IsEnabled(Ical.Net.Logging.LogLevel level) =>
msLogger.IsEnabled(ConvertLogLevel(level));

private static LogLevel ConvertLogLevel(Ical.Net.Logging.LogLevel level)
{
return level switch
{
Ical.Net.Logging.LogLevel.Trace => LogLevel.Trace,
Ical.Net.Logging.LogLevel.Debug => LogLevel.Debug,
Ical.Net.Logging.LogLevel.Information => LogLevel.Information,
Ical.Net.Logging.LogLevel.Warning => LogLevel.Warning,
Ical.Net.Logging.LogLevel.Error => LogLevel.Error,
Ical.Net.Logging.LogLevel.Critical => LogLevel.Critical,
_ => LogLevel.None
};
}
}
#pragma warning restore CA2254
67 changes: 67 additions & 0 deletions Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Copyright ical.net project maintainers and contributors.
// Licensed under the MIT license.
//
#nullable enable
using System;

namespace Ical.Net.Tests.Logging.Adapters;

/// <summary>
/// Adapts an <see cref="Microsoft.Extensions.Logging.ILoggerFactory"/>
/// to the <see cref="Ical.Net.Logging.ILoggerFactory"/> interface.
/// </summary>
/// <remarks>This class provides a bridge between the <see cref="Microsoft.Extensions.Logging"/> framework and the
/// <see cref="Ical.Net.Logging.ILoggerFactory"/> abstraction. It allows the creation of
/// <see cref="Ical.Net.Logging.ILoggerFactory"/> instances using the underlying
/// <see cref="Microsoft.Extensions.Logging.ILoggerFactory"/>.
/// </remarks>
internal class MicrosoftLoggerFactoryAdapter : Ical.Net.Logging.ILoggerFactory
{
private readonly Microsoft.Extensions.Logging.ILoggerFactory _msLoggerFactory;
private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="MicrosoftLoggerFactoryAdapter"/> class, using the specified
/// Microsoft.Extensions.Logging <see cref="Microsoft.Extensions.Logging.ILoggerFactory"/>.
/// </summary>
/// <param name="msLoggerFactory">The <see cref="Microsoft.Extensions.Logging.ILoggerFactory"/> instance used to create loggers.
/// This parameter cannot be <see langword="null"/>.
/// </param>
public MicrosoftLoggerFactoryAdapter(Microsoft.Extensions.Logging.ILoggerFactory msLoggerFactory)
{
_msLoggerFactory = msLoggerFactory;
}

/// <summary>
/// Creates a logger instance for the specified category name.
/// </summary>
/// <param name="categoryName">The name of the category for the logger.</param>
/// <returns>A <see cref="Ical.Net.Logging.ILogger"/> instance configured to log messages for the specified category.</returns>
public Ical.Net.Logging.ILogger CreateLogger(string categoryName)
{
var msLogger = _msLoggerFactory.CreateLogger(categoryName);
return new MicrosoftLoggerAdapter(msLogger);
}

protected virtual void Dispose(bool disposing)
{
if (_disposed) return;

if (disposing)
_msLoggerFactory.Dispose();
_disposed = true;
}

/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~MicrosoftLoggerFactoryAdapter()
{
Dispose(false);
}
}
152 changes: 144 additions & 8 deletions Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,49 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using Ical.Net.DataTypes;
using Ical.Net.Logging;
using Microsoft.Extensions.Logging;
using Ical.Net.Logging.Internal;
using NUnit.Framework;


namespace Ical.Net.Tests.Logging;

// LoggingProvider.SetLoggerFactory is static and gets set in each test,
// so we need to ensure that the tests are not run in parallel.
[Parallelizable(ParallelScope.None)]
internal class LoggingProviderUnitTests
{
[Test]
public void InitializingLoggerFactoryTwice_ShouldThrow()
{
using var logging1 = new TestLoggingProvider(new Options { DebugModeOnly = false });

using (Assert.EnterMultipleScope())
{
Assert.That(LoggingProvider.FactoryIsSet, Is.True);
Assert.That(() =>
{
using var logging2 = new TestLoggingProvider(new Options { DebugModeOnly = false });
}, Throws.InvalidOperationException);
}
}

[Test]
public void LoggerFactoryNotSet_ShouldFallBackToNullLoggerFactory()
{
using var logging1 = new TestLoggingProvider(new Options { DebugModeOnly = false });
LoggingProvider.SetLoggerFactory(null, true);
var logger = LoggingProvider.CreateLogger<LoggingProviderUnitTests>();
using (Assert.EnterMultipleScope())
{
Assert.That(LoggingProvider.LoggerFactory, Is.InstanceOf<NullLoggerFactory>());
Assert.That(logger, Is.InstanceOf<Logger<LoggingProviderUnitTests>>());
Assert.That(logger.IsEnabled(LogLevel.Information), Is.False);
Assert.That(() => LoggingProvider.LoggerFactory.Dispose(), Throws.Nothing);
}
}

[TestCase(true)]
[TestCase(false)]
public void SubmittedLogMessages_ShouldBeInTheLogs(bool useFileTarget)
Expand All @@ -35,7 +68,7 @@ public void SubmittedLogMessages_ShouldBeInTheLogs(bool useFileTarget)
var catLogger = LoggingProvider.CreateLogger(typeof(LoggingProviderUnitTests).FullName!);

catLogger.LogInformation("This is an info message.");
genLogger.LogError(new ArgumentException(), "This is an error message.");
genLogger.LogError(new ArgumentException(),"This is an error message.");
genLogger.LogWarning("This is a warning message with structured data. {Data}", new { Key1 = "Value1", Key2 = "Value2" });
var logs = logging.Logs.ToList();

Expand Down Expand Up @@ -89,6 +122,34 @@ public void OnlyLatestMaxLogsCount_ShouldBeReturned(bool useFileTarget)
}
}

[TestCase(true)]
[TestCase(false)]
public void DebugModeOnly_IsTrue_ShouldOnlyLogWhenDebugging(bool useFileTarget)
{
var logFile = useFileTarget
? Path.ChangeExtension(Path.GetTempFileName(), ".log")
: null;

using var logging = logFile is not null
? new TestLoggingProvider(logFile) // Use file target for logging
: new TestLoggingProvider(); // Use in-memory target for logging

var logger = LoggingProvider.CreateLogger<LoggingProviderUnitTests>();
logger.LogInformation("##{Counter}##", 123);

var logs = logging.Logs.ToList();

if (System.Diagnostics.Debugger.IsAttached)
{
Assert.That(logs, Has.Count.EqualTo(1));
}
else
{
// If not in debug mode, no logs should be present
Assert.That(logs, Is.Empty);
}
}

[Test]
public void MissingFileTargetToReadFrom_ShouldThrow()
{
Expand Down Expand Up @@ -126,8 +187,8 @@ public void AllLogLevels_CanBeUsedAsFilters(bool unrecognizedPattern)
DebugModeOnly = false,
Filters = allLevels.Select(l => new Filter
{
MinLogLevel = l,
MaxLogLevel = l,
MinLogLevel = (Microsoft.Extensions.Logging.LogLevel) l,
MaxLogLevel = (Microsoft.Extensions.Logging.LogLevel) l,
LoggerNamePattern = pattern
}).ToList()
};
Expand All @@ -138,31 +199,38 @@ public void AllLogLevels_CanBeUsedAsFilters(bool unrecognizedPattern)
var logger = LoggingProvider.CreateLogger<LoggingProviderUnitTests>();

// Act: log one message for each level
var exception = new InvalidOperationException("Test exception");
foreach (var level in allLevels)
switch (level)
{
case LogLevel.Trace:
logger.LogTrace("Trace message");
logger.LogTrace(exception, string.Empty);
break;
case LogLevel.Debug:
logger.LogDebug("Debug message");
logger.LogDebug(exception, string.Empty);
break;
case LogLevel.Information:
logger.LogInformation("Info message");
logger.LogInformation(exception, string.Empty);
break;
case LogLevel.Warning:
logger.LogWarning("Warning message");
logger.LogWarning(exception, string.Empty);
break;
case LogLevel.Error:
logger.LogError("Error message");
logger.LogError(exception, string.Empty);
break;
case LogLevel.Critical:
logger.LogCritical("Critical message");
logger.LogCritical(exception, string.Empty);
break;
}

var logs = logging.Logs.ToList();
Assert.That(logs, unrecognizedPattern ? Has.Count.EqualTo(6) : Has.Count.EqualTo(0));
Assert.That(logs, unrecognizedPattern ? Has.Count.EqualTo(12) : Has.Count.EqualTo(0));
}

[Test]
Expand All @@ -175,8 +243,8 @@ public void UsingFilters_ShouldExcludeLogMessages()
[
new Filter
{
MinLogLevel = LogLevel.Error,
MaxLogLevel = LogLevel.Error,
MinLogLevel = (Microsoft.Extensions.Logging.LogLevel) LogLevel.Error,
MaxLogLevel = (Microsoft.Extensions.Logging.LogLevel) LogLevel.Error,
LoggerNamePattern = $"{typeof(LoggingProviderUnitTests).FullName}"
}
]
Expand Down Expand Up @@ -217,6 +285,74 @@ public void LoggingForDebugModeOnly_ShouldLog()
: Has.Count.EqualTo(0));
}

[Test]
public void ConcurrentLogging_ShouldLogAllMessagesCorrectly()
{
// Set up in-memory logging with (using 1 ILoggerFactory)
using var logging = new TestLoggingProvider(new Options { DebugModeOnly = false });

const int threadCount = 10;
const int logsPerThread = 20;
var runningThreads = 0;
var maxParallelThreads = 0;

// Create and start multiple threads to log messages concurrently

var threads = Enumerable.Range(0, threadCount)
.Select(t => new Thread(() => LogThread(t)))
.ToArray();

foreach (var thread in threads)
thread.Start();
foreach (var thread in threads)
thread.Join();

var logs = logging.Logs.ToList();

using (Assert.EnterMultipleScope())
{
Assert.That(logs, Has.Count.EqualTo(threadCount * logsPerThread));
Assert.That(maxParallelThreads, Is.GreaterThan(1), "Threads did not run in parallel");
for (var t = 0; t < threadCount; t++)
{
for (var i = 0; i < logsPerThread; i++)
{
var expected = $"Log {i:D3} - Thread {t:D2}";
Assert.That(logs.Any(log => log.Contains(expected)), Is.True, $"Expected log message '{expected}' not found");
}
}
}

return;

// Local functions

void LogThread(int threadNo)
{
var current = Interlocked.Increment(ref runningThreads);
UpdateMaxParallelThreads(current);

var logger = LoggingProvider.CreateLogger<LoggingProviderUnitTests>();
for (var i = 0; i < logsPerThread; i++)
{
logger.LogInformation("Log {Log:D3} - Thread {Thread:D2}", i, threadNo);
}
Interlocked.Decrement(ref runningThreads);
}

void UpdateMaxParallelThreads(int current)
{
do
{
var prevMax = maxParallelThreads;
if (current > prevMax)
{
Interlocked.CompareExchange(ref maxParallelThreads, current, prevMax);
}
} while (current > maxParallelThreads);
}
}

[Test]
public void DemoForOccurrences()
{
Expand Down
Loading
Loading