Skip to content

Commit 431413a

Browse files
committed
Add integration tests
1 parent 140e365 commit 431413a

File tree

8 files changed

+351
-0
lines changed

8 files changed

+351
-0
lines changed

serilog-settings-configuration.sln

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "sample\Sample\Sam
3232
EndProject
3333
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDummies", "test\TestDummies\TestDummies.csproj", "{B7CF5068-DD19-4868-A268-5280BDE90361}"
3434
EndProject
35+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "test\TestApp\TestApp.csproj", "{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}"
36+
EndProject
3537
Global
3638
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3739
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,8 @@ Global
5456
{B7CF5068-DD19-4868-A268-5280BDE90361}.Debug|Any CPU.Build.0 = Debug|Any CPU
5557
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.ActiveCfg = Release|Any CPU
5658
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.Build.0 = Release|Any CPU
59+
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60+
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Release|Any CPU.ActiveCfg = Release|Any CPU
5761
EndGlobalSection
5862
GlobalSection(SolutionProperties) = preSolution
5963
HideSolutionNode = FALSE
@@ -63,6 +67,7 @@ Global
6367
{F793C6E8-C40A-4018-8884-C97E2BE38A54} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
6468
{A00E5E32-54F9-401A-BBA1-2F6FCB6366CD} = {D24872B9-57F3-42A7-BC8D-F9DA222FCE1B}
6569
{B7CF5068-DD19-4868-A268-5280BDE90361} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
70+
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
6671
EndGlobalSection
6772
GlobalSection(ExtensibilityGlobals) = postSolution
6873
SolutionGuid = {485F8843-42D7-4267-B5FB-20FE9181DEE9}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Text;
2+
using CliWrap;
3+
using FluentAssertions;
4+
using FluentAssertions.Execution;
5+
6+
namespace Serilog.Settings.Configuration.Tests;
7+
8+
public sealed class PublishSingleFileTests : IDisposable, IClassFixture<TestApp>
9+
{
10+
readonly TestApp _testApp;
11+
readonly AssertionScope _scope;
12+
13+
public PublishSingleFileTests(TestApp testApp)
14+
{
15+
_testApp = testApp;
16+
_scope = new AssertionScope();
17+
}
18+
19+
public void Dispose()
20+
{
21+
_scope.Dispose();
22+
}
23+
24+
[Theory]
25+
[CombinatorialData]
26+
public async Task RunTestApp_NoUsingAndNoAssembly(bool singleFile)
27+
{
28+
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(singleFile);
29+
stdOut.Should().Be(isSingleFile ? "Expected exception" : "(Main thread) [Information] Expected success");
30+
stdErr.Should().BeEmpty();
31+
}
32+
33+
[Theory]
34+
[CombinatorialData]
35+
public async Task RunTestApp_UsingConsole(bool singleFile)
36+
{
37+
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(singleFile, "--using-console");
38+
stdOut.Should().Be(isSingleFile ? "() [Information] Expected success" : "(Main thread) [Information] Expected success");
39+
if (isSingleFile)
40+
stdErr.Should().Contain("Unable to find a method called WithThreadName");
41+
else
42+
stdErr.Should().BeEmpty();
43+
}
44+
45+
[Theory]
46+
[CombinatorialData]
47+
public async Task RunTestApp_UsingThread(bool singleFile)
48+
{
49+
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(singleFile, "--using-thread");
50+
stdOut.Should().Be(isSingleFile ? "" : "(Main thread) [Information] Expected success");
51+
if (isSingleFile)
52+
stdErr.Should().Contain("Unable to find a method called Console");
53+
else
54+
stdErr.Should().BeEmpty();
55+
}
56+
57+
[Theory]
58+
[CombinatorialData]
59+
public async Task RunTestApp_AssemblyThread(bool singleFile)
60+
{
61+
var (_, stdOut, stdErr) = await RunTestAppAsync(singleFile, "--assembly-thread");
62+
stdOut.Should().BeEmpty();
63+
stdErr.Should().Contain("Unable to find a method called Console");
64+
}
65+
66+
[Theory]
67+
[CombinatorialData]
68+
public async Task RunTestApp_AssemblyConsole(bool singleFile)
69+
{
70+
var (_, stdOut, stdErr) = await RunTestAppAsync(singleFile, "--assembly-console");
71+
stdOut.Should().Be("() [Information] Expected success");
72+
stdErr.Should().Contain("Unable to find a method called WithThreadName");
73+
}
74+
75+
[Theory]
76+
[CombinatorialData]
77+
public async Task RunTestApp_ConsoleAndThread(bool singleFile, [CombinatorialValues("using", "assembly")] string strategy)
78+
{
79+
var (_, stdOut, stdErr) = await RunTestAppAsync(singleFile, $"--{strategy}-console", $"--{strategy}-thread");
80+
stdOut.Should().Be("(Main thread) [Information] Expected success");
81+
stdErr.Should().BeEmpty();
82+
}
83+
84+
async Task<(bool IsSingleFile, string StdOut, string StdErr)> RunTestAppAsync(bool singleFile, params string[] args)
85+
{
86+
// Determine whether the app is a _true_ single file, i.e. not a .NET Core 3.x version which
87+
// [extracts bundled files to disk][1] and thus can find dlls.
88+
// [1]: https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/extract.md
89+
var (isSingleFile, _) = await RunTestAppInternalAsync(singleFile, "is-single-file");
90+
var (stdOut, stdErr) = await RunTestAppInternalAsync(singleFile, args);
91+
return (bool.Parse(isSingleFile), stdOut, stdErr);
92+
}
93+
94+
async Task<(string StdOut, string StdErr)> RunTestAppInternalAsync(bool singleExe, params string[] args)
95+
{
96+
var stdout = new StringBuilder();
97+
var stderr = new StringBuilder();
98+
var testAppPath = singleExe ? _testApp.SingleFileExe.FullName : _testApp.StandardExe.FullName;
99+
var result = await Cli.Wrap(testAppPath)
100+
.WithArguments(args)
101+
.WithValidation(CommandResultValidation.None)
102+
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdout))
103+
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stderr))
104+
.ExecuteAsync();
105+
106+
if (result.ExitCode != 0)
107+
{
108+
throw new Exception($"An unexpected exception has occurred while running {testAppPath}. {stderr}");
109+
}
110+
111+
return (stdout.ToString().Trim(), stderr.ToString().Trim());
112+
}
113+
}

