File: CommandTests\Run\RunFileTests.cs
Web Access
Project: ..\..\..\test\dotnet.Tests\dotnet.Tests.csproj (dotnet.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.Versioning;
using System.Text.Json;
using Basic.CompilerLog.Util;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging.StructuredLogger;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
 
namespace Microsoft.DotNet.Cli.Run.Tests;
 
public sealed class RunFileTests(ITestOutputHelper log) : SdkTest(log)
{
    private static readonly string s_program = /* lang=C#-Test */ """
        if (args.Length > 0)
        {
            Console.WriteLine("echo args:" + string.Join(";", args));
        }
        Console.WriteLine("Hello from " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
        #if !DEBUG
        Console.WriteLine("Release config");
        #endif
        #if CUSTOM_DEFINE
        Console.WriteLine("Custom define");
        #endif
        """;
 
    private static readonly string s_programDependingOnUtil = /* lang=C#-Test */ """
        if (args.Length > 0)
        {
            Console.WriteLine("echo args:" + string.Join(";", args));
        }
        Console.WriteLine("Hello, " + Util.GetMessage());
        """;
 
    private static readonly string s_util = /* lang=C#-Test */ """
        static class Util
        {
            public static string GetMessage()
            {
                return "String from Util";
            }
        }
        """;
 
    private static readonly string s_programReadingEmbeddedResource = /* lang=C#-Test */ """
        var assembly = System.Reflection.Assembly.GetExecutingAssembly();
        var resourceName = assembly.GetManifestResourceNames().SingleOrDefault();
 
        if (resourceName is null)
        {
            Console.WriteLine("Resource not found");
            return;
        }
 
        using var stream = assembly.GetManifestResourceStream(resourceName)!;
        using var reader = new System.Resources.ResourceReader(stream);
        Console.WriteLine(reader.Cast<System.Collections.DictionaryEntry>().Single());
        """;
 
    private static readonly string s_resx = """
        <root>
          <data name="MyString">
            <value>TestValue</value>
          </data>
        </root>
        """;
 
    private static readonly string s_consoleProject = $"""
        <Project Sdk="Microsoft.NET.Sdk">
          <PropertyGroup>
            <OutputType>Exe</OutputType>
            <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
            <ImplicitUsings>enable</ImplicitUsings>
          </PropertyGroup>
        </Project>
        """;
 
    private static readonly string s_launchSettings = """
        {
            "profiles": {
                "TestProfile1": {
                    "commandName": "Project",
                    "environmentVariables": {
                        "Message": "TestProfileMessage1"
                    }
                },
                "TestProfile2": {
                    "commandName": "Project",
                    "environmentVariables": {
                        "Message": "TestProfileMessage2"
                    }
                }
            }
        }
        """;
 
    /// <summary>
    /// Used when we need an out-of-tree base test directory to avoid having implicit build files
    /// like Directory.Build.props in scope and negating the optimizations we want to test.
    /// </summary>
    private static string OutOfTreeBaseDirectory => field ??= PrepareOutOfTreeBaseDirectory();
 
    private static bool HasCaseInsensitiveFileSystem
    {
        get
        {
            return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                || RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
        }
    }
 
    /// <inheritdoc cref="OutOfTreeBaseDirectory"/>
    private static string PrepareOutOfTreeBaseDirectory()
    {
        string outOfTreeBaseDirectory = TestPathUtility.ResolveTempPrefixLink(Path.Join(Path.GetTempPath(), "dotnetSdkTests"));
        Directory.CreateDirectory(outOfTreeBaseDirectory);
 
        // Create NuGet.config in our out-of-tree base directory.
        var sourceNuGetConfig = Path.Join(TestContext.Current.TestExecutionDirectory, "NuGet.config");
        var targetNuGetConfig = Path.Join(outOfTreeBaseDirectory, "NuGet.config");
        File.Copy(sourceNuGetConfig, targetNuGetConfig, overwrite: true);
 
        // Check there are no implicit build files that would prevent testing optimizations.
        VirtualProjectBuildingCommand.CollectImplicitBuildFiles(new DirectoryInfo(outOfTreeBaseDirectory), [], out var exampleMSBuildFile);
        exampleMSBuildFile.Should().BeNull(because: "there should not be any implicit build files in the temp directory or its parents " +
            "so we can test optimizations that would be disabled with implicit build files present");
 
        return outOfTreeBaseDirectory;
    }
 
    internal static string DirectiveError(string path, int line, string messageFormat, params ReadOnlySpan<object> args)
    {
        return $"{path}({line}): {CliCommandStrings.DirectiveError}: {string.Format(messageFormat, args)}";
    }
 
    /// <summary>
    /// <c>dotnet run file.cs</c> succeeds without a project file.
    /// </summary>
    [Theory]
    [InlineData(null, false)] // will be replaced with an absolute path
    [InlineData("Program.cs", false)]
    [InlineData("./Program.cs", false)]
    [InlineData("Program.CS", true)]
    public void FilePath(string? path, bool differentCasing)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
 
        var programPath = Path.Join(testInstance.Path, "Program.cs");
 
        File.WriteAllText(programPath, s_program);
 
        path ??= programPath;
 
        var result = new DotnetCommand(Log, "run", path)
            .WithWorkingDirectory(testInstance.Path)
            .Execute();
 
        if (!differentCasing || HasCaseInsensitiveFileSystem)
        {
            result.Should().Pass()
                .And.HaveStdOut("Hello from Program");
        }
        else
        {
            result.Should().Fail()
                .And.HaveStdErrContaining(string.Format(
                    CliCommandStrings.RunCommandExceptionNoProjects,
                    testInstance.Path,
                    RunCommandParser.ProjectOption.Name));
        }
    }
 
    /// <summary>
    /// <c>dotnet file.cs</c> is equivalent to <c>dotnet run file.cs</c>.
    /// </summary>
    [Fact]
    public void FilePath_WithoutRun()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from Program
                """);
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $"""
            #:property Configuration=Release
            {s_program}
            """);
 
        string expectedOutput = """
            Hello from Program
            Release config
            """;
 
        new DotnetCommand(Log, "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(expectedOutput);
 
        new DotnetCommand(Log, "./Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(expectedOutput);
 
        new DotnetCommand(Log, $".{Path.DirectorySeparatorChar}Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(expectedOutput);
 
        new DotnetCommand(Log, Path.Join(testInstance.Path, "Program.cs"))
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(expectedOutput);
 
        new DotnetCommand(Log, "Program.cs", "-c", "Debug")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from Program
                """);
 
        new DotnetCommand(Log, "Program.cs", "arg1", "arg2")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                echo args:arg1;arg2
                Hello from Program
                Release config
                """);
 
        new DotnetCommand(Log, "Program.cs", "build")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                echo args:build
                Hello from Program
                Release config
                """);
 
        new DotnetCommand(Log, "Program.cs", "arg1", "arg2")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                echo args:arg1;arg2
                Hello from Program
                Release config
                """);
    }
 
    /// <summary>
    /// Casing of the argument is used for the output binary name.
    /// </summary>
    [Fact]
    public void FilePath_DifferentCasing()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        var result = new DotnetCommand(Log, "run", "program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute();
 
        if (HasCaseInsensitiveFileSystem)
        {
            result.Should().Pass()
                .And.HaveStdOut("Hello from program");
        }
        else
        {
            result.Should().Fail()
                .And.HaveStdErrContaining(string.Format(
                    CliCommandStrings.RunCommandExceptionNoProjects,
                    testInstance.Path,
                    RunCommandParser.ProjectOption.Name));
        }
    }
 
    /// <summary>
    /// <c>dotnet run folder/file.cs</c> succeeds without a project file.
    /// </summary>
    [Fact]
    public void FilePath_OutsideWorkDir()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        var dirName = Path.GetFileName(testInstance.Path);
 
        new DotnetCommand(Log, "run", $"{dirName}/Program.cs")
            .WithWorkingDirectory(Path.GetDirectoryName(testInstance.Path)!)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
    }
 
    /// <summary>
    /// <c>dotnet run --project file.cs</c> fails.
    /// </summary>
    [Fact]
    public void FilePath_AsProjectArgument()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "--project", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(CliCommandStrings.RunCommandException);
    }
 
    /// <summary>
    /// Even if there is a file-based app <c>./build</c>, <c>dotnet build</c> should not execute that.
    /// </summary>
    [Theory]
    // error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.
    [InlineData("build", "MSB1003")]
    // dotnet watch: Could not find a MSBuild project file in '...'. Specify which project to use with the --project option.
    [InlineData("watch", "--project")]
    public void Precedence_BuiltInCommand(string cmd, string error)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, cmd), """
            #!/usr/bin/env dotnet
            Console.WriteLine("hello 1");
            """);
        File.WriteAllText(Path.Join(testInstance.Path, $"dotnet-{cmd}"), """
            #!/usr/bin/env dotnet
            Console.WriteLine("hello 2");
            """);
 
        // dotnet build -> built-in command
        new DotnetCommand(Log, cmd)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining(error);
 
        // dotnet ./build -> file-based app
        new DotnetCommand(Log, $"./{cmd}")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("hello 1");
 
        // dotnet run build -> file-based app
        new DotnetCommand(Log, "run", cmd)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("hello 1");
    }
 
    /// <summary>
    /// Even if there is a file-based app <c>./test.dll</c>, <c>dotnet test.dll</c> should not execute that.
    /// </summary>
    [Theory]
    [InlineData("test.dll")]
    [InlineData("./test.dll")]
    public void Precedence_Dll(string arg)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "test.dll"), """
            #!/usr/bin/env dotnet
            Console.WriteLine("hello world");
            """);
 
        // dotnet [./]test.dll -> exec the dll
        new DotnetCommand(Log, arg)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            // A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in ...
            .And.HaveStdErrContaining("hostpolicy");
 
        // dotnet run [./]test.dll -> file-based app
        new DotnetCommand(Log, "run", arg)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("hello world");
    }
 
    //  https://github.com/dotnet/sdk/issues/49665
    //  Failed to load /private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/9.0.0/libhostpolicy.dylib, error: dlopen(/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/9.0.0/libhostpolicy.dylib, 0x0001): tried: '/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/9.0.0/libhostpolicy.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/9.0.0/libhostpolicy.dylib' (no such file), '/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/9.0.0/libhostpolicy.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))
    [PlatformSpecificFact(TestPlatforms.Any & ~TestPlatforms.OSX)]
    public void Precedence_NuGetTool()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "complog"), """
            #!/usr/bin/env dotnet
            Console.WriteLine("hello world");
            """);
 
        new DotnetCommand(Log, "new", "tool-manifest")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DotnetCommand(Log, "tool", "install", "complog@0.7.0")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        // dotnet complog -> NuGet tool
        new DotnetCommand(Log, "complog")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("complog");
 
        // dotnet ./complog -> file-based app
        new DotnetCommand(Log, "./complog")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("hello world");
 
        // dotnet run complog -> file-based app
        new DotnetCommand(Log, "run", "complog")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("hello world");
    }
 
    /// <summary>
    /// <c>dotnet run -</c> reads the C# code from stdin.
    /// </summary>
    [Fact]
    public void ReadFromStdin()
    {
        new DotnetCommand(Log, "run", "-")
            .WithStandardInput("""
                Console.WriteLine("Hello from stdin");
                Console.WriteLine("Read: " + (Console.ReadLine() ?? "null"));
                """)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from stdin
                Read: null
                """);
    }
 
    [Fact]
    public void ReadFromStdin_NoBuild()
    {
        new DotnetCommand(Log, "run", "-", "--no-build")
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.NoBuildOption.Name));
    }
 
    [Fact]
    public void ReadFromStdin_LaunchProfile()
    {
        new DotnetCommand(Log, "run", "-", "--launch-profile=test")
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.LaunchProfileOption.Name));
    }
 
    /// <summary>
    /// <c>dotnet run -- -</c> should NOT read the C# file from stdin,
    /// the hyphen should be considred an app argument instead since it's after <c>--</c>.
    /// </summary>
    [Fact]
    public void ReadFromStdin_AfterDoubleDash()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        new DotnetCommand(Log, "run", "--", "-")
            .WithWorkingDirectory(testInstance.Path)
            .WithStandardInput("""Console.WriteLine("stdin code");""")
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionNoProjects, testInstance.Path, RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// <c>dotnet run folder</c> without a project file is not supported.
    /// </summary>
    [Theory]
    [InlineData(null)] // will be replaced with an absolute path
    [InlineData(".")]
    [InlineData("../MSBuildTestApp")]
    [InlineData("../MSBuildTestApp/")]
    public void FolderPath(string? path)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        path ??= testInstance.Path;
 
        new DotnetCommand(Log, "run", path)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                testInstance.Path,
                RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// <c>dotnet run app.csproj</c> fails if app.csproj does not exist.
    /// </summary>
    [Fact]
    public void ProjectPath_DoesNotExist()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "./App.csproj")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                testInstance.Path,
                RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// <c>dotnet run app.csproj</c> where app.csproj exists
    /// runs the project and passes 'app.csproj' as an argument.
    /// </summary>
    [Fact]
    public void ProjectPath_Exists()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        File.WriteAllText(Path.Join(testInstance.Path, "App.csproj"), s_consoleProject);
 
        new DotnetCommand(Log, "run", "./App.csproj")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                echo args:./App.csproj
                Hello from App
                """);
    }
 
    [Fact]
    public void ProjectInCurrentDirectory_NoRunVerb()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        Directory.CreateDirectory(Path.Join(testInstance.Path, "file"));
        File.WriteAllText(Path.Join(testInstance.Path, "file", "Program.cs"), s_program);
        Directory.CreateDirectory(Path.Join(testInstance.Path, "proj"));
        File.WriteAllText(Path.Join(testInstance.Path, "proj", "App.csproj"), s_consoleProject);
 
        new DotnetCommand(Log, "../file/Program.cs")
            .WithWorkingDirectory(Path.Join(testInstance.Path, "proj"))
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                Hello from Program
                """);
    }
 
    [Fact]
    public void ProjectInCurrentDirectory_FileOption()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        Directory.CreateDirectory(Path.Join(testInstance.Path, "file"));
        File.WriteAllText(Path.Join(testInstance.Path, "file", "Program.cs"), s_program);
        Directory.CreateDirectory(Path.Join(testInstance.Path, "proj"));
        File.WriteAllText(Path.Join(testInstance.Path, "proj", "App.csproj"), s_consoleProject);
 
        new DotnetCommand(Log, "run", "--file", "../file/Program.cs")
            .WithWorkingDirectory(Path.Join(testInstance.Path, "proj"))
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                Hello from Program
                """);
    }
 
    /// <summary>
    /// When a file is not a .cs file, we probe the first characters of the file for <c>#!</c>, and
    /// execute as a single file program if we find them.
    /// </summary>
    [Theory]
    [InlineData("Program")]
    [InlineData("Program.csx")]
    [InlineData("Program.vb")]
    public void NonCsFileExtensionWithShebang(string fileName)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, fileName), """
            #!/usr/bin/env dotnet
            Console.WriteLine("hello world");
            """);
 
        new DotnetCommand(Log, "run", fileName)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("hello world");
    }
 
    /// <summary>
    /// When a file is not a .cs file, we probe the first characters of the file for <c>#!</c>, and
    /// fall back to normal <c>dotnet run</c> behavior if we don't find them.
    /// </summary>
    [Theory]
    [InlineData("Program")]
    [InlineData("Program.csx")]
    [InlineData("Program.vb")]
    public void NonCsFileExtensionWithNoShebang(string fileName)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, fileName), s_program);
 
        new DotnetCommand(Log, "run", fileName)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                testInstance.Path,
                RunCommandParser.ProjectOption.Name));
    }
 
    [Fact]
    public void MultipleEntryPoints()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        File.WriteAllText(Path.Join(testInstance.Path, "Program2.cs"), s_program);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        new DotnetCommand(Log, "run", "Program2.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program2");
    }
 
    /// <summary>
    /// When the entry-point file does not exist, fallback to normal <c>dotnet run</c> behavior.
    /// </summary>
    [Fact]
    public void NoCode()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                testInstance.Path,
                RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// Cannot run a non-entry-point file.
    /// </summary>
    [Fact]
    public void ClassLibrary_EntryPointFileExists()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util);
 
        new DotnetCommand(Log, "run", "Util.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("error CS5001:"); // Program does not contain a static 'Main' method suitable for an entry point
    }
 
    /// <summary>
    /// When the entry-point file does not exist, fallback to normal <c>dotnet run</c> behavior.
    /// </summary>
    [Fact]
    public void ClassLibrary_EntryPointFileDoesNotExist()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util);
 
        new DotnetCommand(Log, "run", "NonExistentFile.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                testInstance.Path,
                RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// Other files in the folder are not part of the compilation.
    /// </summary>
    [Fact]
    public void MultipleFiles_RunEntryPoint()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_programDependingOnUtil);
        File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("error CS0103"); // The name 'Util' does not exist in the current context
 
        // This can be overridden.
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $"""
            #:property EnableDefaultCompileItems=true
            {s_programDependingOnUtil}
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            // warning CS2002: Source file 'Program.cs' specified multiple times
            .And.HaveStdOutContaining("warning CS2002")
            .And.HaveStdOutContaining("Hello, String from Util");
    }
 
    /// <summary>
    /// <c>dotnet run util.cs</c> fails if <c>util.cs</c> is not the entry-point.
    /// </summary>
    [Fact]
    public void MultipleFiles_RunLibraryFile()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_programDependingOnUtil);
        File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util);
 
        new DotnetCommand(Log, "run", "Util.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("error CS5001:"); // Program does not contain a static 'Main' method suitable for an entry point
    }
 
    /// <summary>
    /// If there are nested project files like
    /// <code>
    ///  app/file.cs
    ///  app/nested/x.csproj
    ///  app/nested/another.cs
    /// </code>
    /// executing <c>dotnet run app/file.cs</c> will include the nested <c>.cs</c> file in the compilation.
    /// Hence we could consider reporting an error in this situation.
    /// However, the same problem exists for normal builds with explicit project files
    /// and usually the build fails because there are multiple entry points or other clashes.
    /// </summary>
    [Fact]
    public void NestedProjectFiles()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        Directory.CreateDirectory(Path.Join(testInstance.Path, "nested"));
        File.WriteAllText(Path.Join(testInstance.Path, "nested", "App.csproj"), s_consoleProject);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
    }
 
    /// <summary>
    /// <c>dotnet run folder/app.csproj</c> -> the argument is not recognized as an entry-point file
    /// (it does not have <c>.cs</c> file extension), so this fallbacks to normal <c>dotnet run</c> behavior.
    /// </summary>
    [Fact]
    public void RunNestedProjectFile()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        File.WriteAllText(Path.Join(testInstance.Path, "App.csproj"), s_consoleProject);
 
        var dirName = Path.GetFileName(testInstance.Path);
 
        var workDir = Path.GetDirectoryName(testInstance.Path)!;
 
        new DotnetCommand(Log, "run", $"{dirName}/App.csproj")
            .WithWorkingDirectory(workDir)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                workDir,
                RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// Main method is supported just like top-level statements.
    /// </summary>
    [Fact]
    public void MainMethod()
    {
        var testInstance = _testAssetsManager.CopyTestAsset("MSBuildTestApp").WithSource();
        File.Delete(Path.Join(testInstance.Path, "MSBuildTestApp.csproj"));
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello World!");
    }
 
    /// <summary>
    /// Empty file does not contain entry point, so that's an error.
    /// </summary>
    [Fact]
    public void EmptyFile()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), string.Empty);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("error CS5001:"); // Program does not contain a static 'Main' method suitable for an entry point
    }
 
    /// <summary>
    /// Implicit build files have an effect.
    /// </summary>
    [Fact]
    public void DirectoryBuildProps()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
            <Project>
                <PropertyGroup>
                    <AssemblyName>TestName</AssemblyName>
                </PropertyGroup>
            </Project>
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from TestName");
    }
 
    [Fact]
    public void ComputeRunArguments_Success()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """
            <Project>
              <Target Name="_SetCustomRunArgs" BeforeTargets="ComputeRunArguments">
                <PropertyGroup>
                  <RunArguments>$(RunArguments) extended</RunArguments>
                </PropertyGroup>
              </Target>
            </Project>
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                echo args:extended
                Hello from Program
                """);
    }
 
    [Fact]
    public void ComputeRunArguments_Failure()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """
            <Project>
              <Target Name="_SetCustomRunArgs" BeforeTargets="ComputeRunArguments">
                <Error Code="MYAPP001" Text="Custom error" />
              </Target>
            </Project>
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("""
                MYAPP001: Custom error
                """)
            .And.HaveStdErrContaining(CliCommandStrings.RunCommandException);
    }
 
    /// <summary>
    /// Command-line arguments should be passed through.
    /// </summary>
    [Theory]
    [InlineData("other;args", "other;args")]
    [InlineData("--;other;args", "other;args")]
    [InlineData("--appArg", "--appArg")]
    [InlineData("-c;Debug;--xyz", "--xyz")]
    public void Arguments_PassThrough(string input, string output)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, ["run", "Program.cs", .. input.Split(';')])
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"""
                echo args:{output}
                Hello from Program
                """);
    }
 
    /// <summary>
    /// <c>dotnet run --unknown-arg file.cs</c> fallbacks to normal <c>dotnet run</c> behavior.
    /// </summary>
    [Fact]
    public void Arguments_Unrecognized()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, ["run", "--arg", "Program.cs"])
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(
                CliCommandStrings.RunCommandExceptionNoProjects,
                testInstance.Path,
                RunCommandParser.ProjectOption.Name));
    }
 
    /// <summary>
    /// <c>dotnet run --some-known-arg file.cs</c> is supported.
    /// </summary>
    [Theory, CombinatorialData]
    public void Arguments_Recognized(bool beforeFile)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        string[] args = beforeFile
            ? ["run", "-c", "Release", "Program.cs", "more", "args"]
            : ["run", "Program.cs", "-c", "Release", "more", "args"];
 
        new DotnetCommand(Log, args)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                echo args:more;args
                Hello from Program
                Release config
                """);
    }
 
    /// <summary>
    /// <c>dotnet run --bl file.cs</c> produces a binary log.
    /// </summary>
    [Theory, CombinatorialData]
    public void BinaryLog_Run(bool beforeFile)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        string[] args = beforeFile
            ? ["-bl", "Program.cs"]
            : ["Program.cs", "-bl"];
 
        new DotnetCommand(Log, ["run", "--no-cache", .. args])
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly)
            .Select(f => f.Name)
            .Should().BeEquivalentTo(["msbuild.binlog"]);
    }
 
    [Theory, CombinatorialData]
    public void BinaryLog_Build([CombinatorialValues("restore", "build")] string command, bool beforeFile)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        string[] args = beforeFile
            ? [command, "-bl", "Program.cs"]
            : [command, "Program.cs", "-bl"];
 
        new DotnetCommand(Log, args)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly)
            .Select(f => f.Name)
            .Should().BeEquivalentTo(["msbuild.binlog"]);
    }
 
    [Theory]
    [InlineData("-bl")]
    [InlineData("-BL")]
    [InlineData("-bl:msbuild.binlog")]
    [InlineData("/bl")]
    [InlineData("/bl:msbuild.binlog")]
    [InlineData("--binaryLogger")]
    [InlineData("--binaryLogger:msbuild.binlog")]
    [InlineData("-bl:another.binlog")]
    public void BinaryLog_ArgumentForms(string arg)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "--no-cache", "Program.cs", arg)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        var fileName = arg.Split(':', 2) is [_, { Length: > 0 } value] ? Path.GetFileNameWithoutExtension(value) : "msbuild";
 
        new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly)
            .Select(f => f.Name)
            .Should().BeEquivalentTo([$"{fileName}.binlog"]);
    }
 
    [Fact]
    public void BinaryLog_Multiple()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "--no-cache", "Program.cs", "-bl:one.binlog", "two.binlog", "/bl:three.binlog")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                echo args:two.binlog
                Hello from Program
                """);
 
        new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly)
            .Select(f => f.Name)
            .Should().BeEquivalentTo(["three.binlog"]);
    }
 
    [Fact]
    public void BinaryLog_WrongExtension()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl:test.test")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining("test.test"); // Invalid binary logger parameter(s): "test.test"
 
        new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly)
            .Select(f => f.Name)
            .Should().BeEmpty();
    }
 
    /// <summary>
    /// <c>dotnet run file.cs</c> should not produce a binary log.
    /// </summary>
    [Fact]
    public void BinaryLog_NotSpecified()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly)
            .Select(f => f.Name)
            .Should().BeEmpty();
    }
 
    /// <summary>
    /// Binary logs from our in-memory projects should have evaluation data.
    /// </summary>
    [Fact]
    public void BinaryLog_EvaluationData()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "--no-cache", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        string binaryLogPath = Path.Join(testInstance.Path, "msbuild.binlog");
        new FileInfo(binaryLogPath).Should().Exist();
 
        var records = BinaryLog.ReadRecords(binaryLogPath).ToList();
 
        // There should be at least two - one for restore, one for build.
        // But the restore targets might re-evaluate the project via inner MSBuild task invocations.
        records.Count(static r => r.Args is ProjectEvaluationStartedEventArgs).Should().BeGreaterThanOrEqualTo(2);
        records.Count(static r => r.Args is ProjectEvaluationFinishedEventArgs).Should().BeGreaterThanOrEqualTo(2);
    }
 
    [Theory, CombinatorialData]
    public void TerminalLogger(bool on)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        var result = new DotnetCommand(Log, "run", "Program.cs", "--no-cache")
            .WithWorkingDirectory(testInstance.Path)
            .WithEnvironmentVariable("MSBUILDTERMINALLOGGER", on ? "on" : "off")
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("Hello from Program");
 
        const string terminalLoggerSubstring = "\x1b";
        if (on)
        {
            result.And.HaveStdOutContaining(terminalLoggerSubstring);
        }
        else
        {
            result.And.NotHaveStdOutContaining(terminalLoggerSubstring);
        }
    }
 
    [Fact]
    public void Verbosity_Run()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        new DotnetCommand(Log, "run", "Program.cs", "--no-cache")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            // no additional build messages
            .And.HaveStdOut("Hello from Program")
            .And.NotHaveStdOutContaining("Program.dll")
            .And.NotHaveStdErr();
    }
 
    [Fact] // https://github.com/dotnet/sdk/issues/50227
    public void Verbosity_Build()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        new DotnetCommand(Log, "build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            // should print path to the built DLL
            .And.HaveStdOutContaining("Program.dll");
    }
 
    [Fact]
    public void Verbosity_CompilationDiagnostics()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            string x = null;
            Console.WriteLine("ran" + x);
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            // warning CS8600: Converting null literal or possible null value to non-nullable type.
            .And.HaveStdOutContaining("warning CS8600")
            .And.HaveStdOutContaining("ran");
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Console.Write
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            // error CS1002: ; expected
            .And.HaveStdOutContaining("error CS1002")
            .And.HaveStdErrContaining(CliCommandStrings.RunCommandException);
    }
 
    /// <summary>
    /// Default projects include embedded resources by default.
    /// </summary>
    [Fact]
    public void EmbeddedResource()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_programReadingEmbeddedResource);
        File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                [MyString, TestValue]
                """);
 
        // This behavior can be overridden.
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $"""
            #:property EnableDefaultEmbeddedResourceItems=false
            {s_programReadingEmbeddedResource}
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Resource not found
                """);
    }
 
    /// <summary>
    /// Scripts in repo root should not include <c>.resx</c> files.
    /// Part of <see href="https://github.com/dotnet/sdk/issues/49826"/>.
    /// </summary>
    [Theory, CombinatorialData]
    public void EmbeddedResource_AlongsideProj([CombinatorialValues("sln", "slnx", "csproj", "vbproj", "shproj", "proj")] string ext)
    {
        bool considered = ext is "sln" or "slnx" or "csproj";
 
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_programReadingEmbeddedResource);
        File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx);
        File.WriteAllText(Path.Join(testInstance.Path, $"repo.{ext}"), "");
 
        // Up-to-date check currently doesn't support default items, so we need to pass --no-cache
        // otherwise other runs of this test theory might cause outdated results.
        new DotnetCommand(Log, "run", "--no-cache", "--file", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(considered ? "Resource not found" : "[MyString, TestValue]");
    }
 
    [Fact]
    public void NoRestore_01()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        // It is an error when never restored before.
        new DotnetCommand(Log, "run", "--no-restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("NETSDK1004"); // error NETSDK1004: Assets file '...\obj\project.assets.json' not found. Run a NuGet package restore to generate this file.
 
        // Run restore.
        new DotnetCommand(Log, "restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        // --no-restore works.
        new DotnetCommand(Log, "run", "--no-restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
    }
 
    [Fact]
    public void NoRestore_02()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        // It is an error when never restored before.
        new DotnetCommand(Log, "build", "--no-restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("NETSDK1004"); // error NETSDK1004: Assets file '...\obj\project.assets.json' not found. Run a NuGet package restore to generate this file.
 
        // Run restore.
        new DotnetCommand(Log, "restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        // --no-restore works.
        new DotnetCommand(Log, "build", "--no-restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DotnetCommand(Log, "run", "--no-build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
    }
 
    [Fact]
    public void Restore_StaticGraph_Implicit()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
            <Project>
                <PropertyGroup>
                    <RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
                </PropertyGroup>
            </Project>
            """);
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, "Console.WriteLine();");
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        new DotnetCommand(Log, "restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
    }
 
    [Fact]
    public void Restore_StaticGraph_Explicit()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, """
            #:property RestoreUseStaticGraphEvaluation=true
            Console.WriteLine();
            """);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        new DotnetCommand(Log, "restore", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErr(DirectiveError(programFile, 1, CliCommandStrings.StaticGraphRestoreNotSupported));
    }
 
    [Fact]
    public void NoBuild_01()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        // It is an error when never built before.
        new DotnetCommand(Log, "run", "--no-build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining("An error occurred trying to start process");
 
        // Now build it.
        new DotnetCommand(Log, "build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        // Changing the program has no effect when it is not built.
        File.WriteAllText(programFile, """Console.WriteLine("Changed");""");
        new DotnetCommand(Log, "run", "--no-build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        // The change has an effect when built again.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Changed");
    }
 
    [Fact]
    public void NoBuild_02()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        // It is an error when never built before.
        new DotnetCommand(Log, "run", "--no-build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining("An error occurred trying to start process");
 
        // Now build it.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        // Changing the program has no effect when it is not built.
        File.WriteAllText(programFile, """Console.WriteLine("Changed");""");
        new DotnetCommand(Log, "run", "--no-build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        // The change has an effect when built again.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Changed");
    }
 
    [Fact]
    public void Publish()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var publishDir = Path.Join(testInstance.Path, "artifacts");
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(publishDir).Sub("Program")
            .Should().Exist()
            .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
 
        new RunExeCommand(Log, Path.Join(publishDir, "Program", $"Program{Constants.ExeSuffix}"))
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from Program
                Release config
                """);
    }
 
    [Fact]
    public void PublishWithCustomTarget()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var publishDir = Path.Join(testInstance.Path, "artifacts");
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "Program.cs", "-t", "ComputeContainerConfig", "-p", "PublishAot=false", "--use-current-runtime")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        var appBinaryName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Program.exe" : "Program";
        new DirectoryInfo(publishDir).Sub("Program")
            .Should().Exist()
            .And.HaveFiles([
                appBinaryName,
                "Program.deps.json",
                "Program.runtimeconfig.json"
            ]);
    }
 
    [Fact]
    public void Publish_WithJson()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, """
            #:sdk Microsoft.NET.Sdk.Web
            Console.WriteLine(File.ReadAllText("config.json"));
            """);
 
        File.WriteAllText(Path.Join(testInstance.Path, "config.json"), """
            { "MyKey": "MyValue" }
            """);
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var publishDir = Path.Join(testInstance.Path, "artifacts");
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(publishDir).Sub("Program")
            .Should().Exist()
            .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly) // no deps.json file for AOT-published app
            .And.HaveFile("config.json"); // the JSON is included as content and hence copied
    }
 
    [Fact]
    public void Publish_Options()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var publishDir = Path.Join(testInstance.Path, "artifacts");
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "Program.cs", "-c", "Debug", "-p:PublishAot=false", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(publishDir).Sub("Program")
            .Should().Exist()
            .And.HaveFile("Program.deps.json");
 
        new DirectoryInfo(testInstance.Path).File("msbuild.binlog").Should().Exist();
    }
 
    [Fact]
    public void Publish_PublishDir_IncludesFileName()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "MyCustomProgram.cs");
        File.WriteAllText(programFile, s_program);
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var publishDir = Path.Join(testInstance.Path, "artifacts");
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "MyCustomProgram.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(publishDir).Sub("MyCustomProgram")
            .Should().Exist()
            .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
    }
 
    [Fact]
    public void Publish_PublishDir_CommandLine()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        var customPublishDir = Path.Join(testInstance.Path, "custom-publish");
        if (Directory.Exists(customPublishDir)) Directory.Delete(customPublishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "Program.cs", $"/p:PublishDir={customPublishDir}")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(customPublishDir)
            .Should().Exist()
            .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
    }
 
    [Fact]
    public void Publish_PublishDir_PropertyDirective()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        var publishDir = Path.Join(testInstance.Path, "directive-publish");
        File.WriteAllText(programFile, $"""
            #:property PublishDir={publishDir}
            {s_program}
            """);
 
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(publishDir)
            .Should().Exist()
            .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
    }
 
    [Fact]
    public void Publish_In_SubDir()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var subDir = Directory.CreateDirectory(Path.Combine(testInstance.Path, "subdir"));
 
        var programFile = Path.Join(subDir.FullName, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var publishDir = Path.Join(subDir.FullName, "artifacts");
        if (Directory.Exists(publishDir)) Directory.Delete(publishDir, recursive: true);
 
        new DotnetCommand(Log, "publish", "./subdir/Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(testInstance.Path).Sub("subdir").Sub("artifacts").Sub("Program")
            .Should().Exist()
            .And.NotHaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly); // no deps.json file for AOT-published app
    }
 
    [Fact]
    public void Pack()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "MyFileBasedTool.cs");
        File.WriteAllText(programFile, """
            #:property PackAsTool=true
            Console.WriteLine($"Hello; EntryPointFilePath set? {AppContext.GetData("EntryPointFilePath") is string}");
            #if !DEBUG
            Console.WriteLine("Release config");
            #endif
            """);
 
        // Run unpacked.
        new DotnetCommand(Log, "run", "MyFileBasedTool.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello; EntryPointFilePath set? True");
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var outputDir = Path.Join(testInstance.Path, "artifacts");
        if (Directory.Exists(outputDir)) Directory.Delete(outputDir, recursive: true);
 
        // Pack.
        new DotnetCommand(Log, "pack", "MyFileBasedTool.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        var packageDir = new DirectoryInfo(outputDir).Sub("MyFileBasedTool");
        packageDir.File("MyFileBasedTool.1.0.0.nupkg").Should().Exist();
        new DirectoryInfo(artifactsDir).Sub("package").Should().NotExist();
 
        // Run the packed tool.
        new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", packageDir.FullName)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                Hello; EntryPointFilePath set? False
                Release config
                """);
    }
 
    [Fact]
    public void Pack_CustomPath()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "MyFileBasedTool.cs");
        File.WriteAllText(programFile, """
            #:property PackAsTool=true
            #:property PackageOutputPath=custom
            Console.WriteLine($"Hello; EntryPointFilePath set? {AppContext.GetData("EntryPointFilePath") is string}");
            """);
 
        // Run unpacked.
        new DotnetCommand(Log, "run", "MyFileBasedTool.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello; EntryPointFilePath set? True");
 
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var outputDir = Path.Join(testInstance.Path, "custom");
        if (Directory.Exists(outputDir)) Directory.Delete(outputDir, recursive: true);
 
        // Pack.
        new DotnetCommand(Log, "pack", "MyFileBasedTool.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(outputDir).File("MyFileBasedTool.1.0.0.nupkg").Should().Exist();
        new DirectoryInfo(artifactsDir).Sub("package").Should().NotExist();
 
        // Run the packed tool.
        new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", outputDir)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("Hello; EntryPointFilePath set? False");
    }
 
    [Fact]
    public void Clean()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        var artifactsDir = new DirectoryInfo(VirtualProjectBuildingCommand.GetArtifactsPath(programFile));
        artifactsDir.Should().HaveFiles(["build-start.cache", "build-success.cache"]);
 
        var dllFile = artifactsDir.File("bin/debug/Program.dll");
        dllFile.Should().Exist();
 
        new DotnetCommand(Log, "clean", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        artifactsDir.EnumerateFiles().Should().BeEmpty();
 
        dllFile.Refresh();
        dllFile.Should().NotExist();
    }
 
    [PlatformSpecificFact(TestPlatforms.AnyUnix), UnsupportedOSPlatform("windows")]
    public void ArtifactsDirectory_Permissions()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programFile = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programFile, s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        new DotnetCommand(Log, "build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
 
        new DirectoryInfo(artifactsDir).UnixFileMode
            .Should().Be(UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute, artifactsDir);
 
        // Re-create directory with incorrect permissions.
        Directory.Delete(artifactsDir, recursive: true);
        Directory.CreateDirectory(artifactsDir, UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute);
        var actualMode = new DirectoryInfo(artifactsDir).UnixFileMode
            .Should().NotBe(UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute, artifactsDir).And.Subject;
 
        new DotnetCommand(Log, "build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining("build-start.cache"); // Unhandled exception: Access to the path '.../build-start.cache' is denied.
 
        // Build shouldn't have changed the permissions.
        new DirectoryInfo(artifactsDir).UnixFileMode
            .Should().Be(actualMode, artifactsDir);
    }
 
    [Theory, CombinatorialData]
    public void LaunchProfile(
        bool cscOnly,
        [CombinatorialValues("Properties/launchSettings.json", "Program.run.json")] string relativePath)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: cscOnly ? OutOfTreeBaseDirectory : null);
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program + """
 
            Console.WriteLine($"Message: '{Environment.GetEnvironmentVariable("Message")}'");
            """);
        var fullPath = Path.Join(testInstance.Path, relativePath);
        Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
        File.WriteAllText(fullPath, s_launchSettings);
 
        var prefix = cscOnly
            ? CliCommandStrings.NoBinaryLogBecauseRunningJustCsc + Environment.NewLine
            : string.Empty;
 
        new DotnetCommand(Log, "run", "-bl", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining(prefix + """
                Hello from Program
                Message: 'TestProfileMessage1'
                """);
 
        prefix = CliCommandStrings.NoBinaryLogBecauseUpToDate + Environment.NewLine;
 
        new DotnetCommand(Log, "run", "-bl", "--no-launch-profile", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(prefix + """
                Hello from Program
                Message: ''
                """);
 
        new DotnetCommand(Log, "run", "-bl", "-lp", "TestProfile2", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining(prefix + """
                Hello from Program
                Message: 'TestProfileMessage2'
                """);
    }
 
    /// <summary>
    /// <c>Properties/launchSettings.json</c> takes precedence over <c>Program.run.json</c>.
    /// </summary>
    [Fact]
    public void LaunchProfile_Precedence()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program + """
 
            Console.WriteLine($"Message: '{Environment.GetEnvironmentVariable("Message")}'");
            """);
        Directory.CreateDirectory(Path.Join(testInstance.Path, "Properties"));
        string launchSettings = Path.Join(testInstance.Path, "Properties", "launchSettings.json");
        File.WriteAllText(launchSettings, s_launchSettings.Replace("TestProfileMessage", "PropertiesLaunchSettingsJson"));
        string runJson = Path.Join(testInstance.Path, "Program.run.json");
        File.WriteAllText(runJson, s_launchSettings.Replace("TestProfileMessage", "ProgramRunJson"));
 
        new DotnetCommand(Log, "run", "--no-launch-profile", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from Program
                Message: ''
                """);
 
        // quiet runs here so that launch-profile useage messages don't impact test assertions
        new DotnetCommand(Log, "run", "-v", "q", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"""
                {string.Format(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJson, launchSettings)}
                Hello from Program
                Message: 'PropertiesLaunchSettingsJson1'
                """);
 
        new DotnetCommand(Log, "run", "-v", "q", "-lp", "TestProfile2", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"""
                {string.Format(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJson, launchSettings)}
                Hello from Program
                Message: 'PropertiesLaunchSettingsJson2'
                """);
    }
 
    /// <summary>
    /// Each file-based app in a folder can have separate launch profile.
    /// </summary>
    [Fact]
    public void LaunchProfile_Multiple()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var source = s_program + """
 
            Console.WriteLine($"Message: '{Environment.GetEnvironmentVariable("Message")}'");
            """;
        File.WriteAllText(Path.Join(testInstance.Path, "First.cs"), source);
        File.WriteAllText(Path.Join(testInstance.Path, "First.run.json"), s_launchSettings.Replace("TestProfileMessage", "First"));
        File.WriteAllText(Path.Join(testInstance.Path, "Second.cs"), source);
        File.WriteAllText(Path.Join(testInstance.Path, "Second.run.json"), s_launchSettings.Replace("TestProfileMessage", "Second"));
 
        // do these runs with quiet verbosity so that default run output doesn't impact the tests
        new DotnetCommand(Log, "run", "-v", "q", "First.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from First
                Message: 'First1'
                """);
 
        new DotnetCommand(Log, "run", "-v", "q",  "Second.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("""
                Hello from Second
                Message: 'Second1'
                """);
    }
 
    [Fact]
    public void Define_01()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #if MY_DEFINE
            Console.WriteLine("Test output");
            #endif
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "-p:DefineConstants=MY_DEFINE")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Test output");
    }
 
    [Fact]
    public void Define_02()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #if !MY_DEFINE
            Console.WriteLine("Test output");
            #endif
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "-p:DefineConstants=MY_DEFINE")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("error CS5001:"); // Program does not contain a static 'Main' method suitable for an entry point
    }
 
    [Fact]
    public void PackageReference()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #:package System.CommandLine@2.0.0-beta4.22272.1
            using System.CommandLine;
 
            var rootCommand = new RootCommand("Sample app for System.CommandLine");
            return await rootCommand.InvokeAsync(args);
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "--", "--help")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                Description:
                  Sample app for System.CommandLine
                """);
    }
 
    [Fact]
    public void PackageReference_CentralVersion()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Packages.props"), """
            <Project>
              <PropertyGroup>
                <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
              </PropertyGroup>
              <ItemGroup>
                <PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
              </ItemGroup>
            </Project>
            """);
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #:package System.CommandLine
            using System.CommandLine;
 
            var rootCommand = new RootCommand("Sample app for System.CommandLine");
            return await rootCommand.InvokeAsync(args);
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "--", "--help")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                Description:
                  Sample app for System.CommandLine
                """);
    }
 
    //  https://github.com/dotnet/sdk/issues/49665
    [PlatformSpecificFact(TestPlatforms.Any & ~TestPlatforms.OSX)] // https://github.com/dotnet/sdk/issues/48990
    public void SdkReference()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #:sdk Microsoft.NET.Sdk
            #:sdk Aspire.AppHost.Sdk@9.2.1
            #:package Aspire.Hosting.AppHost@9.2.1
 
            var builder = DistributedApplication.CreateBuilder(args);
            builder.Build().Run();
            """);
 
        new DotnetCommand(Log, "build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
    }
 
    [Fact] // https://github.com/dotnet/sdk/issues/49797
    public void SdkReference_VersionedSdkFirst()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #:sdk Microsoft.NET.Sdk@9.0.0
            Console.WriteLine();
            """);
 
        new DotnetCommand(Log, "build", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass();
    }
 
    [Theory]
    [InlineData("../Lib/Lib.csproj")]
    [InlineData("../Lib")]
    [InlineData(@"..\Lib\Lib.csproj")]
    [InlineData(@"..\Lib")]
    public void ProjectReference(string arg)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
 
        var libDir = Path.Join(testInstance.Path, "Lib");
        Directory.CreateDirectory(libDir);
 
        File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $"""
            <Project Sdk="Microsoft.NET.Sdk">
              <PropertyGroup>
                <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
              </PropertyGroup>
            </Project>
            """);
 
        File.WriteAllText(Path.Join(libDir, "Lib.cs"), """
            namespace Lib;
            public class LibClass
            {
                public static string GetMessage() => "Hello from Lib";
            }
            """);
 
        var appDir = Path.Join(testInstance.Path, "App");
        Directory.CreateDirectory(appDir);
 
        File.WriteAllText(Path.Join(appDir, "Program.cs"), $"""
            #:project {arg}
            Console.WriteLine(Lib.LibClass.GetMessage());
            """);
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(appDir)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Lib");
    }
 
    [Fact]
    public void ProjectReference_Errors()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #:project wrong.csproj
            """);
 
        // Project file does not exist.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, CliCommandStrings.InvalidProjectDirective,
                string.Format(CliStrings.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, "wrong.csproj"))));
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            #:project dir/
            """);
 
        // Project directory does not exist.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, CliCommandStrings.InvalidProjectDirective,
                string.Format(CliStrings.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, "dir/"))));
 
        Directory.CreateDirectory(Path.Join(testInstance.Path, "dir"));
 
        // Directory exists but has no project file.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, CliCommandStrings.InvalidProjectDirective,
                string.Format(CliStrings.CouldNotFindAnyProjectInDirectory, Path.Join(testInstance.Path, "dir/"))));
 
        File.WriteAllText(Path.Join(testInstance.Path, "dir", "proj1.csproj"), "<Project />");
        File.WriteAllText(Path.Join(testInstance.Path, "dir", "proj2.csproj"), "<Project />");
 
        // Directory exists but has multiple project files.
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(DirectiveError(Path.Join(testInstance.Path, "Program.cs"), 1, CliCommandStrings.InvalidProjectDirective,
                string.Format(CliStrings.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, "dir/"))));
    }
 
    /// <summary>
    /// Verifies that msbuild-based runs use CSC args equivalent to csc-only runs.
    /// Can regenerate CSC arguments template in <see cref="CSharpCompilerCommand"/>.
    /// </summary>
    [Fact]
    public void CscArguments()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
        const string programName = "TestProgram";
        const string fileName = $"{programName}.cs";
        string entryPointPath = Path.Join(testInstance.Path, fileName);
        File.WriteAllText(entryPointPath, s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(entryPointPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        // Build using MSBuild.
        new DotnetCommand(Log, "run", fileName, "-bl", "--no-cache")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"Hello from {programName}");
 
        // Find the csc args used by the build.
        var msbuildCall = FindCompilerCall(Path.Join(testInstance.Path, "msbuild.binlog"));
        var msbuildCallArgs = msbuildCall.GetArguments();
        var msbuildCallArgsString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(msbuildCallArgs);
 
        // Generate argument template code.
        string sdkPath = NormalizePath(TestContext.Current.ToolsetUnderTest.SdkFolderUnderTest);
        string dotNetRootPath = NormalizePath(TestContext.Current.ToolsetUnderTest.DotNetRoot);
        string nuGetCachePath = NormalizePath(TestContext.Current.NuGetCachePath!);
        string artifactsDirNormalized = NormalizePath(artifactsDir);
        string objPath = $"{artifactsDirNormalized}/obj/debug";
        string entryPointPathNormalized = NormalizePath(entryPointPath);
        var msbuildArgsToVerify = new List<string>();
        var nuGetPackageFilePaths = new List<string>();
        var code = new StringBuilder();
        code.AppendLine($$"""
            // Licensed to the .NET Foundation under one or more agreements.
            // The .NET Foundation licenses this file to you under the MIT license.
 
            namespace Microsoft.DotNet.Cli.Commands.Run;
 
            // Generated by test `{{nameof(RunFileTests)}}.{{nameof(CscArguments)}}`.
            partial class CSharpCompilerCommand
            {
                private IEnumerable<string> GetCscArguments(
                    string fileNameWithoutExtension,
                    string objDir,
                    string binDir)
                {
                    return
                    [
            """);
        foreach (var arg in msbuildCallArgs)
        {
            // This option needs to be passed on the command line, not in an RSP file.
            if (arg is "/noconfig")
            {
                continue;
            }
 
            // We don't need to generate a ref assembly.
            if (arg.StartsWith("/refout:", StringComparison.Ordinal))
            {
                continue;
            }
 
            // There should be no source link arguments.
            if (arg.StartsWith("/sourcelink:", StringComparison.Ordinal))
            {
                Assert.Fail($"Unexpected source link argument: {arg}");
            }
 
            // PreferredUILang is normally not set by default but can be in builds, so ignore it.
            if (arg.StartsWith("/preferreduilang:", StringComparison.Ordinal))
            {
                continue;
            }
 
            bool needsInterpolation = false;
            bool fromNuGetPackage = false;
 
            // Normalize slashes in paths.
            string rewritten = NormalizePathArg(arg);
 
            // Remove quotes.
            rewritten = RemoveQuotes(rewritten);
 
            string msbuildArgToVerify = rewritten;
 
            // Use variable SDK path.
            if (rewritten.Contains(sdkPath, StringComparison.OrdinalIgnoreCase))
            {
                rewritten = rewritten.Replace(sdkPath, "{SdkPath}", StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
            }
 
            // Use variable .NET root path.
            if (rewritten.Contains(dotNetRootPath, StringComparison.OrdinalIgnoreCase))
            {
                rewritten = rewritten.Replace(dotNetRootPath, "{DotNetRootPath}", StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
            }
 
            // Use variable NuGet cache path.
            if (rewritten.Contains(nuGetCachePath, StringComparison.OrdinalIgnoreCase))
            {
                rewritten = rewritten.Replace(nuGetCachePath, "{NuGetCachePath}", StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
                fromNuGetPackage = true;
            }
 
            // Use variable intermediate dir path.
            if (rewritten.Contains(objPath, StringComparison.OrdinalIgnoreCase))
            {
                // We want to emit the resulting DLL directly into the bin folder.
                bool isOut = arg.StartsWith("/out", StringComparison.Ordinal);
                string replacement = isOut ? "{binDir}" : "{objDir}";
 
                if (isOut)
                {
                    msbuildArgToVerify = msbuildArgToVerify.Replace("/obj/", "/bin/", StringComparison.OrdinalIgnoreCase);
                }
 
                rewritten = rewritten.Replace(objPath, replacement, StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
            }
 
            // Use variable file name.
            if (rewritten.Contains(entryPointPathNormalized, StringComparison.OrdinalIgnoreCase))
            {
                rewritten = rewritten.Replace(entryPointPathNormalized, "{" + nameof(CSharpCompilerCommand.EntryPointFileFullPath) + "}", StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
            }
 
            // Use variable program name.
            if (rewritten.Contains(programName, StringComparison.OrdinalIgnoreCase))
            {
                rewritten = rewritten.Replace(programName, "{fileNameWithoutExtension}", StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
            }
 
            // Use variable runtime version.
            if (rewritten.Contains(CSharpCompilerCommand.RuntimeVersion, StringComparison.OrdinalIgnoreCase))
            {
                rewritten = rewritten.Replace(CSharpCompilerCommand.RuntimeVersion, "{" + nameof(CSharpCompilerCommand.RuntimeVersion) + "}", StringComparison.OrdinalIgnoreCase);
                needsInterpolation = true;
            }
 
            // Ignore `/analyzerconfig` which is not variable (so it comes from the machine or sdk repo).
            if (!needsInterpolation && arg.StartsWith("/analyzerconfig", StringComparison.Ordinal))
            {
                continue;
            }
 
            string prefix = needsInterpolation ? "$" : string.Empty;
 
            code.AppendLine($"""
                            {prefix}"{rewritten}",
                """);
 
            msbuildArgsToVerify.Add(msbuildArgToVerify);
 
            if (fromNuGetPackage)
            {
                nuGetPackageFilePaths.Add(CSharpCompilerCommand.IsPathOption(rewritten, out int colonIndex)
                    ? rewritten.Substring(colonIndex + 1)
                    : rewritten);
            }
        }
        code.AppendLine("""
                    ];
                }
 
                /// <summary>
                /// Files that come from referenced NuGet packages (e.g., analyzers for NativeAOT) need to be checked specially (if they don't exist, MSBuild needs to run).
                /// </summary>
                public static IEnumerable<string> GetPathsOfCscInputsFromNuGetCache()
                {
                    return
                    [
            """);
        foreach (var nuGetPackageFilePath in nuGetPackageFilePaths)
        {
            code.AppendLine($"""
                            $"{nuGetPackageFilePath}",
                """);
        }
        code.AppendLine("""
                    ];
                }
            }
            """);
 
        // Save the code.
        var codeFolder = new DirectoryInfo(Path.Join(
            TestContext.Current.ToolsetUnderTest.RepoRoot,
            "src", "Cli", "dotnet", "Commands", "Run"));
        var nonGeneratedFile = codeFolder.File("CSharpCompilerCommand.cs");
        if (!nonGeneratedFile.Exists)
        {
            Log.WriteLine($"Skipping code generation because file does not exist: {nonGeneratedFile.FullName}");
        }
        else
        {
            var codeFilePath = codeFolder.File("CSharpCompilerCommand.Generated.cs");
            var existingText = codeFilePath.Exists ? File.ReadAllText(codeFilePath.FullName) : string.Empty;
            var newText = code.ToString();
            if (existingText != newText)
            {
                Log.WriteLine($"{codeFilePath.FullName} needs to be updated:");
                Log.WriteLine(newText);
                if (Env.GetEnvironmentVariableAsBool("CI"))
                {
                    throw new InvalidOperationException($"Not updating file in CI: {codeFilePath.FullName}");
                }
                else
                {
                    File.WriteAllText(codeFilePath.FullName, newText);
                    throw new InvalidOperationException($"File outdated, commit the changes: {codeFilePath.FullName}");
                }
            }
        }
 
        // Build using CSC.
        Directory.Delete(artifactsDir, recursive: true);
        new DotnetCommand(Log, "run", fileName, "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"""
                {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc}
                Hello from {programName}
                """);
 
        // Read args from csc.rsp file.
        var rspFilePath = Path.Join(artifactsDir, "csc.rsp");
        var cscOnlyCallArgs = File.ReadAllLines(rspFilePath);
        var cscOnlyCallArgsString = string.Join(' ', cscOnlyCallArgs);
 
        // Check that csc args between MSBuild run and CSC-only run are equivalent.
        var normalizedCscOnlyArgs = cscOnlyCallArgs
            .Select(static a => NormalizePathArg(RemoveQuotes(a)));
        Log.WriteLine("CSC-only args:");
        Log.WriteLine(string.Join(Environment.NewLine, normalizedCscOnlyArgs));
        Log.WriteLine("MSBuild args:");
        Log.WriteLine(string.Join(Environment.NewLine, msbuildArgsToVerify));
        normalizedCscOnlyArgs.Should().Equal(msbuildArgsToVerify,
            "the generated file might be outdated, run this test locally to regenerate it");
 
        static CompilerCall FindCompilerCall(string binaryLogPath)
        {
            using var reader = BinaryLogReader.Create(binaryLogPath);
            return reader.ReadAllCompilerCalls().Should().ContainSingle().Subject;
        }
 
        static string NormalizePathArg(string arg)
        {
            return CSharpCompilerCommand.IsPathOption(arg, out int colonIndex)
                ? string.Concat(arg.AsSpan(0, colonIndex + 1), NormalizePath(arg.Substring(colonIndex + 1)))
                : NormalizePath(arg);
        }
 
        static string NormalizePath(string path)
        {
            return PathUtility.GetPathWithForwardSlashes(TestPathUtility.ResolveTempPrefixLink(path));
        }
 
        static string RemoveQuotes(string arg)
        {
            return arg.Replace("\"", string.Empty);
        }
    }
 
    /// <summary>
    /// Verifies that csc-only runs emit auxiliary files equivalent to msbuild-based runs.
    /// </summary>
    [Theory]
    [InlineData("Program.cs")]
    [InlineData("test.cs")]
    [InlineData("noext")]
    public void CscVsMSBuild(string fileName)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
        string entryPointPath = Path.Join(testInstance.Path, fileName);
        File.WriteAllText(entryPointPath, $"""
            #!/test
            {s_program}
            """);
 
        string programName = Path.GetFileNameWithoutExtension(fileName);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(entryPointPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
        var artifactsBackupDir = Path.ChangeExtension(artifactsDir, ".bak");
        if (Directory.Exists(artifactsBackupDir)) Directory.Delete(artifactsBackupDir, recursive: true);
 
        // Build using CSC.
        new DotnetCommand(Log, "run", fileName, "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"""
                {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc}
                Hello from {programName}
                """);
 
        // Backup the artifacts directory.
        Directory.Move(artifactsDir, artifactsBackupDir);
 
        // Build using MSBuild.
        new DotnetCommand(Log, "run", fileName, "-bl", "--no-cache")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"Hello from {programName}");
 
        // Check that files generated by MSBuild and CSC-only runs are equivalent.
        var cscOnlyFiles = Directory.EnumerateFiles(artifactsBackupDir, "*", SearchOption.AllDirectories)
            .Where(f =>
                Path.GetDirectoryName(f) != artifactsBackupDir && // exclude top-level marker files
                Path.GetFileName(f) != programName && // binary on unix
                Path.GetExtension(f) is not (".dll" or ".exe" or ".pdb")); // other binaries
        bool hasErrors = false;
        foreach (var cscOnlyFile in cscOnlyFiles)
        {
            var relativePath = Path.GetRelativePath(relativeTo: artifactsBackupDir, path: cscOnlyFile);
            var msbuildFile = Path.Join(artifactsDir, relativePath);
 
            if (!File.Exists(msbuildFile))
            {
                throw new InvalidOperationException($"File exists in CSC-only run but not in MSBuild run: {cscOnlyFile}");
            }
 
            var cscOnlyFileText = File.ReadAllText(cscOnlyFile);
            var msbuildFileText = File.ReadAllText(msbuildFile);
            if (cscOnlyFileText.ReplaceLineEndings() != msbuildFileText.ReplaceLineEndings())
            {
                Log.WriteLine($"File differs between MSBuild and CSC-only runs (if this is expected, find the template in '{nameof(CSharpCompilerCommand)}.cs' and update it): {cscOnlyFile}");
                const int limit = 3_000;
                if (cscOnlyFileText.Length < limit && msbuildFileText.Length < limit)
                {
                    Log.WriteLine("MSBuild file content:");
                    Log.WriteLine(msbuildFileText);
                    Log.WriteLine("CSC-only file content:");
                    Log.WriteLine(cscOnlyFileText);
                }
                else
                {
                    Log.WriteLine($"MSBuild file size: {msbuildFileText.Length} chars");
                    Log.WriteLine($"CSC-only file size: {cscOnlyFileText.Length} chars");
                }
                hasErrors = true;
            }
        }
        hasErrors.Should().BeFalse("some file contents do not match, see the test output for details");
    }
 
    [Fact]
    public void UpToDate()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Console.WriteLine("Hello v1");
            """);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs"));
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "Hello v1");
 
        Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1");
 
        Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1");
 
        // Change the source file (a rebuild is necessary).
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        Build(testInstance, BuildLevel.Csc);
 
        Build(testInstance, BuildLevel.None);
 
        // Change an unrelated source file (no rebuild necessary).
        File.WriteAllText(Path.Join(testInstance.Path, "Program2.cs"), "test");
 
        Build(testInstance, BuildLevel.None);
 
        // Add an implicit build file (a rebuild is necessary).
        string buildPropsFile = Path.Join(testInstance.Path, "Directory.Build.props");
        File.WriteAllText(buildPropsFile, """
            <Project>
                <PropertyGroup>
                    <DefineConstants>$(DefineConstants);CUSTOM_DEFINE</DefineConstants>
                </PropertyGroup>
            </Project>
            """);
 
        Build(testInstance, BuildLevel.All, expectedOutput: """
            Hello from Program
            Custom define
            """);
 
        Build(testInstance, BuildLevel.None, expectedOutput: """
            Hello from Program
            Custom define
            """);
 
        // Change the implicit build file (a rebuild is necessary).
        string importedFile = Path.Join(testInstance.Path, "Settings.props");
        File.WriteAllText(importedFile, """
            <Project>
            </Project>
            """);
        File.WriteAllText(buildPropsFile, """
            <Project>
                <Import Project="Settings.props" />
            </Project>
            """);
 
        Build(testInstance, BuildLevel.All);
 
        // Change the imported build file (this is not recognized).
        File.WriteAllText(importedFile, """
            <Project>
                <PropertyGroup>
                    <DefineConstants>$(DefineConstants);CUSTOM_DEFINE</DefineConstants>
                </PropertyGroup>
            </Project>
            """);
 
        Build(testInstance, BuildLevel.None);
 
        // Force rebuild.
        Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: """
            Hello from Program
            Custom define
            """);
 
        // Remove an implicit build file (a rebuild is necessary).
        File.Delete(buildPropsFile);
        Build(testInstance, BuildLevel.Csc);
 
        // Force rebuild.
        Build(testInstance, BuildLevel.All, args: ["--no-cache"]);
 
        Build(testInstance, BuildLevel.None);
 
        // Pass argument (no rebuild necessary).
        Build(testInstance, BuildLevel.None, args: ["--", "test-arg"], expectedOutput: """
            echo args:test-arg
            Hello from Program
            """);
 
        // Change config (a rebuild is necessary).
        Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: """
            Hello from Program
            Release config
            """);
 
        // Keep changed config (no rebuild necessary).
        Build(testInstance, BuildLevel.None, args: ["-c", "Release"], expectedOutput: """
            Hello from Program
            Release config
            """);
 
        // Change config back (a rebuild is necessary).
        Build(testInstance, BuildLevel.Csc);
 
        // Build with a failure.
        new DotnetCommand(Log, ["run", "Program.cs", "-p:LangVersion=Invalid"])
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining("error CS1617"); // Invalid option 'Invalid' for /langversion.
 
        // A rebuild is necessary since the last build failed.
        Build(testInstance, BuildLevel.Csc);
    }
 
    private void Build(TestDirectory testInstance, BuildLevel expectedLevel, ReadOnlySpan<string> args = default, string expectedOutput = "Hello from Program", string programFileName = "Program.cs")
    {
        string prefix = expectedLevel switch
        {
            BuildLevel.None => CliCommandStrings.NoBinaryLogBecauseUpToDate + Environment.NewLine,
            BuildLevel.Csc => CliCommandStrings.NoBinaryLogBecauseRunningJustCsc + Environment.NewLine,
            BuildLevel.All => string.Empty,
            _ => throw new ArgumentOutOfRangeException(paramName: nameof(expectedLevel)),
        };
 
        new DotnetCommand(Log, ["run", programFileName, "-bl", .. args])
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(prefix + expectedOutput);
 
        var binlogs = new DirectoryInfo(testInstance.Path)
            .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly);
 
        binlogs.Select(f => f.Name)
            .Should().BeEquivalentTo(
                expectedLevel switch
                {
                    BuildLevel.None or BuildLevel.Csc => [],
                    BuildLevel.All => ["msbuild.binlog"],
                    _ => throw new ArgumentOutOfRangeException(paramName: nameof(expectedLevel), message: expectedLevel.ToString()),
                });
 
        foreach (var binlog in binlogs)
        {
            binlog.Delete();
        }
    }
 
    [Fact]
    public void UpToDate_InvalidOptions()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "run", "Program.cs", "--no-cache", "--no-build")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdErrContaining(string.Format(CliCommandStrings.CannotCombineOptions, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name));
    }
 
    /// <summary>
    /// Up-to-date checks and optimizations currently don't support other included files.
    /// </summary>
    [Fact]
    public void UpToDate_DefaultItems()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_programReadingEmbeddedResource);
        File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, TestValue]");
 
        // Update the RESX file.
        File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue"));
 
        Build(testInstance, BuildLevel.None, expectedOutput: "[MyString, TestValue]"); // note: outdated output (build skipped)
 
        // Update the C# file.
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v2\n" + s_programReadingEmbeddedResource);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "[MyString, TestValue]"); // note: outdated output (only CSC used)
 
        Build(testInstance, BuildLevel.All, ["--no-cache"], expectedOutput: "[MyString, UpdatedValue]");
    }
 
    [Fact]
    public void CscOnly()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Console.WriteLine("v1");
            """);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs"));
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v1");
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Console.WriteLine("v2");
            #if !DEBUG
            Console.WriteLine("Release config");
            #endif
            """);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v2");
 
        // Customizing a property forces MSBuild to be used.
        Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: """
            v2
            Release config
            """);
    }
 
    [Fact]
    public void CscOnly_CompilationDiagnostics()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            string x = null;
            Console.WriteLine("ran" + x);
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
            // warning CS8600: Converting null literal or possible null value to non-nullable type.
            .And.HaveStdOutContaining("warning CS8600")
            .And.HaveStdOutContaining("ran");
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Console.Write
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
            // error CS1002: ; expected
            .And.HaveStdOutContaining("error CS1002")
            .And.HaveStdErrContaining(CliCommandStrings.RunCommandException);
    }
 
    /// <summary>
    /// Checks that the <c>DOTNET_ROOT</c> env var is set the same in csc mode as in msbuild mode.
    /// </summary>
    [Fact]
    public void CscOnly_DotNetRoot()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            foreach (var entry in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process)
                .Cast<System.Collections.DictionaryEntry>()
                .Where(e => ((string)e.Key).StartsWith("DOTNET_ROOT")))
            {
                Console.WriteLine($"{entry.Key}={entry.Value}");
            }
            """);
 
        var expectedDotNetRoot = TestContext.Current.ToolsetUnderTest.DotNetRoot;
 
        var cscResult = new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute();
 
        cscResult.Should().Pass()
            .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
            .And.HaveStdOutContaining("DOTNET_ROOT")
            .And.HaveStdOutContaining($"={expectedDotNetRoot}");
 
        // Add an implicit build file to force use of msbuild instead of csc.
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), "<Project />");
 
        var msbuildResult = new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute();
 
        msbuildResult.Should().Pass()
            .And.NotHaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
            .And.HaveStdOutContaining("DOTNET_ROOT")
            .And.HaveStdOutContaining($"={expectedDotNetRoot}");
 
        // The set of DOTNET_ROOT env vars should be the same in both cases.
        var cscVars = cscResult.StdOut!
            .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
            .Where(line => line.StartsWith("DOTNET_ROOT"));
        var msbuildVars = msbuildResult.StdOut!
            .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
            .Where(line => line.StartsWith("DOTNET_ROOT"));
        cscVars.Should().BeEquivalentTo(msbuildVars);
    }
 
    /// <summary>
    /// In CSC-only mode, the SDK needs to manually create intermediate files
    /// like GlobalUsings.g.cs which are normally generated by MSBuild targets.
    /// This tests the SDK recreates the files when they are outdated.
    /// </summary>
    [Fact]
    public void CscOnly_IntermediateFiles()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Expression<Func<int>> e = () => 1 + 1;
            Console.WriteLine(e);
            """);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs"));
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), "<Project />");
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            // error CS0246: The type or namespace name 'Expression<>' could not be found
            .And.HaveStdOutContaining("error CS0246");
 
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
            <Project>
                <ItemGroup>
                    <Using Include="System.Linq.Expressions" />
                </ItemGroup>
            </Project>
            """);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "() => 2");
 
        File.Delete(Path.Join(testInstance.Path, "Directory.Build.props"));
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
            // error CS0246: The type or namespace name 'Expression<>' could not be found
            .And.HaveStdOutContaining("error CS0246");
    }
 
    /// <summary>
    /// If a file from a NuGet package (which would be used by CSC-only build) does not exist, full MSBuild should be used instead.
    /// </summary>
    [Fact]
    public void CscOnly_NotRestored()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs"));
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl", "--no-restore")
            .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages"))
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Fail()
            // error NETSDK1004: Assets file '...\obj\project.assets.json' not found. Run a NuGet package restore to generate this file.
            .And.HaveStdOutContaining("NETSDK1004");
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages"))
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut("Hello from Program");
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
            Console.WriteLine("v2");
            """);
 
        new DotnetCommand(Log, "run", "Program.cs", "-bl")
            .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages"))
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"""
                {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc}
                v2
                """);
    }
 
    [Fact]
    public void CscOnly_SpacesInPath()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        var programFileName = "Program with spaces.cs";
        var programPath = Path.Join(testInstance.Path, programFileName);
 
        File.WriteAllText(programPath, """
            Console.WriteLine("v1");
            """);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v1", programFileName: programFileName);
    }
 
    [Fact]
    public void CscOnly_AfterMSBuild()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        var code = """
            #:property Configuration=Release
            Console.Write("v1 ");
            #if !DEBUG
            Console.Write("Release");
            #endif
            """;
 
        var programPath = Path.Join(testInstance.Path, "Program.cs");
 
        File.WriteAllText(programPath, code);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v1 Release");
 
        Build(testInstance, BuildLevel.None, expectedOutput: "v1 Release");
 
        code = code.Replace("v1", "v2");
        File.WriteAllText(programPath, code);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v2 Release");
 
        code = code.Replace("v2", "v3");
        File.WriteAllText(programPath, code);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v3 Release");
 
        // Customizing a property forces MSBuild to be used.
        code = code.Replace("Configuration=Release", "Configuration=Debug");
        File.WriteAllText(programPath, code);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v3 ");
 
        // This MSBuild will skip CoreBuild but we still need to preserve CSC args so the next build can be CSC-only.
        Build(testInstance, BuildLevel.All, ["--no-cache"], expectedOutput: "v3 ");
 
        code = code.Replace("v3", "v4");
        File.WriteAllText(programPath, code);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v4 ");
 
        // Customizing a property on the command-line forces MSBuild to be used.
        Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: "v4 Release");
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v4 ");
    }
 
    [Fact]
    public void CscOnly_AfterMSBuild_SpacesInPath()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        var code = """
            #:property Configuration=Release
            Console.Write("v1 ");
            #if !DEBUG
            Console.Write("Release");
            #endif
            """;
 
        var programFileName = "Program with spaces.cs";
        var programPath = Path.Join(testInstance.Path, programFileName);
 
        File.WriteAllText(programPath, code);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v1 Release", programFileName: programFileName);
 
        code = code.Replace("v1", "v2");
        File.WriteAllText(programPath, code);
 
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v2 Release", programFileName: programFileName);
    }
 
    [Fact]
    public void CscOnly_AfterMSBuild_ProjectReferences()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
 
        var libDir = Path.Join(testInstance.Path, "Lib");
        Directory.CreateDirectory(libDir);
 
        File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $"""
            <Project Sdk="Microsoft.NET.Sdk">
              <PropertyGroup>
                <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
              </PropertyGroup>
            </Project>
            """);
 
        var libPath = Path.Join(libDir, "Lib.cs");
        var libCode = """
            namespace Lib;
            public class LibClass
            {
                public static string GetMessage() => "Hello from Lib v1";
            }
            """;
        File.WriteAllText(libPath, libCode);
 
        var appDir = Path.Join(testInstance.Path, "App");
        Directory.CreateDirectory(appDir);
 
        var code = """
            #:project ../Lib
            Console.WriteLine("v1 " + Lib.LibClass.GetMessage());
            """;
 
        var programPath = Path.Join(appDir, "Program.cs");
        File.WriteAllText(programPath, code);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var programFileName = "App/Program.cs";
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName);
 
        code = code.Replace("v1", "v2");
        File.WriteAllText(programPath, code);
 
        libCode = libCode.Replace("v1", "v2");
        File.WriteAllText(libPath, libCode);
 
        // Cannot use CSC because we cannot detect updates in the referenced project.
        Build(testInstance, BuildLevel.All, expectedOutput: "v2 Hello from Lib v2", programFileName: programFileName);
    }
 
    /// <summary>
    /// If users have more complex build customizations, they can opt out of the optimization which
    /// reuses CSC arguments and skips subsequent MSBuild invocations.
    /// </summary>
    [Theory, CombinatorialData]
    public void CscOnly_AfterMSBuild_OptOut(bool canSkipMSBuild, bool inDirectoryBuildProps)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        const string propertyName = VirtualProjectBuildingCommand.FileBasedProgramCanSkipMSBuild;
 
        if (inDirectoryBuildProps)
        {
            File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
                <Project>
                  <PropertyGroup>
                    <{propertyName}>{canSkipMSBuild}</{propertyName}>
                  </PropertyGroup>
                </Project>
                """);
        }
 
        var code = $"""
            #:property Configuration=Release
            {(inDirectoryBuildProps ? "" : $"#:property {propertyName}={canSkipMSBuild}")}
            Console.Write("v1 ");
            #if !DEBUG
            Console.Write("Release");
            #endif
            """;
 
        var programPath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programPath, code);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v1 Release");
 
        code = code.Replace("v1", "v2");
        File.WriteAllText(programPath, code);
 
        Build(testInstance, canSkipMSBuild ? BuildLevel.Csc : BuildLevel.All, expectedOutput: "v2 Release");
    }
 
    [Fact]
    public void CscOnly_AfterMSBuild_AuxiliaryFilesNotReused()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
 
        var code = """
            #:property Configuration=Release
            Console.Write("v1 ");
            #if !DEBUG
            Console.Write("Release");
            #endif
            """;
 
        var programPath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programPath, code);
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        Build(testInstance, BuildLevel.All, expectedOutput: "v1 Release");
 
        code = code.Replace("v1", "v2");
        File.WriteAllText(programPath, code);
 
        // Reusing CSC args from previous run here.
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v2 Release");
 
        code = code.Replace("v2", "v3");
        code = code.Replace("#:property Configuration=Release", "");
        File.WriteAllText(programPath, code);
 
        // Using built-in CSC args here (cannot reuse auxiliary files like csc.rsp here).
        Build(testInstance, BuildLevel.Csc, expectedOutput: "v3 ");
    }
 
    private static string ToJson(string s) => JsonSerializer.Serialize(s);
 
    /// <summary>
    /// Simplifies using interpolated raw strings with nested JSON,
    /// e.g, in <c>$$"""{x:{y:1}}"""</c>, the <c>}}</c> would result in an error.
    /// </summary>
    private const string nop = "";
 
    [Fact]
    public void Api()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programPath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programPath, """
            #!/program
            #:sdk Microsoft.NET.Sdk
            #:sdk Aspire.Hosting.Sdk@9.1.0
            #:property TargetFramework=net11.0
            #:package System.CommandLine@2.0.0-beta4.22272.1
            #:property LangVersion=preview
            Console.WriteLine();
            """);
 
        new DotnetCommand(Log, "run-api")
            .WithStandardInput($$"""
                {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"}
                """)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($$"""
                {"$type":"Project","Version":1,"Content":{{ToJson($"""
                    <Project>
 
                      <PropertyGroup>
                        <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
                        <ArtifactsPath>/artifacts</ArtifactsPath>
                        <PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
                        <PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
                        <FileBasedProgram>true</FileBasedProgram>
                      </PropertyGroup>
 
                      <ItemGroup>
                        <Clean Include="/artifacts/*" />
                      </ItemGroup>
 
                      <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
                      <Import Project="Sdk.props" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />
 
                      <PropertyGroup>
                        <OutputType>Exe</OutputType>
                        <ImplicitUsings>enable</ImplicitUsings>
                        <Nullable>enable</Nullable>
                        <PublishAot>true</PublishAot>
                        <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
                        <DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
                        <RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
                        <TargetFramework>net11.0</TargetFramework>
                        <LangVersion>preview</LangVersion>
                        <Features>$(Features);FileBasedProgram</Features>
                      </PropertyGroup>
 
                      <ItemGroup>
                        <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
                      </ItemGroup>
 
                      <ItemGroup>
                        <Compile Include="{programPath}" />
                      </ItemGroup>
 
                      <ItemGroup>
                        <RuntimeHostConfigurationOption Include="EntryPointFilePath" Value="{programPath}" />
                        <RuntimeHostConfigurationOption Include="EntryPointFileDirectoryPath" Value="{testInstance.Path}" />
                      </ItemGroup>
 
                      <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
                      <Import Project="Sdk.targets" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />
 
                    </Project>

                    """)}},"Diagnostics":[]}
                """);
    }
 
    [Fact]
    public void Api_Diagnostic_01()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programPath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programPath, """
            Console.WriteLine();
            #:property LangVersion=preview
            """);
 
        new DotnetCommand(Log, "run-api")
            .WithStandardInput($$"""
                {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"}
                """)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($$"""
                {"$type":"Project","Version":1,"Content":{{ToJson($"""
                    <Project>
 
                      <PropertyGroup>
                        <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
                        <ArtifactsPath>/artifacts</ArtifactsPath>
                        <PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
                        <PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
                        <FileBasedProgram>true</FileBasedProgram>
                      </PropertyGroup>
 
                      <ItemGroup>
                        <Clean Include="/artifacts/*" />
                      </ItemGroup>
 
                      <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
 
                      <PropertyGroup>
                        <OutputType>Exe</OutputType>
                        <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
                        <ImplicitUsings>enable</ImplicitUsings>
                        <Nullable>enable</Nullable>
                        <PublishAot>true</PublishAot>
                        <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
                        <DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
                        <RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
                        <Features>$(Features);FileBasedProgram</Features>
                      </PropertyGroup>
 
                      <ItemGroup>
                        <Compile Include="{programPath}" />
                      </ItemGroup>
 
                      <ItemGroup>
                        <RuntimeHostConfigurationOption Include="EntryPointFilePath" Value="{programPath}" />
                        <RuntimeHostConfigurationOption Include="EntryPointFileDirectoryPath" Value="{testInstance.Path}" />
                      </ItemGroup>
 
                      <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
 
                    </Project>

                    """)}},"Diagnostics":
                [{"Location":{
                "Path":{{ToJson(programPath)}},
                "Span":{"Start":{"Line":1,"Character":0},"End":{"Line":1,"Character":30}{{nop}}}{{nop}}},
                "Message":{{ToJson(CliCommandStrings.CannotConvertDirective)}}}]}
                """.ReplaceLineEndings(""));
    }
 
    [Fact]
    public void Api_Diagnostic_02()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programPath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programPath, """
            #:unknown directive
            Console.WriteLine();
            """);
 
        new DotnetCommand(Log, "run-api")
            .WithStandardInput($$"""
                {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"}
                """)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($$"""
                {"$type":"Project","Version":1,"Content":{{ToJson($"""
                    <Project>
 
                      <PropertyGroup>
                        <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
                        <ArtifactsPath>/artifacts</ArtifactsPath>
                        <PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
                        <PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
                        <FileBasedProgram>true</FileBasedProgram>
                      </PropertyGroup>
 
                      <ItemGroup>
                        <Clean Include="/artifacts/*" />
                      </ItemGroup>
 
                      <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
 
                      <PropertyGroup>
                        <OutputType>Exe</OutputType>
                        <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
                        <ImplicitUsings>enable</ImplicitUsings>
                        <Nullable>enable</Nullable>
                        <PublishAot>true</PublishAot>
                        <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
                        <DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
                        <RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
                        <Features>$(Features);FileBasedProgram</Features>
                      </PropertyGroup>
 
                      <ItemGroup>
                        <Compile Include="{programPath}" />
                      </ItemGroup>
 
                      <ItemGroup>
                        <RuntimeHostConfigurationOption Include="EntryPointFilePath" Value="{programPath}" />
                        <RuntimeHostConfigurationOption Include="EntryPointFileDirectoryPath" Value="{testInstance.Path}" />
                      </ItemGroup>
 
                      <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
 
                    </Project>

                    """)}},"Diagnostics":
                [{"Location":{
                "Path":{{ToJson(programPath)}},
                "Span":{"Start":{"Line":0,"Character":0},"End":{"Line":1,"Character":0}{{nop}}}{{nop}}},
                "Message":{{ToJson(string.Format(CliCommandStrings.UnrecognizedDirective, "unknown"))}}}]}
                """.ReplaceLineEndings(""));
    }
 
    [Fact]
    public void Api_Error()
    {
        new DotnetCommand(Log, "run-api")
            .WithStandardInput("""
                {"$type":"Unknown1"}
                {"$type":"Unknown2"}
                """)
            .Execute()
            .Should().Pass()
            .And.HaveStdOutContaining("""
                {"$type":"Error","Version":1,"Message":
                """)
            .And.HaveStdOutContaining("Unknown1")
            .And.HaveStdOutContaining("Unknown2");
    }
 
    [Fact]
    public void Api_RunCommand()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var programPath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(programPath, """
            Console.WriteLine();
            """);
 
        string artifactsPath = OperatingSystem.IsWindows() ? @"C:\artifacts" : "/artifacts";
        string executablePath = OperatingSystem.IsWindows() ? @"C:\artifacts\bin\debug\Program.exe" : "/artifacts/bin/debug/Program";
        new DotnetCommand(Log, "run-api")
            // The command outputs only _custom_ environment variables (not inherited ones),
            // so make sure we don't pass DOTNET_ROOT_* so we can assert that it is set by the run command.
            .WithEnvironmentVariable("DOTNET_ROOT", string.Empty)
            .WithEnvironmentVariable($"DOTNET_ROOT_{RuntimeInformation.OSArchitecture.ToString().ToUpperInvariant()}", string.Empty)
            .WithStandardInput($$"""
                {"$type":"GetRunCommand","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":{{ToJson(artifactsPath)}}}
                """)
            .Execute()
            .Should().Pass()
            // DOTNET_ROOT environment variable is platform dependent so we don't verify it fully for simplicity
            .And.HaveStdOutContaining($$"""
                {"$type":"RunCommand","Version":1,"ExecutablePath":{{ToJson(executablePath)}},"CommandLineArguments":"","WorkingDirectory":"","EnvironmentVariables":{"DOTNET_ROOT
                """);
    }
 
    [Theory, CombinatorialData]
    public void EntryPointFilePath(bool cscOnly)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: cscOnly ? OutOfTreeBaseDirectory : null);
        var filePath = Path.Join(testInstance.Path, "Program.cs");
        File.WriteAllText(filePath, """"
            var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string;
            Console.WriteLine($"""EntryPointFilePath: {entryPointFilePath}""");
            """");
 
        // Remove artifacts from possible previous runs of this test.
        var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(filePath);
        if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
 
        var prefix = cscOnly
            ? CliCommandStrings.NoBinaryLogBecauseRunningJustCsc + Environment.NewLine
            : string.Empty;
 
        new DotnetCommand(Log, "run", "-bl", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(prefix + $"EntryPointFilePath: {filePath}");
    }
 
    [Fact]
    public void EntryPointFileDirectoryPath()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """"
            var entryPointFileDirectoryPath = AppContext.GetData("EntryPointFileDirectoryPath") as string;
            Console.WriteLine($"""EntryPointFileDirectoryPath: {entryPointFileDirectoryPath}""");
            """");
 
        new DotnetCommand(Log, "run", "Program.cs")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"EntryPointFileDirectoryPath: {testInstance.Path}");
    }
 
    [Fact]
    public void EntryPointFilePath_WithRelativePath()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var fileName = "Program.cs";
        File.WriteAllText(Path.Join(testInstance.Path, fileName), """
            var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string;
            Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}");
            """);
 
        var relativePath = Path.GetRelativePath(Directory.GetCurrentDirectory(), Path.Join(testInstance.Path, fileName));
        new DotnetCommand(Log, "run", relativePath)
            .WithWorkingDirectory(Directory.GetCurrentDirectory())
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"EntryPointFilePath: {Path.GetFullPath(relativePath)}");
    }
 
    [Fact]
    public void EntryPointFilePath_WithSpacesInPath()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var dirWithSpaces = Path.Join(testInstance.Path, "dir with spaces");
        Directory.CreateDirectory(dirWithSpaces);
        var filePath = Path.Join(dirWithSpaces, "Program.cs");
        File.WriteAllText(filePath, """
        var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string;
        Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}");
        """);
 
        new DotnetCommand(Log, "run", filePath)
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"EntryPointFilePath: {filePath}");
    }
 
    [Fact]
    public void EntryPointFileDirectoryPath_WithDotSlash()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var fileName = "Program.cs";
        File.WriteAllText(Path.Join(testInstance.Path, fileName), """
        var entryPointFileDirectoryPath = AppContext.GetData("EntryPointFileDirectoryPath") as string;
        Console.WriteLine($"EntryPointFileDirectoryPath: {entryPointFileDirectoryPath}");
        """);
 
        new DotnetCommand(Log, "run", $"./{fileName}")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"EntryPointFileDirectoryPath: {testInstance.Path}");
    }
 
    [Fact]
    public void EntryPointFilePath_WithUnicodeCharacters()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        var unicodeFileName = "Программа.cs";
        var filePath = Path.Join(testInstance.Path, unicodeFileName);
        File.WriteAllText(filePath, """
        var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string;
        Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}");
        """);
 
        new DotnetCommand(Log, "run", unicodeFileName)
            .WithWorkingDirectory(testInstance.Path)
            .WithStandardOutputEncoding(Encoding.UTF8)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut($"EntryPointFilePath: {filePath}");
    }
 
    [Fact]
    public void MSBuildGet_Simple()
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        new DotnetCommand(Log, "build", "Program.cs", "-getProperty:TargetFramework")
            .WithWorkingDirectory(testInstance.Path)
            .Execute()
            .Should().Pass()
            .And.HaveStdOut(ToolsetInfo.CurrentTargetFramework);
    }
 
    /// <summary>
    /// Check that <c>-get</c> commands work the same in project-based and file-based apps.
    /// </summary>
    [Theory]
    [InlineData(true, "build", "--getProperty:TargetFramework;Configuration")]
    [InlineData(true, "build", "--getItem:MyItem", "--getProperty:MyProperty")]
    [InlineData(true, "build", "--getItem:MyItem", "--getProperty:MyProperty", "-t:MyTarget")]
    [InlineData(true, "build", "--getItem:MyItem", "--getProperty:MyProperty", "--getTargetResult:MyTarget")]
    [InlineData(true, "build", "/getProperty:TargetFramework")]
    [InlineData(true, "build", "/getProperty:TargetFramework", "-p:LangVersion=wrong")] // evaluated only, so no failure
    [InlineData(false, "build", "/getProperty:TargetFramework", "-t:Build", "-p:LangVersion=wrong")] // fails with build error but still outputs info
    [InlineData(true, "build", "-getProperty:Configuration", "-getResultOutputFile:out.txt")]
    [InlineData(true, "build", "-getProperty:OutputType,Configuration", "-getResultOutputFile:out1.txt", "-getResultOutputFile:out2.txt")]
    [InlineData(true, "run", "-getProperty:Configuration")] // not supported, the arg is passed through to the app
    [InlineData(true, "restore", "-getProperty:Configuration")]
    [InlineData(true, "publish", "-getProperty:OutputType", "-p:PublishAot=false")]
    [InlineData(true, "pack", "-getProperty:OutputType", "-p:PublishAot=false")]
    [InlineData(true, "clean", "-getProperty:Configuration")]
    public void MSBuildGet_Consistent(bool success, string subcommand, params string[] args)
    {
        var testInstance = _testAssetsManager.CreateTestDirectory();
        File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
 
        File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
            <Project>
                <Target Name="MyTarget" Returns="MyTargetReturn">
                    <ItemGroup>
                        <MyItem Include="item.txt" />
                        <MyTargetReturn Include="return.txt" />
                    </ItemGroup>
                    <PropertyGroup>
                        <MyProperty>MyValue</MyProperty>
                    </PropertyGroup>
                </Target>
            </Project>
            """);
 
        var fileBasedResult = new DotnetCommand(Log, [subcommand, "Program.cs", .. args])
            .WithWorkingDirectory(testInstance.Path)
            .Execute();
 
        var fileBasedFiles = ReadFiles();
 
        File.WriteAllText(Path.Join(testInstance.Path, "Program.csproj"), s_consoleProject);
 
        var projectBasedResult = new DotnetCommand(Log, [subcommand, .. args])
            .WithWorkingDirectory(testInstance.Path)
            .Execute();
 
        var projectBasedFiles = ReadFiles();
 
        fileBasedResult.StdOut.Should().Be(projectBasedResult.StdOut);
        fileBasedResult.StdErr.Should().Be(projectBasedResult.StdErr);
        fileBasedResult.ExitCode.Should().Be(projectBasedResult.ExitCode).And.Be(success ? 0 : 1);
        fileBasedFiles.Should().Equal(projectBasedFiles);
 
        Dictionary<string, string> ReadFiles()
        {
            var result = new DirectoryInfo(testInstance.Path)
                .EnumerateFiles()
                .ExceptBy(["Program.cs", "Directory.Build.props", "Program.csproj"], f => f.Name)
                .ToDictionary(f => f.Name, f => File.ReadAllText(f.FullName));
 
            foreach (var (file, text) in result)
            {
                Log.WriteLine($"File '{file}':");
                Log.WriteLine(text);
                File.Delete(Path.Join(testInstance.Path, file));
            }
 
            return result;
        }
    }
}