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:
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
- Added `directory.build.props` and `directory.build.targets`
|
- `directory.build.props` and `directory.build.targets`
|
||||||
- Added `GetDisplayName()` extension for enums
|
- `GetDisplayName()` extension for enums (`DisplayAttribute(Name = "")`)
|
||||||
(`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
|
## [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>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<NrtRevisionFormat>{semvertag:main}</NrtRevisionFormat>
|
<NrtRevisionFormat>{semvertag:main}{!:-mod}</NrtRevisionFormat>
|
||||||
|
|
||||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<Project>
|
<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="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>
|
</Target>
|
||||||
</Project>
|
</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>
|
</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>
|
||||||
|
|
||||||
|
|||||||
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