test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
</ItemGroup>
1818

1919
<ItemGroup>
20+
<PackageReference Include="CliWrap" Version="3.6.0" />
21+
<PackageReference Include="DistributedLock.FileSystem" Version="1.0.1" />
22+
<PackageReference Include="FluentAssertions" Version="6.10.0" />
2023
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
2124
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
25+
<PackageReference Include="NuGet.Frameworks" Version="6.5.0" />
2226
<PackageReference Include="Serilog.Expressions" Version="3.3.0" />
27+
<PackageReference Include="Xunit.Combinatorial" Version="1.5.25" />
2328
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
2429
<PackageReference Include="xunit" Version="2.4.2" />
2530
<PackageReference Include="Shouldly" Version="4.1.0" />
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
using System.Runtime.Versioning;
5+
using System.Xml.Linq;
6+
using System.Xml.XPath;
7+
using CliWrap;
8+
using FluentAssertions;
9+
using Medallion.Threading;
10+
using Medallion.Threading.FileSystem;
11+
using NuGet.Frameworks;
12+
using Xunit.Abstractions;
13+
using Xunit.Sdk;
14+
15+
namespace Serilog.Settings.Configuration.Tests;
16+
17+
public class TestApp : IAsyncLifetime
18+
{
19+
readonly IMessageSink _messageSink;
20+
readonly DirectoryInfo _workingDirectory;
21+
readonly List<DirectoryInfo> _directoriesToCleanup;
22+
readonly IDistributedLock _lock;
23+
IDistributedSynchronizationHandle? _lockHandle;
24+
25+
public TestApp(IMessageSink messageSink)
26+
{
27+
_messageSink = messageSink;
28+
_workingDirectory = GetDirectory("test", "TestApp");
29+
_directoriesToCleanup = new List<DirectoryInfo>();
30+
_lock = new FileDistributedLock(new FileInfo(Path.Combine(_workingDirectory.FullName, "dotnet-restore.lock")));
31+
}
32+
33+
public async Task InitializeAsync()
34+
{
35+
_lockHandle = await _lock.AcquireAsync();
36+
37+
var targetFrameworkAttribute = typeof(TestApp).Assembly.GetCustomAttribute<TargetFrameworkAttribute>();
38+
if (targetFrameworkAttribute == null)
39+
{
40+
throw new Exception($"Assembly {typeof(TestApp).Assembly} does not have a {nameof(TargetFrameworkAttribute)}");
41+
}
42+
43+
var targetFramework = NuGetFramework.Parse(targetFrameworkAttribute.FrameworkName);
44+
foreach (var singleFile in new[] { true, false })
45+
{
46+
var framework = targetFramework.GetShortFolderName();
47+
var isDesktop = targetFramework.IsDesktop();
48+
49+
var outputDirectory = new DirectoryInfo(Path.Combine(_workingDirectory.FullName, framework, singleFile ? "publish-single-file" : "publish-standard"));
50+
_directoriesToCleanup.Add(outputDirectory.Parent!);
51+
52+
var restoreArgs = new[] { "restore", "--no-dependencies", $"-p:TargetFrameworks={string.Join("%3B", GetProjectTargetFrameworks().Append(framework).Distinct())}" };
53+
await RunDotnetAsync(_workingDirectory, restoreArgs);
54+
55+
File.WriteAllText(Path.Combine(_workingDirectory.FullName, "FodyWeavers.xml"), singleFile && isDesktop ? "<Weavers><Costura/></Weavers>" : "<Weavers/>");
56+
57+
var args = new[] { "publish", "--no-restore", "--configuration", "Release", "--output", outputDirectory.FullName, $"-p:TargetFramework={framework}" };
58+
await RunDotnetAsync(_workingDirectory, isDesktop ? args : args.Append($"-p:PublishSingleFile={singleFile}").ToArray());
59+
60+
var executableFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "TestApp.exe" : "TestApp";
61+
var executableFile = new FileInfo(Path.Combine(outputDirectory.FullName, executableFileName));
62+
executableFile.Exists.Should().BeTrue();
63+
var dlls = executableFile.Directory!.EnumerateFiles("*.dll");
64+
if (singleFile)
65+
{
66+
dlls.Should().BeEmpty(because: "the test app was published as single-file");
67+
executableFile.Directory.EnumerateFiles().Should().ContainSingle().Which.FullName.Should().Be(executableFile.FullName);
68+
SingleFileExe = executableFile;
69+
}
70+
else
71+
{
72+
dlls.Should().NotBeEmpty(because: "the test app was _not_ published as single-file");
73+
StandardExe = executableFile;
74+
}
75+
}
76+
}
77+
78+
public async Task DisposeAsync()
79+
{
80+
try
81+
{
82+
foreach (var directoryToCleanup in _directoriesToCleanup.Where(e => e.Exists))
83+
{
84+
directoryToCleanup.Delete(recursive: true);
85+
}
86+
}
87+
finally
88+
{
89+
await _lockHandle!.DisposeAsync();
90+
}
91+
}
92+
93+
public FileInfo SingleFileExe { get; private set; } = null!;
94+
public FileInfo StandardExe { get; private set; } = null!;
95+
96+
async Task RunDotnetAsync(DirectoryInfo workingDirectory, params string[] arguments)
97+
{
98+
_messageSink.OnMessage(new DiagnosticMessage($"cd {workingDirectory}"));
99+
_messageSink.OnMessage(new DiagnosticMessage($"dotnet {string.Join(" ", arguments)}"));
100+
var messageSinkTarget = PipeTarget.ToDelegate(line => _messageSink.OnMessage(new DiagnosticMessage(line)));
101+
await Cli.Wrap("dotnet")
102+
.WithWorkingDirectory(workingDirectory.FullName)
103+
.WithArguments(arguments)
104+
.WithStandardOutputPipe(messageSinkTarget)
105+
.WithStandardErrorPipe(messageSinkTarget)
106+
.ExecuteAsync();
107+
}
108+
109+
static IEnumerable<string> GetProjectTargetFrameworks()
110+
{
111+
var projectFile = GetFile("src", "Serilog.Settings.Configuration", "Serilog.Settings.Configuration.csproj");
112+
var project = XDocument.Load(projectFile.FullName);
113+
var targetFrameworks = project.XPathSelectElement("/Project/PropertyGroup/TargetFrameworks") ?? throw new Exception($"TargetFrameworks element not found in {projectFile}");
114+
return targetFrameworks.Value.Split(';');
115+
}
116+
117+
static DirectoryInfo GetDirectory(params string[] paths)
118+
{
119+
var directory = new DirectoryInfo(GetFullPath(paths));
120+
if (!directory.Exists)
121+
{
122+
throw new DirectoryNotFoundException($"The {directory.Name} directory must exist at {directory.FullName}");
123+
}
124+
return directory;
125+
}
126+
127+
static FileInfo GetFile(params string[] paths)
128+
{
129+
var file = new FileInfo(GetFullPath(paths));
130+
if (!file.Exists)
131+
{
132+
throw new FileNotFoundException($"The {file.Name} file must exist at {file.FullName}");
133+
}
134+
return file;
135+
}
136+
137+
static string GetFullPath(params string[] paths) => Path.GetFullPath(Path.Combine(new[] { GetThisDirectory(), "..", ".." }.Concat(paths).ToArray()));
138+
139+
static string GetThisDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!;
140+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
3+
"diagnosticMessages": true
4+
}

