diff --git a/Build.ps1 b/Build.ps1 index 7989cd2..080f880 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -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 } \ No newline at end of file diff --git a/serilog-settings-configuration.sln b/serilog-settings-configuration.sln index a0f41b6..f0209fd 100644 --- a/serilog-settings-configuration.sln +++ b/serilog-settings-configuration.sln @@ -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 @@ -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 @@ -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} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs index 7fcbe38..9582e6a 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs @@ -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) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/CompositeAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/CompositeAssemblyFinder.cs new file mode 100644 index 0000000..9f3c9ef --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/CompositeAssemblyFinder.cs @@ -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 FindAssembliesContainingName(string nameToFind) + { + var assemblyNames = new List(); + foreach (var assemblyFinder in _assemblyFinders) + { + assemblyNames.AddRange(assemblyFinder.FindAssembliesContainingName(nameToFind)); + } + return assemblyNames; + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs index 13e24f2..2ed07ce 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs @@ -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 FindAssembliesContainingName(string nameToFind) { + if (_dependencyContext == null) + return Array.Empty(); + 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) { diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs index 38ff3c4..c38c88b 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs @@ -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) { diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index 931391c..1f8ff5a 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -369,7 +369,7 @@ static IReadOnlyCollection 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!)) @@ -384,7 +384,19 @@ static IReadOnlyCollection LoadConfigurationAssemblies(IConfiguration assemblies.Add(assumed.FullName!, 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.Values; } void CallConfigurationMethods(ILookup> methods, IReadOnlyCollection configurationMethods, object receiver) diff --git a/test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs b/test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs new file mode 100644 index 0000000..2197e78 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs @@ -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 +{ + 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 + { + public PublishModeTheoryData() + { + foreach (var publishMode in PublishModeExtensions.GetPublishModes()) + { + Add(publishMode); + } + } + } + + class PublishModeAndStrategyTheoryData : TheoryData + { + public PublishModeAndStrategyTheoryData() + { + foreach (var publishMode in PublishModeExtensions.GetPublishModes()) + { + foreach (var strategy in new[] { "using", "assembly" }) + { + Add(publishMode, strategy); + } + } + } + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj index d75874e..f1915c1 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -1,8 +1,8 @@ - + net48 - $(TargetFrameworks);net7.0 + $(TargetFrameworks);net7.0;net6.0 @@ -17,8 +17,12 @@ + + + + diff --git a/test/Serilog.Settings.Configuration.Tests/Support/DirectoryInfoExtensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..2d2e214 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/DirectoryInfoExtensions.cs @@ -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()))); +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs b/test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs new file mode 100644 index 0000000..082d1df --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs @@ -0,0 +1,25 @@ +namespace Serilog.Settings.Configuration.Tests.Support; + +/// +/// The possible application publish modes for the TestApp. +/// See also the .NET application publishing overview documentation. +/// +public enum PublishMode +{ + /// + /// Standard app publish, all dlls and related files are copied along the main executable. + /// + Standard, + + /// + /// Publish a single file as a framework-dependent binary. + /// + /// On .NET Framework, Costura is used to publish as a single file. + SingleFile, + + /// + /// Publish a single file as a self contained binary, i.e. including the .NET libraries and target runtime. + /// + /// This mode is ignored on .NET Framework as it doesn't make sense. + SelfContained, +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/PublishModeExtensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/PublishModeExtensions.cs new file mode 100644 index 0000000..d3f4982 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/PublishModeExtensions.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.Versioning; +using NuGet.Frameworks; + +namespace Serilog.Settings.Configuration.Tests.Support; + +public static class PublishModeExtensions +{ + static PublishModeExtensions() + { + var targetFrameworkAttribute = typeof(TestApp).Assembly.GetCustomAttribute(); + if (targetFrameworkAttribute == null) + { + throw new Exception($"Assembly {typeof(TestApp).Assembly} does not have a {nameof(TargetFrameworkAttribute)}"); + } + + var framework = NuGetFramework.Parse(targetFrameworkAttribute.FrameworkName); + + TargetFramework = framework.GetShortFolderName(); + IsDesktop = framework.IsDesktop(); + } + + public static bool IsDesktop { get; } + + public static string TargetFramework { get; } + + public static IEnumerable GetPublishModes() + { + return IsDesktop ? new[] { PublishMode.Standard, PublishMode.SingleFile } : Enum.GetValues(typeof(PublishMode)).Cast(); + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/TestApp.cs b/test/Serilog.Settings.Configuration.Tests/Support/TestApp.cs new file mode 100644 index 0000000..913bbe2 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/TestApp.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using CliWrap; +using CliWrap.Exceptions; +using FluentAssertions; +using Polly; +using Xunit.Abstractions; +using Xunit.Sdk; +using static Serilog.Settings.Configuration.Tests.Support.PublishModeExtensions; + +namespace Serilog.Settings.Configuration.Tests.Support; + +public class TestApp : IAsyncLifetime +{ + readonly IMessageSink _messageSink; + readonly DirectoryInfo _workingDirectory; + readonly Dictionary _executables; + + public TestApp(IMessageSink messageSink) + { + _messageSink = messageSink; + _workingDirectory = GetDirectory("test", $"TestApp-{TargetFramework}"); + _workingDirectory.Create(); + foreach (var file in GetDirectory("test", "TestApp").EnumerateFiles()) + { + file.CopyTo(_workingDirectory.File(file.Name).FullName, overwrite: true); + } + _executables = new Dictionary(); + } + + async Task IAsyncLifetime.InitializeAsync() + { + // Retry 3 times because pack / restore / publish may try to access the same files across different target frameworks and fail with System.IO.IOException: + // The process cannot access the file [Serilog.Settings.Configuration.deps.json or Serilog.Settings.Configuration.dll] because it is being used by another process. + var retryPolicy = Policy.Handle().RetryAsync(3); + await retryPolicy.ExecuteAsync(CreateTestAppAsync); + } + + Task IAsyncLifetime.DisposeAsync() + { + _workingDirectory.Delete(recursive: true); + return Task.CompletedTask; + } + + public string GetExecutablePath(PublishMode publishMode) => _executables[publishMode].FullName; + + async Task CreateTestAppAsync() + { + // It might be tempting to do pack -> restore -> build --no-restore -> publish --no-build (and parallelize over publish modes) + // But this would fail because of https://github.com/dotnet/sdk/issues/17526 and probably because of other unforeseen bugs + // preventing from running multiple `dotnet publish` commands with different parameters. + + await PackAsync(); + await RestoreAsync(); + foreach (var publishMode in GetPublishModes()) + { + await PublishAsync(publishMode); + } + } + + async Task PackAsync() + { + var projectFile = GetFile("src", "Serilog.Settings.Configuration", "Serilog.Settings.Configuration.csproj"); + var packArgs = new[] { + "pack", projectFile.FullName, + "--configuration", "Release", + "--output", _workingDirectory.FullName, + "-p:Version=0.0.0-IntegrationTest.0", + }; + await RunDotnetAsync(_workingDirectory, packArgs); + } + + async Task RestoreAsync() + { + // Can't use "--source . --source https://api.nuget.org/v3/index.json" because of https://github.com/dotnet/sdk/issues/27202 => a nuget.config file is used instead. + // It also has the benefit of using settings _only_ from the specified config file, ignoring the global nuget.config where package source mapping could interfere with the local source. + var restoreArgs = new[] { + "restore", + "--configfile", "nuget.config", + "-p:Configuration=Release", + $"-p:TargetFramework={TargetFramework}", + }; + await RunDotnetAsync(_workingDirectory, restoreArgs); + } + + async Task PublishAsync(PublishMode publishMode) + { + var publishDirectory = _workingDirectory.SubDirectory("publish"); + var fodyWeaversXml = _workingDirectory.File("FodyWeavers.xml"); + + var outputDirectory = publishDirectory.SubDirectory(publishMode.ToString()); + + File.WriteAllText(fodyWeaversXml.FullName, publishMode == PublishMode.SingleFile && IsDesktop ? "" : ""); + + var publishArgs = new[] { + "publish", + "--no-restore", + "--configuration", "Release", + "--output", outputDirectory.FullName, + $"-p:TargetFramework={TargetFramework}" + }; + var publishSingleFile = $"-p:PublishSingleFile={publishMode is PublishMode.SingleFile or PublishMode.SelfContained}"; + var selfContained = $"-p:SelfContained={publishMode is PublishMode.SelfContained}"; + await RunDotnetAsync(_workingDirectory, IsDesktop ? publishArgs : publishArgs.Append(publishSingleFile).Append(selfContained).ToArray()); + + var executableFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "TestApp.exe" : "TestApp"; + var executableFile = new FileInfo(Path.Combine(outputDirectory.FullName, executableFileName)); + executableFile.Exists.Should().BeTrue(); + var dlls = executableFile.Directory!.EnumerateFiles("*.dll"); + if (publishMode == PublishMode.Standard) + { + dlls.Should().NotBeEmpty(because: $"the test app was _not_ published as single-file ({publishMode})"); + } + else + { + dlls.Should().BeEmpty(because: $"the test app was published as single-file ({publishMode})"); + executableFile.Directory.EnumerateFiles().Should().ContainSingle().Which.FullName.Should().Be(executableFile.FullName); + } + + _executables[publishMode] = executableFile; + } + + async Task RunDotnetAsync(DirectoryInfo workingDirectory, params string[] arguments) + { + _messageSink.OnMessage(new DiagnosticMessage($"cd {workingDirectory}")); + _messageSink.OnMessage(new DiagnosticMessage($"dotnet {string.Join(" ", arguments)}")); + var outBuilder = new StringBuilder(); + var errBuilder = new StringBuilder(); + var command = Cli.Wrap("dotnet") + .WithValidation(CommandResultValidation.None) + .WithWorkingDirectory(workingDirectory.FullName) + .WithArguments(arguments) + .WithStandardOutputPipe(PipeTarget.ToDelegate(line => + { + outBuilder.AppendLine(line); + _messageSink.OnMessage(new DiagnosticMessage($"==> out: {line}")); + })) + .WithStandardErrorPipe(PipeTarget.ToDelegate(line => + { + errBuilder.AppendLine(line); + _messageSink.OnMessage(new DiagnosticMessage($"==> err: {line}")); + })); + + var result = await command.ExecuteAsync(); + if (result.ExitCode != 0) + { + throw new CommandExecutionException(command, result.ExitCode, $"An unexpected exception has occurred while running {command}{Environment.NewLine}{errBuilder}{outBuilder}".Trim()); + } + } + + static DirectoryInfo GetDirectory(params string[] paths) => new(GetFullPath(paths)); + + static FileInfo GetFile(params string[] paths) => new(GetFullPath(paths)); + + static string GetFullPath(params string[] paths) => Path.GetFullPath(Path.Combine(new[] { GetThisDirectory(), "..", "..", ".." }.Concat(paths).ToArray())); + + static string GetThisDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!; +} diff --git a/test/Serilog.Settings.Configuration.Tests/xunit.runner.json b/test/Serilog.Settings.Configuration.Tests/xunit.runner.json new file mode 100644 index 0000000..6c0d1e4 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true +} diff --git a/test/TestApp/.gitignore b/test/TestApp/.gitignore new file mode 100644 index 0000000..1cc29f1 --- /dev/null +++ b/test/TestApp/.gitignore @@ -0,0 +1,2 @@ +FodyWeavers.xml +FodyWeavers.xsd diff --git a/test/TestApp/Program.cs b/test/TestApp/Program.cs new file mode 100644 index 0000000..56a3235 --- /dev/null +++ b/test/TestApp/Program.cs @@ -0,0 +1,56 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Serilog; +using Serilog.Debugging; +using Serilog.Settings.Configuration; + +if (args.Length == 1 && args[0] == "is-single-file") +{ + if (typeof(Program).Assembly.GetManifestResourceNames().Any(e => e.StartsWith("costura."))) + { + Console.WriteLine(true); + return 0; + } + // IL3000: 'System.Reflection.Assembly.Location' always returns an empty string for assemblies embedded in a single-file app +#pragma warning disable IL3000 + Console.WriteLine(string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location)); +#pragma warning restore + return 0; +} + +SelfLog.Enable(Console.Error); + +Thread.CurrentThread.Name = "Main thread"; + +var configurationValues = new Dictionary +{ + ["Serilog:Enrich:0"] = "WithThreadName", + ["Serilog:WriteTo:0:Name"] = "Console", + ["Serilog:WriteTo:0:Args:outputTemplate"] = "({ThreadName}) [{Level}] {Message}{NewLine}", +}; + +if (args.Contains("--using-thread")) configurationValues["Serilog:Using:Thread"] = "Serilog.Enrichers.Thread"; +if (args.Contains("--using-console")) configurationValues["Serilog:Using:Console"] = "Serilog.Sinks.Console"; + +var assemblies = new List(); +if (args.Contains("--assembly-thread")) assemblies.Add(typeof(ThreadLoggerConfigurationExtensions).Assembly); +if (args.Contains("--assembly-console")) assemblies.Add(typeof(ConsoleLoggerConfigurationExtensions).Assembly); + +try +{ + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValues).Build(); + var options = assemblies.Count > 0 ? new ConfigurationReaderOptions(assemblies.ToArray()) : null; + var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration, options).CreateLogger(); + logger.Information("Expected success"); + return 0; +} +catch (InvalidOperationException exception) when (exception.Message.StartsWith("No Serilog:Using configuration section is defined and no Serilog assemblies were found.")) +{ + Console.WriteLine("Expected exception"); + return 0; +} +catch (Exception exception) +{ + Console.Error.WriteLine(exception); + return 1; +} diff --git a/test/TestApp/TestApp.csproj b/test/TestApp/TestApp.csproj new file mode 100644 index 0000000..88f0ca1 --- /dev/null +++ b/test/TestApp/TestApp.csproj @@ -0,0 +1,29 @@ + + + + Exe + net48 + embedded + false + false + false + none + true + + + + + + + + + + + + + + + + + + diff --git a/test/TestApp/nuget.config b/test/TestApp/nuget.config new file mode 100644 index 0000000..cfec8fc --- /dev/null +++ b/test/TestApp/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + + +