Added FileLogger to collection, some CI changes
- Building within one container - Updated UnitTest references - Extending DebugLocal
This commit is contained in:
@@ -1,44 +1,38 @@
|
||||
image: mcr.microsoft.com/dotnet/sdk
|
||||
# The image has to use the same version as the .NET UnitTest project
|
||||
image: mcr.microsoft.com/dotnet/sdk:6.0
|
||||
|
||||
variables:
|
||||
TZ: "Europe/Berlin"
|
||||
LANG: "de"
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
build:
|
||||
stage: build
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- bash build.sh
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*.nupkg
|
||||
- artifacts/*.snupkg
|
||||
expire_in: 1 day
|
||||
|
||||
test:
|
||||
stage: test
|
||||
debug-job:
|
||||
tags:
|
||||
- docker
|
||||
except:
|
||||
- tags
|
||||
# branch-coverage
|
||||
# coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
#coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
# line-coverage
|
||||
coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
|
||||
script:
|
||||
- dotnet test -c Release
|
||||
dependencies:
|
||||
- build
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet build -c Debug --nologo --no-restore --no-incremental
|
||||
- dotnet test -c Debug --nologo --no-restore --no-build
|
||||
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate **/*.nupkg
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
|
||||
release-job:
|
||||
tags:
|
||||
- docker
|
||||
only:
|
||||
- tags
|
||||
# branch-coverage
|
||||
#coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
# line-coverage
|
||||
coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
|
||||
script:
|
||||
- dotnet nuget push -k $APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg
|
||||
dependencies:
|
||||
- build
|
||||
- test
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet build -c Release --nologo --no-restore --no-incremental
|
||||
- dotnet test -c Release --nologo --no-restore --no-build
|
||||
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate **/*.nupkg
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
329
AMWD.Common/Logging/FileLogger.cs
Normal file
329
AMWD.Common/Logging/FileLogger.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
|
||||
|
||||
namespace AMWD.Common.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a file logging based on the <see cref="ILogger"/> interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation is also implementing the <see cref="IDisposable"/> interface!
|
||||
/// <br/>
|
||||
/// Inspired by <a href="https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs" />
|
||||
/// </remarks>
|
||||
/// <seealso cref="ILogger" />
|
||||
/// <seealso cref="IDisposable" />
|
||||
public class FileLogger : ILogger, IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private bool isDisposed = false;
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
|
||||
private readonly StreamWriter fileWriter;
|
||||
private readonly Task writeTask;
|
||||
|
||||
private readonly AsyncQueue<QueueItem> queue = new();
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The log file.</param>
|
||||
/// <param name="append">A value indicating whether to append lines to an existing file (Default: <c>false</c>).</param>
|
||||
/// <param name="encoding">The file encoding (Default: <see cref="Encoding.UTF8"/>).</param>
|
||||
public FileLogger(string file, bool append = false, Encoding encoding = null)
|
||||
{
|
||||
FileName = file;
|
||||
fileWriter = new StreamWriter(FileName, append, encoding ?? Encoding.UTF8);
|
||||
writeTask = Task.Run(() => WriteFileAsync(cancellationTokenSource.Token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new named instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The log file.</param>
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
||||
/// <param name="encoding">The file encoding.</param>
|
||||
public FileLogger(string file, string name, bool append = false, Encoding encoding = null)
|
||||
: this(file, append, encoding)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The log file.</param>
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <param name="parentLogger">The parent logger.</param>
|
||||
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
||||
/// <param name="encoding">The file encoding.</param>
|
||||
public FileLogger(string file, string name, FileLogger parentLogger, bool append = false, Encoding encoding = null)
|
||||
: this(file, name, append, encoding)
|
||||
{
|
||||
ParentLogger = parentLogger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The log file.</param>
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <param name="scopeProvider">The scope provider.</param>
|
||||
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
||||
/// <param name="encoding">The file encoding.</param>
|
||||
public FileLogger(string file, string name, IExternalScopeProvider scopeProvider, bool append = false, Encoding encoding = null)
|
||||
: this(file, name, append, encoding)
|
||||
{
|
||||
ScopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The log file.</param>
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <param name="parentLogger">The parent logger.</param>
|
||||
/// <param name="scopeProvider">The scope provider.</param>
|
||||
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
||||
/// <param name="encoding">The file encoding.</param>
|
||||
public FileLogger(string file, string name, FileLogger parentLogger, IExternalScopeProvider scopeProvider, bool append = false, Encoding encoding = null)
|
||||
: this(file, name, parentLogger, append, encoding)
|
||||
{
|
||||
ScopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the log file.
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the logger.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent logger.
|
||||
/// </summary>
|
||||
public FileLogger ParentLogger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp format.
|
||||
/// </summary>
|
||||
public string TimestampFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the timestamp is in UTC.
|
||||
/// </summary>
|
||||
public bool UseUtcTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum level to log.
|
||||
/// </summary>
|
||||
public LogLevel MinLevel { get; set; }
|
||||
|
||||
internal IExternalScopeProvider ScopeProvider { get; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region ILogger implementation
|
||||
|
||||
/// <inheritdoc cref="ILogger.BeginScope{TState}(TState)" />
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
return ScopeProvider?.Push(state) ?? NullScope.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ILogger.IsEnabled(LogLevel)" />
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
return logLevel >= MinLevel;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception, Func{TState, Exception, string})" />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
if (formatter == null)
|
||||
throw new ArgumentNullException(nameof(formatter));
|
||||
|
||||
string message = formatter(state, exception);
|
||||
|
||||
if (!string.IsNullOrEmpty(message) || exception != null)
|
||||
{
|
||||
if (ParentLogger == null)
|
||||
{
|
||||
WriteMessage(Name, logLevel, eventId.Id, message, exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParentLogger.WriteMessage(Name, logLevel, eventId.Id, message, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion ILogger implementation
|
||||
|
||||
#region IDisposable implementation
|
||||
|
||||
/// <inheritdoc cref="IDisposable.Dispose" />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!isDisposed)
|
||||
{
|
||||
isDisposed = true;
|
||||
|
||||
cancellationTokenSource.Cancel();
|
||||
writeTask.GetAwaiter().GetResult();
|
||||
fileWriter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IDisposable implementation
|
||||
|
||||
#region Private methods
|
||||
|
||||
private void WriteMessage(string name, LogLevel logLevel, int eventId, string message, Exception exception)
|
||||
{
|
||||
queue.Enqueue(new QueueItem
|
||||
{
|
||||
Timestamp = UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now,
|
||||
Name = name,
|
||||
LogLevel = logLevel,
|
||||
EventId = eventId,
|
||||
Message = message,
|
||||
Exception = exception
|
||||
});
|
||||
}
|
||||
|
||||
private async Task WriteFileAsync(CancellationToken token)
|
||||
{
|
||||
string timestampPadding = "";
|
||||
string logLevelPadding = new(' ', 7);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
QueueItem[] items;
|
||||
try
|
||||
{
|
||||
items = await queue.DequeueAvailableAsync(cancellationToken: token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
sb.Clear();
|
||||
|
||||
string timestamp = "";
|
||||
string message = item.Message;
|
||||
|
||||
if (!string.IsNullOrEmpty(TimestampFormat))
|
||||
{
|
||||
timestamp = item.Timestamp.ToString(TimestampFormat) + " | ";
|
||||
sb.Append(timestamp);
|
||||
timestampPadding = new string(' ', timestamp.Length);
|
||||
}
|
||||
|
||||
string logLevel = item.LogLevel switch
|
||||
{
|
||||
LogLevel.Trace => "TRCE | ",
|
||||
LogLevel.Debug => "DBUG | ",
|
||||
LogLevel.Information => "INFO | ",
|
||||
LogLevel.Warning => "WARN | ",
|
||||
LogLevel.Error => "FAIL | ",
|
||||
LogLevel.Critical => "CRIT | ",
|
||||
_ => " | ",
|
||||
};
|
||||
sb.Append(logLevel);
|
||||
logLevelPadding = new string(' ', logLevel.Length);
|
||||
|
||||
if (ScopeProvider != null)
|
||||
{
|
||||
int initLength = sb.Length;
|
||||
|
||||
ScopeProvider.ForEachScope((scope, state) =>
|
||||
{
|
||||
var (builder, length) = state;
|
||||
bool first = length == builder.Length;
|
||||
builder.Append(first ? "=>" : " => ").Append(scope);
|
||||
}, (sb, initLength));
|
||||
|
||||
if (sb.Length > initLength)
|
||||
sb.Insert(initLength, timestampPadding + logLevelPadding);
|
||||
}
|
||||
|
||||
if (item.Exception != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
message += Environment.NewLine + item.Exception.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = item.Exception.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Name))
|
||||
sb.Append($"[{item.Name}] ");
|
||||
|
||||
sb.Append(message.Replace("\n", "\n" + timestampPadding + logLevelPadding));
|
||||
|
||||
await fileWriter.WriteLineAsync(sb.ToString());
|
||||
}
|
||||
|
||||
await fileWriter.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Private methods
|
||||
|
||||
private class QueueItem
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
|
||||
public EventId EventId { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public Exception Exception { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
25
AMWD.Common/Logging/NullScope.cs
Normal file
25
AMWD.Common/Logging/NullScope.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Common.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty scope without any logic.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class NullScope : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="NullScope"/> instance to use.
|
||||
/// </summary>
|
||||
public static NullScope Instance { get; } = new NullScope();
|
||||
|
||||
private NullScope()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added `directory.build.props` and `directory.build.targets`
|
||||
- Added `GetDisplayName()` extension for enums
|
||||
(`DisplayAttribute(Name = "")`)
|
||||
- `directory.build.props` and `directory.build.targets`
|
||||
- `GetDisplayName()` extension for enums (`DisplayAttribute(Name = "")`)
|
||||
- `FileLogger` as additional `ILogger` implementation (from `Microsoft.Extensions.Logging`)
|
||||
|
||||
|
||||
## [v1.10.0](https://git.am-wd.de/AM.WD/common/compare/v1.9.0...v1.10.0) - 2022-09-18
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<NrtRevisionFormat>{semvertag:main}</NrtRevisionFormat>
|
||||
<NrtRevisionFormat>{semvertag:main}{!:-mod}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<Project>
|
||||
<Target Condition="'$(Configuration)' == 'DebugLocal'" Name="MovePackageToLocal" AfterTargets="Pack">
|
||||
<Target Condition="'$(Configuration)' == 'DebugLocal'" Name="CopyPackageToLocal" AfterTargets="Pack">
|
||||
<Delete Files="D:\NuGetLocal\$(PackageId).$(PackageVersion).nupkg" />
|
||||
<Delete Files="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).snupkg" />
|
||||
<Delete Files="D:\NuGetLocal\$(PackageId).$(PackageVersion).snupkg" />
|
||||
|
||||
<Move SourceFiles="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).nupkg" DestinationFolder="D:\NuGetLocal" />
|
||||
<Copy SourceFiles="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).nupkg" DestinationFolder="D:\NuGetLocal" />
|
||||
<Copy SourceFiles="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).snupkg" DestinationFolder="D:\NuGetLocal" />
|
||||
</Target>
|
||||
|
||||
<Target Condition="'$(Configuration)' == 'DebugLocal'" Name="PushToLocalFeed" AfterTargets="Pack">
|
||||
<Exec Command="dotnet nuget push -s https://nuget.syshorst.de/v3/index.json $(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).nupkg" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
371
UnitTests/Common/Logging/FileLoggerTests.cs
Normal file
371
UnitTests/Common/Logging/FileLoggerTests.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace UnitTests.Common.Logging
|
||||
{
|
||||
[TestClass]
|
||||
public class FileLoggerTests
|
||||
{
|
||||
private Mock<StreamWriter> streamWriterMock;
|
||||
|
||||
private List<string> lines;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
lines = new List<string>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldCreateInstance()
|
||||
{
|
||||
// arrange
|
||||
string path = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// act
|
||||
using var logger = new FileLogger(path);
|
||||
|
||||
// assert
|
||||
Assert.IsNotNull(logger);
|
||||
Assert.IsTrue(File.Exists(path));
|
||||
|
||||
Assert.AreEqual(path, logger.FileName);
|
||||
Assert.AreEqual(LogLevel.Trace, logger.MinLevel);
|
||||
Assert.IsNull(logger.Name);
|
||||
Assert.IsNull(logger.ParentLogger);
|
||||
Assert.IsNull(logger.TimestampFormat);
|
||||
Assert.IsNull(logger.ScopeProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldCreateInstanceAsNamedInstance()
|
||||
{
|
||||
// arrange
|
||||
string name = "NamedInstance";
|
||||
string path = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// act
|
||||
using var logger = new FileLogger(path, name);
|
||||
|
||||
// assert
|
||||
Assert.IsNotNull(logger);
|
||||
Assert.IsTrue(File.Exists(path));
|
||||
|
||||
Assert.AreEqual(path, logger.FileName);
|
||||
Assert.AreEqual(LogLevel.Trace, logger.MinLevel);
|
||||
Assert.AreEqual(name, logger.Name);
|
||||
|
||||
Assert.IsNull(logger.ParentLogger);
|
||||
Assert.IsNull(logger.TimestampFormat);
|
||||
Assert.IsNull(logger.ScopeProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public void ShouldThrowDisposedOnIsEnabled()
|
||||
{
|
||||
// arrange
|
||||
var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
logger.Dispose();
|
||||
logger.IsEnabled(LogLevel.Error);
|
||||
|
||||
// assert - ObjectDisposedException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public void ShouldThrowDisposedOnLog()
|
||||
{
|
||||
// arrange
|
||||
var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
logger.Dispose();
|
||||
logger.Log(LogLevel.None, "Some Message");
|
||||
|
||||
// assert - ObjectDisposedException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public void ShouldThrowDisposedOnBeginScope()
|
||||
{
|
||||
// arrange
|
||||
var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
logger.Dispose();
|
||||
logger.BeginScope("foo");
|
||||
|
||||
// assert - ObjectDisposedException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(LogLevel.Trace, false)]
|
||||
[DataRow(LogLevel.Debug, false)]
|
||||
[DataRow(LogLevel.Information, false)]
|
||||
[DataRow(LogLevel.Warning, true)]
|
||||
[DataRow(LogLevel.Error, true)]
|
||||
[DataRow(LogLevel.Critical, true)]
|
||||
[DataRow(LogLevel.None, true)]
|
||||
public void ShouldReturnIsEnabled(LogLevel logLevel, bool expectedResult)
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
logger.MinLevel = LogLevel.Warning;
|
||||
|
||||
// act
|
||||
bool result = logger.IsEnabled(logLevel);
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(expectedResult, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogMessage()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
logger.Log(LogLevel.Information, "Test Message");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
|
||||
// assert
|
||||
Assert.AreEqual("INFO | Test Message", lines.First());
|
||||
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync(It.IsAny<string>()), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogAllLevels()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
foreach (LogLevel level in Enum.GetValues<LogLevel>())
|
||||
logger.Log(level, "Test Message");
|
||||
|
||||
SpinWait.SpinUntil(() => lines.Count == 7);
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("TRCE | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("DBUG | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("INFO | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("WARN | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("FAIL | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("CRIT | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync(" | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.AtLeastOnce);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogOnlyAboveMinLevel()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
logger.MinLevel = LogLevel.Error;
|
||||
|
||||
// act
|
||||
foreach (LogLevel level in Enum.GetValues<LogLevel>())
|
||||
logger.Log(level, "Test Message");
|
||||
|
||||
SpinWait.SpinUntil(() => lines.Count == 3);
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("FAIL | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("CRIT | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync(" | Test Message"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.AtLeastOnce);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogWithLocalTimestamp()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
logger.UseUtcTimestamp = false;
|
||||
logger.TimestampFormat = "yyyy-MM-dd HH:mm";
|
||||
|
||||
// act
|
||||
logger.LogWarning("Some Warning");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
|
||||
// assert
|
||||
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync($"{DateTime.Now:yyyy-MM-dd HH:mm} | WARN | Some Warning"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogWithUtcTimestamp()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
logger.UseUtcTimestamp = true;
|
||||
logger.TimestampFormat = "yyyy-MM-dd HH:mm";
|
||||
|
||||
// act
|
||||
logger.LogWarning("Some Warning");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync($"{DateTime.UtcNow:yyyy-MM-dd HH:mm} | WARN | Some Warning"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldUseParentLogger()
|
||||
{
|
||||
// arrange
|
||||
string file = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
using var parent = GetFileLogger();
|
||||
parent.UseUtcTimestamp = false;
|
||||
parent.TimestampFormat = "yyyy-MM-dd HH:mm";
|
||||
|
||||
using var logger = new FileLogger(file, "NamedInstance", parent);
|
||||
|
||||
// act
|
||||
logger.LogWarning("Some Warning");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync($"{DateTime.Now:yyyy-MM-dd HH:mm} | WARN | [NamedInstance] Some Warning"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
|
||||
Assert.AreEqual(0, new FileInfo(file).Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldUseScopeProvider()
|
||||
{
|
||||
// arrange
|
||||
var scopeProvider = new Mock<IExternalScopeProvider>();
|
||||
using var logger = GetFileLogger("NamedInstance", scopeProvider.Object);
|
||||
|
||||
// act
|
||||
using (var scope = logger.BeginScope("scope"))
|
||||
{
|
||||
logger.LogError("Test");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
}
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("FAIL | [NamedInstance] Test"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
|
||||
scopeProvider.Verify(sp => sp.Push("scope"), Times.Once);
|
||||
scopeProvider.Verify(sp => sp.ForEachScope(It.IsAny<Action<object, It.IsAnyType>>(), It.IsAny<It.IsAnyType>()), Times.Once);
|
||||
scopeProvider.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogException()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
logger.LogCritical(new Exception("TestException"), "");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync("CRIT | System.Exception: TestException"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldLogExceptionWithMessage()
|
||||
{
|
||||
// arrange
|
||||
using var logger = GetFileLogger();
|
||||
|
||||
// act
|
||||
logger.LogCritical(new Exception("TestException"), "Bad things happen...");
|
||||
SpinWait.SpinUntil(() => lines.Count == 1);
|
||||
|
||||
// assert
|
||||
streamWriterMock.Verify(sw => sw.WriteLineAsync($"CRIT | Bad things happen...{Environment.NewLine} System.Exception: TestException"), Times.Once);
|
||||
streamWriterMock.Verify(sw => sw.FlushAsync(), Times.Once);
|
||||
streamWriterMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
private FileLogger GetFileLogger(string name = null, IExternalScopeProvider scopeProvider = null)
|
||||
{
|
||||
string tmpFilePath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
streamWriterMock = new Mock<StreamWriter>(Stream.Null);
|
||||
streamWriterMock
|
||||
.Setup(sw => sw.WriteLineAsync(It.IsAny<string>()))
|
||||
.Callback<string>(line => lines.Add(line))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
FileLogger fileLogger;
|
||||
if (name == null || scopeProvider == null)
|
||||
{
|
||||
fileLogger = new FileLogger(tmpFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileLogger = new FileLogger(tmpFilePath, name, scopeProvider);
|
||||
}
|
||||
|
||||
var fieldInfo = fileLogger.GetType().GetField("fileWriter", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
(fieldInfo.GetValue(fileLogger) as StreamWriter).Dispose();
|
||||
fieldInfo.SetValue(fileLogger, streamWriterMock.Object);
|
||||
|
||||
return fileLogger;
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tmpFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.msbuild" Version="3.1.2">
|
||||
<PackageReference Include="coverlet.msbuild" Version="3.2.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="DNS" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
<PackageReference Include="ReflectionMagic" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
52
build.cmd
52
build.cmd
@@ -1,52 +0,0 @@
|
||||
@echo off
|
||||
set Configuration=Release
|
||||
|
||||
cd "%~dp0"
|
||||
powershell write-host -fore Blue Restoring solution
|
||||
|
||||
rmdir /S /Q artifacts
|
||||
mkdir artifacts
|
||||
|
||||
dotnet restore -v q
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common"
|
||||
powershell write-host -fore Blue Building AMWD.Common
|
||||
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
move bin\%Configuration%\*.nupkg ..\artifacts
|
||||
move bin\%Configuration%\*.snupkg ..\artifacts
|
||||
|
||||
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common.AspNetCore"
|
||||
powershell write-host -fore Blue Building AMWD.Common.AspNetCore
|
||||
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
move bin\%Configuration%\*.nupkg ..\artifacts
|
||||
move bin\%Configuration%\*.snupkg ..\artifacts
|
||||
|
||||
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common.EntityFrameworkCore"
|
||||
powershell write-host -fore Blue Building AMWD.Common.EntityFrameworkCore
|
||||
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
move bin\%Configuration%\*.nupkg ..\artifacts
|
||||
move bin\%Configuration%\*.snupkg ..\artifacts
|
||||
|
||||
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common.Moq"
|
||||
powershell write-host -fore Blue Building AMWD.Common.Moq
|
||||
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
move bin\%Configuration%\*.nupkg ..\artifacts
|
||||
move bin\%Configuration%\*.snupkg ..\artifacts
|
||||
36
build.sh
36
build.sh
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
CONFIGURATION=Release
|
||||
|
||||
cd "$(dirname "${0}")"
|
||||
rm -rf artifacts
|
||||
mkdir artifacts
|
||||
|
||||
dotnet restore -v q
|
||||
|
||||
pushd AMWD.Common
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
mv bin/${CONFIGURATION}/*.nupkg ../artifacts
|
||||
mv bin/${CONFIGURATION}/*.snupkg ../artifacts
|
||||
popd
|
||||
|
||||
pushd AMWD.Common.AspNetCore
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
mv bin/${CONFIGURATION}/*.nupkg ../artifacts
|
||||
mv bin/${CONFIGURATION}/*.snupkg ../artifacts
|
||||
popd
|
||||
|
||||
pushd AMWD.Common.EntityFrameworkCore
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
mv bin/${CONFIGURATION}/*.nupkg ../artifacts
|
||||
mv bin/${CONFIGURATION}/*.snupkg ../artifacts
|
||||
popd
|
||||
|
||||
pushd AMWD.Common.Moq
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
mv bin/${CONFIGURATION}/*.nupkg ../artifacts
|
||||
mv bin/${CONFIGURATION}/*.snupkg ../artifacts
|
||||
popd
|
||||
Reference in New Issue
Block a user