test/TestApp/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FodyWeavers.xml
2+
FodyWeavers.xsd

test/TestApp/Program.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Reflection;
2+
using Microsoft.Extensions.Configuration;
3+
using Serilog;
4+
using Serilog.Debugging;
5+
using Serilog.Settings.Configuration;
6+
7+
if (args.Length == 1 && args[0] == "is-single-file")
8+
{
9+
if (typeof(Program).Assembly.GetManifestResourceNames().Any(e => e.StartsWith("costura.")))
10+
{
11+
Console.WriteLine(true);
12+
return 0;
13+
}
14+
// IL3000: 'System.Reflection.Assembly.Location' always returns an empty string for assemblies embedded in a single-file app
15+
#pragma warning disable IL3000
16+
Console.WriteLine(string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location));
17+
#pragma warning restore
18+
return 0;
19+
}
20+
21+
SelfLog.Enable(Console.Error);
22+
23+
Thread.CurrentThread.Name = "Main thread";
24+
25+
var configurationValues = new Dictionary<string, string>
26+
{
27+
["Serilog:Enrich:0"] = "WithThreadName",
28+
["Serilog:WriteTo:0:Name"] = "Console",
29+
["Serilog:WriteTo:0:Args:outputTemplate"] = "({ThreadName}) [{Level}] {Message}{NewLine}",
30+
};
31+
32+
if (args.Contains("--using-thread")) configurationValues["Serilog:Using:Thread"] = "Serilog.Enrichers.Thread";
33+
if (args.Contains("--using-console")) configurationValues["Serilog:Using:Console"] = "Serilog.Sinks.Console";
34+
35+
var assemblies = new List<Assembly>();
36+
if (args.Contains("--assembly-thread")) assemblies.Add(typeof(ThreadLoggerConfigurationExtensions).Assembly);
37+
if (args.Contains("--assembly-console")) assemblies.Add(typeof(ConsoleLoggerConfigurationExtensions).Assembly);
38+
39+
try
40+
{
41+
var configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValues).Build();
42+
var options = assemblies.Count > 0 ? new ConfigurationReaderOptions(assemblies.ToArray()) : null;
43+
var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration, options).CreateLogger();
44+
logger.Information("Expected success");
45+
return 0;
46+
}
47+
catch (InvalidOperationException exception) when (exception.Message.StartsWith("No Serilog:Using configuration section is defined and no Serilog assemblies were found."))
48+
{
49+
Console.WriteLine("Expected exception");
50+
return 0;
51+
}
52+
catch (Exception exception)
53+
{
54+
Console.Error.WriteLine(exception);
55+
return 1;
56+
}

test/TestApp/TestApp.csproj

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net48</TargetFramework>
6+
<DebugType>embedded</DebugType>
7+
<AutoGenerateBindingRedirects>false</AutoGenerateBindingRedirects>
8+
<GenerateSupportedRuntime>false</GenerateSupportedRuntime>
9+
<PublishReferencesDocumentationFiles>false</PublishReferencesDocumentationFiles>
10+
<AllowedReferenceRelatedFileExtensions>none</AllowedReferenceRelatedFileExtensions>
11+
<SelfContained>false</SelfContained>
12+
<UseCurrentRuntimeIdentifier>true</UseCurrentRuntimeIdentifier>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\..\src\Serilog.Settings.Configuration\Serilog.Settings.Configuration.csproj" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Costura.Fody" Version="5.7.0" PrivateAssets="all" />
21+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
22+
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
23+
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
24+
</ItemGroup>
25+
26+
</Project>

0 commit comments

Comments
 (0)