File: CommonTemplatesTests.cs
Web Access
Project: ..\..\..\test\dotnet-new.IntegrationTests\dotnet-new.IntegrationTests.csproj (dotnet-new.IntegrationTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.DotNet.Cli.Commands.New;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Authoring.TemplateVerifier;
using Microsoft.TemplateEngine.TestHelper;
using TestLoggerFactory = Microsoft.NET.TestFramework.TestLoggerFactory;
 
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
    public class CommonTemplatesTests : BaseIntegrationTest, IClassFixture<SharedHomeDirectory>
    {
        private readonly SharedHomeDirectory _fixture;
        private readonly ITestOutputHelper _log;
        private readonly ILogger _logger;
 
        public CommonTemplatesTests(SharedHomeDirectory fixture, ITestOutputHelper log) : base(log)
        {
            _fixture = fixture;
            _log = log;
            _logger = new TestLoggerFactory(log).CreateLogger(nameof(CommonTemplatesTests));
        }
 
        [Theory]
        [InlineData("global.json file", "globaljson", null)]
        [InlineData("global.json file", "globaljson", new[] { "--sdk-version", "6.0.200" })]
        [InlineData("global.json file", "globaljson", new[] { "--sdk-version", "6.0.200", "--roll-forward", "major" })]
        [InlineData("global.json file", "globaljson", new[] { "--test-runner", "VSTest" })]
        [InlineData("global.json file", "globaljson", new[] { "--test-runner", "Microsoft.Testing.Platform" })]
        [InlineData("global.json file", "globaljson", new[] { "--sdk-version", "6.0.200", "--test-runner", "VSTest" })]
        [InlineData("global.json file", "globaljson", new[] { "--roll-forward", "major", "--test-runner", "Microsoft.Testing.Platform" })]
        [InlineData("global.json file", "global.json", null)]
        [InlineData("global.json file", "global.json", new[] { "--sdk-version", "6.0.200" })]
        [InlineData("global.json file", "global.json", new[] { "--sdk-version", "6.0.200", "--roll-forward", "major" })]
        [InlineData("global.json file", "global.json", new[] { "--test-runner", "VSTest" })]
        [InlineData("global.json file", "global.json", new[] { "--test-runner", "Microsoft.Testing.Platform" })]
        [InlineData("NuGet Config", "nugetconfig", null)]
        [InlineData("NuGet Config", "nuget.config", null)]
        [InlineData("dotnet gitignore file", "gitignore", null)]
        [InlineData("dotnet gitignore file", ".gitignore", null)]
        [InlineData("Solution File", "sln", null)]
        [InlineData("Solution File", "sln", new[] { "--format", "sln" })]
        [InlineData("Solution File", "sln", new[] { "--format", "slnx" })]
        [InlineData("Solution File", "solution", null)]
        [InlineData("Dotnet local tool manifest file", "tool-manifest", null)]
        [InlineData("Web Config", "webconfig", null)]
        [InlineData("EditorConfig file", "editorconfig", null)]
        [InlineData("EditorConfig file", "editorconfig", new[] { "--empty" })]
        [InlineData("EditorConfig file", ".editorconfig", null)]
        [InlineData("EditorConfig file", ".editorconfig", new[] { "--empty" })]
        [InlineData("MSBuild Directory.Build.props file", "buildprops", new[] { "--inherit", "--use-artifacts" })]
        [InlineData("MSBuild Directory.Build.targets file", "buildtargets", new[] { "--inherit" })]
        public async Task AllCommonItemsCreate(string expectedTemplateName, string templateShortName, string[]? args)
        {
            Dictionary<string, string> environmentUnderTest = new() { ["DOTNET_NOLOGO"] = false.ToString() };
            TestContext.Current.AddTestEnvironmentVariables(environmentUnderTest);
 
            string itemName = expectedTemplateName.Replace(' ', '-').Replace('.', '-');
 
            TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName)
            {
                // squashing snapshots by creating output unique for template (but not alias) and preventing item to have name by alias
                TemplateSpecificArgs = new[] { "-o", itemName, "-n", "item" }.Concat(args ?? Enumerable.Empty<string>()),
                SnapshotsDirectory = "Approvals",
                VerifyCommandOutput = true,
                VerificationExcludePatterns = new[] { "*/stderr.txt", "*\\stderr.txt" },
                SettingsDirectory = _fixture.HomeDirectory,
                DotnetExecutablePath = TestContext.Current.ToolsetUnderTest?.DotNetHostPath,
                DoNotPrependTemplateNameToScenarioName = true,
                UniqueFor = expectedTemplateName.Equals("NuGet Config") ? UniqueForOption.OsPlatform : null,
            }
            .WithCustomEnvironment(environmentUnderTest)
            .WithCustomScrubbers(
                ScrubbersDefinition.Empty
                    .AddScrubber(sb => sb.UnixifyNewlines(), "out")
                    .AddScrubber((path, content) =>
                    {
                        if (path.Replace(Path.DirectorySeparatorChar, '/') == "std-streams/stdout.txt")
                        {
                            content.UnixifyDirSeparators().Replace(expectedTemplateName, "%TEMPLATE_NAME%");
                        }
                    })
            );
 
            // globaljson is appending current sdk version. Due to the 'base' dotnet used to run test this version differs
            //  on dev and CI runs and possibly from the version within test host. Easiest is just to scrub it away
            if (expectedTemplateName.Equals("global.json file") &&
                (args == null || !args.Contains("--sdk-version")))
            {
                string sdkVersionUnderTest = await new SdkInfoProvider().GetCurrentVersionAsync(default);
                options.CustomScrubbers?.AddScrubber(sb => sb.Replace(sdkVersionUnderTest, "%CURRENT-VER%"), "json");
            }
 
            VerificationEngine engine = new(_logger);
            await engine.Execute(options);
        }
 
        //
        // Sample of a custom verifier callback
        // To be uncommented in case editorconfig template will start to genearate dynamic content
        //
 
        //[Fact]
        //public async Task EditorConfigTests_Default()
        //{
        //    TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "editorconfig")
        //    {
        //        SnapshotsDirectory = "Approvals",
        //        SettingsDirectory = _fixture.HomeDirectory,
        //    }
        //    .WithCustomDirectoryVerifier(async (content, contentFetcher) =>
        //    {
        //        await foreach (var (filePath, scrubbedContent) in contentFetcher.Value)
        //        {
        //            filePath.Replace(Path.DirectorySeparatorChar, '/').Should().BeEquivalentTo(@"editorconfig/.editorconfig");
        //            scrubbedContent.Should().Contain("dotnet_naming_rule");
        //            scrubbedContent.Should().Contain("dotnet_style_");
        //            scrubbedContent.Should().Contain("dotnet_naming_symbols");
        //        }
        //    });
 
        //    VerificationEngine engine = new VerificationEngine(_logger);
        //    await engine.Execute(options).ConfigureAwait(false);
        //}
 
        [Fact]
        public void NuGetConfigPermissions()
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                //runs only on Unix
                return;
            }
 
            string templateShortName = "nugetconfig";
            string expectedTemplateName = "NuGet Config";
            string workingDir = TestUtils.CreateTemporaryFolder();
 
            new DotnetNewCommand(_log, templateShortName)
                .WithCustomHive(_fixture.HomeDirectory)
                .WithWorkingDirectory(workingDir)
                .Execute()
                .Should()
                .ExitWith(0)
                .And.NotHaveStdErr()
                .And.HaveStdOutContaining($@"The template ""{expectedTemplateName}"" was created successfully.");
 
            var process = Process.Start(new ProcessStartInfo()
            {
                FileName = "/bin/sh",
                Arguments = "-c \"ls -la\"",
                WorkingDirectory = workingDir
            });
 
            new Command(process)
                .WorkingDirectory(workingDir)
                .CaptureStdOut()
                .CaptureStdErr()
                .Execute()
                .Should()
                .ExitWith(0)
                .And.HaveStdOutMatching("^-rw-------.*nuget.config$", RegexOptions.Multiline);
 
            Directory.Delete(workingDir, true);
        }
 
        [Theory]
        [InlineData(new object[] { "console", "C#" })]
        [InlineData(new object[] { "console", "VB" })]
        public async Task AotVariants(string name, string language)
        {
            // template framework needs to be hardcoded here during the major version transition.
            string currentDefaultFramework = $"net{Environment.Version.Major}.{Environment.Version.Minor}";
            // string currentDefaultFramework = "net10.0";
 
            string workingDir = CreateTemporaryFolder(folderName: $"{name}-{language}");
            string outputDir = "MyProject";
            string projName = name;
 
            List<string> args = new() { "-o", outputDir };
            // VB build would fail for name 'console' (root namespace would conflict with BCL namespace)
            if (language.Equals("VB") == true && name.Equals("console"))
            {
                projName = "vb-console";
                args.Add("-n");
                args.Add(projName);
            }
            args.Add("--aot");
 
            // Do not bother restoring. This would need to restore the AOT compiler.
            // We would need a nuget.config for that and it's a waste of time anyway.
            args.Add("--no-restore");
 
            string extension = language == "VB" ? "vbproj" : "csproj";
 
            string projectDir = Path.Combine(workingDir, outputDir);
            string finalProjectName = Path.Combine(projectDir, $"{projName}.{extension}");
 
            Dictionary<string, string> environmentUnderTest = new() { ["DOTNET_NOLOGO"] = false.ToString() };
            TestContext.Current.AddTestEnvironmentVariables(environmentUnderTest);
 
            TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: name)
            {
                TemplateSpecificArgs = args,
                SnapshotsDirectory = "Approvals",
                OutputDirectory = workingDir,
                SettingsDirectory = _fixture.HomeDirectory,
                VerifyCommandOutput = true,
                DoNotPrependTemplateNameToScenarioName = false,
                DoNotAppendTemplateArgsToScenarioName = true,
                ScenarioName = language.Replace('#', 's').ToLower(),
                VerificationExcludePatterns = new[] { "*/stderr.txt", "*\\stderr.txt" },
                DotnetExecutablePath = TestContext.Current.ToolsetUnderTest?.DotNetHostPath,
            }
            .WithCustomEnvironment(environmentUnderTest)
            .WithCustomScrubbers(
                ScrubbersDefinition.Empty
                    .AddScrubber(sb => sb.Replace($"<TargetFramework>{currentDefaultFramework}</TargetFramework>", "<TargetFramework>%FRAMEWORK%</TargetFramework>"))
                    .AddScrubber(sb => sb.Replace(finalProjectName, "%PROJECT_PATH%").UnixifyDirSeparators().ScrubByRegex("(^  Restored .* \\()(.*)(\\)\\.)", "$1%DURATION%$3", RegexOptions.Multiline), "txt")
            );
 
            VerificationEngine engine = new(_logger);
            await engine.Execute(options);
 
            Directory.Delete(workingDir, true);
        }
 
        #region Project templates language features tests
 
        /// <summary>
        /// Creates all possible combinations for supported templates, language versions and frameworks.
        /// </summary>
        public static IEnumerable<object?[]> FeaturesSupport_Data()
        {
            const string consoleTemplateShortname = "console";
 
            var templatesToTest = new[]
            {
                new { Template = consoleTemplateShortname,  Frameworks = new[] { null, "net6.0", "net8.0" } },
                new { Template = "classlib", Frameworks = new[] { null, "net6.0", "net8.0", "netstandard2.0", "netstandard2.1" } }
            };
 
            //features: top-level statements; nullables; implicit usings; filescoped namespaces
 
            string[] unsupportedLanguageVersions = { "1", "ISO-1" };
            //C# 12 is not supported yet - https://github.com/dotnet/sdk/issues/29195
            string?[] supportedLanguageVersions = { null, "ISO-2", "2", "3", "4", "5", "6", "7", "7.1", "7.2", "7.3", "8.0", "9.0", "10.0", "11.0", "11", /*"12",*/ "latest", "latestMajor", "default", "preview" };
 
            string?[] nullableSupportedInFrameworkByDefault = { null, "net6.0", "net8.0", "netstandard2.1" };
            string?[] implicitUsingsSupportedInFramework = { null, "net6.0", "net8.0" };
            string?[] fileScopedNamespacesSupportedFrameworkByDefault = { null, "net6.0", "net8.0" };
 
            string?[] nullableSupportedLanguages = { "8.0", "9.0", "10.0", "11.0", "11", /*"12",*/ "latest", "latestMajor", "default", "preview" };
            string?[] topLevelStatementSupportedLanguages = { null, "9.0", "10.0", "11", "11.0", /*"12",*/ "latest", "latestMajor", "default", "preview" };
            string?[] implicitUsingsSupportedLanguages = { null, "10.0", "11.0", "11", /*"12",*/ "latest", "latestMajor", "default", "preview" };
            string?[] fileScopedNamespacesSupportedLanguages = { "10.0", "11.0", "11", /*"12",*/ "latest", "latestMajor", "default", "preview" };
 
            string?[] supportedLangs = { null, "C#", "F#", "VB" };
 
            foreach (var template in templatesToTest)
            {
                foreach (string? langVersion in unsupportedLanguageVersions.Concat(supportedLanguageVersions))
                {
                    IEnumerable<string?> frameworks = template.Frameworks;
                    IEnumerable<string?> langs = new string?[] { null };
                    if (langVersion == null)
                    {
                        langs = supportedLangs;
                    }
 
                    foreach (string? framework in frameworks)
                    {
                        // Skip tests due to https://github.com/dotnet/templating/issues/5668#issuecomment-1327438284
                        if (framework == "net6.0" && double.TryParse(langVersion, out double lv) && lv >= 11)
                        {
                            continue;
                        }
 
                        foreach (string? lang in langs)
                        {
                            yield return CreateParams(template.Template, langVersion, lang, framework, false)!;
                            var testParams = CreateParams(template.Template, langVersion, lang, framework, true);
                            if (testParams != null)
                            {
                                yield return testParams;
                            }
                        }
                    }
                }
            }
 
            object?[]? CreateParams(string templateName, string? langVersion, string? lang, string? framework, bool forceDisableTopLevel)
            {
                bool supportsTopLevel = topLevelStatementSupportedLanguages.Contains(langVersion);
 
                // If forceDisableTopLevel is requested - then generate params only if it makes sense - for C# console project of a version that
                //  supports top level statements. Otherwise it doesn't make sense to test this overwritting functionality
                if ((!supportsTopLevel || !templateName.Equals(consoleTemplateShortname) || (lang != null && lang != "C#")) && forceDisableTopLevel)
                {
                    return null;
                }
 
                return new object?[]
                {
                    templateName,
                    // buildPass
                    supportedLanguageVersions.Contains(langVersion),
                    framework,
                    langVersion,
                    // langVersionUnsupported
                    unsupportedLanguageVersions.Contains(langVersion),
                    lang,
                    // supportsNullable
                    nullableSupportedLanguages.Contains(langVersion)
                     || langVersion == null && nullableSupportedInFrameworkByDefault.Contains(framework),
                    supportsTopLevel,
                    forceDisableTopLevel,
                    // supportsImplicitUsings
                    implicitUsingsSupportedLanguages.Contains(langVersion) && implicitUsingsSupportedInFramework.Contains(framework),
                    // supportsFileScopedNs
                    fileScopedNamespacesSupportedLanguages.Contains(langVersion)
                     || langVersion == null && fileScopedNamespacesSupportedFrameworkByDefault.Contains(framework),
                };
            }
        }
 
        [Theory]
        //creates all possible combinations for supported templates, language versions and frameworks
        [MemberData(nameof(FeaturesSupport_Data))]
        public async Task FeaturesSupport(
            string name,
            bool buildPass,
            string? framework,
            string? langVersion,
            bool langVersionUnsupported,
            string? language,
            bool supportsNullable,
            bool supportsTopLevel,
            bool forceDisableTopLevel,
            bool supportsImplicitUsings,
            bool supportsFileScopedNs)
        {
            // string currentDefaultFramework = "net10.0";
            string currentDefaultFramework = $"net{Environment.Version.Major}.{Environment.Version.Minor}";
 
            string workingDir = CreateTemporaryFolder(folderName: $"{name}-{langVersion ?? "null"}-{framework ?? "null"}");
            string outputDir = "MyProject";
            string projName = name;
 
            List<string> args = new() { "-o", outputDir };
            // VB build would fail for name 'console' (root namespace would conflict with BCL namespace)
            if (language?.Equals("VB") == true && name.Equals("console"))
            {
                projName = "vb-console";
                args.Add("-n");
                args.Add(projName);
            }
            if (!string.IsNullOrWhiteSpace(framework))
            {
                args.Add("--framework");
                args.Add(framework);
            }
            if (!string.IsNullOrWhiteSpace(langVersion))
            {
                args.Add("--langVersion");
                args.Add(langVersion);
            }
            if (!string.IsNullOrWhiteSpace(language))
            {
                args.Add("--language");
                args.Add(language);
            }
            if (!buildPass)
            {
                args.Add("--no-restore");
            }
            if (forceDisableTopLevel)
            {
                args.Add("--use-program-main");
                supportsTopLevel = false;
            }
 
            string extension = language switch
            {
                "F#" => "fsproj",
                "VB" => "vbproj",
                _ => "csproj"
            };
 
            string projectDir = Path.Combine(workingDir, outputDir);
            string finalProjectName = Path.Combine(projectDir, $"{projName}.{extension}");
 
            Dictionary<string, string> environmentUnderTest = new() { ["DOTNET_NOLOGO"] = false.ToString() };
            environmentUnderTest["CheckEolTargetFramework"] = false.ToString();
            TestContext.Current.AddTestEnvironmentVariables(environmentUnderTest);
 
            TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: name)
            {
                TemplateSpecificArgs = args,
                SnapshotsDirectory = "Approvals",
                OutputDirectory = workingDir,
                SettingsDirectory = _fixture.HomeDirectory,
                VerifyCommandOutput = true,
                DoNotPrependTemplateNameToScenarioName = false,
                DoNotAppendTemplateArgsToScenarioName = true,
                ScenarioName =
                    $"Nullable-{supportsNullable}#TopLevel-{supportsTopLevel}#ImplicitUsings-{supportsImplicitUsings}#FileScopedNs-{supportsFileScopedNs}"
                    + (string.IsNullOrEmpty(framework) ? string.Empty : $"#Framework-{framework}")
                    + '#' + (language == null ? "cs" : language.Replace('#', 's').ToLower())
                    + (langVersion == null ? "#NoLangVer" : (langVersionUnsupported ? "#UnsuportedLangVer" : null)),
                VerificationExcludePatterns = new[] { "*/stderr.txt", "*\\stderr.txt" },
                DotnetExecutablePath = TestContext.Current.ToolsetUnderTest?.DotNetHostPath,
            }
            .WithCustomEnvironment(environmentUnderTest)
            .WithCustomScrubbers(
                ScrubbersDefinition.Empty
                    .AddScrubber(sb => sb.Replace($"<LangVersion>{langVersion}</LangVersion>", "<LangVersion>%LANG%</LangVersion>"))
                    .AddScrubber(sb => sb.Replace($"<TargetFramework>{framework ?? currentDefaultFramework}</TargetFramework>", "<TargetFramework>%FRAMEWORK%</TargetFramework>"))
                    .AddScrubber(sb => sb.Replace(finalProjectName, "%PROJECT_PATH%").UnixifyDirSeparators().ScrubByRegex("(^  Restored .* \\()(.*)(\\)\\.)", "$1%DURATION%$3", RegexOptions.Multiline), "txt")
            );
 
            VerificationEngine engine = new(_logger);
            await engine.Execute(options);
 
            if (buildPass)
            {
                new DotnetBuildCommand(_log, "MyProject")
                    .WithWorkingDirectory(workingDir)
                    .Execute("/p:CheckEolTargetFramework=false")
                    .Should()
                    .Pass()
                    .And.NotHaveStdErr();
            }
 
            Directory.Delete(workingDir, true);
        }
 
        #endregion
    }
}