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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ if($LASTEXITCODE -ne 0) { exit 2 }

Write-Output "build: Testing"

& dotnet test test\Serilog.Settings.Configuration.Tests --configuration Release --no-build --no-restore
# Dotnet test doesn't run separate TargetFrameworks in parallel: https://github.com/dotnet/sdk/issues/19147
# Workaround: use `dotnet test` on dlls directly in order to pass the `--parallel` option to vstest.
# The _reported_ runtime is wrong but the _actual_ used runtime is correct, see https://github.com/microsoft/vstest/issues/2037#issuecomment-720549173
& dotnet test test\Serilog.Settings.Configuration.Tests\bin\Release\*\Serilog.Settings.Configuration.Tests.dll --parallel

if($LASTEXITCODE -ne 0) { exit 3 }
5 changes: 5 additions & 0 deletions serilog-settings-configuration.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "sample\Sample\Sam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDummies", "test\TestDummies\TestDummies.csproj", "{B7CF5068-DD19-4868-A268-5280BDE90361}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "test\TestApp\TestApp.csproj", "{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -54,6 +56,8 @@ Global
{B7CF5068-DD19-4868-A268-5280BDE90361}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.Build.0 = Release|Any CPU
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -63,6 +67,7 @@ Global
{F793C6E8-C40A-4018-8884-C97E2BE38A54} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
{A00E5E32-54F9-401A-BBA1-2F6FCB6366CD} = {D24872B9-57F3-42A7-BC8D-F9DA222FCE1B}
{B7CF5068-DD19-4868-A268-5280BDE90361} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {485F8843-42D7-4267-B5FB-20FE9181DEE9}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,7 @@ protected static bool IsCaseInsensitiveMatch(string? text, string textToFind)

public static AssemblyFinder Auto()
{
try
{
// Need to check `Assembly.GetEntryAssembly()` first because
// `DependencyContext.Default` throws an exception when `Assembly.GetEntryAssembly()` returns null
if (Assembly.GetEntryAssembly() != null && DependencyContext.Default != null)
{
return new DependencyContextAssemblyFinder(DependencyContext.Default);
}
}
catch (NotSupportedException) when (typeof(object).Assembly.Location is "") // bundled mode detection
{
}

return new DllScanningAssemblyFinder();
return new CompositeAssemblyFinder(new DependencyContextAssemblyFinder(DependencyContext.Default), new DllScanningAssemblyFinder());
}

public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Reflection;

namespace Serilog.Settings.Configuration.Assemblies;

class CompositeAssemblyFinder : AssemblyFinder
{
readonly AssemblyFinder[] _assemblyFinders;

public CompositeAssemblyFinder(params AssemblyFinder[] assemblyFinders)
{
_assemblyFinders = assemblyFinders;
}

public override IReadOnlyList<AssemblyName> FindAssembliesContainingName(string nameToFind)
{
var assemblyNames = new List<AssemblyName>();
foreach (var assemblyFinder in _assemblyFinders)
{
assemblyNames.AddRange(assemblyFinder.FindAssembliesContainingName(nameToFind));
}
return assemblyNames;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@ namespace Serilog.Settings.Configuration.Assemblies;

sealed class DependencyContextAssemblyFinder : AssemblyFinder
{
readonly DependencyContext _dependencyContext;
readonly DependencyContext? _dependencyContext;

public DependencyContextAssemblyFinder(DependencyContext dependencyContext)
public DependencyContextAssemblyFinder(DependencyContext? dependencyContext)
{
_dependencyContext = dependencyContext ?? throw new ArgumentNullException(nameof(dependencyContext));
_dependencyContext = dependencyContext;
}

public override IReadOnlyList<AssemblyName> FindAssembliesContainingName(string nameToFind)
{
if (_dependencyContext == null)
return Array.Empty<AssemblyName>();

var query = from library in _dependencyContext.RuntimeLibraries
where IsReferencingSerilog(library)
from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext)
where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind)
select assemblyName;

return query.ToList().AsReadOnly();
return query.ToList();

static bool IsReferencingSerilog(Library library)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ where IsCaseInsensitiveMatch(assemblyFileName, nameToFind)
where assemblyName != null
select assemblyName;

return query.ToList().AsReadOnly();
return query.ToList();

static AssemblyName? TryGetAssemblyNameFrom(string path)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSecti
static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfiguration section, AssemblyFinder assemblyFinder)
{
var serilogAssembly = typeof(ILogger).Assembly;
var assemblies = new Dictionary<string, Assembly> { [serilogAssembly.FullName] = serilogAssembly };
var assemblies = new HashSet<Assembly> { serilogAssembly };

var usingSection = section.GetSection("Using");
if (usingSection.GetChildren().Any())
Expand All @@ -368,22 +368,32 @@ static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfiguration
{
if (string.IsNullOrWhiteSpace(simpleName))
throw new InvalidOperationException(
"A zero-length or whitespace assembly name was supplied to a Serilog.Using configuration statement.");
$"A zero-length or whitespace assembly name was supplied to a {usingSection.Path} configuration statement.");

var assembly = Assembly.Load(new AssemblyName(simpleName));
if (!assemblies.ContainsKey(assembly.FullName))
assemblies.Add(assembly.FullName, assembly);
assemblies.Add(assembly);
}
}

foreach (var assemblyName in assemblyFinder.FindAssembliesContainingName("serilog"))
{
var assumed = Assembly.Load(assemblyName);
if (assumed != null && !assemblies.ContainsKey(assumed.FullName))
assemblies.Add(assumed.FullName, assumed);
assemblies.Add(assumed);
}

return assemblies.Values.ToList().AsReadOnly();
if (assemblies.Count == 1)
{
var message = $"""
No {usingSection.Path} configuration section is defined and no Serilog assemblies were found.
This is most likely because the application is published as single-file.
Either add a {usingSection.Path} section or explicitly specify assemblies that contains sinks and other types through the reader options. For example:
var options = new ConfigurationReaderOptions(typeof(ConsoleLoggerConfigurationExtensions).Assembly, typeof(SerilogExpression).Assembly);
new LoggerConfiguration().ReadFrom.Configuration(configuration, options);
""";
throw new InvalidOperationException(message);
}

return assemblies;
}

