File: TargetsTests.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Build.Containers.IntegrationTests\Microsoft.NET.Build.Containers.IntegrationTests.csproj (Microsoft.NET.Build.Containers.IntegrationTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Build.Execution;
using Microsoft.NET.Build.Containers.IntegrationTests;
using static Microsoft.NET.Build.Containers.KnownStrings.Properties;
 
namespace Microsoft.NET.Build.Containers.Targets.IntegrationTests;
 
[Collection(nameof(MSBuildCollection))]
public class TargetsTests
{
    [Theory]
    [MemberData(nameof(ContainerAppCommands))]
    public void CanDeferContainerAppCommand(
        string os,
        string prop,
        bool value,
        params string[] expectedAppCommandArgs)
    {
        var (project, _, d) = ProjectInitializer.InitProject(new()
        {
            [prop] = value.ToString(),
            [ContainerRuntimeIdentifier] = $"{os}-x64",
 
        }, projectName: $"{nameof(CanDeferContainerAppCommand)}_{prop}_{value}_{string.Join("_", expectedAppCommandArgs)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build([ ComputeContainerConfig ], []);
        var computedAppCommand = instance.GetItems(ContainerAppCommand).Select(i => i.EvaluatedInclude);
 
        // The test was not testing anything previously, as the list returned was zero length,
        // and the Zip didn't yield any results.
        // So, to make sure we actually test something, we check that we actually get the expected collection.
        computedAppCommand.Should().BeEquivalentTo(expectedAppCommandArgs);
    }
 
    public static TheoryData<string, string, bool, string[]> ContainerAppCommands()
    {
        char s = Path.DirectorySeparatorChar;
        return new TheoryData<string, string, bool, string[]>
        {
            { "win", "SelfContained", true, [$"C:{s}app{s}foo.exe"] },
            { "win", "SelfContained", false, ["dotnet", $"C:{s}app{s}foo.dll"] },
            { "win", "PublishSelfContained", true, [$"C:{s}app{s}foo.exe"] },
            { "win", "PublishSelfContained", false, ["dotnet", $"C:{s}app{s}foo.dll"] },
            { "linux", "SelfContained", true, ["/app/foo"] },
            { "linux", "SelfContained", false, ["dotnet", "/app/foo.dll"] },
            { "linux", "PublishSelfContained", true, ["/app/foo"] },
            { "linux", "PublishSelfContained", false, ["dotnet", "/app/foo.dll"] },
        };
    }
 
    [Fact]
    public void CanDeferToContainerImageNameWhenPresent()
    {
        var customImageName = "my-container-app";
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            [ContainerImageName] = customImageName
        });
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger });
        logger.Warnings.Should().HaveCount(1, "a warning for the use of the old ContainerImageName property should have been created");
        logger.Warnings[0].Code.Should().Be(KnownStrings.ErrorCodes.CONTAINER003);
        Assert.Equal(customImageName, instance.GetPropertyValue(ContainerRepository));
    }
 
    [InlineData("WebApplication44", "webapplication44", true)]
    [InlineData("friendly-suspicious-alligator", "friendly-suspicious-alligator", true)]
    [InlineData("*friendly-suspicious-alligator", "", false)]
    [InlineData("web/app2+7", "web/app2-7", true)]
    [InlineData("Microsoft.Apps.Demo.ContosoWeb", "microsoft-apps-demo-contosoweb", true)]
    [Theory]
    public void CanNormalizeInputContainerNames(string projectName, string expectedContainerImageName, bool shouldPass)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            [AssemblyName] = projectName
        }, projectName: $"{nameof(CanNormalizeInputContainerNames)}_{projectName}_{expectedContainerImageName}_{shouldPass}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().Be(shouldPass, String.Join(Environment.NewLine, logger.AllMessages));
        Assert.Equal(expectedContainerImageName, instance.GetPropertyValue(ContainerRepository));
    }
 
    [InlineData("7.0.100", true)]
    [InlineData("8.0.100", true)]
    [InlineData("7.0.100-preview.7", true)]
    [InlineData("7.0.100-rc.1", true)]
    [InlineData("6.0.100", false)]
    [InlineData("7.0.100-preview.1", false)]
    [Theory]
    public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NETCoreSdkVersion"] = sdkVersion,
            ["PublishProfile"] = "DefaultContainer"
        }, projectName: $"{nameof(CanWarnOnInvalidSDKVersions)}_{sdkVersion}_{isAllowed}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { "_ContainerVerifySDKVersion" }, new[] { logger }, null, out var outputs).Should().Be(isAllowed);
        var derivedIsAllowed = Boolean.Parse(project.GetProperty("_IsSDKContainerAllowedVersion").EvaluatedValue);
        if (isAllowed)
        {
            logger.Errors.Should().HaveCount(0, "an error should not have been created");
            derivedIsAllowed.Should().Be(true, "SDK version {0} should have been allowed", sdkVersion);
        }
        else
        {
            logger.Errors.Should().HaveCount(1, "an error should have been created").And.Satisfy(error => error.Code == KnownStrings.ErrorCodes.CONTAINER002);
            derivedIsAllowed.Should().Be(false, "SDK version {0} should not have been allowed", sdkVersion);
        }
    }
 
    [InlineData(true)]
    [InlineData(false)]
    [Theory]
    public void GetsConventionalLabelsByDefault(bool shouldEvaluateLabels)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            [ContainerGenerateLabels] = shouldEvaluateLabels.ToString()
        }, projectName: $"{nameof(GetsConventionalLabelsByDefault)}_{shouldEvaluateLabels}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        var success = instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs);
        success.Should().BeTrue("Build should have succeeded");
        if (shouldEvaluateLabels)
        {
            instance.GetItems(ContainerLabel).Should().NotBeEmpty("Should have evaluated some labels by default");
        }
        else
        {
            instance.GetItems(ContainerLabel).Should().BeEmpty("Should not have evaluated any labels by default");
        }
    }
 
    private static bool LabelMatch(string label, string value, ProjectItemInstance item) => item.EvaluatedInclude == label && item.GetMetadata("Value") is { } v && v.EvaluatedValue == value;
 
    [InlineData(true)]
    [InlineData(false)]
    [Theory]
    public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSourceControl)
    {
        var commitHash = "abcdef";
        var repoUrl = "https://git.cosmere.com/shard/whimsy.git";
 
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["PublishRepositoryUrl"] = includeSourceControl.ToString(),
            ["PrivateRepositoryUrl"] = repoUrl,
            ["SourceRevisionId"] = commitHash
        }, projectName: $"{nameof(ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn)}_{includeSourceControl}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages));
        var labels = instance.GetItems(ContainerLabel);
        if (includeSourceControl)
        {
            labels.Should().NotBeEmpty("Should have evaluated some labels by default")
                .And.ContainSingle(label => LabelMatch("org.opencontainers.image.source", repoUrl, label))
                .And.ContainSingle(label => LabelMatch("org.opencontainers.image.revision", commitHash, label)); ;
        }
        else
        {
            labels.Should().NotBeEmpty("Should have evaluated some labels by default")
                .And.NotContain(label => LabelMatch("org.opencontainers.image.source", repoUrl, label))
                .And.NotContain(label => LabelMatch("org.opencontainers.image.revision", commitHash, label)); ;
        };
    }
 
    [InlineData("https://git.cosmere.com/shard/whimsy.git", "https://git.cosmere.com/shard/whimsy")]
    [InlineData("https://repos.git.cosmere.com/shard/whimsy.git", "https://repos.git.cosmere.com/shard/whimsy")]
    [Theory]
    public void ShouldTrimTrailingGitSuffixFromRepoUrls(string repoUrl, string expectedLabel)
    {
        var commitHash = "abcdef";
 
        static string NormalizeString(string s) => s.Replace(':', '_').Replace('/', '_');
 
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["PublishRepositoryUrl"] = true.ToString(),
            ["PrivateRepositoryUrl"] = repoUrl,
            ["SourceRevisionId"] = commitHash,
            ["RepositoryType"] = "git"
        }, projectName: $"{nameof(ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn)}_{NormalizeString(repoUrl)}_{NormalizeString(expectedLabel)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages));
        var labels = instance.GetItems(ContainerLabel);
 
        labels.Should().NotBeEmpty("Should have evaluated some labels by default")
            .And.ContainSingle(label => LabelMatch("org.opencontainers.image.source", expectedLabel, label), String.Join(",", logger.AllMessages));
    }
 
    [InlineData(true)]
    [InlineData(false)]
    [Theory]
    public void ShouldIncludeBaseImageLabelsUnlessUserOptsOut(bool includeBaseImageLabels)
    {
        var expectedBaseImage = "mcr.microsoft.com/dotnet/runtime:7.0";
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["ContainerGenerateLabelsImageBaseName"] = includeBaseImageLabels.ToString(),
            ["ContainerBaseImage"] = expectedBaseImage,
            ["ContainerGenerateLabels"] = true.ToString()
        }, projectName: $"{nameof(ShouldIncludeBaseImageLabelsUnlessUserOptsOut)}_{includeBaseImageLabels}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages));
        var labels = instance.GetItems(ContainerLabel);
        if (includeBaseImageLabels)
        {
            labels.Should().NotBeEmpty("Should have evaluated some labels by default")
                .And.ContainSingle(label => LabelMatch("org.opencontainers.image.base.name", expectedBaseImage, label));
        }
        else
        {
            labels.Should().NotBeEmpty("Should have evaluated some labels by default")
                .And.NotContain(label => LabelMatch("org.opencontainers.image.base.name", expectedBaseImage, label));
        };
    }
 
    [InlineData(true)]
    [InlineData(false)]
    [Theory]
    public void ShouldIncludeSDKAndRuntimeVersionLabelsUnlessUserOptsOut(bool includeToolsetVersionLabels)
    {
        var runtimeMajorMinor = "7.0";
        var randomSdkVersion = "8.0.100";
        var expectedBaseImage = $"mcr.microsoft.com/dotnet/runtime:{runtimeMajorMinor}";
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["ContainerGenerateLabelsDotnetToolset"] = includeToolsetVersionLabels.ToString(),
            ["ContainerBaseImage"] = expectedBaseImage,
            ["ContainerGenerateLabels"] = true.ToString(), // always include other labels, but not necessarily the toolset labels
            ["NETCoreSdkVersion"] = randomSdkVersion // not functionally relevant for the test, just need a known version
        }, projectName: $"{nameof(ShouldIncludeSDKAndRuntimeVersionLabelsUnlessUserOptsOut)}_{includeToolsetVersionLabels}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages));
        var labels = instance.GetItems(ContainerLabel);
        if (includeToolsetVersionLabels)
        {
            labels.Should().NotBeEmpty("Should have evaluated some labels by default")
                .And.ContainSingle(label => LabelMatch("net.dot.runtime.majorminor", runtimeMajorMinor, label))
                .And.ContainSingle(label => LabelMatch("net.dot.sdk.version", randomSdkVersion, label));
        }
        else
        {
            labels.Should().NotBeEmpty("Should have evaluated some labels by default")
                .And.NotContain(label => LabelMatch("net.dot.runtime.majorminor", runtimeMajorMinor, label))
                .And.NotContain(label => LabelMatch("net.dot.sdk.version", randomSdkVersion, label));
        };
    }
 
    [InlineData("7.0.100", "v7.0", "7.0")]
    [InlineData("7.0.100-preview.7", "v7.0", "7.0")]
    [InlineData("7.0.100-rc.1", "v7.0", "7.0")]
    [InlineData("8.0.100", "v8.0", "8.0")]
    [InlineData("8.0.100", "v7.0", "7.0")]
    [InlineData("8.0.100-preview.7", "v8.0", "8.0.0-preview.7")]
    [InlineData("8.0.100-rc.1", "v8.0", "8.0.0-rc.1")]
    [InlineData("8.0.100-rc.1", "v7.0", "7.0")]
    [InlineData("8.0.200", "v8.0", "8.0")]
    [InlineData("8.0.200", "v7.0", "7.0")]
    [InlineData("8.0.200-preview3", "v7.0", "7.0")]
    [InlineData("8.0.200-preview3", "v8.0", "8.0")]
    [InlineData("6.0.100", "v6.0", "6.0")]
    [InlineData("6.0.100-preview.1", "v6.0", "6.0")]
    [InlineData("8.0.100-dev", "v8.0", "8.0-preview")]
    [InlineData("8.0.100-ci", "v8.0", "8.0-preview")]
    [InlineData("8.0.100-rtm.23502.3", "v8.0", "8.0")]
    [InlineData("8.0.100-servicing.23502.3", "v8.0", "8.0")]
    [InlineData("8.0.100-alpha.12345", "v8.0", "8.0-preview")]
    [InlineData("9.0.100-alpha.12345", "v9.0", "9.0-preview")]
    [Theory]
    public void CanComputeTagsForSupportedSDKVersions(string sdkVersion, string tfm, string expectedTag)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["TargetFrameworkIdentifier"] = ".NETCoreApp",
            ["NETCoreSdkVersion"] = sdkVersion,
            ["TargetFrameworkVersion"] = tfm,
            ["PublishProfile"] = "DefaultContainer"
        }, projectName: $"{nameof(CanComputeTagsForSupportedSDKVersions)}_{sdkVersion}_{tfm}_{expectedTag}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, new[] { logger }, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedTag = instance.GetProperty(ContainerBaseImage).EvaluatedValue;
        computedTag.Should().EndWith(expectedTag);
    }
 
    [InlineData("v8.0", "linux-x64", null)]
    [InlineData("v8.0", "linux-musl-x64", null)]
    [InlineData("v8.0", "win-x64", "ContainerUser")]
    [InlineData("v7.0", "linux-x64", null)]
    [InlineData("v7.0", "win-x64", null)]
    [InlineData("v9.0", "linux-x64", null)]
    [InlineData("v9.0", "win-x64", "ContainerUser")]
    [Theory]
    public void CanComputeContainerUser(string tfm, string rid, string? expectedUser)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["TargetFrameworkIdentifier"] = ".NETCoreApp",
            ["TargetFrameworkVersion"] = tfm,
            ["TargetFramework"] = "net" + tfm.TrimStart('v'),
            ["ContainerRuntimeIdentifier"] = rid,
        }, projectName: $"{nameof(CanComputeContainerUser)}_{tfm}_{rid}_{expectedUser}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedTag = instance.GetProperty("ContainerUser")?.EvaluatedValue;
        computedTag.Should().Be(expectedUser);
    }
 
    [InlineData("linux-x64", "linux-x64")]
    [InlineData("linux-arm64", "linux-arm64")]
    [InlineData("windows-x64", "linux-x64")]
    [InlineData("windows-arm64", "linux-arm64")]
    [Theory]
    public void WindowsUsersGetLinuxContainers(string sdkPortableRid, string expectedRid)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["TargetFrameworkVersion"] = "v6.0",
            ["NETCoreSdkPortableRuntimeIdentifier"] = sdkPortableRid
        }, projectName: $"{nameof(WindowsUsersGetLinuxContainers)}_{sdkPortableRid}_{expectedRid}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedRid = instance.GetProperty(ContainerRuntimeIdentifier)?.EvaluatedValue;
        computedRid.Should().Be(expectedRid);
    }
 
    [InlineData("8.0.100", "v7.0", "", "7.0")]
    [InlineData("8.0.100-preview.2", "v8.0", "", "8.0.0-preview.2")]
    [InlineData("8.0.100-preview.2", "v8.0", "jammy", "8.0.0-preview.2-jammy")]
    [InlineData("8.0.100-preview.2", "v8.0", "jammy-chiseled", "8.0.0-preview.2-jammy-chiseled")]
    [InlineData("8.0.100-rc.2", "v8.0", "jammy-chiseled", "8.0.0-rc.2-jammy-chiseled")]
    [InlineData("8.0.100", "v8.0", "jammy-chiseled", "8.0-jammy-chiseled-extra")]
    [InlineData("8.0.200", "v8.0", "jammy-chiseled", "8.0-jammy-chiseled-extra")]
    [InlineData("8.0.300", "v8.0", "noble-chiseled", "8.0-noble-chiseled-extra")]
    [InlineData("8.0.300", "v8.0", "jammy-chiseled", "8.0-jammy-chiseled-extra")]
    [Theory]
    public void CanTakeContainerBaseFamilyIntoAccount(string sdkVersion, string tfmMajMin, string containerFamily, string expectedTag)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = sdkVersion,
            ["TargetFrameworkVersion"] = tfmMajMin,
            [ContainerFamily] = containerFamily,
        }, projectName: $"{nameof(CanTakeContainerBaseFamilyIntoAccount)}_{sdkVersion}_{tfmMajMin}_{containerFamily}_{expectedTag}");
        using var _ = d;
        var instance = project.CreateProjectInstance(ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().EndWith(expectedTag);
    }
 
    [InlineData("v6.0", "linux-musl-x64", "mcr.microsoft.com/dotnet/runtime:6.0-alpine")]
    [InlineData("v6.0", "linux-x64", "mcr.microsoft.com/dotnet/runtime:6.0")]
    [InlineData("v7.0", "linux-musl-x64", "mcr.microsoft.com/dotnet/runtime:7.0-alpine")]
    [InlineData("v7.0", "linux-x64", "mcr.microsoft.com/dotnet/runtime:7.0")]
    [InlineData("v8.0", "linux-musl-x64", "mcr.microsoft.com/dotnet/runtime:8.0-alpine")]
    [InlineData("v8.0", "linux-x64", "mcr.microsoft.com/dotnet/runtime:8.0")]
    [Theory]
    public void MuslRidsGetAlpineContainers(string tfm, string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = tfm,
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
        }, projectName: $"{nameof(MuslRidsGetAlpineContainers)}_{tfm}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData("linux-musl-x64;linux-musl-arm64", "mcr.microsoft.com/dotnet/runtime:8.0-alpine")]
    [InlineData("linux-x64;linux-arm64", "mcr.microsoft.com/dotnet/runtime:8.0")]
    [Theory]
    public void AllMuslRidsGetAlpineContainers(string rids, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rids,
        }, projectName: $"{nameof(AllMuslRidsGetAlpineContainers)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [Fact]
    public void NotAllMuslRidsLogsError()
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = "linux-musl-x64;linux-arm64",
        }, projectName: $"{nameof(NotAllMuslRidsLogsError)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, [logger], null, out var outputs).Should().BeFalse(String.Join(Environment.NewLine, logger.Errors));
        logger.Errors.Should().ContainSingle(error => error.Message == Resources.Strings.InvalidTargetRuntimeIdentifiers);
    }
 
    [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine")]
    [InlineData("linux-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy-chiseled")]
    [Theory]
    public void AOTAppsGetExpectedImages(string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishAot] = true.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = true.ToString(),
        }, projectName: $"{nameof(AOTAppsGetExpectedImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine-extra")]
    [InlineData("linux-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled-extra")]
    [Theory]
    public void AOTAppsWithCulturesGetExtraImages(string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.300",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishAot] = true.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = false.ToString()
        }, projectName: $"{nameof(AOTAppsWithCulturesGetExtraImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine-extra")]
    [InlineData("linux-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled-extra")]
    [Theory]
    public void TrimmedAppsWithCulturesGetExtraImages(string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.300",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishTrimmed] = true.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = false.ToString()
        }, projectName: $"{nameof(TrimmedAppsWithCulturesGetExtraImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine")]
    [InlineData("linux-x64", "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled")]
    [Theory]
    public void TrimmedAppsWithoutCulturesGetbaseImages(string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.300",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishTrimmed] = true.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = true.ToString()
        }, projectName: $"{nameof(TrimmedAppsWithCulturesGetExtraImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData(true, false, "linux-musl-x64", true, "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine")]
    [InlineData(true, false, "linux-musl-x64", false, "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine-extra")]
    [InlineData(false, true, "linux-musl-x64", true, "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine")]
    [InlineData(false, true, "linux-musl-x64", false, "mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine-extra")]
 
    [InlineData(true, false, "linux-x64", true, "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled")]
    [InlineData(true, false, "linux-x64", false, "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled-extra")]
    [InlineData(false, true, "linux-x64", true, "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled")]
    [InlineData(false, true, "linux-x64", false, "mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled-extra")]
    [Theory]
    public void TheBigMatrixOfTrimmingInference(bool trimmed, bool aot, string rid, bool invariant, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.300",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishTrimmed] = trimmed.ToString(),
            [KnownStrings.Properties.PublishAot] = aot.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = invariant.ToString()
        }, projectName: $"{nameof(TrimmedAppsWithCulturesGetExtraImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine")]
    [InlineData("linux-x64", "mcr.microsoft.com/dotnet/runtime-deps:7.0")]
    [Theory]
    public void AOTAppsLessThan8DoNotGetAOTImages(string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v7.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishAot] = true.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = true.ToString(),
        }, projectName: $"{nameof(AOTAppsLessThan8DoNotGetAOTImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [Fact]
    public void FDDConsoleAppWithCulturesAndOptingIntoChiseledGetsExtras()
    {
        var expectedImage = "mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled-extra";
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = "linux-x64",
            [KnownStrings.Properties.ContainerFamily] = "jammy-chiseled",
            [KnownStrings.Properties.InvariantGlobalization] = false.ToString(),
        }, projectName: $"{nameof(FDDConsoleAppWithCulturesAndOptingIntoChiseledGetsExtras)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [Fact]
    public void FDDAspNetAppWithCulturesAndOptingIntoChiseledGetsExtras()
    {
        var expectedImage = "mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled-extra";
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = "linux-x64",
            [KnownStrings.Properties.ContainerFamily] = "jammy-chiseled",
            [KnownStrings.Properties.InvariantGlobalization] = false.ToString(),
        }, bonusItems: new()
        {
            [KnownStrings.Items.FrameworkReference] = KnownFrameworkReferences.WebApp
        }, projectName: $"{nameof(FDDAspNetAppWithCulturesAndOptingIntoChiseledGetsExtras)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine")]
    [InlineData("linux-x64", "mcr.microsoft.com/dotnet/runtime-deps:7.0")]
    [Theory]
    public void AOTAppsLessThan8WithCulturesDoNotGetExtraImages(string rid, string expectedImage)
    {
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.100",
            ["TargetFrameworkVersion"] = "v7.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = rid,
            [KnownStrings.Properties.PublishSelfContained] = true.ToString(),
            [KnownStrings.Properties.PublishAot] = true.ToString(),
            [KnownStrings.Properties.InvariantGlobalization] = false.ToString()
        }, projectName: $"{nameof(AOTAppsWithCulturesGetExtraImages)}_{rid}_{expectedImage}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    [Fact]
    public void AspNetFDDAppsGetAspNetBaseImage()
    {
        var expectedImage = "mcr.microsoft.com/dotnet/aspnet:8.0";
        var (project, logger, d) = ProjectInitializer.InitProject(new()
        {
            ["NetCoreSdkVersion"] = "8.0.200",
            ["TargetFrameworkVersion"] = "v8.0",
            [KnownStrings.Properties.ContainerRuntimeIdentifier] = "linux-x64",
        }, bonusItems: new()
        {
            [KnownStrings.Items.FrameworkReference] = KnownFrameworkReferences.WebApp
        }, projectName: $"{nameof(AspNetFDDAppsGetAspNetBaseImage)}");
        using var _ = d;
        var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
        instance.Build(new[] { ComputeContainerBaseImage }, [logger], null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
        var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue;
        computedBaseImageTag.Should().BeEquivalentTo(expectedImage);
    }
 
    private static class KnownFrameworkReferences
    {
        public static Microsoft.Build.Framework.ITaskItem[] ConsoleApp { get; } = [new Microsoft.Build.Utilities.TaskItem("Microsoft.NETCore.App")];
        public static Microsoft.Build.Framework.ITaskItem[] WebApp { get; } = [.. ConsoleApp, new Microsoft.Build.Utilities.TaskItem("Microsoft.AspNetCore.App")];
    }
}