1
0

Merge main

This commit is contained in:
2023-03-18 12:44:38 +01:00
11 changed files with 787 additions and 127 deletions

View File

@@ -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: variables:
TZ: "Europe/Berlin" TZ: "Europe/Berlin"
LANG: "de"
stages:
- build
- test
- deploy
build: debug-job:
stage: build
tags:
- docker
script:
- bash build.sh
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 day
test:
stage: test
tags: tags:
- docker - docker
except:
- tags
# branch-coverage # branch-coverage
# coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/' #coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
# line-coverage # line-coverage
coverage: '/Total[^|]*\|\s*([0-9.%]+)/' coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
script: script:
- dotnet test -c Release - dotnet restore --no-cache --force
dependencies: - dotnet build -c Debug --nologo --no-restore --no-incremental
- build - 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: tags:
- docker - docker
only:
- tags
# branch-coverage
#coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
# line-coverage
coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
script: script:
- dotnet nuget push -k $APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg - dotnet restore --no-cache --force
dependencies: - dotnet build -c Release --nologo --no-restore --no-incremental
- build - dotnet test -c Release --nologo --no-restore --no-build
- test - dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate **/*.nupkg

View File

@@ -29,7 +29,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Moq" Version="4.18.2" /> <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3"> <PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
namespace System namespace System
@@ -40,5 +41,13 @@ namespace System
/// <returns>The description or the string representation of the value.</returns> /// <returns>The description or the string representation of the value.</returns>
public static string GetDescription(this Enum value) public static string GetDescription(this Enum value)
=> value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString(); => value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
/// <summary>
/// Returns the name from <see cref="DisplayAttribute"/>.
/// </summary>
/// <param name="value">The enum value.</param>
/// <returns>The display name or the string representation of the value.</returns>
public static string GetDisplayName(this Enum value)
=> value.GetAttribute<DisplayAttribute>()?.Name ?? value.ToString();
} }
} }

View 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; }
}
}
}

View 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()
{
}
}
}

View File

@@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added `directory.build.props` and `directory.build.targets` - `directory.build.props` and `directory.build.targets`
- Added `ArReader` and `ArWriter` for Unix archives - `GetDisplayName()` extension for enums (`DisplayAttribute(Name = "")`)
- Added `TarReader` and `TarWriter` for TAR archives - `FileLogger` as additional `ILogger` implementation (from `Microsoft.Extensions.Logging`)
- `ArReader` and `ArWriter` for Unix archives
- `TarReader` and `TarWriter` for TAR archives
## [v1.10.0](https://git.am-wd.de/AM.WD/common/compare/v1.9.0...v1.10.0) - 2022-09-18 ## [v1.10.0](https://git.am-wd.de/AM.WD/common/compare/v1.9.0...v1.10.0) - 2022-09-18

View File

@@ -1,7 +1,8 @@
using System; using System;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using UnitTests.Common.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTests.Common.Utils;
using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; using DescriptionAttribute = System.ComponentModel.DescriptionAttribute;
namespace UnitTests.Common.Extensions namespace UnitTests.Common.Extensions
@@ -107,6 +108,22 @@ namespace UnitTests.Common.Extensions
Assert.IsFalse(list.Any()); Assert.IsFalse(list.Any());
} }
[TestMethod]
public void ShouldReturnDisplayNameOrStringRepresentation()
{
// arrange
var enumWithDisplayName = TestEnum.Two;
var enumWithoutDisplayName = TestEnum.Zero;
// act
string displayName = enumWithDisplayName.GetDisplayName();
string noDisplayName = enumWithoutDisplayName.GetDisplayName();
// assert
Assert.AreEqual("Zwei", displayName);
Assert.AreEqual(enumWithoutDisplayName.ToString(), noDisplayName);
}
internal enum TestEnum internal enum TestEnum
{ {
[CustomMultiple("nix")] [CustomMultiple("nix")]
@@ -115,6 +132,7 @@ namespace UnitTests.Common.Extensions
Zero, Zero,
[Description("Eins")] [Description("Eins")]
One, One,
[Display(Name = "Zwei")]
Two, Two,
} }
} }

View 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);
}
}
}
}

View File

@@ -9,15 +9,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="3.1.2"> <PackageReference Include="coverlet.msbuild" Version="3.2.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="DNS" Version="7.0.0" /> <PackageReference Include="DNS" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.2" /> <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" /> <PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" /> <PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="ReflectionMagic" Version="4.1.0" /> <PackageReference Include="ReflectionMagic" Version="4.1.0" />
</ItemGroup> </ItemGroup>

View File

@@ -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

View File

@@ -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