File: ProjectBuildTests.cs
Web Access
Project: ..\..\..\test\EndToEnd.Tests\EndToEnd.Tests.csproj (EndToEnd.Tests)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
#nullable disable
 
using EndToEnd.Tests.Utilities;
 
namespace EndToEnd.Tests
{
    public class ProjectBuildTests(ITestOutputHelper log) : SdkTest(log)
    {
        [Fact]
        public void ItCanNewRestoreBuildRunCleanMSBuildProject()
        {
            var directory = _testAssetsManager.CreateTestDirectory();
            string projectDirectory = directory.Path;
 
            new DotnetNewCommand(Log, "console", "--no-restore")
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            string projectPath = Path.Combine(projectDirectory, new DirectoryInfo(directory.Path).Name + ".csproj");
 
            var project = XDocument.Load(projectPath);
            var ns = project.Root.Name.Namespace;
 
            project.Root.Element(ns + "PropertyGroup")
                .Element(ns + "TargetFramework").Value = ToolsetInfo.CurrentTargetFramework;
            project.Save(projectPath);
 
            new RestoreCommand(Log, projectPath)
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            new BuildCommand(Log, projectPath)
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            new DotnetCommand(Log, "run")
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass().And.HaveStdOutContaining("Hello, World!");
 
            var binDirectory = new DirectoryInfo(projectDirectory).Sub("bin");
            binDirectory.Should().HaveFilesMatching("*.dll", SearchOption.AllDirectories);
 
            new CleanCommand(Log, projectPath)
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            binDirectory.Should().NotHaveFilesMatching("*.dll", SearchOption.AllDirectories);
        }
 
        [Fact]
        public void ItCanRunAnAppUsingTheWebSdk()
        {
            var directory = _testAssetsManager.CreateTestDirectory();
            string projectDirectory = directory.Path;
 
            new DotnetNewCommand(Log, "console", "--no-restore")
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            string projectPath = Path.Combine(projectDirectory, new DirectoryInfo(directory.Path).Name + ".csproj");
 
            var project = XDocument.Load(projectPath);
            var ns = project.Root.Name.Namespace;
 
            project.Root.Attribute("Sdk").Value = "Microsoft.NET.Sdk.Web";
            project.Root.Element(ns + "PropertyGroup")
                .Element(ns + "TargetFramework").Value = ToolsetInfo.CurrentTargetFramework;
            project.Save(projectPath);
 
            new BuildCommand(Log, projectPath)
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            new DotnetCommand(Log, "run")
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass().And.HaveStdOutContaining("Hello, World!");
        }
 
        [Theory]
        [InlineData("current", true)]
        [InlineData("current", false)]
        public void ItCanPublishArm64Winforms(string targetFramework, bool selfContained)
        {
            var directory = _testAssetsManager.CreateTestDirectory();
            string projectDirectory = directory.Path;
 
            string[] newArgs = [
                "winforms",
                "--no-restore",
                .. targetFramework != "current" ? ["-f", targetFramework] : Array.Empty<string>()
            ];
            new DotnetNewCommand(Log)
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute(newArgs).Should().Pass();
 
            string[] publishArgs = [
                "-r",
                "win-arm64",
                .. selfContained ? ["--self-contained"] : Array.Empty<string>(),
                .. RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Array.Empty<string>() : ["/p:EnableWindowsTargeting=true"],
            ];
            new DotnetPublishCommand(Log, publishArgs)
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            var selfContainedPublishDir = new DirectoryInfo(projectDirectory)
                .Sub("bin").Sub(targetFramework != "current" ? "Debug" : "Release")
                .GetDirectories().FirstOrDefault().Sub("win-arm64").Sub("publish");
 
            if (selfContained)
            {
                selfContainedPublishDir.Should().HaveFilesMatching("System.Windows.Forms.dll", SearchOption.TopDirectoryOnly);
            }
            selfContainedPublishDir.Should().HaveFilesMatching($"{new DirectoryInfo(directory.Path).Name}.dll", SearchOption.TopDirectoryOnly);
        }
 