void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigurationArgumentValue>> methods, IReadOnlyCollection<MethodInfo> configurationMethods, object receiver)
Expand Down
158 changes: 158 additions & 0 deletions test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Diagnostics;
using System.Text;
using CliWrap;
using CliWrap.Exceptions;
using FluentAssertions;
using FluentAssertions.Execution;
using Serilog.Settings.Configuration.Tests.Support;
using Xunit.Abstractions;

namespace Serilog.Settings.Configuration.Tests;

[Trait("Category", "Integration")]
public sealed class PublishSingleFileTests : IDisposable, IClassFixture<TestApp>
{
readonly ITestOutputHelper _outputHelper;
readonly TestApp _testApp;
readonly AssertionScope _scope;

public PublishSingleFileTests(ITestOutputHelper outputHelper, TestApp testApp)
{
_outputHelper = outputHelper;
_testApp = testApp;
_scope = new AssertionScope();
}

public void Dispose()
{
_scope.Dispose();
}

[Theory]
[ClassData(typeof(PublishModeTheoryData))]
public async Task RunTestApp_NoUsingAndNoAssembly(PublishMode publishMode)
{
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode);
stdOut.Should().Be(isSingleFile ? "Expected exception" : "(Main thread) [Information] Expected success");
stdErr.Should().BeEmpty();
}

[Theory]
[ClassData(typeof(PublishModeTheoryData))]
public async Task RunTestApp_UsingConsole(PublishMode publishMode)
{
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--using-console");
stdOut.Should().Be(isSingleFile ? "() [Information] Expected success" : "(Main thread) [Information] Expected success");
if (isSingleFile)
stdErr.Should().Contain("Unable to find a method called WithThreadName");
else
stdErr.Should().BeEmpty();
}

[Theory]
[ClassData(typeof(PublishModeTheoryData))]
public async Task RunTestApp_UsingThread(PublishMode publishMode)
{
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--using-thread");
stdOut.Should().Be(isSingleFile ? "" : "(Main thread) [Information] Expected success");
if (isSingleFile)
stdErr.Should().Contain("Unable to find a method called Console");
else
stdErr.Should().BeEmpty();
}

[Theory]
[ClassData(typeof(PublishModeTheoryData))]
public async Task RunTestApp_AssemblyThread(PublishMode publishMode)
{
var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--assembly-thread");
stdOut.Should().BeEmpty();
stdErr.Should().Contain("Unable to find a method called Console");
}

[Theory]
[ClassData(typeof(PublishModeTheoryData))]
public async Task RunTestApp_AssemblyConsole(PublishMode publishMode)
{
var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--assembly-console");
stdOut.Should().Be("() [Information] Expected success");
stdErr.Should().Contain("Unable to find a method called WithThreadName");
}

