File: Pipelines\BundleBuildWrapperTests.cs
Web Access
Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json;
using Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
public sealed class BundleBuildWrapperTests(ITestOutputHelper output)
{
    [Theory]
    [InlineData("bash", "/p:", "true")]
    [InlineData("bash", "-p:", "true")]
    [InlineData("bash", "/p:", "false")]
    [InlineData("pwsh", "/p:", "true")]
    [InlineData("pwsh", "-p:", "true")]
    [InlineData("pwsh", "/p:", "false")]
    [RequiresTools(["pwsh", "bash"])]
    public async Task BundleWrappersTranslateLegacyNativeSkip(string shell, string prefix, string value)
    {
        using var workspace = TemporaryWorkspace.Create(output);
        string[] arguments = [prefix + "SkipNativeBuild=" + value];
 
        await ExecuteAsync(workspace, shell, arguments);
 
        Assert.Equal(value == "true" ? ["ARG|/p:SkipCliArtifacts=true"] : [],
            PropertyArguments(workspace, "bundle"));
        Assert.Equal(arguments.Select(argument => "ARG|" + argument), PropertyArguments(workspace, "repository"));
    }
 
    [Theory]
    [InlineData("bash")]
    [InlineData("pwsh")]
    [RequiresTools(["pwsh", "bash"])]
    public async Task BundleWrappersLeaveAdvancedControlsToDirectMsbuild(string shell)
    {
        using var workspace = TemporaryWorkspace.Create(output);
        string[] arguments = ["/p:SkipDashboardPublish=true", "/p:SkipCliArtifacts=true"];
 
        await ExecuteAsync(workspace, shell, arguments);
 
        Assert.Empty(PropertyArguments(workspace, "bundle"));
        Assert.Equal(arguments.Select(argument => "ARG|" + argument), PropertyArguments(workspace, "repository"));
    }
 
    [Fact]
    [RequiresTools(["bash"])]
    public async Task StabilizationSmokeSkipsCliArtifactsWhenBuildingItsPayload()
    {
        Assert.SkipWhen(OperatingSystem.IsWindows(), "The stabilization smoke requires a Unix host.");
        using var workspace = TemporaryWorkspace.Create(output);
        var root = workspace.CreateDirectory("repository with spaces").FullName;
        var scripts = Directory.CreateDirectory(Path.Combine(root, "eng", "scripts")).FullName;
        var script = Path.Combine(scripts, "stabilization-smoke-init-restore.sh");
        File.Copy(Path.Combine(RepoRoot.Path, "eng", "scripts", "stabilization-smoke-init-restore.sh"), script);
        var feed = workspace.CreateDirectory("stable feed").FullName;
        foreach (var package in new[] { "Aspire.Hosting", "Aspire.ProjectTemplates", "Aspire.AppHost.Sdk" })
        {
            await File.WriteAllTextAsync(Path.Combine(feed, $"{package}.17.0.0.nupkg"), "");
        }
 
        var dotnet = Path.Combine(root, "dotnet.sh");
        // Stop at the separate driver publish: this exercises the complete smoke
        // entry point's bundle invocation without rebuilding product binaries.
        await File.WriteAllTextAsync(dotnet, """
            #!/usr/bin/env bash
            case "$1" in
              --info) printf ' RID: linux-x64\n' ;;
              build) for arg in "$@"; do printf 'ARG|%s\n' "$arg"; done > "$TEST_ROOT/bundle" ;;
              publish) exit 73 ;;
              *) exit 74 ;;
            esac
            """);
        if (!OperatingSystem.IsWindows())
        {
            File.SetUnixFileMode(dotnet, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
        }
 
        var result = await TestProcessRunner.RunAsync(
            output, "bash", [script], root,
            new Dictionary<string, string>
            {
                ["TEST_ROOT"] = workspace.Path,
                ["STABILIZATION_SMOKE_FEED"] = feed
            });
 
        Assert.Equal(73, result.ExitCode);
        Assert.Equal(["ARG|-p:SkipCliArtifacts=true"], PropertyArguments(workspace, "bundle"));
    }
 
    private async Task ExecuteAsync(TemporaryWorkspace workspace, string shell, string[] arguments)
    {
        Assert.SkipWhen(shell == "bash" && OperatingSystem.IsWindows(), "The Bash wrapper requires a Unix host.");
        var root = workspace.CreateDirectory("repository with spaces").FullName;
        var common = Directory.CreateDirectory(Path.Combine(root, "eng", "common")).FullName;
        var sdk = Directory.CreateDirectory(Path.Combine(root, ".dotnet")).FullName;
        // Run complete production entry points; replace only the external build
        // driver and dotnet command with argument recorders.
        foreach (var name in new[] { "build.sh", "build.ps1" })
        {
            File.Copy(Path.Combine(RepoRoot.Path, "eng", name), Path.Combine(root, "eng", name));
        }
        await File.WriteAllTextAsync(Path.Combine(common, "build.sh"), """
            #!/usr/bin/env bash
            for arg in "$@"; do printf 'ARG|%s\n' "$arg"; done > "$TEST_ROOT/repository"
            """);
        await File.WriteAllTextAsync(Path.Combine(common, "build.ps1"), """
            [IO.File]::WriteAllLines("$env:TEST_ROOT/repository", @($args | ForEach-Object { "ARG|$_" }))
            $global:LASTEXITCODE = 0
            """);
        await File.WriteAllTextAsync(Path.Combine(sdk, "dotnet"), """
            #!/usr/bin/env bash
            for arg in "$@"; do printf 'ARG|%s\n' "$arg"; done > "$TEST_ROOT/bundle"
            """);
        if (!OperatingSystem.IsWindows())
        {
            foreach (var path in new[] { Path.Combine(common, "build.sh"), Path.Combine(sdk, "dotnet") })
            {
                File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
            }
        }
        var launcher = Path.Combine(root, "launch.ps1");
        await File.WriteAllTextAsync(Path.Combine(root, "arguments.json"), JsonSerializer.Serialize(arguments));
        await File.WriteAllTextAsync(launcher, """
            $ErrorActionPreference = 'Stop'
            $buildArguments = @(Get-Content "$PSScriptRoot/arguments.json" -Raw | ConvertFrom-Json)
            function global:dotnet {
              [IO.File]::WriteAllLines("$env:TEST_ROOT/bundle", @($args | ForEach-Object { "ARG|$_" }))
              $global:LASTEXITCODE = 0
            }
            if ($env:TEST_SHELL -eq 'bash') {
              & bash "$PSScriptRoot/eng/build.sh" --bundle --warnAsError false @buildArguments
            } else {
              & "$PSScriptRoot/eng/build.ps1" -bundle -warnAsError false @buildArguments
            }
            exit $LASTEXITCODE
            """);
        using var command = new PowerShellCommand(launcher, output)
            .WithWorkingDirectory(root)
            .WithTimeout(TimeSpan.FromMinutes(1))
            .WithEnvironmentVariable("TEST_ROOT", workspace.Path)
            .WithEnvironmentVariable("TEST_SHELL", shell)
            .WithEnvironmentVariable("CI", "false");
        var result = await command.ExecuteAsync();
        result.EnsureSuccessful();
    }
 
    private static IEnumerable<string> PropertyArguments(TemporaryWorkspace workspace, string name) =>
        File.ReadAllLines(Path.Combine(workspace.Path, name))
            .Where(line => line.StartsWith("ARG|/p:Skip", StringComparison.OrdinalIgnoreCase) ||
                           line.StartsWith("ARG|-p:Skip", StringComparison.OrdinalIgnoreCase));
}