File: CommandLine\ProgramTests.cs
Web Access
Project: ..\..\..\test\dotnet-watch.Tests\dotnet-watch.Tests.csproj (dotnet-watch.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
namespace Microsoft.DotNet.Watch.UnitTests
{
    public class ProgramTests(ITestOutputHelper logger) : DotNetWatchTestBase(logger)
    {
        [Fact]
        public async Task ConsoleCancelKey()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchKitchenSink")
                .WithSource();
 
            var console = new TestConsole(Logger);
            var reporter = new TestReporter(Logger);
            var loggerFactory = new LoggerFactory(reporter);
 
            var watching = reporter.RegisterSemaphore(MessageDescriptor.WatchingWithHotReload);
            var shutdownRequested = reporter.RegisterSemaphore(MessageDescriptor.ShutdownRequested);
 
            var program = Program.TryCreate(
                TestOptions.GetCommandLineOptions(["--verbose"]),
                console,
                TestOptions.GetEnvironmentOptions(workingDirectory: testAsset.Path, TestContext.Current.ToolsetUnderTest.DotNetHostPath, testAsset),
                loggerFactory,
                reporter,
                out var errorCode);
 
            Assert.Equal(0, errorCode);
            Assert.NotNull(program);
 
            var run = program.RunAsync();
 
            await watching.WaitAsync();
 
            console.PressKey(new ConsoleKeyInfo('C', ConsoleKey.C, shift: false, alt: false, control: true));
 
            var exitCode = await run;
            Assert.Equal(0, exitCode);
 
            await shutdownRequested.WaitAsync();
        }
 
        [Theory]
        [InlineData(new[] { "--no-hot-reload", "run" }, "")]
        [InlineData(new[] { "--no-hot-reload", "run", "args" }, "args")]
        [InlineData(new[] { "--no-hot-reload", "--", "run", "args" }, "run,args")]
        [InlineData(new[] { "--no-hot-reload" }, "")]
        [InlineData(new string[] { }, "")]
        [InlineData(new[] { "run" }, "")]
        [InlineData(new[] { "run", "args" }, "args")]
        [InlineData(new[] { "--", "run", "args" }, "run,args")]
        [InlineData(new[] { "--", "test", "args" }, "test,args")]
        [InlineData(new[] { "--", "build", "args" }, "build,args")]
        [InlineData(new[] { "abc" }, "abc")]
        public async Task Arguments(string[] arguments, string expectedApplicationArgs)
        {
            var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp", identifier: string.Join(",", arguments))
                .WithSource();
 
            App.DotnetWatchArgs.Clear();
            App.Start(testAsset, arguments);
 
            Assert.Equal(expectedApplicationArgs, await App.AssertOutputLineStartsWith("Arguments = "));
        }
 
        [Theory]
        [InlineData(new[] { "--no-hot-reload", "--", "run", "args" }, "Argument Specified in Props,run,args")]
        [InlineData(new[] { "--", "run", "args" }, "Argument Specified in Props,run,args")]
        // if arguments specified on command line the ones from launch profile are ignored
        [InlineData(new[] { "-lp", "P1", "--", "run", "args" },"Argument Specified in Props,run,args")]
        // arguments specified in build file override arguments in launch profile
        [InlineData(new[] { "-lp", "P1" }, "Argument Specified in Props")]
        public async Task Arguments_HostArguments(string[] arguments, string expectedApplicationArgs)
        {
            var testAsset = TestAssets.CopyTestAsset("WatchHotReloadAppCustomHost", identifier: string.Join(",", arguments))
                .WithSource();
 
            App.Start(testAsset, arguments);
 
            AssertEx.Equal(expectedApplicationArgs, await App.AssertOutputLineStartsWith("Arguments = "));
        }
 
        //  https://github.com/dotnet/sdk/issues/49665
        [PlatformSpecificFact(TestPlatforms.Any & ~TestPlatforms.OSX)]
        public async Task RunArguments_NoHotReload()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchHotReloadAppMultiTfm")
                .WithSource();
 
            App.DotnetWatchArgs.Clear();
            App.Start(testAsset, arguments:
            [
                "--no-hot-reload",
                "run",
                "-f",
                "net6.0",
                "--property:AssemblyVersion=1.2.3.4",
                "--property",
                "AssemblyTitle= | A=B'\tC | ",
                "-v",
                "minimal",
                "--",         // the following args are application args
                "-v",         
            ]);
 
            Assert.Equal("-v", await App.AssertOutputLineStartsWith("Arguments = "));
            Assert.Equal("WatchHotReloadAppMultiTfm, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null", await App.AssertOutputLineStartsWith("AssemblyName = "));
            Assert.Equal("' | A=B'\tC | '", await App.AssertOutputLineStartsWith("AssemblyTitle = "));
            Assert.Equal(".NETCoreApp,Version=v6.0", await App.AssertOutputLineStartsWith("TFM = "));
 
            // expected output from build (-v minimal):
            Assert.Contains(App.Process.Output, l => l.Contains("Determining projects to restore..."));
 
            // not expected to find verbose output of dotnet watch
            Assert.DoesNotContain(App.Process.Output, l => l.Contains("Working directory:"));
        }
 
        //  https://github.com/dotnet/sdk/issues/49665
        [PlatformSpecificFact(TestPlatforms.Any & ~TestPlatforms.OSX)]
        public async Task RunArguments_HotReload()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchHotReloadAppMultiTfm")
                .WithSource();
 
            App.DotnetWatchArgs.Clear();
            App.Start(testAsset, arguments:
            [
                "run",
                "-f",         // dotnet watch does not recognize this arg -> dotnet run arg
                "net6.0",
                "--property",
                "AssemblyVersion=1.2.3.4",
                "--property",
                "AssemblyTitle= | A=B'\tC | ",
                "--",         // the following args are not dotnet run args
                "-v",         // dotnet build argument
                "minimal"
            ]);
 
            Assert.Equal("WatchHotReloadAppMultiTfm, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null", await App.AssertOutputLineStartsWith("AssemblyName = "));
            Assert.Equal("' | A=B'\tC | '", await App.AssertOutputLineStartsWith("AssemblyTitle = "));
            Assert.Equal(".NETCoreApp,Version=v6.0", await App.AssertOutputLineStartsWith("TFM = "));
 
            // not expected to find verbose output of dotnet watch
            Assert.DoesNotContain(App.Process.Output, l => l.Contains("Working directory:"));
 
            Assert.Contains(App.Process.Output, l => l.Contains("Hot reload enabled."));
        }
 
        [Theory]
        [InlineData("P1", "argP1")]
        [InlineData("P and Q and \"R\"", "argPQR")]
        public async Task ArgumentsFromLaunchSettings_Watch(string profileName, string expectedArgs)
        {
            var testAsset = TestAssets.CopyTestAsset("WatchAppWithLaunchSettings", identifier: profileName)
                .WithSource();
 
            App.Start(testAsset, arguments: new[]
            {
                "--verbose",
                "--no-hot-reload",
                "-lp",
                profileName
            });
 
            Assert.Equal(expectedArgs, await App.AssertOutputLineStartsWith("Arguments: "));
 
            Assert.Contains(App.Process.Output, l => l.Contains($"Found named launch profile '{profileName}'."));
            Assert.Contains(App.Process.Output, l => l.Contains("Hot Reload disabled by command line switch."));
        }
 
        [Theory]
        [InlineData("P1", "argP1")]
        [InlineData("P and Q and \"R\"", "argPQR")]
        public async Task ArgumentsFromLaunchSettings_HotReload(string profileName, string expectedArgs)
        {
            var testAsset = TestAssets.CopyTestAsset("WatchAppWithLaunchSettings", identifier: profileName)
                .WithSource();
 
            App.Start(testAsset, arguments: new[]
            {
                "--verbose",
                "-lp",
                profileName
            });
 
            Assert.Equal(expectedArgs, await App.AssertOutputLineStartsWith("Arguments: "));
 
            Assert.Contains(App.Process.Output, l => l.Contains($"Found named launch profile '{profileName}'."));
        }
 
        [Fact]
        public async Task TestCommand()
        {
            var testAsset = TestAssets.CopyTestAsset("XunitCore")
                .WithSource();
 
            App.Start(testAsset, ["--verbose", "test", "--list-tests", "/p:VSTestUseMSBuildOutput=false"]);
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            App.AssertOutputContains("The following Tests are available:");
            App.AssertOutputContains("    TestNamespace.VSTestXunitTests.VSTestXunitPassTest");
            App.Process.ClearOutput();
 
            // update file:
            var testFile = Path.Combine(testAsset.Path, "UnitTest1.cs");
            var content = File.ReadAllText(testFile, Encoding.UTF8);
            File.WriteAllText(testFile, content.Replace("VSTestXunitPassTest", "VSTestXunitPassTest2"), Encoding.UTF8);
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            App.AssertOutputContains("The following Tests are available:");
            App.AssertOutputContains("    TestNamespace.VSTestXunitTests.VSTestXunitPassTest2");
        }
 
        [Fact]
        public async Task TestCommand_MultiTargeting()
        {
            var testAsset = TestAssets.CopyTestAsset("XunitMulti")
                .WithSource();
 
            App.Start(testAsset, ["--verbose", "test", "--framework", ToolsetInfo.CurrentTargetFramework, "--list-tests", "/p:VSTestUseMSBuildOutput=false"]);
 
            await App.AssertOutputLineEquals("The following Tests are available:");
            await App.AssertOutputLineEquals("    TestNamespace.VSTestXunitTests.VSTestXunitFailTestNetCoreApp");
        }
 
        [Fact]
        public async Task BuildCommand()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchNoDepsApp")
                .WithSource();
 
            App.Start(testAsset, ["--verbose", "--property", "TestProperty=123", "build", "/t:TestTarget"]);
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            // evaluation affected by -c option:
            Assert.Contains("TestProperty", App.Process.Output.Single(line => line.Contains("/t:GenerateWatchList")));
 
            App.AssertOutputContains("dotnet watch ⌚ Command 'build' does not support Hot Reload.");
            App.AssertOutputContains("warning : The value of property is '123'");
        }
 
        [Fact]
        public async Task MSBuildCommand()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchNoDepsApp")
                .WithSource();
 
            App.Start(testAsset, ["--verbose", "/p:TestProperty=123", "msbuild", "/t:TestTarget"]);
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            // TestProperty is not passed to evaluation since msbuild command doesn't include it in forward options:
            Assert.DoesNotContain("TestProperty", App.Process.Output.Single(line => line.Contains("/t:GenerateWatchList")));
 
            App.AssertOutputContains("dotnet watch ⌚ Command 'msbuild' does not support Hot Reload.");
            App.AssertOutputContains("warning : The value of property is '123'");
        }
 
        [Fact]
        public async Task PackCommand()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchNoDepsApp")
                .WithSource();
 
            App.Start(testAsset, ["--verbose", "pack", "-c", "Release"]);
 
            var packagePath = Path.Combine(testAsset.Path, "bin", "Release", "WatchNoDepsApp.1.0.0.nupkg");
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            // evaluation affected by -c option:
            Assert.Contains("-property:Configuration=Release", App.Process.Output.Single(line => line.Contains("/t:GenerateWatchList")));
 
            App.AssertOutputContains("dotnet watch ⌚ Command 'pack' does not support Hot Reload.");
            App.AssertOutputContains($"Successfully created package '{packagePath}'");
        }
 
        [Fact]
        public async Task PublishCommand()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchNoDepsApp")
                .WithSource();
 
            App.Start(testAsset, ["--verbose", "publish", "-c", "Release"]);
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            // evaluation affected by -c option:
            Assert.Contains("-property:Configuration=Release", App.Process.Output.Single(line => line.Contains("/t:GenerateWatchList")));
            
            App.AssertOutputContains("dotnet watch ⌚ Command 'publish' does not support Hot Reload.");
 
            App.AssertOutputContains(Path.Combine("Release", ToolsetInfo.CurrentTargetFramework, "publish"));
        }
 
        [Fact]
        public async Task FormatCommand()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchNoDepsApp")
                .WithSource();
 
            App.DotnetWatchArgs.Clear();
            App.Start(testAsset, ["--verbose", "format", "--verbosity", "detailed"]);
 
            await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
 
            App.AssertOutputContains("dotnet watch ⌚ Command 'format' does not support Hot Reload.");
 
            App.AssertOutputContains("format --verbosity detailed");
            App.AssertOutputContains("Format complete in");
        }
 
        [Fact]
        public async Task ProjectGraphLoadFailure()
        {
            var testAsset = TestAssets
                .CopyTestAsset("WatchAppWithProjectDeps")
                .WithSource()
                .WithProjectChanges((path, proj) =>
                {
                    if (Path.GetFileName(path) == "App.WithDeps.csproj")
                    {
                        proj.Root.Descendants()
                            .Single(e => e.Name.LocalName == "ItemGroup")
                            .Add(XElement.Parse("""
                            <ProjectReference Include="NonExistentDirectory\X.csproj" />
                            """));
                    }
                });
 
            App.Start(testAsset, [], "AppWithDeps");
 
            await App.AssertOutputLineStartsWith("dotnet watch ⌚ Fix the error to continue or press Ctrl+C to exit.");
 
            App.AssertOutputContains(@"dotnet watch 🔨 Failed to load project graph.");
            App.AssertOutputContains($"dotnet watch ❌ The project file could not be loaded. Could not find a part of the path '{Path.Combine(testAsset.Path, "AppWithDeps", "NonExistentDirectory", "X.csproj")}'");
        }
 
        [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307")
        public async Task ListsFiles()
        {
            var testAsset = TestAssets.CopyTestAsset("WatchGlobbingApp")
               .WithSource();
 
            App.DotnetWatchArgs.Clear();
            App.Start(testAsset, ["--list"]);
            var lines = await App.Process.GetAllOutputLinesAsync(CancellationToken.None);
            var files = lines.Where(l => !l.StartsWith("dotnet watch ⌚") && l.Trim() != "");
 
            AssertEx.EqualFileList(
                testAsset.Path,
                new[]
                {
                    "Program.cs",
                    "include/Foo.cs",
                    "WatchGlobbingApp.csproj",
                },
                files);
        }
    }
}