        [WindowsOnlyTheory]
        [InlineData("current", true)]
        [InlineData("current", false)]
        public void ItCanPublishArm64Wpf(string targetFramework, bool selfContained)
        {
            var directory = _testAssetsManager.CreateTestDirectory();
            string projectDirectory = directory.Path;
 
            string[] newArgs = [
                "wpf",
                "--no-restore",
                .. targetFramework != "current" ? ["-f", targetFramework] : Array.Empty<string>()
            ];
            new DotnetNewCommand(Log)
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute(newArgs).Should().Pass();
 
            string[] publishArgs = [
                "-r",
                "win-arm64",
                .. selfContained ? ["--self-contained"] : Array.Empty<string>()
            ];
            new DotnetPublishCommand(Log, publishArgs)
                .WithWorkingDirectory(projectDirectory)
                .Execute().Should().Pass();
 
            var selfContainedPublishDir = new DirectoryInfo(projectDirectory)
                .Sub("bin").Sub(targetFramework != "current" ? "Debug" : "Release")
                .GetDirectories().FirstOrDefault().Sub("win-arm64").Sub("publish");
 
            if (selfContained)
            {
                selfContainedPublishDir.Should().HaveFilesMatching("PresentationCore.dll", SearchOption.TopDirectoryOnly);
                selfContainedPublishDir.Should().HaveFilesMatching("PresentationNative_*.dll", SearchOption.TopDirectoryOnly);
            }
            selfContainedPublishDir.Should().HaveFilesMatching($"{new DirectoryInfo(directory.Path).Name}.dll", SearchOption.TopDirectoryOnly);
        }
 
        [Theory]
        // microsoft.dotnet.common.projectemplates templates
        [InlineData("console")]
        [InlineData("console", "C#")]
        [InlineData("console", "VB")]
        [InlineData("console", "F#")]
        [InlineData("classlib")]
        [InlineData("classlib", "C#")]
        [InlineData("classlib", "VB")]
        [InlineData("classlib", "F#")]
        [InlineData("mstest")]
        [InlineData("nunit")]
        [InlineData("web")]
        [InlineData("mvc")]
        public void ItCanBuildTemplates(string templateName, string language = "") => TestTemplateCreateAndBuild(templateName, language: language);
 