[Theory]
[ClassData(typeof(PublishModeAndStrategyTheoryData))]
public async Task RunTestApp_ConsoleAndThread(PublishMode publishMode, string strategy)
{
var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, $"--{strategy}-console", $"--{strategy}-thread");
stdOut.Should().Be("(Main thread) [Information] Expected success");
stdErr.Should().BeEmpty();
}

async Task<(bool IsSingleFile, string StdOut, string StdErr)> RunTestAppAsync(PublishMode publishMode, params string[] args)
{
// Determine whether the app is a _true_ single file, i.e. not a .NET Core 3.x version which
// [extracts bundled files to disk][1] and thus can find dlls.
// [1]: https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/extract.md
var (isSingleFile, _) = await RunTestAppInternalAsync(publishMode, "is-single-file");
var (stdOut, stdErr) = await RunTestAppInternalAsync(publishMode, args);
return (bool.Parse(isSingleFile), stdOut, stdErr);
}

async Task<(string StdOut, string StdErr)> RunTestAppInternalAsync(PublishMode publishMode, params string[] args)
{
var stdOutBuilder = new StringBuilder();
var stdErrBuilder = new StringBuilder();

var command = Cli.Wrap(_testApp.GetExecutablePath(publishMode))
.WithArguments(args)
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuilder))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuilder));

_outputHelper.WriteLine(command.ToString());

var stopwatch = Stopwatch.StartNew();
var result = await command.ExecuteAsync();
var executionTime = stopwatch.ElapsedMilliseconds;

var stdOut = stdOutBuilder.ToString().Trim();
var stdErr = stdErrBuilder.ToString().Trim();

_outputHelper.WriteLine($"Executed in {executionTime} ms");
_outputHelper.WriteLine(stdOut.Length > 0 ? $"stdout: {stdOut}" : "nothing on stdout");
_outputHelper.WriteLine(stdErr.Length > 0 ? $"stderr: {stdErr}" : "nothing on stderr");
_outputHelper.WriteLine("");

if (result.ExitCode != 0)
{
throw new CommandExecutionException(command, result.ExitCode, $"An unexpected exception has occurred while running {command}{Environment.NewLine}{stdErr}".Trim());
}

return (stdOut, stdErr);
}

class PublishModeTheoryData : TheoryData<PublishMode>
{
public PublishModeTheoryData()
{
foreach (var publishMode in PublishModeExtensions.GetPublishModes())
{
Add(publishMode);
}
}
}

class PublishModeAndStrategyTheoryData : TheoryData<PublishMode, string>
{
public PublishModeAndStrategyTheoryData()
{
foreach (var publishMode in PublishModeExtensions.GetPublishModes())
{
foreach (var strategy in new[] { "using", "assembly" })
{
Add(publishMode, strategy);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT'">net48</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworks);net7.0;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworks);net7.0;net6.0;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand All @@ -17,8 +17,12 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.0" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NuGet.Frameworks" Version="6.5.0" />
<PackageReference Include="Polly" Version="7.2.3" />
<PackageReference Include="Serilog.Expressions" Version="3.3.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageReference Include="xunit" Version="2.4.2" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Serilog.Settings.Configuration.Tests;

static class DirectoryInfoExtensions
{
public static DirectoryInfo SubDirectory(this DirectoryInfo directory, params string[] paths)
=> new(Path.GetFullPath(Path.Combine(paths.Prepend(directory.FullName).ToArray())));

public static FileInfo File(this DirectoryInfo directory, params string[] paths)
=> new(Path.GetFullPath(Path.Combine(paths.Prepend(directory.FullName).ToArray())));
}
25 changes: 25 additions & 0 deletions test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Serilog.Settings.Configuration.Tests.Support;

/// <summary>
/// The possible application publish modes for the TestApp.
/// See also the <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/">.NET application publishing overview</a> documentation.
/// </summary>
public enum PublishMode
{
/// <summary>
/// Standard app publish, all dlls and related files are copied along the main executable.
/// </summary>
Standard,

/// <summary>
/// Publish a single file as a framework-dependent binary.
/// </summary>
/// <remarks>On .NET Framework, <a href="https://github.com/Fody/Costura">Costura</a> is used to publish as a single file.</remarks>
SingleFile,

/// <summary>
/// Publish a single file as a self contained binary, i.e. including the .NET libraries and target runtime.
/// </summary>
/// <remarks>This mode is ignored on .NET Framework as it doesn't make sense.</remarks>
SelfContained,
}
Loading