File: PowerShellScripts\StageNativeCliToolPackagesTests.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 Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
public sealed class StageNativeCliToolPackagesTests : IDisposable
{
    private const string PackageVersion = "13.4.0-preview.1.26229.13";
    private const string DifferentPackageVersion = "13.4.1-preview.1.26229.13";
 
    private static readonly string[] s_productionRids =
    [
        "linux-x64",
        "linux-arm64",
        "linux-musl-x64",
        "osx-x64",
        "osx-arm64",
        "win-x64",
        "win-arm64"
    ];
 
    private readonly TemporaryWorkspace _workspace;
    private readonly string _scriptPath;
    private readonly ITestOutputHelper _output;
 
    public StageNativeCliToolPackagesTests(ITestOutputHelper output)
    {
        _output = output;
        _workspace = TemporaryWorkspace.Create(output);
        _scriptPath = Path.Combine(RepoRoot.Path, "eng", "scripts", "stage-native-cli-tool-packages.ps1");
    }
 
    public void Dispose() => _workspace.Dispose();
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task StagesCanonicalPointerAndRidPackages()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        foreach (var rid in s_productionRids)
        {
            CreateNativeArchivePackages(downloadRoot, rid);
        }
        CreatePackage(downloadRoot, "unrelated", "Release", "Shipping", $"Aspire.Cli.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        result.EnsureSuccessful();
 
        var stagedPackageNames = GetStagedPackageNames(shippingDir);
        Assert.Equal(
            [
                $"Aspire.Cli.{PackageVersion}.nupkg",
                $"Aspire.Cli.linux-arm64.{PackageVersion}.nupkg",
                $"Aspire.Cli.linux-musl-x64.{PackageVersion}.nupkg",
                $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg",
                $"Aspire.Cli.osx-arm64.{PackageVersion}.nupkg",
                $"Aspire.Cli.osx-x64.{PackageVersion}.nupkg",
                $"Aspire.Cli.win-arm64.{PackageVersion}.nupkg",
                $"Aspire.Cli.win-x64.{PackageVersion}.nupkg"
            ],
            stagedPackageNames);
        Assert.Contains("Skipping non-canonical Aspire.Cli pointer package", result.Output);
        Assert.Contains("Skipping Aspire.Cli package outside native archive artifacts", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task StagesNpmPackagesWhenPresent()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg");
        CreateNativeArchiveNpmPackages(downloadRoot, "win-x64");
        CreateNativeArchiveNpmPackages(downloadRoot, "linux-x64");
 
        var result = await RunScript(downloadRoot, shippingDir, "-RequireNpmPackages");
 
        result.EnsureSuccessful();
 
        var stagedPackageNames = GetStagedPackageNames(shippingDir);
        Assert.Equal(
            [
                $"Aspire.Cli.{PackageVersion}.nupkg",
                $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg",
                $"Aspire.Cli.win-x64.{PackageVersion}.nupkg",
                $"microsoft-aspire-cli-{PackageVersion}.tgz",
                $"microsoft-aspire-cli-linux-x64-{PackageVersion}.tgz",
                $"microsoft-aspire-cli-win-x64-{PackageVersion}.tgz"
            ],
            stagedPackageNames);
        Assert.Contains("Skipping non-canonical Aspire CLI npm pointer package", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenRequiredNpmPackagesAreMissing()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir, "-RequireNpmPackages");
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("No Aspire CLI npm packages were found", result.Output);
        Assert.Contains("native_archives_<rid>", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenCanonicalPointerPackageIsMissing()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "linux-x64");
        CreateNativeArchivePackage(downloadRoot, "win-x64", $"Aspire.Cli.win-x64.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Expected exactly one canonical Aspire.Cli pointer package", result.Output);
        Assert.Contains("native_archives_win_x64", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenDuplicateCanonicalPointerPackagesAreDownloaded()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreatePackage(downloadRoot, "native_archives_win_x64", "Duplicate", "Shipping", $"Aspire.Cli.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Expected exactly one canonical Aspire.Cli pointer package", result.Output);
        Assert.Contains("but found 2", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenRidPackageDoesNotMatchNativeArchiveRoot()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.osx-x64.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Unexpected Aspire.Cli package", result.Output);
        Assert.Contains("Aspire.Cli.linux-x64 RID-specific package", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenRidPackageNameIsMalformed()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", "Aspire.Cli.linux-x64.13x.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Unexpected Aspire.Cli package", result.Output);
        Assert.Contains("Aspire.Cli.linux-x64 RID-specific package", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenDuplicateRidPackagesAreDownloaded()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg");
        CreatePackage(downloadRoot, "native_archives_linux_x64", "Duplicate", "Shipping", $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Expected exactly one Aspire.Cli RID-specific package per RID", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenDuplicateRidPackagesHaveDifferentVersions()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.linux-x64.{DifferentPackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Expected exactly one Aspire.Cli RID-specific package per RID", result.Output);
        Assert.Contains($"Aspire.Cli.linux-x64.{PackageVersion}.nupkg", result.Output);
        Assert.Contains($"Aspire.Cli.linux-x64.{DifferentPackageVersion}.nupkg", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenRidPackageVersionDoesNotMatchCanonicalPointerPackage()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "win-x64");
        CreateNativeArchivePackage(downloadRoot, "linux-x64", $"Aspire.Cli.linux-x64.{DifferentPackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("must match canonical pointer", result.Output);
        Assert.Contains(PackageVersion, result.Output);
        Assert.Contains($"Aspire.Cli.linux-x64.{DifferentPackageVersion}.nupkg", result.Output);
        Assert.Contains(DifferentPackageVersion, result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task FailsWhenNoPackagesAreUnderNativeArchiveArtifacts()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreatePackage(downloadRoot, "unrelated", "Release", "Shipping", $"Aspire.Cli.{PackageVersion}.nupkg");
        CreatePackage(downloadRoot, "also-unrelated", "Release", "Shipping", $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg");
 
        var result = await RunScript(downloadRoot, shippingDir);
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("No native CLI tool packages were found", result.Output);
        Assert.Contains("native_archives_<rid>", result.Output);
    }
 
    [Fact]
    [RequiresTools(["pwsh"])]
    public async Task UsesConfiguredCanonicalPointerArtifactName()
    {
        var downloadRoot = CreateDownloadRoot();
        var shippingDir = CreateShippingDir();
 
        CreateNativeArchivePackages(downloadRoot, "linux-x64");
        CreateNativeArchivePackage(downloadRoot, "win-x64", $"Aspire.Cli.win-x64.{PackageVersion}.nupkg");
 
        var result = await RunScript(
            downloadRoot,
            shippingDir,
            "-CanonicalPointerArtifactName", "native_archives_linux_x64");
 
        result.EnsureSuccessful();
 
        var stagedPackageNames = GetStagedPackageNames(shippingDir);
        Assert.Equal(
            [
                $"Aspire.Cli.{PackageVersion}.nupkg",
                $"Aspire.Cli.linux-x64.{PackageVersion}.nupkg",
                $"Aspire.Cli.win-x64.{PackageVersion}.nupkg"
            ],
            stagedPackageNames);
    }
 
    private async Task<CommandResult> RunScript(string downloadRoot, string shippingDir, params string[] additionalArgs)
    {
        using var cmd = new PowerShellCommand(_scriptPath, _output)
            .WithTimeout(TimeSpan.FromMinutes(2));
 
        var args = new List<string>
        {
            "-DownloadRoot", $"\"{downloadRoot}\"",
            "-ShippingDir", $"\"{shippingDir}\""
        };
        args.AddRange(additionalArgs);
 
        return await cmd.ExecuteAsync([.. args]);
    }
 
    private string CreateDownloadRoot()
    {
        var path = Path.Combine(_workspace.Path, Path.GetRandomFileName(), "download");
        Directory.CreateDirectory(path);
        return path;
    }
 
    private string CreateShippingDir()
    {
        return Path.Combine(_workspace.Path, Path.GetRandomFileName(), "shipping");
    }
 
    private static void CreateNativeArchivePackages(string downloadRoot, string rid)
    {
        CreateNativeArchivePackage(downloadRoot, rid, $"Aspire.Cli.{PackageVersion}.nupkg");
        CreateNativeArchivePackage(downloadRoot, rid, $"Aspire.Cli.{rid}.{PackageVersion}.nupkg");
    }
 
    private static void CreateNativeArchiveNpmPackages(string downloadRoot, string rid)
    {
        CreateNativeArchivePackage(downloadRoot, rid, $"microsoft-aspire-cli-{PackageVersion}.tgz");
        CreateNativeArchivePackage(downloadRoot, rid, $"microsoft-aspire-cli-{rid}-{PackageVersion}.tgz");
    }
 
    private static void CreateNativeArchivePackage(string downloadRoot, string rid, string packageName)
    {
        CreatePackage(downloadRoot, $"native_archives_{rid.Replace('-', '_')}", "Release", "Shipping", packageName);
    }
 
    private static void CreatePackage(string downloadRoot, params string[] pathParts)
    {
        var path = Path.Combine([downloadRoot, .. pathParts]);
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        File.WriteAllText(path, "package");
    }
 
    private static string[] GetStagedPackageNames(string shippingDir)
    {
        return Directory.GetFiles(shippingDir)
            .Select(Path.GetFileName)
            .Order(StringComparer.Ordinal)
            .ToArray()!;
    }
}