Skip to content

Commit

Permalink
Add an integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed Mar 11, 2024
1 parent 280130c commit 4226674
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Chisel.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{89268D80-B21
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AC8C6685-EDF9-443A-BAF6-A5E7CF777B2A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "tests\TestApp\TestApp.csproj", "{8B1B3D6A-7100-4DFB-97C9-CF5ACF1A3B08}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -43,10 +45,13 @@ Global
{845EDA2A-5207-4C6D-ABE9-9635F4630D90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{845EDA2A-5207-4C6D-ABE9-9635F4630D90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{845EDA2A-5207-4C6D-ABE9-9635F4630D90}.Release|Any CPU.Build.0 = Release|Any CPU
{8B1B3D6A-7100-4DFB-97C9-CF5ACF1A3B08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B1B3D6A-7100-4DFB-97C9-CF5ACF1A3B08}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{845EDA2A-5207-4C6D-ABE9-9635F4630D90} = {0CC84E67-19D2-480B-B36A-6BB15A9109E7}
{F4CAEC64-3B0C-4ACD-BF87-760A838A5D86} = {89268D80-B21D-4C76-AF7F-796AAD1E00D9}
{EC3CCB92-1AA1-4C33-B296-D7111EEF84E4} = {AC8C6685-EDF9-443A-BAF6-A5E7CF777B2A}
{8B1B3D6A-7100-4DFB-97C9-CF5ACF1A3B08} = {AC8C6685-EDF9-443A-BAF6-A5E7CF777B2A}
EndGlobalSection
EndGlobal
4 changes: 4 additions & 0 deletions tests/Chisel.Tests/Chisel.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
Expand All @@ -18,6 +19,9 @@
</ItemGroup>

<ItemGroup>
<Reference Include="NuGet.Frameworks">
<HintPath>$([MSBuild]::NormalizePath('$(MSBuildExtensionsPath)', 'NuGet.Frameworks.dll'))</HintPath>
</Reference>
<Reference Include="NuGet.ProjectModel">
<HintPath>$([MSBuild]::NormalizePath('$(MSBuildExtensionsPath)', 'NuGet.ProjectModel.dll'))</HintPath>
</Reference>
Expand Down
92 changes: 92 additions & 0 deletions tests/Chisel.Tests/ChiseledAppTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CliWrap;
using CliWrap.Exceptions;
using FluentAssertions;
using FluentAssertions.Execution;
using Xunit;
using Xunit.Abstractions;

namespace Chisel.Tests;

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

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

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

public static readonly TheoryData<PublishMode> PublishModeData = new(Enum.GetValues<PublishMode>());

[Theory]
[MemberData(nameof(PublishModeData))]
public async Task RunTestApp(PublishMode publishMode)
{
var (stdOut, stdErr) = await RunTestAppAsync(publishMode);
var allDlls = stdOut.Split(Environment.NewLine).Where(e => e.EndsWith(".dll"));
var expectedDlls = new[]
{
"Microsoft.Data.SqlClient.dll",
"Microsoft.Data.SqlClient.SNI.dll",
"Microsoft.Extensions.DependencyModel.dll",
"Microsoft.Identity.Client.dll",
"Microsoft.IdentityModel.Abstractions.dll",
"Microsoft.SqlServer.Server.dll",
"System.Configuration.ConfigurationManager.dll",
"System.Diagnostics.EventLog.dll",
"System.Diagnostics.EventLog.Messages.dll",
"System.Runtime.Caching.dll",
"System.Security.Cryptography.ProtectedData.dll",
"TestApp.dll",
};
allDlls.Except(expectedDlls).Should().BeEmpty();
stdOut.Should().Contain("✅");
stdErr.Should().BeEmpty();
}

private async Task<(string StdOut, string StdErr)> RunTestAppAsync(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);

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);
}
}
13 changes: 13 additions & 0 deletions tests/Chisel.Tests/Support/DirectoryInfoExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.IO;
using System.Linq;

namespace Chisel.Tests;

internal 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())));
}
18 changes: 18 additions & 0 deletions tests/Chisel.Tests/Support/PublishMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Chisel.Tests;

/// <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>
SingleFile,
}
160 changes: 160 additions & 0 deletions tests/Chisel.Tests/Support/TestApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using CliWrap;
using CliWrap.Exceptions;
using FluentAssertions;
using NuGet.Frameworks;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Chisel.Tests;

public class TestApp : IAsyncLifetime
{
private readonly IMessageSink _messageSink;
private readonly DirectoryInfo _workingDirectory;
private readonly Dictionary<PublishMode, FileInfo> _executables;

public TestApp(IMessageSink messageSink)
{
_messageSink = messageSink;
var tfm = NuGetFramework.Parse(typeof(TestApp).Assembly.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName ?? throw new InvalidOperationException("TargetFrameworkAttribute not found"));
_workingDirectory = GetDirectory("tests", $"TestApp-{tfm}");
_workingDirectory.Create();
foreach (var file in GetDirectory("tests", "TestApp").EnumerateFiles())
{
file.CopyTo(_workingDirectory.File(file.Name).FullName, overwrite: true);
}
_executables = new Dictionary<PublishMode, FileInfo>();
}

async Task IAsyncLifetime.InitializeAsync()
{
await CreateTestAppAsync();
}

Task IAsyncLifetime.DisposeAsync()
{
_workingDirectory.Delete(recursive: true);
return Task.CompletedTask;
}

public string GetExecutablePath(PublishMode publishMode) => _executables[publishMode].FullName;

private 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 Enum.GetValues<PublishMode>())
{
await PublishAsync(publishMode);
}
}

private async Task PackAsync()
{
var projectFile = GetFile("src", "Chisel", "Chisel.csproj");
var packArgs = new[] {
"pack", projectFile.FullName,
"--configuration", "Release",
"--output", _workingDirectory.FullName,
"-p:MinVerSkip=true",
"-p:Version=0.0.0-IntegrationTest.0",
};
await RunDotnetAsync(_workingDirectory, packArgs);
}

private 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",
};
await RunDotnetAsync(_workingDirectory, restoreArgs);
}

private async Task PublishAsync(PublishMode publishMode)
{
var publishDirectory = _workingDirectory.SubDirectory("publish");

var outputDirectory = publishDirectory.SubDirectory(publishMode.ToString());

var publishArgsBase = new[] {
"publish",
"--no-restore",
"--configuration", "Release",
"--output", outputDirectory.FullName,
};
var publishSingleFile = $"-p:PublishSingleFile={publishMode is PublishMode.SingleFile}";
var publishArgs = publishArgsBase.Append(publishSingleFile).ToArray();
await RunDotnetAsync(_workingDirectory, publishArgs);

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

private async Task RunDotnetAsync(DirectoryInfo workingDirectory, params string[] 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}"));
}));

_messageSink.OnMessage(new DiagnosticMessage($"📁 {workingDirectory.FullName} 🛠️ {command}"));

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

private static DirectoryInfo GetDirectory(params string[] paths) => new(GetFullPath(paths));

private static FileInfo GetFile(params string[] paths) => new(GetFullPath(paths));

private static string GetFullPath(params string[] paths) => Path.GetFullPath(Path.Combine(new[] { GetThisDirectory(), "..", "..", ".." }.Concat(paths).ToArray()));

private static string GetThisDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!;
}
Loading

0 comments on commit 4226674

Please sign in to comment.