        /// <summary>
        /// The test checks if dotnet new shows curated list correctly after the SDK installation and template insertion.
        /// </summary>
        [Fact]
        public void DotnetNewShowsCuratedListCorrectly()
        {
            string locale = Thread.CurrentThread.CurrentUICulture.Name;
            if (!string.IsNullOrWhiteSpace(locale)
                && !locale.StartsWith("en", StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine($"[{nameof(DotnetNewShowsCuratedListCorrectly)}] CurrentUICulture: {locale}");
                Console.WriteLine($"[{nameof(DotnetNewShowsCuratedListCorrectly)}] Test is skipped as it supports only 'en' or invariant culture.");
                return;
            }
 
            string expectedOutput =
@"[\-\s]+
[\w \.\(\)]+blazor\s+\[C#\][\w\ \/]+
[\w \.\(\)]+classlib\s+\[C#\],F#,VB[\w\ \/]+
[\w \.\(\)]+console\s+\[C#\],F#,VB[\w\ \/]+
[\w \.\(\)]+mstest\s+\[C#\],F#,VB[\w\ \/]+
";
 
            expectedOutput +=
@"[\w \.\(\)]+winforms\s+\[C#\],VB[\w\ \/]+
[\w \.\(\)]+\wpf\s+\[C#\],VB[\w\ \/]+
";
 
            //list should end with new line
            expectedOutput += Environment.NewLine;
 
            new DotnetNewCommand(Log)
                .WithVirtualHive()
                .Execute().Should().Pass()
                .And.HaveStdOutMatching(expectedOutput);
        }
 
        [Theory]
        // microsoft.dotnet.common.itemtemplates templates
        [InlineData("globaljson")]
        [InlineData("nugetconfig")]
        [InlineData("webconfig")]
        [InlineData("gitignore")]
        [InlineData("tool-manifest")]
        [InlineData("sln")]
        public void ItCanCreateItemTemplate(string templateName)
        {
            var directory = _testAssetsManager.CreateTestDirectory(identifier: templateName);
            string projectDirectory = directory.Path;
 
            string newArgs = $"{templateName}";
 
            new DotnetNewCommand(Log)
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute(newArgs).Should().Pass();
 
            //check if the template created files
            var directoryInfo = new DirectoryInfo(directory.Path);
            Assert.True(directoryInfo.Exists);
            Assert.True(directoryInfo.EnumerateFileSystemInfos().Any());
 
            // delete test directory for some tests so we aren't leaving behind non-compliant nuget files
            if (templateName.Equals("nugetconfig"))
            {
                directoryInfo.Delete(true);
            }
        }
 
        [Theory]
        // microsoft.dotnet.common.itemtemplates templates
        [InlineData("class")]
        [InlineData("struct")]
        [InlineData("enum")]
        [InlineData("record")]
        [InlineData("interface")]
        [InlineData("class", "C#")]
        [InlineData("class", "VB")]
        [InlineData("struct", "VB")]
        [InlineData("enum", "VB")]
        [InlineData("interface", "VB")]
        public void ItCanCreateItemTemplateWithProjectRestriction(string templateName, string language = "")
        {
            var languageExtensionMap = new Dictionary<string, string>()
            {
                { "", ".cs" },
                { "C#", ".cs" },
                { "VB", ".vb" }
            };
 
            var directory = InstantiateProjectTemplate("classlib", language, withNoRestore: false, itemName: templateName);
            string projectDirectory = directory.Path;
            string expectedItemName = $"TestItem_{templateName}";
 
            string[] newArgs = [
                templateName,
                "--name",
                expectedItemName,
                .. !string.IsNullOrWhiteSpace(language) ? ["--language", language] : Array.Empty<string>()
            ];
            new DotnetNewCommand(Log)
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute(newArgs).Should().Pass();
 
            //check if the template created files
            var directoryInfo = new DirectoryInfo(directory.Path);
            Assert.True(directoryInfo.Exists);
            Assert.True(directoryInfo.EnumerateFileSystemInfos().Any());
            Assert.True(directoryInfo.File($"{expectedItemName}.{languageExtensionMap[language]}") != null);
        }
 
        [Theory]
        [InlineData("wpf")]
        [InlineData("winforms")]
        public void ItCanBuildDesktopTemplates(string templateName) => TestTemplateCreateAndBuild(templateName);
 
        [Theory]
        [InlineData("wpf")]
        public void ItCanBuildDesktopTemplatesSelfContained(string templateName) => TestTemplateCreateAndBuild(templateName, selfContained: true);
 
        [Theory]
        [InlineData("web")]
        [InlineData("console")]
        public void ItCanBuildTemplatesSelfContained(string templateName) => TestTemplateCreateAndBuild(templateName, selfContained: true);
 
        /// <summary>
        /// The test checks if the template creates the template for correct framework by default.
        /// For .NET 6 the templates should create the projects targeting net6.0
        /// </summary>
        [Theory]
        [InlineData("console")]
        [InlineData("console", "C#")]
        [InlineData("console", "VB")]
        [InlineData("console", "F#")]
        [InlineData("classlib")]
        [InlineData("classlib", "C#")]
        [InlineData("classlib", "VB")]
        [InlineData("classlib", "F#")]
        [InlineData("worker")]
        [InlineData("worker", "C#")]
        [InlineData("worker", "F#")]
        [InlineData("mstest")]
        [InlineData("mstest", "C#")]
        [InlineData("mstest", "VB")]
        [InlineData("mstest", "F#")]
        [InlineData("nunit")]
        [InlineData("nunit", "C#")]
        [InlineData("nunit", "VB")]
        [InlineData("nunit", "F#")]
        [InlineData("xunit")]
        [InlineData("xunit", "C#")]
        [InlineData("xunit", "VB")]
        [InlineData("xunit", "F#")]
        [InlineData("blazorwasm")]
        [InlineData("web")]
        [InlineData("web", "C#")]
        [InlineData("web", "F#")]
        [InlineData("mvc")]
        [InlineData("mvc", "C#")]
        [InlineData("mvc", "F#")]
        [InlineData("webapi")]
        [InlineData("webapi", "C#")]
        [InlineData("webapi", "F#")]
        [InlineData("webapp")]
        [InlineData("razorclasslib")]
        public void ItCanCreateAndBuildTemplatesWithDefaultFramework(string templateName, string language = "")
        {
            string framework = DetectExpectedDefaultFramework(templateName);
            TestTemplateCreateAndBuild(templateName, selfContained: false, language: language, framework: framework);
        }
 
        /// <summary>
        /// The test checks if the template creates the template for correct framework by default.
        /// For .NET 6 the templates should create the projects targeting net6.0.
        /// </summary>
        [Theory]
        [InlineData("wpf")]
        [InlineData("wpf", "C#")]
        [InlineData("wpf", "VB")]
        [InlineData("wpflib")]
        [InlineData("wpflib", "C#")]
        [InlineData("wpflib", "VB")]
        [InlineData("wpfcustomcontrollib")]
        [InlineData("wpfcustomcontrollib", "C#")]
        [InlineData("wpfcustomcontrollib", "VB")]
        [InlineData("wpfusercontrollib")]
        [InlineData("wpfusercontrollib", "C#")]
        [InlineData("wpfusercontrollib", "VB")]
        [InlineData("winforms")]
        [InlineData("winforms", "C#")]
        [InlineData("winforms", "VB")]
        [InlineData("winformslib")]
        [InlineData("winformslib", "C#")]
        [InlineData("winformslib", "VB")]
        [InlineData("winformscontrollib")]
        [InlineData("winformscontrollib", "C#")]
        [InlineData("winformscontrollib", "VB")]
        public void ItCanCreateAndBuildTemplatesWithDefaultFramework_Windows(string templateName, string language = "")
        {
            string framework = DetectExpectedDefaultFramework(templateName);
            TestTemplateCreateAndBuild(templateName, selfContained: false, language: language, framework: $"{framework}-windows");
        }
 
        /// <summary>
        /// [project is not built on linux-musl]
        /// The test checks if the template creates the template for correct framework by default.
        /// For .NET 6 the templates should create the projects targeting net6.0.
        /// </summary>
        [Theory]
        [InlineData("grpc")]
        public void ItCanCreateAndBuildTemplatesWithDefaultFramework_DisableBuildOnLinuxMusl(string templateName)
        {
            string framework = DetectExpectedDefaultFramework(templateName);
 
            if (RuntimeInformation.RuntimeIdentifier.StartsWith("linux-musl"))
            {
                TestTemplateCreateAndBuild(templateName, build: false, framework: framework);
            }
            else
            {
                TestTemplateCreateAndBuild(templateName, selfContained: true, framework: framework);
            }
        }
 
        private static string DetectExpectedDefaultFramework(string template = "")
        {
            string dotnetFolder = Path.GetDirectoryName(TestContext.Current.ToolsetUnderTest.DotNetHostPath);
            string[] runtimeFolders = Directory.GetDirectories(Path.Combine(dotnetFolder, "shared", "Microsoft.NETCore.App"));
            int latestMajorVersion = runtimeFolders.Select(folder => int.Parse(Path.GetFileName(folder).Split('.').First())).Max();
            if (latestMajorVersion == 10)
            {
                return $"net{latestMajorVersion}.0";
            }
 
            throw new Exception("Unsupported version of SDK");
        }
 
        private void TestTemplateCreateAndBuild(string templateName, bool build = true, bool selfContained = false, string language = "", string framework = "", bool deleteTestDirectory = false)
        {
            var directory = InstantiateProjectTemplate(templateName, language);
            string projectDirectory = directory.Path;
 
            XDocument GetProjectXml()
            {
                string expectedExtension = language switch
                {
                    "C#" => "*.csproj",
                    "F#" => "*.fsproj",
                    "VB" => "*.vbproj",
                    _ => "*.csproj"
                };
                string projectFile = Directory.GetFiles(projectDirectory, expectedExtension).Single();
                XDocument projectXml = XDocument.Load(projectFile);
                return projectXml;
            }
 
            if (!string.IsNullOrWhiteSpace(framework))
            {
                //check if MSBuild TargetFramework property for *proj is set to expected framework
                var projectXml = GetProjectXml();
                XNamespace ns = projectXml.Root.Name.Namespace;
                Assert.Equal(framework, projectXml.Root.Element(ns + "PropertyGroup").Element(ns + "TargetFramework").Value);
            }
 
            bool needsEnableWindowsTargeting = false;
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                string effectiveFramework = framework;
                if (string.IsNullOrEmpty(effectiveFramework))
                {
                    var projectXml = GetProjectXml();
                    XNamespace ns = projectXml.Root.Name.Namespace;
                    effectiveFramework = projectXml.Root.Element(ns + "PropertyGroup").Element(ns + "TargetFramework").Value;
                }
 
                if (effectiveFramework.Contains("windows"))
                {
                    needsEnableWindowsTargeting = true;
                }
            }
 
            if (build)
            {
                string[] buildArgs = [
                    .. selfContained ? ["-r", RuntimeInformation.RuntimeIdentifier] : Array.Empty<string>(),
                    .. !string.IsNullOrWhiteSpace(framework) ? ["--framework", framework] : Array.Empty<string>(),
                    // Remove this (or formalize it) after https://github.com/dotnet/installer/issues/12479 is resolved.
                    .. language == "F#" ? ["/p:_NETCoreSdkIsPreview=true"] : Array.Empty<string>(),
                    .. needsEnableWindowsTargeting ? ["/p:EnableWindowsTargeting=true"] : Array.Empty<string>(),
                    $"/bl:{templateName}-{(selfContained ? "selfcontained" : "fdd")}-{language}-{framework}-{{}}.binlog"
                ];
 
                string dotnetRoot = Path.GetDirectoryName(TestContext.Current.ToolsetUnderTest.DotNetHostPath);
                new DotnetBuildCommand(Log, projectDirectory)
                     .WithEnvironmentVariable("PATH", dotnetRoot) // override PATH since razor rely on PATH to find dotnet
                     .WithWorkingDirectory(projectDirectory)
                     .Execute(buildArgs).Should().Pass();
            }
 
            // delete test directory for some tests so we aren't leaving behind non-compliant package files
            if (deleteTestDirectory)
            {
                new DirectoryInfo(directory.Path).Delete(true);
            }
        }
 
        private TestDirectory InstantiateProjectTemplate(string templateName, string language = "", bool withNoRestore = true, string itemName = "")
        {
            var identifier = templateName;
            if (!string.IsNullOrWhiteSpace(language))
            {
                identifier += $"[{language}]";
            }
            if (!string.IsNullOrWhiteSpace(itemName))
            {
                identifier += $"({itemName})";
            }
            var directory = _testAssetsManager.CreateTestDirectory(identifier: identifier);
            string projectDirectory = directory.Path;
 
            string[] newArgs = [
                templateName,
                .. withNoRestore ? ["--no-restore"] : Array.Empty<string>(),
                // Remove this (or formalize it) after https://github.com/dotnet/installer/issues/12479 is resolved.
                .. !string.IsNullOrWhiteSpace(language) ? ["--language", language] : Array.Empty<string>()
            ];
            new DotnetNewCommand(Log)
                .WithVirtualHive()
                .WithWorkingDirectory(projectDirectory)
                .Execute(newArgs).Should().Pass();
 
            return directory;
        }
    }
}