From d88975602491e7ba612d4e2b2059a177a242d6d5 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 22 Apr 2026 14:22:49 +0200 Subject: [PATCH 1/3] Split RunFileTests into multiple files to match main branch structure Splits the monolithic RunFileTests.cs into: - RunFileTestBase.cs: shared base class, fields, helpers - RunFileTests_General.cs: general file path, stdin, project tests - RunFileTests_BuildOptions.cs: build options, binary logs, verbosity tests - RunFileTests_BuildCommands.cs: restore, build, publish, pack, clean tests - RunFileTests_Directives.cs: directive tests (define, package, sdk, ref, include) - RunFileTests_CscOnlyAndApi.cs: csc-only, up-to-date, API tests This matches the split structure on main to minimize merge conflicts when flowing changes between branches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Convert/DotnetProjectConvertTests.cs | 6 +- .../CommandTests/Run/RunFileTestBase.cs | 215 + .../CommandTests/Run/RunFileTests.cs | 7519 ----------------- .../Run/RunFileTests_BuildCommands.cs | 924 ++ .../Run/RunFileTests_BuildOptions.cs | 1007 +++ .../Run/RunFileTests_CscOnlyAndApi.cs | 2574 ++++++ .../Run/RunFileTests_Directives.cs | 1686 ++++ .../CommandTests/Run/RunFileTests_General.cs | 1180 +++ 8 files changed, 7589 insertions(+), 7522 deletions(-) create mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs delete mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTests.cs create mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs create mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs create mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs create mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs create mode 100644 test/dotnet.Tests/CommandTests/Run/RunFileTests_General.cs diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index edf0e2c1841e..e559f47539b3 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -1266,7 +1266,7 @@ public void ProcessingFails() .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Fail() - .And.HaveStdErrContaining(RunFileTests.DirectiveError(filePath, 1, FileBasedProgramsResources.UnrecognizedDirective, "invalid")); + .And.HaveStdErrContaining(RunFileTestBase.DirectiveError(filePath, 1, FileBasedProgramsResources.UnrecognizedDirective, "invalid")); new DirectoryInfo(Path.Join(testInstance.Path)) .EnumerateDirectories().Should().BeEmpty(); @@ -1719,8 +1719,8 @@ public void Directives_IncludeExclude() expectedCSharp: "", expectedErrors: [ - (7, string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", RunFileTests.s_includeExcludeDefaultKnownExtensions)), - (8, string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:exclude", RunFileTests.s_includeExcludeDefaultKnownExtensions)), + (7, string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", RunFileTestBase.s_includeExcludeDefaultKnownExtensions)), + (8, string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:exclude", RunFileTestBase.s_includeExcludeDefaultKnownExtensions)), (1, string.Format(Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "A.cs"))), (1, string.Format(Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "|.cs"))), ]); diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs new file mode 100644 index 000000000000..71d0e567721c --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging.StructuredLogger; +using Microsoft.DotNet.Cli.Commands; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; +using Microsoft.DotNet.ProjectTools; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +public abstract class RunFileTestBase(ITestOutputHelper log) : SdkTest(log) +{ + internal static string s_includeExcludeDefaultKnownExtensions + => field ??= string.Join(", ", CSharpDirective.IncludeOrExclude.DefaultMapping.Select(static e => e.Extension)); + + internal 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 + """; + + internal static readonly string s_programDependingOnUtil = /* lang=C#-Test */ """ + if (args.Length > 0) + { + Console.WriteLine("echo args:" + string.Join(";", args)); + } + Console.WriteLine("Hello, " + Util.GetMessage()); + """; + + internal static readonly string s_util = /* lang=C#-Test */ """ + static class Util + { + public static string GetMessage() + { + return "String from Util"; + } + } + """; + + internal 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().Single()); + """; + + internal static readonly string s_resx = """ + + + TestValue + + + """; + + internal static readonly string s_consoleProject = $""" + + + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + + + """; + + internal static readonly string s_launchSettings = """ + { + "profiles": { + "TestProfile1": { + "commandName": "Project", + "environmentVariables": { + "Message": "TestProfileMessage1" + } + }, + "TestProfile2": { + "commandName": "Project", + "environmentVariables": { + "Message": "TestProfileMessage2" + } + } + } + } + """; + + /// + /// 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. + /// + internal static string OutOfTreeBaseDirectory => field ??= PrepareOutOfTreeBaseDirectory(); + + internal static bool HasCaseInsensitiveFileSystem + { + get + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + } + + /// + 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(SdkTestContext.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 args) + { + return $"{path}({line}): {FileBasedProgramsResources.DirectiveError}: {string.Format(messageFormat, args)}"; + } + + internal static void EnableRefDirective(TestDirectory testInstance) + { + var propsPath = Path.Join(testInstance.Path, "Directory.Build.props"); + var propsContent = File.Exists(propsPath) ? File.ReadAllText(propsPath) : null; + if (propsContent is not null && propsContent.Contains(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective)) + { + return; + } + + File.WriteAllText(propsPath, $""" + + + <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true + + + """); + } + + + internal static void VerifyBinLogEvaluationDataCount(string binaryLogPath, int expectedCount) + { + var records = BinaryLog.ReadRecords(binaryLogPath).ToList(); + records.Count(static r => r.Args is ProjectEvaluationStartedEventArgs).Should().Be(expectedCount); + records.Count(static r => r.Args is ProjectEvaluationFinishedEventArgs).Should().Be(expectedCount); + } + + private protected void Build( + TestDirectory testInstance, + BuildLevel expectedLevel, + ReadOnlySpan args = default, + string expectedOutput = "Hello from Program", + string programFileName = "Program.cs", + string? workDir = null, + Func? customizeCommand = null) + { + 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)), + }; + + var command = new DotnetCommand(Log, ["run", programFileName, "-bl", .. args]) + .WithWorkingDirectory(workDir ?? testInstance.Path); + + if (customizeCommand != null) + { + command = customizeCommand(command); + } + + command.Execute() + .Should().Pass() + .And.HaveStdOut(prefix + expectedOutput); + + var binlogs = new DirectoryInfo(workDir ?? 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(); + } + } + +} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs deleted file mode 100644 index 3a459765653b..000000000000 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ /dev/null @@ -1,7519 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Runtime.Versioning; -using System.Text.Json; -using Basic.CompilerLog.Util; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Framework; -using Microsoft.Build.Logging.StructuredLogger; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.DotNet.Cli.Commands; -using Microsoft.DotNet.Cli.Commands.NuGet; -using Microsoft.DotNet.Cli.Commands.Run; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.FileBasedPrograms; -using Microsoft.DotNet.ProjectTools; - -namespace Microsoft.DotNet.Cli.Run.Tests; - -public sealed class RunFileTests(ITestOutputHelper log) : SdkTest(log) -{ - internal static string s_includeExcludeDefaultKnownExtensions - => field ??= string.Join(", ", CSharpDirective.IncludeOrExclude.DefaultMapping.Select(static e => e.Extension)); - - 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().Single()); - """; - - private static readonly string s_resx = """ - - - TestValue - - - """; - - private static readonly string s_consoleProject = $""" - - - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - - - """; - - private static readonly string s_launchSettings = """ - { - "profiles": { - "TestProfile1": { - "commandName": "Project", - "environmentVariables": { - "Message": "TestProfileMessage1" - } - }, - "TestProfile2": { - "commandName": "Project", - "environmentVariables": { - "Message": "TestProfileMessage2" - } - } - } - } - """; - - /// - /// 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. - /// - private static string OutOfTreeBaseDirectory => field ??= PrepareOutOfTreeBaseDirectory(); - - private static bool HasCaseInsensitiveFileSystem - { - get - { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - } - } - - /// - 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(SdkTestContext.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 args) - { - return $"{path}({line}): {FileBasedProgramsResources.DirectiveError}: {string.Format(messageFormat, args)}"; - } - - private static void EnableRefDirective(TestDirectory testInstance) - { - var propsPath = Path.Join(testInstance.Path, "Directory.Build.props"); - var propsContent = File.Exists(propsPath) ? File.ReadAllText(propsPath) : null; - if (propsContent is not null && propsContent.Contains(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective)) - { - return; - } - - File.WriteAllText(propsPath, $""" - - - <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true - - - """); - } - - /// - /// dotnet run file.cs succeeds without a project file. - /// - [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, - "--project")); - } - } - - /// - /// dotnet file.cs is equivalent to dotnet run file.cs. - /// - [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 - """); - - // https://github.com/dotnet/sdk/issues/52108 - new DotnetCommand(Log, "Program.cs", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:Program.cs - Hello from Program - Release config - """); - } - - /// - /// Casing of the argument is used for the output binary name. - /// - [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, - "--project")); - } - } - - /// - /// dotnet run folder/file.cs succeeds without a project file. - /// - [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"); - } - - /// - /// dotnet run --project file.cs fails. - /// - [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); - } - - /// - /// Even if there is a file-based app ./build, dotnet build should not execute that. - /// - [Theory] - // error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file. - [InlineData("build", "MSB1003", false)] - // dotnet watch: Could not find a MSBuild project file in '...'. Specify which project to use with the --project option. - [InlineData("watch", "--project", true)] - public void Precedence_BuiltInCommand(string cmd, string error, bool errorInStdErr) - { - 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 - var failure = new DotnetCommand(Log, cmd) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail(); - - if (errorInStdErr) - { - failure.And.HaveStdErrContaining(error); - } - else - { - failure.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"); - } - - /// - /// Even if there is a file-based app ./test.dll, dotnet test.dll should not execute that. - /// - [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"); - } - - /// - /// dotnet run - reads the C# code from stdin. - /// - [Fact] - public void ReadFromStdin() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(testInstance.Path) - .WithStandardInput(""" - Console.WriteLine("Hello from stdin"); - Console.WriteLine("Read: " + (Console.ReadLine() ?? "null")); - Console.WriteLine("Working directory: " + Environment.CurrentDirectory); - """) - .Execute() - .Should().Pass() - .And.HaveStdOut($""" - Hello from stdin - Read: null - Working directory: {testInstance.Path} - """); - } - - /// - /// Directory.Build.props doesn't have any effect on dotnet run -. - /// - [Fact] - public void ReadFromStdin_BuildProps() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ - - - disable - - - """); - - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(testInstance.Path) - .WithStandardInput(""" - Console.WriteLine("Hello from stdin"); - """) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from stdin"); - - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(testInstance.Path) - .WithStandardInput(""" - #:property ImplicitUsings=disable - Console.WriteLine("Hello from stdin"); - """) - .Execute() - .Should().Fail() - // error CS0103: The name 'Console' does not exist in the current context - .And.HaveStdOutContaining("error CS0103"); - } - - /// - /// Directory.Build.props doesn't have any effect on dotnet run -. - /// - [Fact] - public void ReadFromStdin_ProjectReference() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var libDir = Path.Join(testInstance.Path, "lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - 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); - - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(appDir) - .WithStandardInput($""" - #:project $(MSBuildStartupDirectory)/../lib - Console.WriteLine(Lib.LibClass.GetMessage()); - """) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from Lib"); - - // Relative paths are resolved from the isolated temp directory, hence they don't work. - - var errorParts = DirectiveError("app.cs", 1, FileBasedProgramsResources.InvalidProjectDirective, - string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, "{}")).Split("{}"); - errorParts.Should().HaveCount(2); - - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(appDir) - .WithStandardInput($""" - #:project ../lib - Console.WriteLine(Lib.LibClass.GetMessage()); - """) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(errorParts[0]) - .And.HaveStdErrContaining(errorParts[1]); - } - - /// - /// dotnet run - with #:ref uses $(MSBuildStartupDirectory) to resolve paths. - /// Relative paths don't work from stdin since the file is in an isolated temp directory. - /// Analogous to . - /// - [Fact] - public void ReadFromStdin_RefDirective() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var libDir = Path.Join(testInstance.Path, "lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "mylib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello from lib!"; - } - """); - - var appDir = Path.Join(testInstance.Path, "app"); - Directory.CreateDirectory(appDir); - - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(appDir) - .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") - .WithStandardInput(""" - #:ref $(MSBuildStartupDirectory)/../lib/mylib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from lib!"); - - // Relative paths are resolved from the isolated temp directory, hence they don't work. - - var errorParts = DirectiveError("app.cs", 1, FileBasedProgramsResources.InvalidRefDirective, - string.Format(FileBasedProgramsResources.CouldNotFindRefFile, "{}")).Split("{}"); - errorParts.Should().HaveCount(2); - - new DotnetCommand(Log, "run", "-") - .WithWorkingDirectory(appDir) - .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") - .WithStandardInput(""" - #:ref ../lib/mylib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(errorParts[0]) - .And.HaveStdErrContaining(errorParts[1]); - } - - [Fact] - public void ReadFromStdin_NoBuild() - { - new DotnetCommand(Log, "run", "-", "--no-build") - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionForStdin, "--no-build")); - } - - [Fact] - public void ReadFromStdin_LaunchProfile() - { - new DotnetCommand(Log, "run", "-", "--launch-profile=test") - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionForStdin, "--launch-profile")); - } - - /// - /// dotnet run -- - should NOT read the C# file from stdin, - /// the hyphen should be considred an app argument instead since it's after --. - /// - [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, "--project")); - } - - /// - /// dotnet run folder without a project file is not supported. - /// - [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, - "--project")); - } - - /// - /// dotnet run app.csproj fails if app.csproj does not exist. - /// - [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, - "--project")); - } - - /// - /// dotnet run app.csproj where app.csproj exists - /// runs the project and passes 'app.csproj' as an argument. - /// - [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.NotHaveStdErr() - .And.HaveStdOut(""" - 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.NotHaveStdErr() - .And.HaveStdOut("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.NotHaveStdErr() - .And.HaveStdOut("Hello from Program"); - } - - /// - /// dotnet run --project App.csproj Program.cs does not warn - /// because --project was explicitly specified. - /// - [Fact] - public void ProjectInCurrentDirectory_ProjectOption_NoWarning() - { - 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", "--project", "App.csproj", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.NotHaveStdErr() - .And.HaveStdOut(""" - echo args:Program.cs - Hello from App - """); - } - - /// - /// dotnet run file.cs in a directory with a project file warns - /// because file.cs is passed as an application argument to the project instead of running as a file-based program. - /// - [Fact] - public void ProjectInCurrentDirectory_Warns() - { - 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", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:Program.cs - Hello from App - """) - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, - "Program.cs", - Path.Join(testInstance.Path, "App.csproj"))); - } - - /// - /// dotnet run nonexistent.cs in a directory with a project file warns - /// even though the file does not exist, because the .cs extension suggests it was intended as a file-based program. - /// - [Fact] - public void ProjectInCurrentDirectory_NonExistentCsFile_Warns() - { - 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", "nonexistent.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:nonexistent.cs - Hello from App - """) - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.RunCommandWarningCsFileArgumentPassedToProject, - "nonexistent.cs", - Path.Join(testInstance.Path, "App.csproj"))); - } - - /// - /// dotnet run -- file.cs in a directory with a project file does not warn - /// because -- signals that the arguments are intentional. - /// - [Fact] - public void ProjectInCurrentDirectory_DoubleDash_NoWarning() - { - 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", "--", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:Program.cs - Hello from App - """) - .And.NotHaveStdErr(); - } - - /// - /// dotnet run file.cs -- other still warns because file.cs appears before --. - /// - [Fact] - public void ProjectInCurrentDirectory_DoubleDashAfterFile_Warns() - { - 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", "Program.cs", "--", "otherArg") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:Program.cs;otherArg - Hello from App - """) - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, - "Program.cs", - Path.Join(testInstance.Path, "App.csproj"))); - } - - /// - /// dotnet run someArg file.cs in a directory with a project warns - /// when an unrecognized argument prevents file.cs from being treated as a file-based program entry point. - /// - [Fact] - public void ProjectInCurrentDirectory_UnrecognizedArg_Warns() - { - 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", "someArg", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:someArg;Program.cs - Hello from App - """) - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, - "Program.cs", - Path.Join(testInstance.Path, "App.csproj"))); - } - - /// - /// dotnet run -c Release Program.cs in a directory with a project warns because - /// known options like -c don't suppress the warning; only --project, --file, or -- do. - /// - [Fact] - public void ProjectInCurrentDirectory_KnownOption_Warns() - { - 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", "-c", "Release", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:Program.cs - Hello from App - Release config - """) - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, - "Program.cs", - Path.Join(testInstance.Path, "App.csproj"))); - } - - /// - /// dotnet run someArg -- file.cs does not warn because the .cs file is after --. - /// - [Fact] - public void ProjectInCurrentDirectory_UnrecognizedArg_DoubleDash_NoWarning() - { - 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", "someArg", "--", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - echo args:someArg;Program.cs - Hello from App - """) - .And.NotHaveStdErr(); - } - - /// - /// dotnet build someArg Program.cs warns because 'Program.cs' is a valid file-based entry point - /// but additional positional arguments cause it to fall back to MSBuild. - /// - [Theory] - [InlineData("build", "someArg", "Program.cs")] - [InlineData("clean", "someArg", "Program.cs")] - [InlineData("publish", "someArg", "Program.cs")] - [InlineData("build", "Program.cs", "-consoleLoggerParameters:NoSummary")] - public void ExtraArgWithFileEntryPoint_Warns(string command, string arg1, string arg2) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); - - new DotnetCommand(Log, command, arg1, arg2) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.WarningFileArgumentPassedToMSBuild, - "Program.cs", - command)); - } - - /// - /// dotnet build nonexistent.cs warns because the .cs extension suggests it was intended as a file-based program. - /// - [Theory] - [InlineData("build")] - [InlineData("clean")] - [InlineData("publish")] - public void NonExistentCsFile_Warns(string command) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - new DotnetCommand(Log, command, "nonexistent.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(string.Format( - CliCommandStrings.WarningCsFileArgumentPassedToMSBuild, - "nonexistent.cs", - command)); - } - - /// - /// dotnet build --no-incremental Program.cs is handled as file-based (known option + single positional arg) and does not warn. - /// - [Theory] - [InlineData("Program.cs")] - [InlineData("--no-incremental", "Program.cs")] - public void SingleFileEntryPoint_NoWarning(params string[] extraArgs) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); - - new DotnetCommand(Log, ["build", .. extraArgs]) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.NotHaveStdErr(); - } - - /// - /// When a file is not a .cs file, we probe the first characters of the file for #!, and - /// execute as a single file program if we find them. - /// - [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"); - } - - /// - /// When a file is not a .cs file, we probe the first characters of the file for #!, and - /// fall back to normal dotnet run behavior if we don't find them. - /// - [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, - "--project")); - } - - [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"); - } - - /// - /// When the entry-point file does not exist, fallback to normal dotnet run behavior. - /// - [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, - "--project")); - } - - /// - /// Cannot run a non-entry-point file. - /// - [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 - } - - /// - /// When the entry-point file does not exist, fallback to normal dotnet run behavior. - /// - [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, - "--project")); - } - - /// - /// Other files in the folder are not part of the compilation. - /// See . - /// - [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"), $""" - #!/usr/bin/env dotnet - #:property EnableDefaultCompileItems=true - {s_programDependingOnUtil} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, String from Util"); - } - - /// - /// Setting EnableDefaultCompileItems=true via Directory.Build.props should not cause CS2002 warning. - /// See . - /// - [Fact] - public void MultipleFiles_EnableDefaultCompileItemsViaDirectoryBuildProps() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" - #!/usr/bin/env dotnet - {s_programDependingOnUtil} - """); - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util); - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ - - - true - - - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, String from Util"); - } - - /// - /// Directives in other files are considered even if those files are included via manual MSBuild rather than #:include. - /// - [Fact] - public void MultipleFiles_DirectivesInOtherFiles() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "A.cs"), """ - #!/usr/bin/env dotnet - Console.WriteLine(B.M()); - #if !DEBUG - Console.WriteLine("Release config"); - #endif - """); - File.WriteAllText(Path.Join(testInstance.Path, "B.cs"), """ - #:property Configuration=Release - public static class B - { - public static string M() => "String from Util"; - } - """); - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" - - - - - - """); - - new DotnetCommand(Log, "run", "A.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - String from Util - Release config - """); - } - - /// - /// dotnet run util.cs fails if util.cs is not the entry-point. - /// - [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 - } - - /// - /// If there are nested project files like - /// - /// app/file.cs - /// app/nested/x.csproj - /// app/nested/another.cs - /// - /// executing dotnet run app/file.cs will include the nested .cs 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. - /// - [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"); - } - - /// - /// dotnet run folder/app.csproj -> the argument is not recognized as an entry-point file - /// (it does not have .cs file extension), so this fallbacks to normal dotnet run behavior. - /// - [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, - "--project")); - } - - /// - /// Main method is supported just like top-level statements. - /// - [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!"); - } - - /// - /// Empty file does not contain entry point, so that's an error. - /// - [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 - } - - /// - /// See . - /// - [Theory, CombinatorialData] - public void WorkingDirectory(bool cscOnly) - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: cscOnly ? OutOfTreeBaseDirectory : null); - var programPath = Path.Join(testInstance.Path, "Program.cs"); - - var code = """ - Console.WriteLine("v1"); - Console.WriteLine(Environment.CurrentDirectory); - Console.WriteLine(Directory.GetCurrentDirectory()); - Console.WriteLine(new DirectoryInfo(".").FullName); - Console.WriteLine(AppContext.GetData("EntryPointFileDirectoryPath")); - """; - - File.WriteAllText(programPath, code); - - var tempDir = Directory.CreateTempSubdirectory(); - var workDir = TestPathUtility.ResolveTempPrefixLink(tempDir.FullName).TrimEnd(Path.DirectorySeparatorChar); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, - expectedLevel: cscOnly ? BuildLevel.Csc : BuildLevel.All, - programFileName: programPath, - workDir: workDir, - expectedOutput: GetExpectedOutput("v1", workDir)); - - code = code.Replace("v1", "v2"); - File.WriteAllText(programPath, code); - - Build(testInstance, - expectedLevel: BuildLevel.Csc, - programFileName: programPath, - workDir: workDir, - expectedOutput: GetExpectedOutput("v2", workDir)); - - tempDir.Delete(); - - string GetExpectedOutput(string version, string workDir) => $""" - {version} - {workDir} - {workDir} - {workDir} - {Path.GetDirectoryName(programPath)} - """; - } - - /// - /// Combination of and . - /// - [Fact] - public void WorkingDirectory_CscOnly_AfterMSBuild() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - var programPath = Path.Join(testInstance.Path, "Program.cs"); - - var code = """ - #:property Configuration=Release - Console.WriteLine("v1"); - Console.WriteLine(Environment.CurrentDirectory); - Console.WriteLine(Directory.GetCurrentDirectory()); - Console.WriteLine(new DirectoryInfo(".").FullName); - Console.WriteLine(AppContext.GetData("EntryPointFileDirectoryPath")); - """; - - File.WriteAllText(programPath, code); - - var tempDir = Directory.CreateTempSubdirectory(); - var workDir = TestPathUtility.ResolveTempPrefixLink(tempDir.FullName).TrimEnd(Path.DirectorySeparatorChar); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, - expectedLevel: BuildLevel.All, - programFileName: programPath, - workDir: workDir, - expectedOutput: GetExpectedOutput("v1", workDir)); - - Build(testInstance, - expectedLevel: BuildLevel.None, - programFileName: programPath, - workDir: workDir, - expectedOutput: GetExpectedOutput("v1", workDir)); - - code = code.Replace("v1", "v2"); - File.WriteAllText(programPath, code); - - Build(testInstance, - expectedLevel: BuildLevel.Csc, - programFileName: programPath, - workDir: workDir, - expectedOutput: GetExpectedOutput("v2", workDir)); - - // Can be overridden with a #:property. - var workDir2 = Path.Join(testInstance.Path, "dir2"); - Directory.CreateDirectory(workDir2); - code = $""" - #:property RunWorkingDirectory={workDir2} - {code} - """; - File.WriteAllText(programPath, code); - - Build(testInstance, - expectedLevel: BuildLevel.All, - programFileName: programPath, - workDir: workDir, - expectedOutput: GetExpectedOutput("v2", workDir2)); - - tempDir.Delete(); - - string GetExpectedOutput(string version, string workDir) => $""" - {version} - {workDir} - {workDir} - {workDir} - {Path.GetDirectoryName(programPath)} - """; - } - - /// - /// Implicit build files have an effect. - /// - [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"), """ - - - TestName - - - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from TestName"); - } - - /// - /// Implicit build files are taken from the folder of the symbolic link itself, not its target. - /// This is equivalent to the behavior of symlinked project files. - /// See . - /// - [Fact] - public void DirectoryBuildProps_SymbolicLink() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var dir1 = Path.Join(testInstance.Path, "dir1"); - Directory.CreateDirectory(dir1); - - var originalPath = Path.Join(dir1, "original.cs"); - File.WriteAllText(originalPath, s_program); - - File.WriteAllText(Path.Join(dir1, "Directory.Build.props"), """ - - - OriginalAssemblyName - - - """); - - var dir2 = Path.Join(testInstance.Path, "dir2"); - Directory.CreateDirectory(dir2); - - var programFileName = "linked.cs"; - var programPath = Path.Join(dir2, programFileName); - - File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); - - File.WriteAllText(Path.Join(dir2, "Directory.Build.props"), """ - - - LinkedAssemblyName - - - """); - - new DotnetCommand(Log, "run", programFileName) - .WithWorkingDirectory(dir2) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from LinkedAssemblyName"); - - // Removing the Directory.Build.props should be detected by up-to-date check. - File.Delete(Path.Join(dir2, "Directory.Build.props")); - - new DotnetCommand(Log, "run", programFileName) - .WithWorkingDirectory(dir2) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from linked"); - } - - /// - /// Overriding default (implicit) properties of file-based apps via implicit build files. - /// - [Fact] - public void DefaultProps_DirectoryBuildProps() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - Console.WriteLine("Hi"); - """); - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ - - - disable - - - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - // error CS0103: The name 'Console' does not exist in the current context - .And.HaveStdOutContaining("error CS0103"); - - // Converting to a project should not change the behavior. - - new DotnetCommand(Log, "project", "convert", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run") - .WithWorkingDirectory(Path.Join(testInstance.Path, "Program")) - .Execute() - .Should().Fail() - // error CS0103: The name 'Console' does not exist in the current context - .And.HaveStdOutContaining("error CS0103"); - } - - /// - /// Overriding default (implicit) properties of file-based apps from custom SDKs. - /// - [Fact] - public void DefaultProps_CustomSdk() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var sdkDir = Path.Join(testInstance.Path, "MySdk"); - Directory.CreateDirectory(sdkDir); - File.WriteAllText(Path.Join(sdkDir, "Sdk.props"), """ - - - disable - - - """); - File.WriteAllText(Path.Join(sdkDir, "Sdk.targets"), """ - - """); - File.WriteAllText(Path.Join(sdkDir, "MySdk.csproj"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - MSBuildSdk - false - - - - - - """); - - new DotnetCommand(Log, "pack") - .WithWorkingDirectory(sdkDir) - .Execute() - .Should().Pass(); - - var appDir = Path.Join(testInstance.Path, "app"); - Directory.CreateDirectory(appDir); - File.WriteAllText(Path.Join(appDir, "NuGet.config"), $""" - - - - - - - """); - File.WriteAllText(Path.Join(appDir, "Program.cs"), """ - #:sdk Microsoft.NET.Sdk - #:sdk MySdk@1.0.0 - Console.WriteLine("Hi"); - """); - - // Use custom package cache to avoid reuse of the custom SDK packed by previous test runs. - var packagesDir = Path.Join(testInstance.Path, ".packages"); - - new DotnetCommand(Log, "run", "Program.cs") - .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) - .WithWorkingDirectory(appDir) - .Execute() - .Should().Fail() - // error CS0103: The name 'Console' does not exist in the current context - .And.HaveStdOutContaining("error CS0103"); - - // Converting to a project should not change the behavior. - - new DotnetCommand(Log, "project", "convert", "Program.cs") - .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) - .WithWorkingDirectory(appDir) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run") - .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) - .WithWorkingDirectory(Path.Join(appDir, "Program")) - .Execute() - .Should().Fail() - // error CS0103: The name 'Console' does not exist in the current context - .And.HaveStdOutContaining("error CS0103"); - } - - [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"), """ - - - - $(RunArguments) extended - - - - """); - - 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"), """ - - - - - - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdOutContaining(""" - MYAPP001: Custom error - """) - .And.HaveStdErrContaining(CliCommandStrings.RunCommandException); - } - - /// - /// Command-line arguments should be passed through. - /// - [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 - """); - } - - /// - /// dotnet run --unknown-arg file.cs fallbacks to normal dotnet run behavior. - /// - [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, - "--project")); - } - - /// - /// dotnet run --some-known-arg file.cs is supported. - /// - [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 - """); - } - - /// - /// dotnet run -bl file.cs produces a binary log. - /// - [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(); - } - - /// - /// dotnet run file.cs should not produce a binary log. - /// - [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(); - } - - /// - /// Binary logs from our in-memory projects should have evaluation data. - /// - [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(); - - // There should be exactly three - two for restore, one for build. - VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: 3); - } - - private static void VerifyBinLogEvaluationDataCount(string binaryLogPath, int expectedCount) - { - var records = BinaryLog.ReadRecords(binaryLogPath).ToList(); - records.Count(static r => r.Args is ProjectEvaluationStartedEventArgs).Should().Be(expectedCount); - records.Count(static r => r.Args is ProjectEvaluationFinishedEventArgs).Should().Be(expectedCount); - } - - /// - /// Binary logs from our in-memory projects should have evaluation data. - /// - [Fact] - public void BinaryLog_EvaluationData_MultiFile() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), - $""" - #!/usr/bin/env dotnet - #:include *.cs - {s_programDependingOnUtil} - """); - - var utilPath = Path.Join(testInstance.Path, "Util.cs"); - File.WriteAllText(utilPath, s_util); - - new DotnetCommand(Log, "run", "--no-cache", "Program.cs", "-bl:first.binlog") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, String from Util"); - - string binaryLogPath = Path.Join(testInstance.Path, "first.binlog"); - new FileInfo(binaryLogPath).Should().Exist(); - - // There should be exactly four - two for restore and one for build as usual, plus one for initial directive evaluation. - var expectedCount = 4; - VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: expectedCount); - - File.WriteAllText(utilPath, s_util.Replace("String from Util", "v2")); - - new DotnetCommand(Log, "run", "Program.cs", "-bl:second.binlog") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, v2"); - - binaryLogPath = Path.Join(testInstance.Path, "second.binlog"); - new FileInfo(binaryLogPath).Should().Exist(); - - // After rebuild, there should be the same number of evaluations. - VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: expectedCount); - } - - /// - /// If we skip build due to up-to-date check, no binlog should be created. - /// - [Fact] - public void BinaryLog_EvaluationData_UpToDate() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, s_program); - - var expectedOutput = "Hello from Program"; - - new DotnetCommand(Log, "run", "--no-cache", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - - string binaryLogPath = Path.Join(testInstance.Path, "msbuild.binlog"); - new FileInfo(binaryLogPath).Should().NotExist(); - - new DotnetCommand(Log, "run", "Program.cs", "-bl") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut($""" - {CliCommandStrings.NoBinaryLogBecauseUpToDate} - {expectedOutput} - """); - - new FileInfo(binaryLogPath).Should().NotExist(); - } - - [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); - } - - [Fact] - public void MissingShebangWarning() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - // Single-file program without shebang should NOT produce CA2266 - // (the warning only fires when there are multiple files via #:include). - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - Console.WriteLine("hello"); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.NotHaveStdOutContaining("CA2266") - .And.HaveStdOutContaining("hello"); - - // Included file without shebang should not produce CA2266. - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ - class Util { public static string Greet() => "hello"; } - """); - - // Entry point with shebang and #:include — no warning. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #!/usr/bin/env dotnet - #:include Util.cs - Console.WriteLine(Util.Greet()); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.NotHaveStdOutContaining("CA2266") - .And.HaveStdOutContaining("hello"); - - // Entry point without shebang and #:include — CA2266 warning expected. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #:include Util.cs - Console.WriteLine(Util.Greet()); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("warning CA2266") - .And.HaveStdOutContaining("hello"); - - // CA2266 can be suppressed via NoWarn. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #:property NoWarn=CA2266 - #:include Util.cs - Console.WriteLine(Util.Greet()); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.NotHaveStdOutContaining("CA2266") - .And.HaveStdOutContaining("hello"); - } - - [Fact] - public void MissingShebangWarning_CompileItemFromDirectoryBuildProps() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - // Directory.Build.props adds a Compile item, effectively making - // the compilation multi-file (same as #:include). - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ - class Util { public static string Greet() => "hello"; } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ - - - - - - """); - - // Entry point without shebang — CA2266 warning expected - // because Directory.Build.props added another Compile item. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - Console.WriteLine(Util.Greet()); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("warning CA2266") - .And.HaveStdOutContaining("hello"); - - // Adding shebang resolves the warning. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #!/usr/bin/env dotnet - Console.WriteLine(Util.Greet()); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("hello"); - } - - /// - /// File-based projects using the default SDK do not include embedded resources by default. - /// - [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); - - // By default, with the default SDK, embedded resources are not included. - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - Resource not found - """); - - // This behavior can be overridden to enable embedded resources. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" - #:property EnableDefaultEmbeddedResourceItems=true - {s_programReadingEmbeddedResource} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - [MyString, TestValue] - """); - - // When using a non-default SDK, embedded resources are included by default. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" - #:sdk Microsoft.NET.Sdk.Web - {s_programReadingEmbeddedResource} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - [MyString, TestValue] - """); - - // When using the default SDK explicitly, embedded resources are not included. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" - #:sdk Microsoft.NET.Sdk - {s_programReadingEmbeddedResource} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(""" - Resource not found - """); - } - - /// - /// Scripts in repo root should not include .resx files. - /// Part of . - /// - [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"), $""" - #:property EnableDefaultEmbeddedResourceItems=true - {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 Restore_NonExistentPackage() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programFile, """ - #:package Microsoft.ThisPackageDoesNotExist@1.0.0 - Console.WriteLine(); - """); - - new DotnetCommand(Log, "restore", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdOutContaining("Program.cs.csproj : error NU1101"); - } - - [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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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"), """ - - - true - - - """); - var programFile = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programFile, "Console.WriteLine();"); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.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 = VirtualProjectBuilder.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, FileBasedProgramsResources.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 Build_Library() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "lib.cs"); - File.WriteAllText(programFile, """ - #:property OutputType=Library - class C; - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "lib.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "lib.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - VirtualProjectBuilder.GetVirtualProjectPath(programFile), - ToolsetInfo.CurrentTargetFrameworkVersion, - "Library")); - } - - [Fact] - public void Build_Library_MultiTarget() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "lib.cs"); - File.WriteAllText(programFile, $""" - #:property OutputType=Library - #:property PublishAot=false - #:property LangVersion=preview - #:property TargetFramework= - #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} - class C; - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "lib.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "lib.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute("--no-interactive") - .Should().Fail() - .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); - - new DotnetCommand(Log, "run", "lib.cs", "--framework", ToolsetInfo.CurrentTargetFramework) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - VirtualProjectBuilder.GetVirtualProjectPath(programFile), - ToolsetInfo.CurrentTargetFrameworkVersion, - "Library")); - } - - [Fact] - public void Build_Module() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "module.cs"); - File.WriteAllText(programFile, """ - #:property OutputType=Module - #:property ProduceReferenceAssembly=false - class C; - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "module.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "module.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - VirtualProjectBuilder.GetVirtualProjectPath(programFile), - ToolsetInfo.CurrentTargetFrameworkVersion, - "Module")); - } - - [Fact] - public void Build_WinExe() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "winexe.cs"); - File.WriteAllText(programFile, """ - #:property OutputType=WinExe - Console.WriteLine("Hello WinExe"); - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "winexe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "winexe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello WinExe"); - } - - [Fact] - public void Build_Exe() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "exe.cs"); - File.WriteAllText(programFile, """ - #:property OutputType=Exe - Console.WriteLine("Hello Exe"); - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "exe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "exe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello Exe"); - } - - [Fact] - public void Build_Exe_MultiTarget() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "exe.cs"); - File.WriteAllText(programFile, $""" - #:property OutputType=Exe - #:property PublishAot=false - #:property LangVersion=preview - #:property TargetFramework= - #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} - Console.WriteLine("Hello Exe"); - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "exe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "exe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); - - new DotnetCommand(Log, "run", "exe.cs", "--framework", ToolsetInfo.CurrentTargetFramework) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello Exe"); - } - - [Fact] - public void Build_AppContainerExe() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "appcontainerexe.cs"); - File.WriteAllText(programFile, """ - #:property OutputType=AppContainerExe - Console.WriteLine("Hello AppContainerExe"); - """); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "appcontainerexe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DotnetCommand(Log, "run", "appcontainerexe.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - VirtualProjectBuilder.GetVirtualProjectPath(programFile), - ToolsetInfo.CurrentTargetFrameworkVersion, - "AppContainerExe")); - } - - [Fact] - public void Publish() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFile = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programFile, s_program); - - var artifactsDir = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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, """ - 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 = VirtualProjectBuilder.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 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 = VirtualProjectBuilder.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(VirtualProjectBuilder.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 = VirtualProjectBuilder.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' - """); - } - - /// - /// Properties/launchSettings.json takes precedence over Program.run.json. - /// - [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 usage 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' - """); - } - - /// - /// Each file-based app in a folder can have separate launch profile. - /// - [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"), """ - - - true - - - - - - """); - 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")] - [InlineData("$(MSBuildProjectDirectory)/../$(LibProjectName)")] - [InlineData(@"$(MSBuildProjectDirectory)/../Lib\$(LibProjectName).csproj")] - 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"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - 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} - #:property LibProjectName=Lib - Console.WriteLine(Lib.LibClass.GetMessage()); - """); - - var expectedOutput = "Hello from Lib"; - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(appDir) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - - // Running from a different working directory shouldn't affect handling of the relative project paths. - new DotnetCommand(Log, "run", "App/Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - } - - [Theory] - [InlineData(null)] - [InlineData("app")] - public void ProjectReference_Errors(string? subdir) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var relativeFilePath = Path.Join(subdir, "Program.cs"); - var filePath = Path.Join(testInstance.Path, relativeFilePath); - Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); - File.WriteAllText(filePath, """ - #:project wrong.csproj - """); - - // Project file does not exist. - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, - string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "wrong.csproj")))); - - File.WriteAllText(filePath, """ - #:project dir/ - """); - - // Project directory does not exist. - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, - string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "dir/")))); - - Directory.CreateDirectory(Path.Join(testInstance.Path, subdir, "dir")); - - // Directory exists but has no project file. - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, - string.Format(FileBasedProgramsResources.CouldNotFindAnyProjectInDirectory, Path.Join(testInstance.Path, subdir, "dir/")))); - - File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj1.csproj"), ""); - File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj2.csproj"), ""); - - // Directory exists but has multiple project files. - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, - string.Format(FileBasedProgramsResources.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, subdir, "dir/")))); - - // Malformed MSBuild variable syntax. - File.WriteAllText(filePath, """ - #:project $(Test - """); - - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, - string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "$(Test")))); - } - - [Theory] - [InlineData(null)] - [InlineData("app")] - public void ProjectReference_Duplicate(string? subdir) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var relativeFilePath = Path.Join(subdir, "Program.cs"); - var filePath = Path.Join(testInstance.Path, relativeFilePath); - Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); - Directory.CreateDirectory(Path.Join(testInstance.Path, subdir, "dir")); - File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj1.csproj"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - File.WriteAllText(filePath, """ - #:project dir/ - #:project dir/ - Console.WriteLine("Hello"); - """); - - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 2, FileBasedProgramsResources.DuplicateDirective, "#:project dir/")); - - File.WriteAllText(filePath, """ - #:project dir/ - #:project dir/proj1.csproj - Console.WriteLine("Hello"); - """); - - // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate project reference - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello"); - - File.WriteAllText(filePath, """ - #:project dir/ - #:project $(MSBuildProjectDirectory)/dir/ - Console.WriteLine("Hello"); - """); - - // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate project reference - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello"); - } - - [Fact] - public void RefDirective() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet(string name) => $"Hello, {name}!"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib.cs - Console.WriteLine(MyLib.Greeter.Greet("World")); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, World!"); - } - - [Fact] - public void RefDirective_Subdirectory() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - var libDir = Path.Join(testInstance.Path, "lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "mylib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet(string name) => $"Hello, {name}!"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib/mylib.cs - Console.WriteLine(MyLib.Greeter.Greet("World")); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, World!"); - } - - /// - /// Analogous to but for #:ref. - /// - [Theory] - [InlineData(null)] - [InlineData("app")] - public void RefDirective_Errors(string? subdir) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - var relativeFilePath = Path.Join(subdir, "Program.cs"); - var filePath = Path.Join(testInstance.Path, relativeFilePath); - Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); - - // Missing name. - File.WriteAllText(filePath, """ - #:ref - """); - - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.MissingDirectiveName, "ref")); - - // File does not exist. - File.WriteAllText(filePath, """ - #:ref nonexistent.cs - """); - - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidRefDirective, - string.Format(FileBasedProgramsResources.CouldNotFindRefFile, Path.Join(testInstance.Path, subdir, "nonexistent.cs")))); - } - - /// - /// Verifies that #:ref produces a metadata (assembly) reference, - /// meaning internal members are not accessible unless InternalsVisibleTo is used. - /// - [Fact] - public void RefDirective_InternalsNotAccessible() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class PublicClass - { - public static string PublicMethod() => "public"; - internal static string InternalMethod() => "internal"; - } - internal static class InternalClass - { - public static string Method() => "internal class"; - } - """); - - // Accessing internal member should fail. - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib.cs - Console.WriteLine(MyLib.PublicClass.InternalMethod()); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdOutContaining("error CS"); - - // Accessing public member should succeed. - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib.cs - Console.WriteLine(MyLib.PublicClass.PublicMethod()); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("public"); - } - - /// - /// Verifies transitive #:ref references work: app.cs → lib1.cs → lib2.cs. - /// - [Fact] - public void RefDirective_Transitive() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - File.WriteAllText(Path.Join(testInstance.Path, "lib2.cs"), """ - #:property OutputType=Library - namespace Lib2; - public static class Base - { - public static string Value() => "from lib2"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "lib1.cs"), """ - #:property OutputType=Library - #:ref lib2.cs - namespace Lib1; - public static class Middle - { - public static string Value() => $"from lib1 and {Lib2.Base.Value()}"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib1.cs - Console.WriteLine(Lib1.Middle.Value()); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("from lib1 and from lib2"); - } - - /// - /// #:ref with various path formats (forward slashes, backslashes, MSBuild properties, parent dirs). - /// Analogous to . - /// - [Theory] - [InlineData("../Lib/lib.cs")] - [InlineData(@"..\Lib\lib.cs")] - [InlineData("$(MSBuildProjectDirectory)/../$(LibDirName)/lib.cs")] - [InlineData(@"$(MSBuildProjectDirectory)\..\Lib\lib.cs")] - public void RefDirective_PathFormats(string arg) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - var libDir = Path.Join(testInstance.Path, "Lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet(string name) => $"Hello, {name}!"; - } - """); - - var appDir = Path.Join(testInstance.Path, "App"); - Directory.CreateDirectory(appDir); - - File.WriteAllText(Path.Join(appDir, "app.cs"), $""" - #:ref {arg} - #:property LibDirName=Lib - Console.WriteLine(MyLib.Greeter.Greet("World")); - """); - - var expectedOutput = "Hello, World!"; - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(appDir) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - - // Running from a different working directory shouldn't affect handling of the relative paths. - new DotnetCommand(Log, "run", "App/app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - } - - /// - /// #:ref duplicate detection. - /// Analogous to . - /// - [Theory] - [InlineData(null)] - [InlineData("app")] - public void RefDirective_Duplicate(string? subdir) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - var relativeFilePath = Path.Join(subdir, "Program.cs"); - var filePath = Path.Join(testInstance.Path, relativeFilePath); - Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); - - File.WriteAllText(Path.Join(testInstance.Path, subdir, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello!"; - } - """); - - File.WriteAllText(filePath, """ - #:ref lib.cs - #:ref lib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 2, FileBasedProgramsResources.DuplicateDirective, "#:ref lib.cs")); - - File.WriteAllText(filePath, """ - #:ref lib.cs - #:ref ./lib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate ref - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello!"); - - File.WriteAllText(filePath, """ - #:ref lib.cs - #:ref $(MSBuildProjectDirectory)/lib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate ref - new DotnetCommand(Log, "run", relativeFilePath) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello!"); - } - - /// - /// #:ref is an experimental feature that must be opted into. - /// Analogous to . - /// - [Fact] - public void RefDirective_FeatureFlag() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var libPath = Path.Join(testInstance.Path, "lib.cs"); - File.WriteAllText(libPath, """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello!"; - } - """); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, """ - #:ref lib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr($""" - {DirectiveError(programPath, 1, Resources.ExperimentalFeatureDisabled, CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective)} - - {CliCommandStrings.RunCommandException} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello!"); - } - - /// - /// Combining #:ref and #:include in the same file-based app. - /// - [Fact] - public void RefDirective_WithInclude() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" - - - <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true - - - """); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #!/usr/bin/env dotnet - #:property OutputType=Library - #:include LibHelper.cs - #:include LibFormatter.cs - namespace MyLib; - public static class Greeter - { - public static string Greet(string name) => LibFormatter.Format(LibHelper.Prefix, name); - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "LibHelper.cs"), """ - namespace MyLib; - public static class LibHelper - { - public static string Prefix => "Hello"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "LibFormatter.cs"), """ - namespace MyLib; - public static class LibFormatter - { - public static string Format(string prefix, string name) => $"{prefix}, {name}!"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ - static class Util - { - public static string GetName() => "World"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #!/usr/bin/env dotnet - #:ref lib.cs - #:include Util.cs - Console.WriteLine(MyLib.Greeter.Greet(Util.GetName())); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, World!"); - } - - /// - /// A #:ref library can target a different framework (e.g., netstandard2.0) - /// than the referencing app (net10.0). - /// - [Fact] - public void RefDirective_DifferentTargetFramework() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - #:property TargetFramework=netstandard2.0 - #:property LangVersion=latest - #:property ImplicitUsings=disable - #:property PublishAot=false - namespace MyLib; - public static class Greeter - { - #if NETSTANDARD2_0 - public static string Greet() => "Hello from netstandard2.0!"; - #else - public static string Greet() => "Hello from other!"; - #endif - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib.cs - #if NET10_0_OR_GREATER - Console.WriteLine("App is net10.0+: " + MyLib.Greeter.Greet()); - #else - Console.WriteLine("App is older: " + MyLib.Greeter.Greet()); - #endif - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("App is net10.0+: Hello from netstandard2.0!"); - } - - /// - /// #:ref *.cs does not expand globs — it looks for a literal file named *.cs. - /// - [Fact] - public void RefDirective_Glob() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello!"; - } - """); - - var filePath = Path.Join(testInstance.Path, "app.cs"); - File.WriteAllText(filePath, """ - #:ref *.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidRefDirective, - string.Format(FileBasedProgramsResources.CouldNotFindRefFile, Path.Join(testInstance.Path, "*.cs")))); - } - - /// - /// Verifies that cyclic #:ref references (lib1 → lib2 → lib1) do not cause an infinite loop. - /// - [Fact] - public void RefDirective_Cycle() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - File.WriteAllText(Path.Join(testInstance.Path, "lib1.cs"), """ - #:property OutputType=Library - #:ref lib2.cs - namespace Lib1; - public static class C1 { public static string Get() => "lib1"; } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "lib2.cs"), """ - #:property OutputType=Library - #:ref lib1.cs - namespace Lib2; - public static class C2 { public static string Get() => "lib2"; } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib1.cs - Console.WriteLine(Lib1.C1.Get()); - """); - - // Should not hang. The cycle is broken by processedFiles deduplication. - // error NU1108: Cycle detected. - // error NU1108: lib1 -> lib2 -> lib1. - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdOutContaining("error NU1108"); - } - - /// - /// Two #:include'd files each have #:ref to the same library. - /// The deduplication via processedFiles should ensure the library is only processed once. - /// - [Fact] - public void RefDirective_DuplicateRefFromIncludedFiles() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" - - - <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true - - - """); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello!"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "helper1.cs"), """ - #:ref lib.cs - static class Helper1 - { - public static string Get() => MyLib.Greeter.Greet(); - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "helper2.cs"), """ - #:ref lib.cs - static class Helper2 - { - public static string Get() => MyLib.Greeter.Greet(); - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #!/usr/bin/env dotnet - #:include helper1.cs - #:include helper2.cs - Console.WriteLine(Helper1.Get() + " " + Helper2.Get()); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello! Hello!"); - } - - /// - /// Two #:include'd files in different directories each have #:ref to the same library - /// using different relative paths. Deduplication via processedFiles uses the resolved (absolute) path, - /// so the library is only processed once. - /// - [Fact] - public void RefDirective_DuplicateRefFromIncludedFiles_Subdirectories() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" - - - <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true - - - """); - - // lib.cs is in the root directory. - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello!"; - } - """); - - // helper1.cs is in sub1/, refers to lib.cs via ../lib.cs. - var sub1 = Path.Join(testInstance.Path, "sub1"); - Directory.CreateDirectory(sub1); - File.WriteAllText(Path.Join(sub1, "helper1.cs"), """ - #:ref ../lib.cs - static class Helper1 - { - public static string Get() => MyLib.Greeter.Greet(); - } - """); - - // helper2.cs is in sub2/nested/, refers to lib.cs via ../../lib.cs (different relative path, same resolved path). - var sub2 = Path.Join(testInstance.Path, "sub2", "nested"); - Directory.CreateDirectory(sub2); - File.WriteAllText(Path.Join(sub2, "helper2.cs"), """ - #:ref ../../lib.cs - static class Helper2 - { - public static string Get() => MyLib.Greeter.Greet(); - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #!/usr/bin/env dotnet - #:include sub1/helper1.cs - #:include sub2/nested/helper2.cs - Console.WriteLine(Helper1.Get() + " " + Helper2.Get()); - """); - - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello! Hello!"); - } - - /// - /// Both #:include and #:ref pointing at the same file. - /// The file ends up both compiled into the current assembly and referenced as a separate assembly. - /// This is expected to produce a compilation error (duplicate type definitions). - /// - [Fact] - public void RefDirective_IncludeAndRefSameFile() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" - - - <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true - - - """); - - File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello!"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ - #:ref lib.cs - #:include lib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - // The #:include brings in lib.cs's #:property OutputType=Library, making the app a library. - // error CS8805: Program using top-level statements must be an executable. - new DotnetCommand(Log, "run", "app.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdOutContaining("error CS8805"); - } - - [Theory, CombinatorialData] - public void IncludeDirective( - [CombinatorialValues("Util.cs", "**/*.cs", "**/*.$(MyProp1)")] string includePattern, - [CombinatorialValues("", "#:exclude Program.$(MyProp1)")] string additionalDirectives) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" - #!/usr/bin/env dotnet - #:include {includePattern} - {additionalDirectives} - #:property MyProp1=cs - {s_programDependingOnUtil} - """); - - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, String from Util"); - } - - [Fact] - public void IncludeDirective_WorkingDirectory() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var srcDir = Path.Join(testInstance.Path, "src"); - Directory.CreateDirectory(srcDir); - - var a = """ - Console.WriteLine(B.M()); - """; - - File.WriteAllText(Path.Join(srcDir, "A.cs"), $""" - #!/usr/bin/env dotnet - #:include B.cs - {a} - """); - - var b = """ - static class B { public static string M() => "Hello from B"; } - """; - - File.WriteAllText(Path.Join(srcDir, "B.cs"), b); - - var expectedOutput = "Hello from B"; - - new DotnetCommand(Log, "run", "src/A.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - - // Convert to a project. - new DotnetCommand(Log, "project", "convert", "src/A.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - new DirectoryInfo(testInstance.Path) - .Should().HaveSubtree(""" - src/ - src/A.cs - src/A/ - src/A/A.cs - src/A/A.csproj - src/A/B.cs - src/B.cs - """) - .And.HaveFileContent("src/A/A.cs", a) - .And.HaveFileContent("src/A/B.cs", b) - .And.HaveFileContentPattern("src/A/A.csproj", """ - - - - Exe - net10.0 - enable - enable - true - true - A-* - - - - - """); - - // Run the converted project. - new DotnetCommand(Log, "run") - .WithWorkingDirectory(Path.Join(testInstance.Path, "src/A")) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - } - - [Fact] - public void IncludeDirective_Transitive() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - Directory.CreateDirectory(Path.Join(testInstance.Path, "dir1/dir2")); - Directory.CreateDirectory(Path.Join(testInstance.Path, "dir3")); - - var a = """ - B.M(); - """; - - File.WriteAllText(Path.Join(testInstance.Path, "dir1/A.cs"), $""" - #!/usr/bin/env dotnet - #:include dir2/B.cs - {a} - """); - - var b = """ - static class B { public static void M() { C.M(); } } - """; - - File.WriteAllText(Path.Join(testInstance.Path, "dir1/dir2/B.cs"), $""" - #:include ../../dir3/$(P1).cs - #:property P1=C - {b} - """); - - var c = """ - static class C { public static void M() { D.M(); } } - """; - - File.WriteAllText(Path.Join(testInstance.Path, "dir3/C.cs"), $""" - #:include ../$(P1).cs - {c} - """); - - var d = """ - static class D - { - public static void M() - { - var asm = System.Reflection.Assembly.GetExecutingAssembly(); - using var stream = asm.GetManifestResourceStream($"{asm.GetName().Name}.Resources.resources")!; - using var reader = new System.Resources.ResourceReader(stream); - Console.WriteLine(reader.Cast().Single()); - } - } - """; - - File.WriteAllText(Path.Join(testInstance.Path, "C.cs"), $""" - #:include Resources.resx - {d} - """); - - File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); - - var expectedOutput = "[MyString, TestValue]"; - - new DotnetCommand(Log, "run", "A.cs") - .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1")) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - - // Convert to a project. - new DotnetCommand(Log, "project", "convert", "A.cs") - .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1")) - .Execute() - .Should().Pass(); - - new DirectoryInfo(Path.Join(testInstance.Path, "dir1/A")) - .Should().HaveSubtree(""" - A.cs - A.csproj - C.cs - C_2.cs - Resources.resx - dir2/ - dir2/B.cs - """) - .And.HaveFileContent("A.cs", a) - .And.HaveFileContent("dir2/B.cs", b) - .And.HaveFileContent("C.cs", c) - .And.HaveFileContent("C_2.cs", d) - .And.HaveFileContent("Resources.resx", s_resx) - .And.HaveFileContentPattern("A.csproj", """ - - - - Exe - net10.0 - enable - enable - true - true - A-* - C - - - - - """); - - // Run the converted project. - new DotnetCommand(Log, "run") - .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1/A")) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedOutput); - } - - [Fact] - public void IncludeDirective_FileNotFound() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "A.cs"); - - File.WriteAllText(programPath, """ - #:include B.cs - Console.WriteLine("Hello"); - """); - - new DotnetCommand(Log, "run", "A.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErrContaining(DirectiveError(programPath, 1, Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "B.cs"))); - } - - /// - /// Combination of optimization and #:include directive. - /// - [Theory] - [InlineData("*")] - [InlineData("$(_Star)")] - [InlineData("Util?")] - public void IncludeDirective_UpToDate_Glob(string glob) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, $""" - #!/usr/bin/env dotnet - #:include {glob}.cs - #:property _Star=* - {s_programDependingOnUtil} - """); - - var utilPath = Path.Join(testInstance.Path, "Util1.cs"); - var utilCode = s_util; - File.WriteAllText(utilPath, utilCode); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - var expectedOutput = "Hello, String from Util"; - - Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); - - Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); - - utilCode = utilCode.Replace("String from Util", "v2"); - File.WriteAllText(utilPath, utilCode); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v2"); - - utilCode = utilCode.Replace("v2", "v3"); - File.WriteAllText(utilPath, utilCode); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v3"); - - var util2Path = Path.Join(testInstance.Path, "Util2.cs"); - File.WriteAllText(util2Path, """ - using System.Runtime.CompilerServices; - - file class C - { - [ModuleInitializer] - internal static void Initialize() - { - Console.WriteLine("Hello from Util2"); - } - } - """); - - Build(testInstance, BuildLevel.All, expectedOutput: """ - Hello from Util2 - Hello, v3 - """); - } - - /// - /// Combination of optimization and #:include directive. - /// - [Fact] - public void IncludeDirective_UpToDate_NoGlob() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, $""" - #!/usr/bin/env dotnet - #:include Util.cs - {s_programDependingOnUtil} - """); - - var utilPath = Path.Join(testInstance.Path, "Util.cs"); - var utilCode = s_util; - File.WriteAllText(utilPath, utilCode); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - var expectedOutput = "Hello, String from Util"; - - Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); - - Build(testInstance, BuildLevel.None, expectedOutput: expectedOutput); - - utilCode = utilCode.Replace("String from Util", "v2"); - File.WriteAllText(utilPath, utilCode); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v2"); - - utilCode = utilCode.Replace("v2", "v3"); - File.WriteAllText(utilPath, utilCode); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v3"); - - var util2Path = Path.Join(testInstance.Path, "Util2.cs"); - File.WriteAllText(util2Path, """ - using System.Runtime.CompilerServices; - - file class C - { - [ModuleInitializer] - internal static void Initialize() - { - Console.WriteLine("Hello from Util2"); - } - } - """); - - Build(testInstance, BuildLevel.None, expectedOutput: "Hello, v3"); - - Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: "Hello, v3"); - } - - /// - /// Combination of test and #:include directive. - /// - [Fact] - public void IncludeDirective_UpToDate_ProjectReference() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var libDir = Path.Join(testInstance.Path, "Lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - var libPath = Path.Join(libDir, "Lib.cs"); - var libCode = """ - namespace Lib; - public class LibClass - { - public static string GetMessage() => "Lib(v1)"; - } - """; - File.WriteAllText(libPath, libCode); - - var appDir = Path.Join(testInstance.Path, "App"); - Directory.CreateDirectory(appDir); - - var utilPath = Path.Join(appDir, "Util.cs"); - var utilCode = """ - #:project ../Lib - class UtilClass - { - public static string GetMessage() => "Util(v1) " + Lib.LibClass.GetMessage(); - } - """; - File.WriteAllText(utilPath, utilCode); - - var programPath = Path.Join(appDir, "Program.cs"); - var programCode = """ - #!/usr/bin/env dotnet - #:include Util.cs - Console.WriteLine("Program(v1) " + UtilClass.GetMessage()); - """; - File.WriteAllText(programPath, programCode); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - var expectedOutput = "Program(v1) Util(v1) Lib(v1)"; - - Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir); - - Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir); - - libCode = libCode.Replace("v1", "v2"); - File.WriteAllText(libPath, libCode); - - expectedOutput = "Program(v1) Util(v1) Lib(v2)"; - - Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir); - } - - [Fact] - public void IncludeDirective_CustomMapping() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, $""" - #!/usr/bin/env dotnet - #:property FileBasedProgramsItemMapping=.json=Content - #:include *.cs - {s_programDependingOnUtil} - """); - - var utilPath = Path.Join(testInstance.Path, "Util.cs"); - File.WriteAllText(utilPath, s_util); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr($""" - {DirectiveError(programPath, 3, FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", ".json")} - - {CliCommandStrings.RunCommandException} - """); - - File.WriteAllText(programPath, $""" - #!/usr/bin/env dotnet - #:property FileBasedProgramsItemMapping=.cs=Content - #:include *.cs - {s_programDependingOnUtil} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - // error CS0103: The name 'Util' does not exist in the current context - .And.HaveStdOutContaining("error CS0103"); - - File.WriteAllText(programPath, $""" - #!/usr/bin/env dotnet - #:property FileBasedProgramsItemMapping=.cs=Compile - #:include *.cs - {s_programDependingOnUtil} - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello, String from Util"); - } - - [Fact] - public void IncludeDirective_CustomMapping_ParseErrors() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, """ - #:property FileBasedProgramsItemMapping=x - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - // error CS5001: Program does not contain a static 'Main' method suitable for an entry point - .And.HaveStdOutContaining("error CS5001"); - - File.WriteAllText(programPath, """ - #:property FileBasedProgramsItemMapping=x - #:include *.* - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr($""" - {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingEntry, "x")} - - {CliCommandStrings.RunCommandException} - """); - - File.WriteAllText(programPath, """ - #:property FileBasedProgramsItemMapping=.=X;y - #:include *.* - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr($""" - {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingExtension, ".", ".=X")} - - {CliCommandStrings.RunCommandException} - """); - - File.WriteAllText(programPath, """ - #:property FileBasedProgramsItemMapping=.cs=;y - #:include *.* - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr($""" - {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingItemType, "", ".cs=")} - - {CliCommandStrings.RunCommandException} - """); - - File.WriteAllText(programPath, """ - #:property FileBasedProgramsItemMapping=.x=X;y - #:include *.* - """); - - new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdErr($""" - {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingEntry, "y")} - - {CliCommandStrings.RunCommandException} - """); - } - - /// - /// Demonstrates that consumers (e.g., IDE) can use the API to create an approximate virtual project without needing to know the full mapping. - /// - [Fact] - public void IncludeDirective_CustomMapping_Api() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - - var code = """ - #:include B.cs - #:include C.proto - Console.WriteLine(); - """; - - var builder = new VirtualProjectBuilder( - entryPointFileFullPath: programPath, - targetFramework: VirtualProjectBuildingCommand.TargetFramework, - sourceText: SourceText.From(code, Encoding.UTF8)); - - var directives = FileLevelDirectiveHelpers.FindDirectives( - builder.EntryPointSourceFile, - reportAllErrors: true, - VirtualProjectBuildingCommand.ThrowingReporter); - - ImmutableArray<(string Extension, string ItemType)> mapping = [(".cs", "Compile")]; - - var evaluatedBuilder = ImmutableArray.CreateBuilder(directives.Length); - - foreach (var directive in directives) - { - if (directive is CSharpDirective.IncludeOrExclude includeOrExcludeDirective) - { - var evaluated = includeOrExcludeDirective.WithDeterminedItemType(ErrorReporters.IgnoringReporter, mapping); - evaluatedBuilder.Add(evaluated); - } - else - { - evaluatedBuilder.Add(directive); - } - } - - var evaluatedDirectives = evaluatedBuilder.DrainToImmutable(); - - var projectWriter = new System.IO.StringWriter(); - VirtualProjectBuilder.WriteProjectFile( - projectWriter, - evaluatedDirectives, - VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFrameworkVersion), - isVirtualProject: true, - entryPointFilePath: programPath, - artifactsPath: builder.ArtifactsPath); - - var actualProject = projectWriter.ToString(); - - Log.WriteLine(actualProject); - - actualProject.Should().Contain(""""""); - - actualProject.Should().NotContain(".proto"); - } - - [Fact] - public void IncludeDirective_DefaultMapping_InSync() - { - var parsed = CSharpDirective.IncludeOrExclude.ParseMapping(CSharpDirective.IncludeOrExclude.DefaultMappingString, - sourceFile: default, - VirtualProjectBuildingCommand.ThrowingReporter); - parsed.Should().BeEquivalentTo(CSharpDirective.IncludeOrExclude.DefaultMapping); - } - - [Theory] // https://github.com/dotnet/aspnetcore/issues/63440 - [InlineData(true, null)] - [InlineData(false, null)] - [InlineData(true, "test-id")] - [InlineData(false, "test-id")] - public void UserSecrets(bool useIdArg, string? userSecretsId) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - string code = $""" - #:package Microsoft.Extensions.Configuration.UserSecrets@{CSharpCompilerCommand.RuntimeVersion} - {(userSecretsId is null ? "" : $"#:property UserSecretsId={userSecretsId}")} - - using Microsoft.Extensions.Configuration; - - IConfigurationRoot config = new ConfigurationBuilder() - .AddUserSecrets() - .Build(); - - Console.WriteLine("v1"); - Console.WriteLine(config.GetDebugView()); - """; - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, code); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - if (useIdArg) - { - if (userSecretsId == null) - { - var result = new DotnetCommand(Log, "build", "-getProperty:UserSecretsId", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute(); - result.Should().Pass(); - userSecretsId = result.StdOut!.Trim(); - } - - new DotnetCommand(Log, "user-secrets", "set", "MySecret", "MyValue", "--id", userSecretsId) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - } - else - { - new DotnetCommand(Log, "user-secrets", "set", "MySecret", "MyValue", "--file", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - } - - Build(testInstance, BuildLevel.All, expectedOutput: """ - v1 - MySecret=MyValue (JsonConfigurationProvider for 'secrets.json' (Optional)) - """); - - code = code.Replace("v1", "v2"); - File.WriteAllText(programPath, code); - - Build(testInstance, BuildLevel.Csc, expectedOutput: """ - v2 - MySecret=MyValue (JsonConfigurationProvider for 'secrets.json' (Optional)) - """); - } - - /// - /// Verifies that msbuild-based runs use CSC args equivalent to csc-only runs. - /// Can regenerate CSC arguments template in . - /// - [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 = VirtualProjectBuilder.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(SdkTestContext.Current.ToolsetUnderTest.SdkFolderUnderTest); - string dotNetRootPath = NormalizePath(SdkTestContext.Current.ToolsetUnderTest.DotNetRoot); - string nuGetCachePath = NormalizePath(SdkTestContext.Current.NuGetCachePath!); - string artifactsDirNormalized = NormalizePath(artifactsDir); - string objPath = $"{artifactsDirNormalized}/obj/debug"; - string entryPointPathNormalized = NormalizePath(entryPointPath); - var msbuildArgsToVerify = new List(); - var nuGetPackageFilePaths = new List(); - bool referenceSpreadInserted = false; - bool analyzerSpreadInserted = false; - const string NetCoreAppRefPackPath = "packs/Microsoft.NETCore.App.Ref/"; - 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. - - using System.Text.Json; - - namespace Microsoft.DotNet.Cli.Commands.Run; - - // Generated by test `{{nameof(RunFileTests)}}.{{nameof(CscArguments)}}`. - partial class CSharpCompilerCommand - { - private IEnumerable GetCscArguments( - 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 path. - if (rewritten.Contains(entryPointPathNormalized, StringComparison.OrdinalIgnoreCase)) - { - rewritten = rewritten.Replace(entryPointPathNormalized, "{" + nameof(CSharpCompilerCommand.EntryPointFileFullPath) + "}", StringComparison.OrdinalIgnoreCase); - needsInterpolation = true; - } - - // Use variable file name. - if (rewritten.Contains(fileName, StringComparison.OrdinalIgnoreCase)) - { - rewritten = rewritten.Replace(fileName, "{FileName}", 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; - } - - // Use GetFrameworkReferenceArguments() for framework references instead of hard-coding them. - if (arg.StartsWith("/reference:", StringComparison.Ordinal)) - { - if (!referenceSpreadInserted) - { - code.AppendLine(""" - .. GetFrameworkReferenceArguments(), - """); - referenceSpreadInserted = true; - } - - msbuildArgsToVerify.Add(msbuildArgToVerify); - continue; - } - - // Use GetFrameworkAnalyzerArguments() for targeting-pack analyzers instead of hard-coding them. - if (arg.StartsWith("/analyzer:", StringComparison.Ordinal) - && rewritten.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase)) - { - if (!analyzerSpreadInserted) - { - code.AppendLine(""" - .. GetFrameworkAnalyzerArguments(), - """); - analyzerSpreadInserted = true; - } - - msbuildArgsToVerify.Add(msbuildArgToVerify); - 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(""" - ]; - } - - /// - /// 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). - /// - public static IEnumerable GetPathsOfCscInputsFromNuGetCache() - { - return - [ - """); - foreach (var nuGetPackageFilePath in nuGetPackageFilePaths) - { - code.AppendLine($""" - $"{nuGetPackageFilePath}", - """); - } - code.AppendLine(""" - ]; - } - """); - - // Generate file content templates. - var baseDirectory = TestPathUtility.ResolveTempPrefixLink(Path.GetDirectoryName(entryPointPath)!); - var replacements = new List<(string, string)> - { - (TestPathUtility.ResolveTempPrefixLink(entryPointPath), nameof(CSharpCompilerCommand.EntryPointFileFullPath)), - (baseDirectory + Path.DirectorySeparatorChar, nameof(CSharpCompilerCommand.BaseDirectoryWithTrailingSeparator)), - (baseDirectory, nameof(CSharpCompilerCommand.BaseDirectory)), - (programName, nameof(CSharpCompilerCommand.FileNameWithoutExtension)), - (CSharpCompilerCommand.TargetFrameworkVersion, nameof(CSharpCompilerCommand.TargetFrameworkVersion)), - (CSharpCompilerCommand.TargetFramework, nameof(CSharpCompilerCommand.TargetFramework)), - (CSharpCompilerCommand.DefaultRuntimeVersion, nameof(CSharpCompilerCommand.DefaultRuntimeVersion)), - }; - var emittedFiles = Directory.EnumerateFiles(artifactsDir, "*", SearchOption.AllDirectories).Order(); - foreach (var emittedFile in emittedFiles) - { - var emittedFileName = Path.GetFileName(emittedFile); - var generatedMethodName = GetGeneratedMethodName(emittedFileName); - if (generatedMethodName is null) - { - Log.WriteLine($"Skipping unrecognized file '{emittedFile}'."); - continue; - } - - var emittedFileContent = File.ReadAllText(emittedFile); - - string interpolatedString = emittedFileContent; - string interpolationPrefix; - - if (emittedFileName.EndsWith(".json", StringComparison.Ordinal)) - { - interpolationPrefix = "$$"; - foreach (var (key, value) in replacements) - { - interpolatedString = interpolatedString.Replace(JsonSerializer.Serialize(key), "{{JsonSerializer.Serialize(" + value + ")}}"); - } - } - else - { - interpolationPrefix = "$"; - foreach (var (key, value) in replacements) - { - interpolatedString = interpolatedString.Replace(key, "{" + value + "}"); - } - } - - if (interpolatedString == emittedFileContent) - { - interpolationPrefix = ""; - } - - code.AppendLine($$"""" - - private string Get{{generatedMethodName}}Content() - { - return {{interpolationPrefix}}""" - {{interpolatedString}} - """; - } - """"); - } - - code.AppendLine(""" - } - """); - - // Save the code. - var codeFolder = new DirectoryInfo(Path.Join( - SdkTestContext.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))) - .ToList(); - Log.WriteLine("CSC-only args:"); - Log.WriteLine(string.Join(Environment.NewLine, normalizedCscOnlyArgs)); - Log.WriteLine("MSBuild args:"); - Log.WriteLine(string.Join(Environment.NewLine, msbuildArgsToVerify)); - - // References and targeting-pack analyzers may be in a different order (FrameworkList.xml vs. MSBuild), - // so compare them as sets. All other args must be in the same order. - var cscOnlyRefArgs = normalizedCscOnlyArgs.Where(static a => a.StartsWith("/reference:", StringComparison.Ordinal)).ToList(); - var cscOnlyAnalyzerArgs = normalizedCscOnlyArgs.Where(a => a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase)).ToList(); - var cscOnlyOtherArgs = normalizedCscOnlyArgs.Where(a => !a.StartsWith("/reference:", StringComparison.Ordinal) && !(a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase))).ToList(); - var msbuildRefArgs = msbuildArgsToVerify.Where(static a => a.StartsWith("/reference:", StringComparison.Ordinal)).ToList(); - var msbuildAnalyzerArgs = msbuildArgsToVerify.Where(a => a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase)).ToList(); - var msbuildOtherArgs = msbuildArgsToVerify.Where(a => !a.StartsWith("/reference:", StringComparison.Ordinal) && !(a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase))).ToList(); - cscOnlyRefArgs.Should().NotBeEmpty( - "framework references should be resolved from FrameworkList.xml"); - cscOnlyRefArgs.Should().BeEquivalentTo(msbuildRefArgs, - "the generated file might be outdated, run this test locally to regenerate it"); - cscOnlyAnalyzerArgs.Should().NotBeEmpty( - "framework analyzers should be resolved from FrameworkList.xml"); - cscOnlyAnalyzerArgs.Should().BeEquivalentTo(msbuildAnalyzerArgs, - "the generated file might be outdated, run this test locally to regenerate it"); - cscOnlyOtherArgs.Should().Equal(msbuildOtherArgs, - "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); - } - - static string? GetGeneratedMethodName(string assetFileName) - { - return assetFileName switch - { - $".NETCoreApp,Version=v{ToolsetInfo.CurrentTargetFrameworkVersion}.AssemblyAttributes.cs" => "AssemblyAttributes", - $"{fileName}.GlobalUsings.g.cs" => "GlobalUsings", - $"{fileName}.AssemblyInfo.cs" => "AssemblyInfo", - $"{fileName}.GeneratedMSBuildEditorConfig.editorconfig" => "GeneratedMSBuildEditorConfig", - $"{programName}{FileNameSuffixes.RuntimeConfigJson}" => "RuntimeConfig", - _ => null, - }; - } - } - - /// - /// Verifies that csc-only runs emit auxiliary files equivalent to msbuild-based runs. - /// - [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 = VirtualProjectBuilder.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, run test '{nameof(CscArguments)}' locally to re-generate the template): {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 = VirtualProjectBuilder.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, """ - - - $(DefineConstants);CUSTOM_DEFINE - - - """); - - 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, """ - - - """); - File.WriteAllText(buildPropsFile, """ - - - - """); - - Build(testInstance, BuildLevel.All); - - // Change the imported build file (this is not recognized). - File.WriteAllText(importedFile, """ - - - $(DefineConstants);CUSTOM_DEFINE - - - """); - - 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 args = default, - string expectedOutput = "Hello from Program", - string programFileName = "Program.cs", - string? workDir = null, - Func? customizeCommand = null) - { - 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)), - }; - - var command = new DotnetCommand(Log, ["run", programFileName, "-bl", .. args]) - .WithWorkingDirectory(workDir ?? testInstance.Path); - - if (customizeCommand != null) - { - command = customizeCommand(command); - } - - command.Execute() - .Should().Pass() - .And.HaveStdOut(prefix + expectedOutput); - - var binlogs = new DirectoryInfo(workDir ?? 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, "--no-cache", "--no-build")); - } - - /// - /// optimization should see through symlinks. - /// See . - /// - [Fact] - public void UpToDate_SymbolicLink() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var originalPath = Path.Join(testInstance.Path, "original.cs"); - var code = """ - #!/usr/bin/env dotnet - Console.WriteLine("v1"); - """; - var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - File.WriteAllText(originalPath, code, utf8NoBom); - - var programFileName = "linked"; - var programPath = Path.Join(testInstance.Path, programFileName); - - File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName); - - Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName); - - code = code.Replace("v1", "v2"); - File.WriteAllText(originalPath, code, utf8NoBom); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); - } - - /// - /// Similar to but with a chain of symlinks. - /// - [Fact] - public void UpToDate_SymbolicLink2() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var originalPath = Path.Join(testInstance.Path, "original.cs"); - var code = """ - #!/usr/bin/env dotnet - Console.WriteLine("v1"); - """; - var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - File.WriteAllText(originalPath, code, utf8NoBom); - - var intermediateFileName = "linked1"; - var intermediatePath = Path.Join(testInstance.Path, intermediateFileName); - - File.CreateSymbolicLink(path: intermediatePath, pathToTarget: originalPath); - - var programFileName = "linked2"; - var programPath = Path.Join(testInstance.Path, programFileName); - - File.CreateSymbolicLink(path: programPath, pathToTarget: intermediatePath); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName); - - Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName); - - code = code.Replace("v1", "v2"); - File.WriteAllText(originalPath, code, utf8NoBom); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); - } - - /// - /// optimization currently does not support #:project references and hence is disabled if those are present. - /// See . - /// - [Fact] - public void UpToDate_ProjectReferences() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var libDir = Path.Join(testInstance.Path, "Lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - 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 = VirtualProjectBuilder.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); - - // We cannot detect changes in referenced projects, so we always rebuild. - Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName); - - libCode = libCode.Replace("v1", "v2"); - File.WriteAllText(libPath, libCode); - - Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v2", programFileName: programFileName); - } - - /// - /// optimization currently does not support #:ref references and hence is disabled if those are present. - /// Analogous to . - /// - [Fact] - public void UpToDate_RefDirectives() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - var libPath = Path.Join(testInstance.Path, "lib.cs"); - var libCode = """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "v1"; - } - """; - File.WriteAllText(libPath, libCode); - - var programCode = """ - #:ref lib.cs - Console.WriteLine("Hello " + MyLib.Greeter.Greet()); - """; - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, programCode); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1"); - - // We cannot detect changes in referenced files, so we always rebuild. - Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1"); - - libCode = libCode.Replace("v1", "v2"); - File.WriteAllText(libPath, libCode); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello v2"); - } - - /// - /// optimization considers default items. - /// Also tests optimization. - /// (We cannot test because that optimization doesn't support neither #:property nor #:sdk which we need to enable default items.) - /// See . - /// - [Theory, CombinatorialData] - public void UpToDate_DefaultItems(bool optOut) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var code = $""" - {(optOut ? "#:property FileBasedProgramCanSkipMSBuild=false" : "")} - #:property EnableDefaultEmbeddedResourceItems=true - {s_programReadingEmbeddedResource} - """; - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), code); - - Build(testInstance, BuildLevel.All, expectedOutput: "Resource not found"); - - File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); - - if (!optOut) - { - // Adding a default item is currently not recognized (https://github.com/dotnet/sdk/issues/50912). - Build(testInstance, BuildLevel.None, expectedOutput: "Resource not found"); - Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: "[MyString, TestValue]"); - } - else - { - 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.All, expectedOutput: "[MyString, UpdatedValue]"); - - // Update the C# file. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v2\n" + code); - - Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: "[MyString, UpdatedValue]"); - - // Update the RESX file again (to verify the CSC only compilation didn't corrupt the list of additional files in the cache). - File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue2")); - - Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, UpdatedValue2]"); - } - - /// - /// Similar to but for .razor files instead of .resx files. - /// - [Fact] - public void UpToDate_DefaultItems_Razor() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var programFileName = "MyRazorApp.cs"; - File.WriteAllText(Path.Join(testInstance.Path, programFileName), """ - #:sdk Microsoft.NET.Sdk.Web - _ = new MyRazorApp.MyCoolApp(); - Console.WriteLine("Hello from Program"); - """); - - var razorFilePath = Path.Join(testInstance.Path, "MyCoolApp.razor"); - File.WriteAllText(razorFilePath, ""); - - Build(testInstance, BuildLevel.All, programFileName: programFileName); - - Build(testInstance, BuildLevel.None, programFileName: programFileName); - - File.Delete(razorFilePath); - - new DotnetCommand(Log, "run", programFileName) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - // error CS0246: The type or namespace name 'MyRazorApp' could not be found - .And.HaveStdOutContaining("error CS0246"); - } - - [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 = VirtualProjectBuilder.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); - } - - /// - /// Checks that the DOTNET_ROOT env var is set the same in csc mode as in msbuild mode. - /// - [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() - .Where(e => ((string)e.Key).StartsWith("DOTNET_ROOT"))) - { - Console.WriteLine($"{entry.Key}={entry.Value}"); - } - """); - - var expectedDotNetRoot = SdkTestContext.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"), ""); - - 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); - } - - /// - /// 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. - /// - [Fact] - public void CscOnly_IntermediateFiles() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - Expression> e = () => 1 + 1; - Console.WriteLine(e); - """); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.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"), ""); - - 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"), """ - - - - - - """); - - 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"); - } - - /// - /// If a file from a NuGet package (which would be used by CSC-only build) does not exist, full MSBuild should be used instead. - /// - [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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v1", programFileName: programFileName); - } - - [Fact] // https://github.com/dotnet/sdk/issues/50778 - public void CscOnly_Args() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, s_program); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.Csc, args: ["test", "args"], expectedOutput: """ - echo args:test;args - Hello from Program - """); - } - - /// - /// Combination of and . - /// - [Fact] - public void CscOnly_SymbolicLink() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - - var originalPath = Path.Join(testInstance.Path, "original.cs"); - var code = """ - #!/usr/bin/env dotnet - Console.WriteLine("v1"); - """; - var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - File.WriteAllText(originalPath, code, utf8NoBom); - - var programFileName = "linked"; - var programPath = Path.Join(testInstance.Path, programFileName); - - File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v1", programFileName: programFileName); - - Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName); - - code = code.Replace("v1", "v2"); - File.WriteAllText(originalPath, code, utf8NoBom); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); - } - - /// - /// Tests an optimization which remembers CSC args from prior MSBuild runs and can skip subsequent MSBuild invocations and call CSC directly. - /// This optimization kicks in when the file has some #: directives (then the simpler "hard-coded CSC args" optimization cannot be used). - /// - [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 = VirtualProjectBuilder.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 "); - } - - /// - /// See . - /// - [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 = VirtualProjectBuilder.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); - } - - /// - /// Testing optimization . - /// When compilation fails, the obj dll should not be copied to bin directory. - /// This prevents spurious errors if the dll file was not even produced by roslyn due to compilation errors. - /// - [Fact] - public void CscOnly_AfterMSBuild_CompilationFailure_NoCopyToBin() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - - // First, create a valid program and build it successfully - var programPath = Path.Join(testInstance.Path, "Program.cs"); - var code = """ - #:property PublishAot=false - Console.WriteLine("version 1"); - """; - File.WriteAllText(programPath, code); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: "version 1"); - - // Verify that the dlls were created - var objDll = Path.Join(artifactsDir, "obj", "debug", "Program.dll"); - new FileInfo(objDll).Should().Exist(); - var binDll = Path.Join(artifactsDir, "bin", "debug", "Program.dll"); - new FileInfo(binDll).Should().Exist(); - - // Delete the dlls - File.Delete(objDll); - File.Delete(binDll); - - // Write invalid code that causes compilation to fail - code = code + "\n#error my custom error"; - File.WriteAllText(programPath, code); - - // Try to build the invalid code - new DotnetCommand(Log, "run", "-bl", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Fail() - .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) - .And.HaveStdOutContaining("my custom error") - .And.HaveStdErrContaining(CliCommandStrings.RunCommandException); - - new FileInfo(objDll).Should().NotExist(); - new FileInfo(binDll).Should().NotExist(); - } - - /// - /// See . - /// - [Fact] - public void CscOnly_AfterMSBuild_Args() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - var programPath = Path.Join(testInstance.Path, "Program.cs"); - - var code = $""" - #:property Configuration=Release - {s_program} - """; - - File.WriteAllText(programPath, code); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, args: ["test", "args"], expectedOutput: """ - echo args:test;args - Hello from Program - Release config - """); - - code = code.Replace("Hello", "Hi"); - File.WriteAllText(programPath, code); - - Build(testInstance, BuildLevel.Csc, args: ["test", "args"], expectedOutput: """ - echo args:test;args - Hi from Program - Release config - """); - } - - /// - /// See . - /// If hard links are enabled, the bin/app.dll and obj/app.dll files are going to be the same, - /// so our "copy obj to bin" logic must account for that. - /// - [Fact] - public void CscOnly_AfterMSBuild_HardLinks() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - var programPath = Path.Join(testInstance.Path, "Program.cs"); - - var code = $""" - #:property CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true - #:property CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible=true - {s_program} - """; - - File.WriteAllText(programPath, code); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All); - - code = code.Replace("Hello", "Hi"); - File.WriteAllText(programPath, code); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "Hi from Program"); - } - - /// - /// Combination of and . - /// - [Fact] - public void CscOnly_AfterMSBuild_SymbolicLink() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - - var originalPath = Path.Join(testInstance.Path, "original.cs"); - var code = """ - #!/usr/bin/env dotnet - #:property Configuration=Release - Console.WriteLine("v1"); - """; - var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - File.WriteAllText(originalPath, code, utf8NoBom); - - var programFileName = "linked"; - var programPath = Path.Join(testInstance.Path, programFileName); - - File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName); - - code = code.Replace("v1", "v2"); - File.WriteAllText(originalPath, code, utf8NoBom); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); - } - - /// - /// Interaction of optimization and Directory.Build.props file. - /// - [Theory, CombinatorialData] - public void CscOnly_AfterMSBuild_DirectoryBuildProps(bool touch1, bool touch2) - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var propsPath = Path.Join(testInstance.Path, "Directory.Build.props"); - var propsContent = """ - - - CustomAssemblyName - - - """; - File.WriteAllText(propsPath, propsContent); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - var programVersion = 0; - void WriteProgramContent() - { - programVersion++; - - // #: directive ensures we get CscOnly_AfterMSBuild optimization instead of CscOnly. - File.WriteAllText(programPath, $""" - #:property Configuration=Debug - Console.WriteLine("v{programVersion} " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Name); - """); - } - WriteProgramContent(); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} CustomAssemblyName"); - - File.Delete(propsPath); - - if (touch1) WriteProgramContent(); - - Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} Program"); - - File.WriteAllText(propsPath, propsContent); - - if (touch2) WriteProgramContent(); - - Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} CustomAssemblyName"); - } - - /// - /// See . - /// This optimization currently does not support #:project references and hence is disabled if those are present. - /// - [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"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - 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 = VirtualProjectBuilder.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); - } - - /// - /// See . - /// This optimization currently does not support #:ref references and hence is disabled if those are present. - /// Analogous to . - /// - [Fact] - public void CscOnly_AfterMSBuild_RefDirectives() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - EnableRefDirective(testInstance); - - var libPath = Path.Join(testInstance.Path, "lib.cs"); - var libCode = """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "v1"; - } - """; - File.WriteAllText(libPath, libCode); - - var programCode = """ - #:ref lib.cs - Console.WriteLine("Hello " + MyLib.Greeter.Greet()); - """; - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, programCode); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1"); - - programCode = programCode.Replace("Hello", "Hi"); - File.WriteAllText(programPath, programCode); - - libCode = libCode.Replace("v1", "v2"); - File.WriteAllText(libPath, libCode); - - // Cannot use CSC because we cannot detect updates in the referenced file. - Build(testInstance, BuildLevel.All, expectedOutput: "Hi v2"); - } - - /// - /// See . - /// If users have more complex build customizations, they can opt out of the optimization. - /// - [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"), $""" - - - <{propertyName}>{canSkipMSBuild} - - - """); - } - - 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 = VirtualProjectBuilder.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"); - } - - /// - /// See . - /// - [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 = VirtualProjectBuilder.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 "); - } - - /// - /// Verifies that csc.rsp is written to disk after a full MSBuild build, - /// so that IDEs can read it to create a virtual project. - /// - [Fact] - public void MSBuild_WritesCscRsp() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, """ - #:property Configuration=Release - Console.Write("Hello"); - """); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - // A build directive forces a full MSBuild build. - Build(testInstance, BuildLevel.All, expectedOutput: "Hello"); - - // csc.rsp should be written to disk after a full MSBuild build. - var rspPath = Path.Join(artifactsDir, "csc.rsp"); - File.Exists(rspPath).Should().BeTrue("csc.rsp should be written after a full MSBuild build"); - File.ReadAllLines(rspPath).Should().NotBeEmpty("csc.rsp should contain compiler arguments"); - } - - /// - /// Verifies that csc.rsp is written to disk after dotnet build file.cs, - /// so that IDEs can read it to create a virtual project. - /// - [Fact] - public void DotnetBuild_WritesCscRsp() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, """ - Console.Write("Hello"); - """); - - // Remove artifacts from possible previous runs of this test. - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - new DotnetCommand(Log, "build", "Program.cs") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass(); - - // csc.rsp should be written to disk after dotnet build. - var rspPath = Path.Join(artifactsDir, "csc.rsp"); - File.Exists(rspPath).Should().BeTrue("csc.rsp should be written after dotnet build file.cs"); - File.ReadAllLines(rspPath).Should().NotBeEmpty("csc.rsp should contain compiler arguments"); - } - - /// - /// Testing optimization when the NuGet cache is cleared between builds. - /// See . - /// - [Fact] - public void CscOnly_NuGetCacheCleared() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - - var code = """ - Console.Write("v1"); - """; - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, code); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - var packageDir = Path.Join(testInstance.Path, "packages"); - TestCommand CustomizeCommand(TestCommand command) => command.WithEnvironmentVariable("NUGET_PACKAGES", packageDir); - - Assert.False(Directory.Exists(packageDir)); - - // Ensure the packages exist first. - Build(testInstance, BuildLevel.All, expectedOutput: "v1", customizeCommand: CustomizeCommand); - - Assert.True(Directory.Exists(packageDir)); - - // Now clear the build outputs (but not packages) to verify CSC is used even from "first run". - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - code = code.Replace("v1", "v2"); - File.WriteAllText(programPath, code); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", customizeCommand: CustomizeCommand); - - code = code.Replace("v2", "v3"); - File.WriteAllText(programPath, code); - - // Clear NuGet cache. - Directory.Delete(packageDir, recursive: true); - Assert.False(Directory.Exists(packageDir)); - - Build(testInstance, BuildLevel.All, expectedOutput: "v3", customizeCommand: CustomizeCommand); - - Assert.True(Directory.Exists(packageDir)); - } - - /// - /// Combination of and . - /// - [Fact] - public void CscOnly_AfterMSBuild_NuGetCacheCleared() - { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - - var code = """ - #:property PublishAot=false - #:package System.CommandLine@2.0.0-beta4.22272.1 - new System.CommandLine.RootCommand("v1"); - Console.WriteLine("v1"); - """; - - var programPath = Path.Join(testInstance.Path, "Program.cs"); - File.WriteAllText(programPath, code); - - var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); - if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); - - var packageDir = Path.Join(testInstance.Path, "packages"); - TestCommand CustomizeCommand(TestCommand command) => command.WithEnvironmentVariable("NUGET_PACKAGES", packageDir); - - Assert.False(Directory.Exists(packageDir)); - - Build(testInstance, BuildLevel.All, expectedOutput: "v1", customizeCommand: CustomizeCommand); - - Assert.True(Directory.Exists(packageDir)); - - code = code.Replace("v1", "v2"); - File.WriteAllText(programPath, code); - - Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", customizeCommand: CustomizeCommand); - - code = code.Replace("v2", "v3"); - File.WriteAllText(programPath, code); - - // Clear NuGet cache. - Directory.Delete(packageDir, recursive: true); - Assert.False(Directory.Exists(packageDir)); - - Build(testInstance, BuildLevel.All, expectedOutput: "v3", customizeCommand: CustomizeCommand); - - Assert.True(Directory.Exists(packageDir)); - } - - private static string ToJson(string s) => JsonSerializer.Serialize(s); - - /// - /// Simplifies using interpolated raw strings with nested JSON, - /// e.g, in $$"""{x:{y:1}}""", the }} would result in an error. - /// - 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.AppHost.Sdk@9.1.0 - #:property TargetFramework=net5.0 - #:package System.CommandLine@2.0.0-beta4.22272.1 - #:property LangVersion=preview - Console.WriteLine(); - """); - - var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); - new DotnetCommand(Log, "run-api") - .WithStandardInput($$""" - {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} - """) - .Execute() - .Should().Pass() - .And.HaveStdOut($$""" - {"$type":"Project","Version":1,"Content":{{ToJson($""" - - - - false - /artifacts - Program - $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) - true - {programPath} - .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content - false - true - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - - - - - - - - - - - net5.0 - preview - false - $(Features);FileBasedProgram - - - - - - - - - - - - - - - - - - - - - """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics":[]} - """); - } - - /// - /// Directives should be evaluated before the project for run-api is constructed. - /// - [Fact] - public void Api_Evaluation() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var programPath = Path.Join(testInstance.Path, "A.cs"); - File.WriteAllText(programPath, """ - #:property P1=cs - #:include B.$(P1) - Console.WriteLine(); - """); - - var bPath = Path.Join(testInstance.Path, "B.cs"); - File.WriteAllText(bPath, ""); - - var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); - new DotnetCommand(Log, "run-api") - .WithStandardInput($$""" - {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} - """) - .Execute() - .Should().Pass() - .And.HaveStdOut($$""" - {"$type":"Project","Version":1,"Content":{{ToJson($""" - - - - false - /artifacts - A - $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) - true - {programPath} - .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content - false - true - false - false - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - - - - - - - - - - cs - false - $(Features);FileBasedProgram - - - - - - - - - - - - - - - - - - - - """)}},"ProjectPath":{{ToJson(projectPath)}},"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 - """); - - var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); - new DotnetCommand(Log, "run-api") - .WithStandardInput($$""" - {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} - """) - .Execute() - .Should().Pass() - .And.HaveStdOut($$""" - {"$type":"Project","Version":1,"Content":{{ToJson($""" - - - - false - /artifacts - Program - $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) - true - {programPath} - .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content - false - true - false - false - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - - - - - - - - - - false - $(Features);FileBasedProgram - - - - - - - - - - - - - - - - """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": - [{"Location":{ - "Path":{{ToJson(programPath)}}, - "Span":{"Start":{"Line":1,"Character":0},"End":{"Line":1,"Character":30}{{nop}}}{{nop}}}, - "Message":{{ToJson(FileBasedProgramsResources.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(); - """); - - var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); - new DotnetCommand(Log, "run-api") - .WithStandardInput($$""" - {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} - """) - .Execute() - .Should().Pass() - .And.HaveStdOut($$""" - {"$type":"Project","Version":1,"Content":{{ToJson($""" - - - - false - /artifacts - Program - $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) - true - {programPath} - .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content - false - true - false - false - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - - - - - - - - - - false - $(Features);FileBasedProgram - - - - - - - - - - - - - - - - """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": - [{"Location":{ - "Path":{{ToJson(programPath)}}, - "Span":{"Start":{"Line":0,"Character":0},"End":{"Line":1,"Character":0}{{nop}}}{{nop}}}, - "Message":{{ToJson(string.Format(FileBasedProgramsResources.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 - """); - } - - [Fact] - public void Api_VirtualProjectBuilder_CreateProjectRootElement() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - var libDir = Path.Join(testInstance.Path, "Lib"); - Directory.CreateDirectory(libDir); - - File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" - - - {ToolsetInfo.CurrentTargetFramework} - - - """); - - 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); - - var appPath = Path.Join(appDir, "Program.cs"); - File.WriteAllText(appPath, """ - #:project ../$(LibProjectName) - #:property LibProjectName=Lib - Console.WriteLine(Lib.LibClass.GetMessage()); - """); - - using var projectCollection = new ProjectCollection(); - var projectRootElement = NuGetVirtualProjectBuilder.Instance.CreateProjectRootElement(appPath, projectCollection); - - var xml = projectRootElement.RawXml; - Log.WriteLine(xml); - - xml.Should() - // directives are evaluated - .Contain("""""".Replace('\\', Path.DirectorySeparatorChar)) - // it's the virtual project - .And.Contain("true") - // correct target framework is used - .And.Contain($"{ToolsetInfo.CurrentTargetFramework}"); - - projectRootElement.FullPath.Should().Be(VirtualProjectBuilder.GetVirtualProjectPath(appPath)); - } - - [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 = VirtualProjectBuilder.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 EntryPointFilePath_SymbolicLink() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - var fileName = "Program.cs"; - var programPath = Path.Join(testInstance.Path, fileName); - File.WriteAllText(programPath, """ - #!/usr/bin/env dotnet - var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string; - Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}"); - """); - - new DotnetCommand(Log, "run", fileName) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut($"EntryPointFilePath: {programPath}"); - - var linkName = "linked"; - var linkPath = Path.Join(testInstance.Path, linkName); - File.CreateSymbolicLink(linkPath, programPath); - - new DotnetCommand(Log, "run", linkName) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut($"EntryPointFilePath: {linkPath}"); - } - - [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); - } - - /// - /// Check that -get commands work the same in project-based and file-based apps. - /// - [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"), """ - - - - - - - - MyValue - - - - """); - - 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!.Replace("Program.cs.csproj", "Program.csproj").Should().Be(projectBasedResult.StdErr); - fileBasedResult.ExitCode.Should().Be(projectBasedResult.ExitCode).And.Be(success ? 0 : 1); - fileBasedFiles.Should().Equal(projectBasedFiles); - - Dictionary 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; - } - } - - /// - /// Regression test for https://github.com/dotnet/sdk/issues/52714. - /// The virtual project's must survive GC - /// even after being evicted from MSBuild's strong cache (LRU of size N). - /// We force eviction via MSBUILDPROJECTROOTELEMENTCACHESIZE=1 - /// and trigger GC via an inline task during NuGet restore. - /// Without the fix (strong reference in VirtualProjectBuilder._projectRootElement), - /// this fails with MSB4025 "The project file could not be loaded". - /// - [Fact] - public void VirtualProject_SurvivesGCDuringRestore() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - Console.WriteLine("Hello from virtual project"); - """); - - // Directory.Build.targets that forces GC during restore, - // after SDK imports have already evicted the virtual PRE from the strong cache. - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ - - - - - - - - <_ForceGCTask /> - - - """); - - new DotnetCommand(Log, "run", "--no-cache", "Program.cs") - // A cache size of 1 ensures the virtual PRE is evicted from the strong cache - // as soon as any SDK .targets/.props file is loaded during evaluation. - .WithEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE", "1") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from virtual project"); - } - - /// - /// Same as but for #:ref referenced projects. - /// The referenced project's must also survive GC. - /// - [Fact] - public void VirtualProject_SurvivesGCDuringRestore_RefDirective() - { - var testInstance = _testAssetsManager.CreateTestDirectory(); - - File.WriteAllText(Path.Join(testInstance.Path, "Lib.cs"), """ - #:property OutputType=Library - namespace MyLib; - public static class Greeter - { - public static string Greet() => "Hello from ref"; - } - """); - - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #:ref Lib.cs - Console.WriteLine(MyLib.Greeter.Greet()); - """); - - // Directory.Build.targets that forces GC during restore, - // after SDK imports have already evicted the virtual PRE from the strong cache. - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ - - - - - - - - <_ForceGCTask /> - - - """); - - new DotnetCommand(Log, "run", "--no-cache", "Program.cs") - // A cache size of 1 ensures the virtual PRE is evicted from the strong cache - // as soon as any SDK .targets/.props file is loaded during evaluation. - .WithEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE", "1") - .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut("Hello from ref"); - } -} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs new file mode 100644 index 000000000000..2e4d17f9d313 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs @@ -0,0 +1,924 @@ +// 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 Microsoft.DotNet.Cli.Commands; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; +using Microsoft.DotNet.ProjectTools; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +public sealed class RunFileTests_BuildCommands(ITestOutputHelper log) : RunFileTestBase(log) +{ + + [Fact] + public void Restore_NonExistentPackage() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, """ + #:package Microsoft.ThisPackageDoesNotExist@1.0.0 + Console.WriteLine(); + """); + + new DotnetCommand(Log, "restore", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining("Program.cs.csproj : error NU1101"); + } + + [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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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"), """ + + + true + + + """); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, "Console.WriteLine();"); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.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 = VirtualProjectBuilder.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, FileBasedProgramsResources.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 Build_Library() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Library + class C; + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + VirtualProjectBuilder.GetVirtualProjectPath(programFile), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Library")); + } + + [Fact] + public void Build_Library_MultiTarget() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(programFile, $""" + #:property OutputType=Library + #:property PublishAot=false + #:property LangVersion=preview + #:property TargetFramework= + #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} + class C; + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute("--no-interactive") + .Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + + new DotnetCommand(Log, "run", "lib.cs", "--framework", ToolsetInfo.CurrentTargetFramework) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + VirtualProjectBuilder.GetVirtualProjectPath(programFile), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Library")); + } + + [Fact] + public void Build_Module() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "module.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Module + #:property ProduceReferenceAssembly=false + class C; + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "module.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "module.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + VirtualProjectBuilder.GetVirtualProjectPath(programFile), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Module")); + } + + [Fact] + public void Build_WinExe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "winexe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=WinExe + Console.WriteLine("Hello WinExe"); + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "winexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "winexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello WinExe"); + } + + [Fact] + public void Build_Exe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "exe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Exe + Console.WriteLine("Hello Exe"); + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello Exe"); + } + + [Fact] + public void Build_Exe_MultiTarget() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "exe.cs"); + File.WriteAllText(programFile, $""" + #:property OutputType=Exe + #:property PublishAot=false + #:property LangVersion=preview + #:property TargetFramework= + #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} + Console.WriteLine("Hello Exe"); + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + + new DotnetCommand(Log, "run", "exe.cs", "--framework", ToolsetInfo.CurrentTargetFramework) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello Exe"); + } + + [Fact] + public void Build_AppContainerExe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "appcontainerexe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=AppContainerExe + Console.WriteLine("Hello AppContainerExe"); + """); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "appcontainerexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "appcontainerexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + VirtualProjectBuilder.GetVirtualProjectPath(programFile), + ToolsetInfo.CurrentTargetFrameworkVersion, + "AppContainerExe")); + } + + [Fact] + public void Publish() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, s_program); + + var artifactsDir = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.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, """ + 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 = VirtualProjectBuilder.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 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 = VirtualProjectBuilder.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(VirtualProjectBuilder.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 = VirtualProjectBuilder.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' + """); + } + + /// + /// Properties/launchSettings.json takes precedence over Program.run.json. + /// + [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 usage 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' + """); + } + + /// + /// Each file-based app in a folder can have separate launch profile. + /// + [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' + """); + } +} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs new file mode 100644 index 000000000000..f1a4d77ee519 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs @@ -0,0 +1,1007 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging.StructuredLogger; +using Microsoft.DotNet.Cli.Commands; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.ProjectTools; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +public sealed class RunFileTests_BuildOptions(ITestOutputHelper log) : RunFileTestBase(log) +{ + + /// + /// Main method is supported just like top-level statements. + /// + [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!"); + } + + /// + /// Empty file does not contain entry point, so that's an error. + /// + [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 + } + + /// + /// See . + /// + [Theory, CombinatorialData] + public void WorkingDirectory(bool cscOnly) + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: cscOnly ? OutOfTreeBaseDirectory : null); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + + var code = """ + Console.WriteLine("v1"); + Console.WriteLine(Environment.CurrentDirectory); + Console.WriteLine(Directory.GetCurrentDirectory()); + Console.WriteLine(new DirectoryInfo(".").FullName); + Console.WriteLine(AppContext.GetData("EntryPointFileDirectoryPath")); + """; + + File.WriteAllText(programPath, code); + + var tempDir = Directory.CreateTempSubdirectory(); + var workDir = TestPathUtility.ResolveTempPrefixLink(tempDir.FullName).TrimEnd(Path.DirectorySeparatorChar); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, + expectedLevel: cscOnly ? BuildLevel.Csc : BuildLevel.All, + programFileName: programPath, + workDir: workDir, + expectedOutput: GetExpectedOutput("v1", workDir)); + + code = code.Replace("v1", "v2"); + File.WriteAllText(programPath, code); + + Build(testInstance, + expectedLevel: BuildLevel.Csc, + programFileName: programPath, + workDir: workDir, + expectedOutput: GetExpectedOutput("v2", workDir)); + + tempDir.Delete(); + + string GetExpectedOutput(string version, string workDir) => $""" + {version} + {workDir} + {workDir} + {workDir} + {Path.GetDirectoryName(programPath)} + """; + } + + /// + /// Combination of and . + /// + [Fact] + public void WorkingDirectory_CscOnly_AfterMSBuild() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + + var code = """ + #:property Configuration=Release + Console.WriteLine("v1"); + Console.WriteLine(Environment.CurrentDirectory); + Console.WriteLine(Directory.GetCurrentDirectory()); + Console.WriteLine(new DirectoryInfo(".").FullName); + Console.WriteLine(AppContext.GetData("EntryPointFileDirectoryPath")); + """; + + File.WriteAllText(programPath, code); + + var tempDir = Directory.CreateTempSubdirectory(); + var workDir = TestPathUtility.ResolveTempPrefixLink(tempDir.FullName).TrimEnd(Path.DirectorySeparatorChar); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, + expectedLevel: BuildLevel.All, + programFileName: programPath, + workDir: workDir, + expectedOutput: GetExpectedOutput("v1", workDir)); + + Build(testInstance, + expectedLevel: BuildLevel.None, + programFileName: programPath, + workDir: workDir, + expectedOutput: GetExpectedOutput("v1", workDir)); + + code = code.Replace("v1", "v2"); + File.WriteAllText(programPath, code); + + Build(testInstance, + expectedLevel: BuildLevel.Csc, + programFileName: programPath, + workDir: workDir, + expectedOutput: GetExpectedOutput("v2", workDir)); + + // Can be overridden with a #:property. + var workDir2 = Path.Join(testInstance.Path, "dir2"); + Directory.CreateDirectory(workDir2); + code = $""" + #:property RunWorkingDirectory={workDir2} + {code} + """; + File.WriteAllText(programPath, code); + + Build(testInstance, + expectedLevel: BuildLevel.All, + programFileName: programPath, + workDir: workDir, + expectedOutput: GetExpectedOutput("v2", workDir2)); + + tempDir.Delete(); + + string GetExpectedOutput(string version, string workDir) => $""" + {version} + {workDir} + {workDir} + {workDir} + {Path.GetDirectoryName(programPath)} + """; + } + + /// + /// Implicit build files have an effect. + /// + [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"), """ + + + TestName + + + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from TestName"); + } + + /// + /// Implicit build files are taken from the folder of the symbolic link itself, not its target. + /// This is equivalent to the behavior of symlinked project files. + /// See . + /// + [Fact] + public void DirectoryBuildProps_SymbolicLink() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var dir1 = Path.Join(testInstance.Path, "dir1"); + Directory.CreateDirectory(dir1); + + var originalPath = Path.Join(dir1, "original.cs"); + File.WriteAllText(originalPath, s_program); + + File.WriteAllText(Path.Join(dir1, "Directory.Build.props"), """ + + + OriginalAssemblyName + + + """); + + var dir2 = Path.Join(testInstance.Path, "dir2"); + Directory.CreateDirectory(dir2); + + var programFileName = "linked.cs"; + var programPath = Path.Join(dir2, programFileName); + + File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); + + File.WriteAllText(Path.Join(dir2, "Directory.Build.props"), """ + + + LinkedAssemblyName + + + """); + + new DotnetCommand(Log, "run", programFileName) + .WithWorkingDirectory(dir2) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from LinkedAssemblyName"); + + // Removing the Directory.Build.props should be detected by up-to-date check. + File.Delete(Path.Join(dir2, "Directory.Build.props")); + + new DotnetCommand(Log, "run", programFileName) + .WithWorkingDirectory(dir2) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from linked"); + } + + /// + /// Overriding default (implicit) properties of file-based apps via implicit build files. + /// + [Fact] + public void DefaultProps_DirectoryBuildProps() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("Hi"); + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + disable + + + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + // Converting to a project should not change the behavior. + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "Program")) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + + /// + /// Overriding default (implicit) properties of file-based apps from custom SDKs. + /// + [Fact] + public void DefaultProps_CustomSdk() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var sdkDir = Path.Join(testInstance.Path, "MySdk"); + Directory.CreateDirectory(sdkDir); + File.WriteAllText(Path.Join(sdkDir, "Sdk.props"), """ + + + disable + + + """); + File.WriteAllText(Path.Join(sdkDir, "Sdk.targets"), """ + + """); + File.WriteAllText(Path.Join(sdkDir, "MySdk.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + MSBuildSdk + false + + + + + + """); + + new DotnetCommand(Log, "pack") + .WithWorkingDirectory(sdkDir) + .Execute() + .Should().Pass(); + + var appDir = Path.Join(testInstance.Path, "app"); + Directory.CreateDirectory(appDir); + File.WriteAllText(Path.Join(appDir, "NuGet.config"), $""" + + + + + + + """); + File.WriteAllText(Path.Join(appDir, "Program.cs"), """ + #:sdk Microsoft.NET.Sdk + #:sdk MySdk@1.0.0 + Console.WriteLine("Hi"); + """); + + // Use custom package cache to avoid reuse of the custom SDK packed by previous test runs. + var packagesDir = Path.Join(testInstance.Path, ".packages"); + + new DotnetCommand(Log, "run", "Program.cs") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(appDir) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + // Converting to a project should not change the behavior. + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(appDir) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(Path.Join(appDir, "Program")) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + + [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"), """ + + + + $(RunArguments) extended + + + + """); + + 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"), """ + + + + + + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining(""" + MYAPP001: Custom error + """) + .And.HaveStdErrContaining(CliCommandStrings.RunCommandException); + } + + /// + /// Command-line arguments should be passed through. + /// + [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 + """); + } + + /// + /// dotnet run --unknown-arg file.cs fallbacks to normal dotnet run behavior. + /// + [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, + "--project")); + } + + /// + /// dotnet run --some-known-arg file.cs is supported. + /// + [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 + """); + } + + /// + /// dotnet run -bl file.cs produces a binary log. + /// + [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(); + } + + /// + /// dotnet run file.cs should not produce a binary log. + /// + [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(); + } + + /// + /// Binary logs from our in-memory projects should have evaluation data. + /// + [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(); + + // There should be exactly three - two for restore, one for build. + VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: 3); + } + + /// + /// Binary logs from our in-memory projects should have evaluation data. + /// + [Fact] + public void BinaryLog_EvaluationData_MultiFile() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), + $""" + #!/usr/bin/env dotnet + #:include *.cs + {s_programDependingOnUtil} + """); + + var utilPath = Path.Join(testInstance.Path, "Util.cs"); + File.WriteAllText(utilPath, s_util); + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs", "-bl:first.binlog") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + + string binaryLogPath = Path.Join(testInstance.Path, "first.binlog"); + new FileInfo(binaryLogPath).Should().Exist(); + + // There should be exactly four - two for restore and one for build as usual, plus one for initial directive evaluation. + var expectedCount = 4; + VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: expectedCount); + + File.WriteAllText(utilPath, s_util.Replace("String from Util", "v2")); + + new DotnetCommand(Log, "run", "Program.cs", "-bl:second.binlog") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, v2"); + + binaryLogPath = Path.Join(testInstance.Path, "second.binlog"); + new FileInfo(binaryLogPath).Should().Exist(); + + // After rebuild, there should be the same number of evaluations. + VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: expectedCount); + } + + /// + /// If we skip build due to up-to-date check, no binlog should be created. + /// + [Fact] + public void BinaryLog_EvaluationData_UpToDate() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, s_program); + + var expectedOutput = "Hello from Program"; + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + string binaryLogPath = Path.Join(testInstance.Path, "msbuild.binlog"); + new FileInfo(binaryLogPath).Should().NotExist(); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($""" + {CliCommandStrings.NoBinaryLogBecauseUpToDate} + {expectedOutput} + """); + + new FileInfo(binaryLogPath).Should().NotExist(); + } + + [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); + } + + [Fact] + public void MissingShebangWarning() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + // Single-file program without shebang should NOT produce CA2266 + // (the warning only fires when there are multiple files via #:include). + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("hello"); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.NotHaveStdOutContaining("CA2266") + .And.HaveStdOutContaining("hello"); + + // Included file without shebang should not produce CA2266. + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + class Util { public static string Greet() => "hello"; } + """); + + // Entry point with shebang and #:include — no warning. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #!/usr/bin/env dotnet + #:include Util.cs + Console.WriteLine(Util.Greet()); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.NotHaveStdOutContaining("CA2266") + .And.HaveStdOutContaining("hello"); + + // Entry point without shebang and #:include — CA2266 warning expected. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:include Util.cs + Console.WriteLine(Util.Greet()); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("warning CA2266") + .And.HaveStdOutContaining("hello"); + + // CA2266 can be suppressed via NoWarn. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:property NoWarn=CA2266 + #:include Util.cs + Console.WriteLine(Util.Greet()); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.NotHaveStdOutContaining("CA2266") + .And.HaveStdOutContaining("hello"); + } + + [Fact] + public void MissingShebangWarning_CompileItemFromDirectoryBuildProps() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + // Directory.Build.props adds a Compile item, effectively making + // the compilation multi-file (same as #:include). + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + class Util { public static string Greet() => "hello"; } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + // Entry point without shebang — CA2266 warning expected + // because Directory.Build.props added another Compile item. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine(Util.Greet()); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("warning CA2266") + .And.HaveStdOutContaining("hello"); + + // Adding shebang resolves the warning. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #!/usr/bin/env dotnet + Console.WriteLine(Util.Greet()); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("hello"); + } + + /// + /// File-based projects using the default SDK do not include embedded resources by default. + /// + [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); + + // By default, with the default SDK, embedded resources are not included. + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + Resource not found + """); + + // This behavior can be overridden to enable embedded resources. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #:property EnableDefaultEmbeddedResourceItems=true + {s_programReadingEmbeddedResource} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + [MyString, TestValue] + """); + + // When using a non-default SDK, embedded resources are included by default. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #:sdk Microsoft.NET.Sdk.Web + {s_programReadingEmbeddedResource} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + [MyString, TestValue] + """); + + // When using the default SDK explicitly, embedded resources are not included. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #:sdk Microsoft.NET.Sdk + {s_programReadingEmbeddedResource} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + Resource not found + """); + } + + /// + /// Scripts in repo root should not include .resx files. + /// Part of . + /// + [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"), $""" + #:property EnableDefaultEmbeddedResourceItems=true + {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]"); + } +} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs new file mode 100644 index 000000000000..54b265c2350f --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs @@ -0,0 +1,2574 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Basic.CompilerLog.Util; +using Microsoft.Build.Evaluation; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.Cli.Commands; +using Microsoft.DotNet.Cli.Commands.NuGet; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; +using Microsoft.DotNet.ProjectTools; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +public sealed class RunFileTests_CscOnlyAndApi(ITestOutputHelper log) : RunFileTestBase(log) +{ + + /// + /// Verifies that msbuild-based runs use CSC args equivalent to csc-only runs. + /// Can regenerate CSC arguments template in . + /// + [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 = VirtualProjectBuilder.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(SdkTestContext.Current.ToolsetUnderTest.SdkFolderUnderTest); + string dotNetRootPath = NormalizePath(SdkTestContext.Current.ToolsetUnderTest.DotNetRoot); + string nuGetCachePath = NormalizePath(SdkTestContext.Current.NuGetCachePath!); + string artifactsDirNormalized = NormalizePath(artifactsDir); + string objPath = $"{artifactsDirNormalized}/obj/debug"; + string entryPointPathNormalized = NormalizePath(entryPointPath); + var msbuildArgsToVerify = new List(); + var nuGetPackageFilePaths = new List(); + bool referenceSpreadInserted = false; + bool analyzerSpreadInserted = false; + const string NetCoreAppRefPackPath = "packs/Microsoft.NETCore.App.Ref/"; + 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. + + using System.Text.Json; + + namespace Microsoft.DotNet.Cli.Commands.Run; + + // Generated by test `{{nameof(RunFileTests_CscOnlyAndApi)}}.{{nameof(CscArguments)}}`. + partial class CSharpCompilerCommand + { + private IEnumerable GetCscArguments( + 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 path. + if (rewritten.Contains(entryPointPathNormalized, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(entryPointPathNormalized, "{" + nameof(CSharpCompilerCommand.EntryPointFileFullPath) + "}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Use variable file name. + if (rewritten.Contains(fileName, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(fileName, "{FileName}", 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; + } + + // Use GetFrameworkReferenceArguments() for framework references instead of hard-coding them. + if (arg.StartsWith("/reference:", StringComparison.Ordinal)) + { + if (!referenceSpreadInserted) + { + code.AppendLine(""" + .. GetFrameworkReferenceArguments(), + """); + referenceSpreadInserted = true; + } + + msbuildArgsToVerify.Add(msbuildArgToVerify); + continue; + } + + // Use GetFrameworkAnalyzerArguments() for targeting-pack analyzers instead of hard-coding them. + if (arg.StartsWith("/analyzer:", StringComparison.Ordinal) + && rewritten.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase)) + { + if (!analyzerSpreadInserted) + { + code.AppendLine(""" + .. GetFrameworkAnalyzerArguments(), + """); + analyzerSpreadInserted = true; + } + + msbuildArgsToVerify.Add(msbuildArgToVerify); + 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(""" + ]; + } + + /// + /// 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). + /// + public static IEnumerable GetPathsOfCscInputsFromNuGetCache() + { + return + [ + """); + foreach (var nuGetPackageFilePath in nuGetPackageFilePaths) + { + code.AppendLine($""" + $"{nuGetPackageFilePath}", + """); + } + code.AppendLine(""" + ]; + } + """); + + // Generate file content templates. + var baseDirectory = TestPathUtility.ResolveTempPrefixLink(Path.GetDirectoryName(entryPointPath)!); + var replacements = new List<(string, string)> + { + (TestPathUtility.ResolveTempPrefixLink(entryPointPath), nameof(CSharpCompilerCommand.EntryPointFileFullPath)), + (baseDirectory + Path.DirectorySeparatorChar, nameof(CSharpCompilerCommand.BaseDirectoryWithTrailingSeparator)), + (baseDirectory, nameof(CSharpCompilerCommand.BaseDirectory)), + (programName, nameof(CSharpCompilerCommand.FileNameWithoutExtension)), + (CSharpCompilerCommand.TargetFrameworkVersion, nameof(CSharpCompilerCommand.TargetFrameworkVersion)), + (CSharpCompilerCommand.TargetFramework, nameof(CSharpCompilerCommand.TargetFramework)), + (CSharpCompilerCommand.DefaultRuntimeVersion, nameof(CSharpCompilerCommand.DefaultRuntimeVersion)), + }; + var emittedFiles = Directory.EnumerateFiles(artifactsDir, "*", SearchOption.AllDirectories).Order(); + foreach (var emittedFile in emittedFiles) + { + var emittedFileName = Path.GetFileName(emittedFile); + var generatedMethodName = GetGeneratedMethodName(emittedFileName); + if (generatedMethodName is null) + { + Log.WriteLine($"Skipping unrecognized file '{emittedFile}'."); + continue; + } + + var emittedFileContent = File.ReadAllText(emittedFile); + + string interpolatedString = emittedFileContent; + string interpolationPrefix; + + if (emittedFileName.EndsWith(".json", StringComparison.Ordinal)) + { + interpolationPrefix = "$$"; + foreach (var (key, value) in replacements) + { + interpolatedString = interpolatedString.Replace(JsonSerializer.Serialize(key), "{{JsonSerializer.Serialize(" + value + ")}}"); + } + } + else + { + interpolationPrefix = "$"; + foreach (var (key, value) in replacements) + { + interpolatedString = interpolatedString.Replace(key, "{" + value + "}"); + } + } + + if (interpolatedString == emittedFileContent) + { + interpolationPrefix = ""; + } + + code.AppendLine($$"""" + + private string Get{{generatedMethodName}}Content() + { + return {{interpolationPrefix}}""" + {{interpolatedString}} + """; + } + """"); + } + + code.AppendLine(""" + } + """); + + // Save the code. + var codeFolder = new DirectoryInfo(Path.Join( + SdkTestContext.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))) + .ToList(); + Log.WriteLine("CSC-only args:"); + Log.WriteLine(string.Join(Environment.NewLine, normalizedCscOnlyArgs)); + Log.WriteLine("MSBuild args:"); + Log.WriteLine(string.Join(Environment.NewLine, msbuildArgsToVerify)); + + // References and targeting-pack analyzers may be in a different order (FrameworkList.xml vs. MSBuild), + // so compare them as sets. All other args must be in the same order. + var cscOnlyRefArgs = normalizedCscOnlyArgs.Where(static a => a.StartsWith("/reference:", StringComparison.Ordinal)).ToList(); + var cscOnlyAnalyzerArgs = normalizedCscOnlyArgs.Where(a => a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase)).ToList(); + var cscOnlyOtherArgs = normalizedCscOnlyArgs.Where(a => !a.StartsWith("/reference:", StringComparison.Ordinal) && !(a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase))).ToList(); + var msbuildRefArgs = msbuildArgsToVerify.Where(static a => a.StartsWith("/reference:", StringComparison.Ordinal)).ToList(); + var msbuildAnalyzerArgs = msbuildArgsToVerify.Where(a => a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase)).ToList(); + var msbuildOtherArgs = msbuildArgsToVerify.Where(a => !a.StartsWith("/reference:", StringComparison.Ordinal) && !(a.StartsWith("/analyzer:", StringComparison.Ordinal) && a.Contains(NetCoreAppRefPackPath, StringComparison.OrdinalIgnoreCase))).ToList(); + cscOnlyRefArgs.Should().NotBeEmpty( + "framework references should be resolved from FrameworkList.xml"); + cscOnlyRefArgs.Should().BeEquivalentTo(msbuildRefArgs, + "the generated file might be outdated, run this test locally to regenerate it"); + cscOnlyAnalyzerArgs.Should().NotBeEmpty( + "framework analyzers should be resolved from FrameworkList.xml"); + cscOnlyAnalyzerArgs.Should().BeEquivalentTo(msbuildAnalyzerArgs, + "the generated file might be outdated, run this test locally to regenerate it"); + cscOnlyOtherArgs.Should().Equal(msbuildOtherArgs, + "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); + } + + static string? GetGeneratedMethodName(string assetFileName) + { + return assetFileName switch + { + $".NETCoreApp,Version=v{ToolsetInfo.CurrentTargetFrameworkVersion}.AssemblyAttributes.cs" => "AssemblyAttributes", + $"{fileName}.GlobalUsings.g.cs" => "GlobalUsings", + $"{fileName}.AssemblyInfo.cs" => "AssemblyInfo", + $"{fileName}.GeneratedMSBuildEditorConfig.editorconfig" => "GeneratedMSBuildEditorConfig", + $"{programName}{FileNameSuffixes.RuntimeConfigJson}" => "RuntimeConfig", + _ => null, + }; + } + } + + /// + /// Verifies that csc-only runs emit auxiliary files equivalent to msbuild-based runs. + /// + [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 = VirtualProjectBuilder.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, run test '{nameof(CscArguments)}' locally to re-generate the template): {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 = VirtualProjectBuilder.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, """ + + + $(DefineConstants);CUSTOM_DEFINE + + + """); + + 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, """ + + + """); + File.WriteAllText(buildPropsFile, """ + + + + """); + + Build(testInstance, BuildLevel.All); + + // Change the imported build file (this is not recognized). + File.WriteAllText(importedFile, """ + + + $(DefineConstants);CUSTOM_DEFINE + + + """); + + 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); + } + + [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, "--no-cache", "--no-build")); + } + + /// + /// optimization should see through symlinks. + /// See . + /// + [Fact] + public void UpToDate_SymbolicLink() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var originalPath = Path.Join(testInstance.Path, "original.cs"); + var code = """ + #!/usr/bin/env dotnet + Console.WriteLine("v1"); + """; + var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + File.WriteAllText(originalPath, code, utf8NoBom); + + var programFileName = "linked"; + var programPath = Path.Join(testInstance.Path, programFileName); + + File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName); + + Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName); + + code = code.Replace("v1", "v2"); + File.WriteAllText(originalPath, code, utf8NoBom); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); + } + + /// + /// Similar to but with a chain of symlinks. + /// + [Fact] + public void UpToDate_SymbolicLink2() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var originalPath = Path.Join(testInstance.Path, "original.cs"); + var code = """ + #!/usr/bin/env dotnet + Console.WriteLine("v1"); + """; + var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + File.WriteAllText(originalPath, code, utf8NoBom); + + var intermediateFileName = "linked1"; + var intermediatePath = Path.Join(testInstance.Path, intermediateFileName); + + File.CreateSymbolicLink(path: intermediatePath, pathToTarget: originalPath); + + var programFileName = "linked2"; + var programPath = Path.Join(testInstance.Path, programFileName); + + File.CreateSymbolicLink(path: programPath, pathToTarget: intermediatePath); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName); + + Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName); + + code = code.Replace("v1", "v2"); + File.WriteAllText(originalPath, code, utf8NoBom); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); + } + + /// + /// optimization currently does not support #:project references and hence is disabled if those are present. + /// See . + /// + [Fact] + public void UpToDate_ProjectReferences() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var libDir = Path.Join(testInstance.Path, "Lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + 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 = VirtualProjectBuilder.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); + + // We cannot detect changes in referenced projects, so we always rebuild. + Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName); + + libCode = libCode.Replace("v1", "v2"); + File.WriteAllText(libPath, libCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v2", programFileName: programFileName); + } + + /// + /// optimization currently does not support #:ref references and hence is disabled if those are present. + /// Analogous to . + /// + [Fact] + public void UpToDate_RefDirectives() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + var libPath = Path.Join(testInstance.Path, "lib.cs"); + var libCode = """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "v1"; + } + """; + File.WriteAllText(libPath, libCode); + + var programCode = """ + #:ref lib.cs + Console.WriteLine("Hello " + MyLib.Greeter.Greet()); + """; + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, programCode); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1"); + + // We cannot detect changes in referenced files, so we always rebuild. + Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1"); + + libCode = libCode.Replace("v1", "v2"); + File.WriteAllText(libPath, libCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello v2"); + } + + /// + /// optimization considers default items. + /// Also tests optimization. + /// (We cannot test because that optimization doesn't support neither #:property nor #:sdk which we need to enable default items.) + /// See . + /// + [Theory, CombinatorialData] + public void UpToDate_DefaultItems(bool optOut) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var code = $""" + {(optOut ? "#:property FileBasedProgramCanSkipMSBuild=false" : "")} + #:property EnableDefaultEmbeddedResourceItems=true + {s_programReadingEmbeddedResource} + """; + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), code); + + Build(testInstance, BuildLevel.All, expectedOutput: "Resource not found"); + + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); + + if (!optOut) + { + // Adding a default item is currently not recognized (https://github.com/dotnet/sdk/issues/50912). + Build(testInstance, BuildLevel.None, expectedOutput: "Resource not found"); + Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: "[MyString, TestValue]"); + } + else + { + 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.All, expectedOutput: "[MyString, UpdatedValue]"); + + // Update the C# file. + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v2\n" + code); + + Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: "[MyString, UpdatedValue]"); + + // Update the RESX file again (to verify the CSC only compilation didn't corrupt the list of additional files in the cache). + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue2")); + + Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, UpdatedValue2]"); + } + + /// + /// Similar to but for .razor files instead of .resx files. + /// + [Fact] + public void UpToDate_DefaultItems_Razor() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFileName = "MyRazorApp.cs"; + File.WriteAllText(Path.Join(testInstance.Path, programFileName), """ + #:sdk Microsoft.NET.Sdk.Web + _ = new MyRazorApp.MyCoolApp(); + Console.WriteLine("Hello from Program"); + """); + + var razorFilePath = Path.Join(testInstance.Path, "MyCoolApp.razor"); + File.WriteAllText(razorFilePath, ""); + + Build(testInstance, BuildLevel.All, programFileName: programFileName); + + Build(testInstance, BuildLevel.None, programFileName: programFileName); + + File.Delete(razorFilePath); + + new DotnetCommand(Log, "run", programFileName) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0246: The type or namespace name 'MyRazorApp' could not be found + .And.HaveStdOutContaining("error CS0246"); + } + + [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 = VirtualProjectBuilder.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); + } + + /// + /// Checks that the DOTNET_ROOT env var is set the same in csc mode as in msbuild mode. + /// + [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() + .Where(e => ((string)e.Key).StartsWith("DOTNET_ROOT"))) + { + Console.WriteLine($"{entry.Key}={entry.Value}"); + } + """); + + var expectedDotNetRoot = SdkTestContext.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"), ""); + + 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); + } + + /// + /// 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. + /// + [Fact] + public void CscOnly_IntermediateFiles() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Expression> e = () => 1 + 1; + Console.WriteLine(e); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.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"), ""); + + 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"), """ + + + + + + """); + + 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"); + } + + /// + /// If a file from a NuGet package (which would be used by CSC-only build) does not exist, full MSBuild should be used instead. + /// + [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 = VirtualProjectBuilder.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 = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v1", programFileName: programFileName); + } + + [Fact] // https://github.com/dotnet/sdk/issues/50778 + public void CscOnly_Args() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, s_program); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.Csc, args: ["test", "args"], expectedOutput: """ + echo args:test;args + Hello from Program + """); + } + + /// + /// Combination of and . + /// + [Fact] + public void CscOnly_SymbolicLink() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + var originalPath = Path.Join(testInstance.Path, "original.cs"); + var code = """ + #!/usr/bin/env dotnet + Console.WriteLine("v1"); + """; + var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + File.WriteAllText(originalPath, code, utf8NoBom); + + var programFileName = "linked"; + var programPath = Path.Join(testInstance.Path, programFileName); + + File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v1", programFileName: programFileName); + + Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName); + + code = code.Replace("v1", "v2"); + File.WriteAllText(originalPath, code, utf8NoBom); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); + } + + /// + /// Tests an optimization which remembers CSC args from prior MSBuild runs and can skip subsequent MSBuild invocations and call CSC directly. + /// This optimization kicks in when the file has some #: directives (then the simpler "hard-coded CSC args" optimization cannot be used). + /// + [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 = VirtualProjectBuilder.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 "); + } + + /// + /// See . + /// + [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 = VirtualProjectBuilder.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); + } + + /// + /// Testing optimization . + /// When compilation fails, the obj dll should not be copied to bin directory. + /// This prevents spurious errors if the dll file was not even produced by roslyn due to compilation errors. + /// + [Fact] + public void CscOnly_AfterMSBuild_CompilationFailure_NoCopyToBin() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + // First, create a valid program and build it successfully + var programPath = Path.Join(testInstance.Path, "Program.cs"); + var code = """ + #:property PublishAot=false + Console.WriteLine("version 1"); + """; + File.WriteAllText(programPath, code); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: "version 1"); + + // Verify that the dlls were created + var objDll = Path.Join(artifactsDir, "obj", "debug", "Program.dll"); + new FileInfo(objDll).Should().Exist(); + var binDll = Path.Join(artifactsDir, "bin", "debug", "Program.dll"); + new FileInfo(binDll).Should().Exist(); + + // Delete the dlls + File.Delete(objDll); + File.Delete(binDll); + + // Write invalid code that causes compilation to fail + code = code + "\n#error my custom error"; + File.WriteAllText(programPath, code); + + // Try to build the invalid code + new DotnetCommand(Log, "run", "-bl", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) + .And.HaveStdOutContaining("my custom error") + .And.HaveStdErrContaining(CliCommandStrings.RunCommandException); + + new FileInfo(objDll).Should().NotExist(); + new FileInfo(binDll).Should().NotExist(); + } + + /// + /// See . + /// + [Fact] + public void CscOnly_AfterMSBuild_Args() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + + var code = $""" + #:property Configuration=Release + {s_program} + """; + + File.WriteAllText(programPath, code); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, args: ["test", "args"], expectedOutput: """ + echo args:test;args + Hello from Program + Release config + """); + + code = code.Replace("Hello", "Hi"); + File.WriteAllText(programPath, code); + + Build(testInstance, BuildLevel.Csc, args: ["test", "args"], expectedOutput: """ + echo args:test;args + Hi from Program + Release config + """); + } + + /// + /// See . + /// If hard links are enabled, the bin/app.dll and obj/app.dll files are going to be the same, + /// so our "copy obj to bin" logic must account for that. + /// + [Fact] + public void CscOnly_AfterMSBuild_HardLinks() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + + var code = $""" + #:property CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true + #:property CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible=true + {s_program} + """; + + File.WriteAllText(programPath, code); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All); + + code = code.Replace("Hello", "Hi"); + File.WriteAllText(programPath, code); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "Hi from Program"); + } + + /// + /// Combination of and . + /// + [Fact] + public void CscOnly_AfterMSBuild_SymbolicLink() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + var originalPath = Path.Join(testInstance.Path, "original.cs"); + var code = """ + #!/usr/bin/env dotnet + #:property Configuration=Release + Console.WriteLine("v1"); + """; + var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + File.WriteAllText(originalPath, code, utf8NoBom); + + var programFileName = "linked"; + var programPath = Path.Join(testInstance.Path, programFileName); + + File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName); + + code = code.Replace("v1", "v2"); + File.WriteAllText(originalPath, code, utf8NoBom); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName); + } + + /// + /// Interaction of optimization and Directory.Build.props file. + /// + [Theory, CombinatorialData] + public void CscOnly_AfterMSBuild_DirectoryBuildProps(bool touch1, bool touch2) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var propsPath = Path.Join(testInstance.Path, "Directory.Build.props"); + var propsContent = """ + + + CustomAssemblyName + + + """; + File.WriteAllText(propsPath, propsContent); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + var programVersion = 0; + void WriteProgramContent() + { + programVersion++; + + // #: directive ensures we get CscOnly_AfterMSBuild optimization instead of CscOnly. + File.WriteAllText(programPath, $""" + #:property Configuration=Debug + Console.WriteLine("v{programVersion} " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Name); + """); + } + WriteProgramContent(); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} CustomAssemblyName"); + + File.Delete(propsPath); + + if (touch1) WriteProgramContent(); + + Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} Program"); + + File.WriteAllText(propsPath, propsContent); + + if (touch2) WriteProgramContent(); + + Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} CustomAssemblyName"); + } + + /// + /// See . + /// This optimization currently does not support #:project references and hence is disabled if those are present. + /// + [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"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + 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 = VirtualProjectBuilder.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); + } + + /// + /// See . + /// This optimization currently does not support #:ref references and hence is disabled if those are present. + /// Analogous to . + /// + [Fact] + public void CscOnly_AfterMSBuild_RefDirectives() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + var libPath = Path.Join(testInstance.Path, "lib.cs"); + var libCode = """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "v1"; + } + """; + File.WriteAllText(libPath, libCode); + + var programCode = """ + #:ref lib.cs + Console.WriteLine("Hello " + MyLib.Greeter.Greet()); + """; + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, programCode); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1"); + + programCode = programCode.Replace("Hello", "Hi"); + File.WriteAllText(programPath, programCode); + + libCode = libCode.Replace("v1", "v2"); + File.WriteAllText(libPath, libCode); + + // Cannot use CSC because we cannot detect updates in the referenced file. + Build(testInstance, BuildLevel.All, expectedOutput: "Hi v2"); + } + + /// + /// See . + /// If users have more complex build customizations, they can opt out of the optimization. + /// + [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"), $""" + + + <{propertyName}>{canSkipMSBuild} + + + """); + } + + 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 = VirtualProjectBuilder.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"); + } + + /// + /// See . + /// + [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 = VirtualProjectBuilder.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 "); + } + + /// + /// Verifies that csc.rsp is written to disk after a full MSBuild build, + /// so that IDEs can read it to create a virtual project. + /// + [Fact] + public void MSBuild_WritesCscRsp() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, """ + #:property Configuration=Release + Console.Write("Hello"); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + // A build directive forces a full MSBuild build. + Build(testInstance, BuildLevel.All, expectedOutput: "Hello"); + + // csc.rsp should be written to disk after a full MSBuild build. + var rspPath = Path.Join(artifactsDir, "csc.rsp"); + File.Exists(rspPath).Should().BeTrue("csc.rsp should be written after a full MSBuild build"); + File.ReadAllLines(rspPath).Should().NotBeEmpty("csc.rsp should contain compiler arguments"); + } + + /// + /// Verifies that csc.rsp is written to disk after dotnet build file.cs, + /// so that IDEs can read it to create a virtual project. + /// + [Fact] + public void DotnetBuild_WritesCscRsp() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, """ + Console.Write("Hello"); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + // csc.rsp should be written to disk after dotnet build. + var rspPath = Path.Join(artifactsDir, "csc.rsp"); + File.Exists(rspPath).Should().BeTrue("csc.rsp should be written after dotnet build file.cs"); + File.ReadAllLines(rspPath).Should().NotBeEmpty("csc.rsp should contain compiler arguments"); + } + + /// + /// Testing optimization when the NuGet cache is cleared between builds. + /// See . + /// + [Fact] + public void CscOnly_NuGetCacheCleared() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + var code = """ + Console.Write("v1"); + """; + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, code); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var packageDir = Path.Join(testInstance.Path, "packages"); + TestCommand CustomizeCommand(TestCommand command) => command.WithEnvironmentVariable("NUGET_PACKAGES", packageDir); + + Assert.False(Directory.Exists(packageDir)); + + // Ensure the packages exist first. + Build(testInstance, BuildLevel.All, expectedOutput: "v1", customizeCommand: CustomizeCommand); + + Assert.True(Directory.Exists(packageDir)); + + // Now clear the build outputs (but not packages) to verify CSC is used even from "first run". + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + code = code.Replace("v1", "v2"); + File.WriteAllText(programPath, code); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", customizeCommand: CustomizeCommand); + + code = code.Replace("v2", "v3"); + File.WriteAllText(programPath, code); + + // Clear NuGet cache. + Directory.Delete(packageDir, recursive: true); + Assert.False(Directory.Exists(packageDir)); + + Build(testInstance, BuildLevel.All, expectedOutput: "v3", customizeCommand: CustomizeCommand); + + Assert.True(Directory.Exists(packageDir)); + } + + /// + /// Combination of and . + /// + [Fact] + public void CscOnly_AfterMSBuild_NuGetCacheCleared() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + var code = """ + #:property PublishAot=false + #:package System.CommandLine@2.0.0-beta4.22272.1 + new System.CommandLine.RootCommand("v1"); + Console.WriteLine("v1"); + """; + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, code); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var packageDir = Path.Join(testInstance.Path, "packages"); + TestCommand CustomizeCommand(TestCommand command) => command.WithEnvironmentVariable("NUGET_PACKAGES", packageDir); + + Assert.False(Directory.Exists(packageDir)); + + Build(testInstance, BuildLevel.All, expectedOutput: "v1", customizeCommand: CustomizeCommand); + + Assert.True(Directory.Exists(packageDir)); + + code = code.Replace("v1", "v2"); + File.WriteAllText(programPath, code); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", customizeCommand: CustomizeCommand); + + code = code.Replace("v2", "v3"); + File.WriteAllText(programPath, code); + + // Clear NuGet cache. + Directory.Delete(packageDir, recursive: true); + Assert.False(Directory.Exists(packageDir)); + + Build(testInstance, BuildLevel.All, expectedOutput: "v3", customizeCommand: CustomizeCommand); + + Assert.True(Directory.Exists(packageDir)); + } + + private static string ToJson(string s) => JsonSerializer.Serialize(s); + + /// + /// Simplifies using interpolated raw strings with nested JSON, + /// e.g, in $$"""{x:{y:1}}""", the }} would result in an error. + /// + 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.AppHost.Sdk@9.1.0 + #:property TargetFramework=net5.0 + #:package System.CommandLine@2.0.0-beta4.22272.1 + #:property LangVersion=preview + Console.WriteLine(); + """); + + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); + new DotnetCommand(Log, "run-api") + .WithStandardInput($$""" + {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} + """) + .Execute() + .Should().Pass() + .And.HaveStdOut($$""" + {"$type":"Project","Version":1,"Content":{{ToJson($""" + + + + false + /artifacts + Program + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) + true + {programPath} + .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + + + + + + + net5.0 + preview + false + $(Features);FileBasedProgram + + + + + + + + + + + + + + + + + + + + + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics":[]} + """); + } + + /// + /// Directives should be evaluated before the project for run-api is constructed. + /// + [Fact] + public void Api_Evaluation() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "A.cs"); + File.WriteAllText(programPath, """ + #:property P1=cs + #:include B.$(P1) + Console.WriteLine(); + """); + + var bPath = Path.Join(testInstance.Path, "B.cs"); + File.WriteAllText(bPath, ""); + + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); + new DotnetCommand(Log, "run-api") + .WithStandardInput($$""" + {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} + """) + .Execute() + .Should().Pass() + .And.HaveStdOut($$""" + {"$type":"Project","Version":1,"Content":{{ToJson($""" + + + + false + /artifacts + A + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) + true + {programPath} + .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content + false + true + false + false + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + + + + + + cs + false + $(Features);FileBasedProgram + + + + + + + + + + + + + + + + + + + + """)}},"ProjectPath":{{ToJson(projectPath)}},"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 + """); + + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); + new DotnetCommand(Log, "run-api") + .WithStandardInput($$""" + {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} + """) + .Execute() + .Should().Pass() + .And.HaveStdOut($$""" + {"$type":"Project","Version":1,"Content":{{ToJson($""" + + + + false + /artifacts + Program + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) + true + {programPath} + .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content + false + true + false + false + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + + + + + + false + $(Features);FileBasedProgram + + + + + + + + + + + + + + + + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": + [{"Location":{ + "Path":{{ToJson(programPath)}}, + "Span":{"Start":{"Line":1,"Character":0},"End":{"Line":1,"Character":30}{{nop}}}{{nop}}}, + "Message":{{ToJson(FileBasedProgramsResources.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(); + """); + + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); + new DotnetCommand(Log, "run-api") + .WithStandardInput($$""" + {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} + """) + .Execute() + .Should().Pass() + .And.HaveStdOut($$""" + {"$type":"Project","Version":1,"Content":{{ToJson($""" + + + + false + /artifacts + Program + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) + true + {programPath} + .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content + false + true + false + false + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + + + + + + false + $(Features);FileBasedProgram + + + + + + + + + + + + + + + + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": + [{"Location":{ + "Path":{{ToJson(programPath)}}, + "Span":{"Start":{"Line":0,"Character":0},"End":{"Line":1,"Character":0}{{nop}}}{{nop}}}, + "Message":{{ToJson(string.Format(FileBasedProgramsResources.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 + """); + } + + [Fact] + public void Api_VirtualProjectBuilder_CreateProjectRootElement() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var libDir = Path.Join(testInstance.Path, "Lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + 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); + + var appPath = Path.Join(appDir, "Program.cs"); + File.WriteAllText(appPath, """ + #:project ../$(LibProjectName) + #:property LibProjectName=Lib + Console.WriteLine(Lib.LibClass.GetMessage()); + """); + + using var projectCollection = new ProjectCollection(); + var projectRootElement = NuGetVirtualProjectBuilder.Instance.CreateProjectRootElement(appPath, projectCollection); + + var xml = projectRootElement.RawXml; + Log.WriteLine(xml); + + xml.Should() + // directives are evaluated + .Contain("""""".Replace('\\', Path.DirectorySeparatorChar)) + // it's the virtual project + .And.Contain("true") + // correct target framework is used + .And.Contain($"{ToolsetInfo.CurrentTargetFramework}"); + + projectRootElement.FullPath.Should().Be(VirtualProjectBuilder.GetVirtualProjectPath(appPath)); + } + + [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 = VirtualProjectBuilder.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 EntryPointFilePath_SymbolicLink() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var fileName = "Program.cs"; + var programPath = Path.Join(testInstance.Path, fileName); + File.WriteAllText(programPath, """ + #!/usr/bin/env dotnet + var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string; + Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}"); + """); + + new DotnetCommand(Log, "run", fileName) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($"EntryPointFilePath: {programPath}"); + + var linkName = "linked"; + var linkPath = Path.Join(testInstance.Path, linkName); + File.CreateSymbolicLink(linkPath, programPath); + + new DotnetCommand(Log, "run", linkName) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($"EntryPointFilePath: {linkPath}"); + } + + [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); + } + + /// + /// Check that -get commands work the same in project-based and file-based apps. + /// + [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"), """ + + + + + + + + MyValue + + + + """); + + 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!.Replace("Program.cs.csproj", "Program.csproj").Should().Be(projectBasedResult.StdErr); + fileBasedResult.ExitCode.Should().Be(projectBasedResult.ExitCode).And.Be(success ? 0 : 1); + fileBasedFiles.Should().Equal(projectBasedFiles); + + Dictionary 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; + } + } + + /// + /// Regression test for https://github.com/dotnet/sdk/issues/52714. + /// The virtual project's must survive GC + /// even after being evicted from MSBuild's strong cache (LRU of size N). + /// We force eviction via MSBUILDPROJECTROOTELEMENTCACHESIZE=1 + /// and trigger GC via an inline task during NuGet restore. + /// Without the fix (strong reference in VirtualProjectBuilder._projectRootElement), + /// this fails with MSB4025 "The project file could not be loaded". + /// + [Fact] + public void VirtualProject_SurvivesGCDuringRestore() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("Hello from virtual project"); + """); + + // Directory.Build.targets that forces GC during restore, + // after SDK imports have already evicted the virtual PRE from the strong cache. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ + + + + + + + + <_ForceGCTask /> + + + """); + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs") + // A cache size of 1 ensures the virtual PRE is evicted from the strong cache + // as soon as any SDK .targets/.props file is loaded during evaluation. + .WithEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE", "1") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from virtual project"); + } + + /// + /// Same as but for #:ref referenced projects. + /// The referenced project's must also survive GC. + /// + [Fact] + public void VirtualProject_SurvivesGCDuringRestore_RefDirective() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello from ref"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:ref Lib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + // Directory.Build.targets that forces GC during restore, + // after SDK imports have already evicted the virtual PRE from the strong cache. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ + + + + + + + + <_ForceGCTask /> + + + """); + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs") + // A cache size of 1 ensures the virtual PRE is evicted from the strong cache + // as soon as any SDK .targets/.props file is loaded during evaluation. + .WithEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE", "1") + .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from ref"); + } +} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs new file mode 100644 index 000000000000..e4f228613be1 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs @@ -0,0 +1,1686 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Text; +using Microsoft.DotNet.Cli.Commands; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.FileBasedPrograms; +using Microsoft.DotNet.ProjectTools; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +public sealed class RunFileTests_Directives(ITestOutputHelper log) : RunFileTestBase(log) +{ + + [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"), """ + + + true + + + + + + """); + 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")] + [InlineData("$(MSBuildProjectDirectory)/../$(LibProjectName)")] + [InlineData(@"$(MSBuildProjectDirectory)/../Lib\$(LibProjectName).csproj")] + 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"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + 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} + #:property LibProjectName=Lib + Console.WriteLine(Lib.LibClass.GetMessage()); + """); + + var expectedOutput = "Hello from Lib"; + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(appDir) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Running from a different working directory shouldn't affect handling of the relative project paths. + new DotnetCommand(Log, "run", "App/Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Theory] + [InlineData(null)] + [InlineData("app")] + public void ProjectReference_Errors(string? subdir) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var relativeFilePath = Path.Join(subdir, "Program.cs"); + var filePath = Path.Join(testInstance.Path, relativeFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + File.WriteAllText(filePath, """ + #:project wrong.csproj + """); + + // Project file does not exist. + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, + string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "wrong.csproj")))); + + File.WriteAllText(filePath, """ + #:project dir/ + """); + + // Project directory does not exist. + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, + string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "dir/")))); + + Directory.CreateDirectory(Path.Join(testInstance.Path, subdir, "dir")); + + // Directory exists but has no project file. + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, + string.Format(FileBasedProgramsResources.CouldNotFindAnyProjectInDirectory, Path.Join(testInstance.Path, subdir, "dir/")))); + + File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj1.csproj"), ""); + File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj2.csproj"), ""); + + // Directory exists but has multiple project files. + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, + string.Format(FileBasedProgramsResources.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, subdir, "dir/")))); + + // Malformed MSBuild variable syntax. + File.WriteAllText(filePath, """ + #:project $(Test + """); + + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidProjectDirective, + string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, Path.Join(testInstance.Path, subdir, "$(Test")))); + } + + [Theory] + [InlineData(null)] + [InlineData("app")] + public void ProjectReference_Duplicate(string? subdir) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var relativeFilePath = Path.Join(subdir, "Program.cs"); + var filePath = Path.Join(testInstance.Path, relativeFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + Directory.CreateDirectory(Path.Join(testInstance.Path, subdir, "dir")); + File.WriteAllText(Path.Join(testInstance.Path, subdir, "dir", "proj1.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + File.WriteAllText(filePath, """ + #:project dir/ + #:project dir/ + Console.WriteLine("Hello"); + """); + + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 2, FileBasedProgramsResources.DuplicateDirective, "#:project dir/")); + + File.WriteAllText(filePath, """ + #:project dir/ + #:project dir/proj1.csproj + Console.WriteLine("Hello"); + """); + + // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate project reference + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello"); + + File.WriteAllText(filePath, """ + #:project dir/ + #:project $(MSBuildProjectDirectory)/dir/ + Console.WriteLine("Hello"); + """); + + // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate project reference + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello"); + } + + [Fact] + public void RefDirective() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet(string name) => $"Hello, {name}!"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib.cs + Console.WriteLine(MyLib.Greeter.Greet("World")); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, World!"); + } + + [Fact] + public void RefDirective_Subdirectory() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + var libDir = Path.Join(testInstance.Path, "lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "mylib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet(string name) => $"Hello, {name}!"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib/mylib.cs + Console.WriteLine(MyLib.Greeter.Greet("World")); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, World!"); + } + + /// + /// Analogous to but for #:ref. + /// + [Theory] + [InlineData(null)] + [InlineData("app")] + public void RefDirective_Errors(string? subdir) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + var relativeFilePath = Path.Join(subdir, "Program.cs"); + var filePath = Path.Join(testInstance.Path, relativeFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + + // Missing name. + File.WriteAllText(filePath, """ + #:ref + """); + + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.MissingDirectiveName, "ref")); + + // File does not exist. + File.WriteAllText(filePath, """ + #:ref nonexistent.cs + """); + + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidRefDirective, + string.Format(FileBasedProgramsResources.CouldNotFindRefFile, Path.Join(testInstance.Path, subdir, "nonexistent.cs")))); + } + + /// + /// Verifies that #:ref produces a metadata (assembly) reference, + /// meaning internal members are not accessible unless InternalsVisibleTo is used. + /// + [Fact] + public void RefDirective_InternalsNotAccessible() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class PublicClass + { + public static string PublicMethod() => "public"; + internal static string InternalMethod() => "internal"; + } + internal static class InternalClass + { + public static string Method() => "internal class"; + } + """); + + // Accessing internal member should fail. + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib.cs + Console.WriteLine(MyLib.PublicClass.InternalMethod()); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining("error CS"); + + // Accessing public member should succeed. + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib.cs + Console.WriteLine(MyLib.PublicClass.PublicMethod()); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("public"); + } + + /// + /// Verifies transitive #:ref references work: app.cs → lib1.cs → lib2.cs. + /// + [Fact] + public void RefDirective_Transitive() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + File.WriteAllText(Path.Join(testInstance.Path, "lib2.cs"), """ + #:property OutputType=Library + namespace Lib2; + public static class Base + { + public static string Value() => "from lib2"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "lib1.cs"), """ + #:property OutputType=Library + #:ref lib2.cs + namespace Lib1; + public static class Middle + { + public static string Value() => $"from lib1 and {Lib2.Base.Value()}"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib1.cs + Console.WriteLine(Lib1.Middle.Value()); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("from lib1 and from lib2"); + } + + /// + /// #:ref with various path formats (forward slashes, backslashes, MSBuild properties, parent dirs). + /// Analogous to . + /// + [Theory] + [InlineData("../Lib/lib.cs")] + [InlineData(@"..\Lib\lib.cs")] + [InlineData("$(MSBuildProjectDirectory)/../$(LibDirName)/lib.cs")] + [InlineData(@"$(MSBuildProjectDirectory)\..\Lib\lib.cs")] + public void RefDirective_PathFormats(string arg) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + var libDir = Path.Join(testInstance.Path, "Lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet(string name) => $"Hello, {name}!"; + } + """); + + var appDir = Path.Join(testInstance.Path, "App"); + Directory.CreateDirectory(appDir); + + File.WriteAllText(Path.Join(appDir, "app.cs"), $""" + #:ref {arg} + #:property LibDirName=Lib + Console.WriteLine(MyLib.Greeter.Greet("World")); + """); + + var expectedOutput = "Hello, World!"; + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(appDir) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Running from a different working directory shouldn't affect handling of the relative paths. + new DotnetCommand(Log, "run", "App/app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + /// + /// #:ref duplicate detection. + /// Analogous to . + /// + [Theory] + [InlineData(null)] + [InlineData("app")] + public void RefDirective_Duplicate(string? subdir) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + var relativeFilePath = Path.Join(subdir, "Program.cs"); + var filePath = Path.Join(testInstance.Path, relativeFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + + File.WriteAllText(Path.Join(testInstance.Path, subdir, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello!"; + } + """); + + File.WriteAllText(filePath, """ + #:ref lib.cs + #:ref lib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 2, FileBasedProgramsResources.DuplicateDirective, "#:ref lib.cs")); + + File.WriteAllText(filePath, """ + #:ref lib.cs + #:ref ./lib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate ref + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello!"); + + File.WriteAllText(filePath, """ + #:ref lib.cs + #:ref $(MSBuildProjectDirectory)/lib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + // https://github.com/dotnet/sdk/issues/51139: we should detect the duplicate ref + new DotnetCommand(Log, "run", relativeFilePath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello!"); + } + + /// + /// #:ref is an experimental feature that must be opted into. + /// Analogous to . + /// + [Fact] + public void RefDirective_FeatureFlag() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var libPath = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(libPath, """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello!"; + } + """); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, """ + #:ref lib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr($""" + {DirectiveError(programPath, 1, Resources.ExperimentalFeatureDisabled, CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective)} + + {CliCommandStrings.RunCommandException} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello!"); + } + + /// + /// Combining #:ref and #:include in the same file-based app. + /// + [Fact] + public void RefDirective_WithInclude() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" + + + <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true + + + """); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #!/usr/bin/env dotnet + #:property OutputType=Library + #:include LibHelper.cs + #:include LibFormatter.cs + namespace MyLib; + public static class Greeter + { + public static string Greet(string name) => LibFormatter.Format(LibHelper.Prefix, name); + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "LibHelper.cs"), """ + namespace MyLib; + public static class LibHelper + { + public static string Prefix => "Hello"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "LibFormatter.cs"), """ + namespace MyLib; + public static class LibFormatter + { + public static string Format(string prefix, string name) => $"{prefix}, {name}!"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + static class Util + { + public static string GetName() => "World"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #!/usr/bin/env dotnet + #:ref lib.cs + #:include Util.cs + Console.WriteLine(MyLib.Greeter.Greet(Util.GetName())); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, World!"); + } + + /// + /// A #:ref library can target a different framework (e.g., netstandard2.0) + /// than the referencing app (net10.0). + /// + [Fact] + public void RefDirective_DifferentTargetFramework() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + #:property TargetFramework=netstandard2.0 + #:property LangVersion=latest + #:property ImplicitUsings=disable + #:property PublishAot=false + namespace MyLib; + public static class Greeter + { + #if NETSTANDARD2_0 + public static string Greet() => "Hello from netstandard2.0!"; + #else + public static string Greet() => "Hello from other!"; + #endif + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib.cs + #if NET10_0_OR_GREATER + Console.WriteLine("App is net10.0+: " + MyLib.Greeter.Greet()); + #else + Console.WriteLine("App is older: " + MyLib.Greeter.Greet()); + #endif + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("App is net10.0+: Hello from netstandard2.0!"); + } + + /// + /// #:ref *.cs does not expand globs — it looks for a literal file named *.cs. + /// + [Fact] + public void RefDirective_Glob() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello!"; + } + """); + + var filePath = Path.Join(testInstance.Path, "app.cs"); + File.WriteAllText(filePath, """ + #:ref *.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(filePath, 1, FileBasedProgramsResources.InvalidRefDirective, + string.Format(FileBasedProgramsResources.CouldNotFindRefFile, Path.Join(testInstance.Path, "*.cs")))); + } + + /// + /// Verifies that cyclic #:ref references (lib1 → lib2 → lib1) do not cause an infinite loop. + /// + [Fact] + public void RefDirective_Cycle() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + EnableRefDirective(testInstance); + + File.WriteAllText(Path.Join(testInstance.Path, "lib1.cs"), """ + #:property OutputType=Library + #:ref lib2.cs + namespace Lib1; + public static class C1 { public static string Get() => "lib1"; } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "lib2.cs"), """ + #:property OutputType=Library + #:ref lib1.cs + namespace Lib2; + public static class C2 { public static string Get() => "lib2"; } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib1.cs + Console.WriteLine(Lib1.C1.Get()); + """); + + // Should not hang. The cycle is broken by processedFiles deduplication. + // error NU1108: Cycle detected. + // error NU1108: lib1 -> lib2 -> lib1. + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining("error NU1108"); + } + + /// + /// Two #:include'd files each have #:ref to the same library. + /// The deduplication via processedFiles should ensure the library is only processed once. + /// + [Fact] + public void RefDirective_DuplicateRefFromIncludedFiles() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" + + + <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true + + + """); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello!"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "helper1.cs"), """ + #:ref lib.cs + static class Helper1 + { + public static string Get() => MyLib.Greeter.Greet(); + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "helper2.cs"), """ + #:ref lib.cs + static class Helper2 + { + public static string Get() => MyLib.Greeter.Greet(); + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #!/usr/bin/env dotnet + #:include helper1.cs + #:include helper2.cs + Console.WriteLine(Helper1.Get() + " " + Helper2.Get()); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello! Hello!"); + } + + /// + /// Two #:include'd files in different directories each have #:ref to the same library + /// using different relative paths. Deduplication via processedFiles uses the resolved (absolute) path, + /// so the library is only processed once. + /// + [Fact] + public void RefDirective_DuplicateRefFromIncludedFiles_Subdirectories() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" + + + <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true + + + """); + + // lib.cs is in the root directory. + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello!"; + } + """); + + // helper1.cs is in sub1/, refers to lib.cs via ../lib.cs. + var sub1 = Path.Join(testInstance.Path, "sub1"); + Directory.CreateDirectory(sub1); + File.WriteAllText(Path.Join(sub1, "helper1.cs"), """ + #:ref ../lib.cs + static class Helper1 + { + public static string Get() => MyLib.Greeter.Greet(); + } + """); + + // helper2.cs is in sub2/nested/, refers to lib.cs via ../../lib.cs (different relative path, same resolved path). + var sub2 = Path.Join(testInstance.Path, "sub2", "nested"); + Directory.CreateDirectory(sub2); + File.WriteAllText(Path.Join(sub2, "helper2.cs"), """ + #:ref ../../lib.cs + static class Helper2 + { + public static string Get() => MyLib.Greeter.Greet(); + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #!/usr/bin/env dotnet + #:include sub1/helper1.cs + #:include sub2/nested/helper2.cs + Console.WriteLine(Helper1.Get() + " " + Helper2.Get()); + """); + + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello! Hello!"); + } + + /// + /// Both #:include and #:ref pointing at the same file. + /// The file ends up both compiled into the current assembly and referenced as a separate assembly. + /// This is expected to produce a compilation error (duplicate type definitions). + /// + [Fact] + public void RefDirective_IncludeAndRefSameFile() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" + + + <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true + + + """); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello!"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #:ref lib.cs + #:include lib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """); + + // The #:include brings in lib.cs's #:property OutputType=Library, making the app a library. + // error CS8805: Program using top-level statements must be an executable. + new DotnetCommand(Log, "run", "app.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining("error CS8805"); + } + + [Theory, CombinatorialData] + public void IncludeDirective( + [CombinatorialValues("Util.cs", "**/*.cs", "**/*.$(MyProp1)")] string includePattern, + [CombinatorialValues("", "#:exclude Program.$(MyProp1)")] string additionalDirectives) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #!/usr/bin/env dotnet + #:include {includePattern} + {additionalDirectives} + #:property MyProp1=cs + {s_programDependingOnUtil} + """); + + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + } + + [Fact] + public void IncludeDirective_WorkingDirectory() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var srcDir = Path.Join(testInstance.Path, "src"); + Directory.CreateDirectory(srcDir); + + var a = """ + Console.WriteLine(B.M()); + """; + + File.WriteAllText(Path.Join(srcDir, "A.cs"), $""" + #!/usr/bin/env dotnet + #:include B.cs + {a} + """); + + var b = """ + static class B { public static string M() => "Hello from B"; } + """; + + File.WriteAllText(Path.Join(srcDir, "B.cs"), b); + + var expectedOutput = "Hello from B"; + + new DotnetCommand(Log, "run", "src/A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert to a project. + new DotnetCommand(Log, "project", "convert", "src/A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .Should().HaveSubtree(""" + src/ + src/A.cs + src/A/ + src/A/A.cs + src/A/A.csproj + src/A/B.cs + src/B.cs + """) + .And.HaveFileContent("src/A/A.cs", a) + .And.HaveFileContent("src/A/B.cs", b) + .And.HaveFileContentPattern("src/A/A.csproj", """ + + + + Exe + net10.0 + enable + enable + true + true + A-* + + + + + """); + + // Run the converted project. + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "src/A")) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Fact] + public void IncludeDirective_Transitive() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + Directory.CreateDirectory(Path.Join(testInstance.Path, "dir1/dir2")); + Directory.CreateDirectory(Path.Join(testInstance.Path, "dir3")); + + var a = """ + B.M(); + """; + + File.WriteAllText(Path.Join(testInstance.Path, "dir1/A.cs"), $""" + #!/usr/bin/env dotnet + #:include dir2/B.cs + {a} + """); + + var b = """ + static class B { public static void M() { C.M(); } } + """; + + File.WriteAllText(Path.Join(testInstance.Path, "dir1/dir2/B.cs"), $""" + #:include ../../dir3/$(P1).cs + #:property P1=C + {b} + """); + + var c = """ + static class C { public static void M() { D.M(); } } + """; + + File.WriteAllText(Path.Join(testInstance.Path, "dir3/C.cs"), $""" + #:include ../$(P1).cs + {c} + """); + + var d = """ + static class D + { + public static void M() + { + var asm = System.Reflection.Assembly.GetExecutingAssembly(); + using var stream = asm.GetManifestResourceStream($"{asm.GetName().Name}.Resources.resources")!; + using var reader = new System.Resources.ResourceReader(stream); + Console.WriteLine(reader.Cast().Single()); + } + } + """; + + File.WriteAllText(Path.Join(testInstance.Path, "C.cs"), $""" + #:include Resources.resx + {d} + """); + + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); + + var expectedOutput = "[MyString, TestValue]"; + + new DotnetCommand(Log, "run", "A.cs") + .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1")) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert to a project. + new DotnetCommand(Log, "project", "convert", "A.cs") + .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1")) + .Execute() + .Should().Pass(); + + new DirectoryInfo(Path.Join(testInstance.Path, "dir1/A")) + .Should().HaveSubtree(""" + A.cs + A.csproj + C.cs + C_2.cs + Resources.resx + dir2/ + dir2/B.cs + """) + .And.HaveFileContent("A.cs", a) + .And.HaveFileContent("dir2/B.cs", b) + .And.HaveFileContent("C.cs", c) + .And.HaveFileContent("C_2.cs", d) + .And.HaveFileContent("Resources.resx", s_resx) + .And.HaveFileContentPattern("A.csproj", """ + + + + Exe + net10.0 + enable + enable + true + true + A-* + C + + + + + """); + + // Run the converted project. + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1/A")) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Fact] + public void IncludeDirective_FileNotFound() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "A.cs"); + + File.WriteAllText(programPath, """ + #:include B.cs + Console.WriteLine("Hello"); + """); + + new DotnetCommand(Log, "run", "A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(programPath, 1, Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "B.cs"))); + } + + /// + /// Combination of optimization and #:include directive. + /// + [Theory] + [InlineData("*")] + [InlineData("$(_Star)")] + [InlineData("Util?")] + public void IncludeDirective_UpToDate_Glob(string glob) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, $""" + #!/usr/bin/env dotnet + #:include {glob}.cs + #:property _Star=* + {s_programDependingOnUtil} + """); + + var utilPath = Path.Join(testInstance.Path, "Util1.cs"); + var utilCode = s_util; + File.WriteAllText(utilPath, utilCode); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var expectedOutput = "Hello, String from Util"; + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); + + utilCode = utilCode.Replace("String from Util", "v2"); + File.WriteAllText(utilPath, utilCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v2"); + + utilCode = utilCode.Replace("v2", "v3"); + File.WriteAllText(utilPath, utilCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v3"); + + var util2Path = Path.Join(testInstance.Path, "Util2.cs"); + File.WriteAllText(util2Path, """ + using System.Runtime.CompilerServices; + + file class C + { + [ModuleInitializer] + internal static void Initialize() + { + Console.WriteLine("Hello from Util2"); + } + } + """); + + Build(testInstance, BuildLevel.All, expectedOutput: """ + Hello from Util2 + Hello, v3 + """); + } + + /// + /// Combination of optimization and #:include directive. + /// + [Fact] + public void IncludeDirective_UpToDate_NoGlob() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, $""" + #!/usr/bin/env dotnet + #:include Util.cs + {s_programDependingOnUtil} + """); + + var utilPath = Path.Join(testInstance.Path, "Util.cs"); + var utilCode = s_util; + File.WriteAllText(utilPath, utilCode); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var expectedOutput = "Hello, String from Util"; + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); + + Build(testInstance, BuildLevel.None, expectedOutput: expectedOutput); + + utilCode = utilCode.Replace("String from Util", "v2"); + File.WriteAllText(utilPath, utilCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v2"); + + utilCode = utilCode.Replace("v2", "v3"); + File.WriteAllText(utilPath, utilCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v3"); + + var util2Path = Path.Join(testInstance.Path, "Util2.cs"); + File.WriteAllText(util2Path, """ + using System.Runtime.CompilerServices; + + file class C + { + [ModuleInitializer] + internal static void Initialize() + { + Console.WriteLine("Hello from Util2"); + } + } + """); + + Build(testInstance, BuildLevel.None, expectedOutput: "Hello, v3"); + + Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: "Hello, v3"); + } + + /// + /// Combination of test and #:include directive. + /// + [Fact] + public void IncludeDirective_UpToDate_ProjectReference() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var libDir = Path.Join(testInstance.Path, "Lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + var libPath = Path.Join(libDir, "Lib.cs"); + var libCode = """ + namespace Lib; + public class LibClass + { + public static string GetMessage() => "Lib(v1)"; + } + """; + File.WriteAllText(libPath, libCode); + + var appDir = Path.Join(testInstance.Path, "App"); + Directory.CreateDirectory(appDir); + + var utilPath = Path.Join(appDir, "Util.cs"); + var utilCode = """ + #:project ../Lib + class UtilClass + { + public static string GetMessage() => "Util(v1) " + Lib.LibClass.GetMessage(); + } + """; + File.WriteAllText(utilPath, utilCode); + + var programPath = Path.Join(appDir, "Program.cs"); + var programCode = """ + #!/usr/bin/env dotnet + #:include Util.cs + Console.WriteLine("Program(v1) " + UtilClass.GetMessage()); + """; + File.WriteAllText(programPath, programCode); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var expectedOutput = "Program(v1) Util(v1) Lib(v1)"; + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir); + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir); + + libCode = libCode.Replace("v1", "v2"); + File.WriteAllText(libPath, libCode); + + expectedOutput = "Program(v1) Util(v1) Lib(v2)"; + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir); + } + + [Fact] + public void IncludeDirective_CustomMapping() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, $""" + #!/usr/bin/env dotnet + #:property FileBasedProgramsItemMapping=.json=Content + #:include *.cs + {s_programDependingOnUtil} + """); + + var utilPath = Path.Join(testInstance.Path, "Util.cs"); + File.WriteAllText(utilPath, s_util); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr($""" + {DirectiveError(programPath, 3, FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", ".json")} + + {CliCommandStrings.RunCommandException} + """); + + File.WriteAllText(programPath, $""" + #!/usr/bin/env dotnet + #:property FileBasedProgramsItemMapping=.cs=Content + #:include *.cs + {s_programDependingOnUtil} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0103: The name 'Util' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + File.WriteAllText(programPath, $""" + #!/usr/bin/env dotnet + #:property FileBasedProgramsItemMapping=.cs=Compile + #:include *.cs + {s_programDependingOnUtil} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + } + + [Fact] + public void IncludeDirective_CustomMapping_ParseErrors() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, """ + #:property FileBasedProgramsItemMapping=x + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS5001: Program does not contain a static 'Main' method suitable for an entry point + .And.HaveStdOutContaining("error CS5001"); + + File.WriteAllText(programPath, """ + #:property FileBasedProgramsItemMapping=x + #:include *.* + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr($""" + {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingEntry, "x")} + + {CliCommandStrings.RunCommandException} + """); + + File.WriteAllText(programPath, """ + #:property FileBasedProgramsItemMapping=.=X;y + #:include *.* + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr($""" + {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingExtension, ".", ".=X")} + + {CliCommandStrings.RunCommandException} + """); + + File.WriteAllText(programPath, """ + #:property FileBasedProgramsItemMapping=.cs=;y + #:include *.* + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr($""" + {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingItemType, "", ".cs=")} + + {CliCommandStrings.RunCommandException} + """); + + File.WriteAllText(programPath, """ + #:property FileBasedProgramsItemMapping=.x=X;y + #:include *.* + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr($""" + {DirectiveError(programPath, 1, FileBasedProgramsResources.InvalidIncludeExcludeMappingEntry, "y")} + + {CliCommandStrings.RunCommandException} + """); + } + + /// + /// Demonstrates that consumers (e.g., IDE) can use the API to create an approximate virtual project without needing to know the full mapping. + /// + [Fact] + public void IncludeDirective_CustomMapping_Api() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + + var code = """ + #:include B.cs + #:include C.proto + Console.WriteLine(); + """; + + var builder = new VirtualProjectBuilder( + entryPointFileFullPath: programPath, + targetFramework: VirtualProjectBuildingCommand.TargetFramework, + sourceText: SourceText.From(code, Encoding.UTF8)); + + var directives = FileLevelDirectiveHelpers.FindDirectives( + builder.EntryPointSourceFile, + reportAllErrors: true, + VirtualProjectBuildingCommand.ThrowingReporter); + + ImmutableArray<(string Extension, string ItemType)> mapping = [(".cs", "Compile")]; + + var evaluatedBuilder = ImmutableArray.CreateBuilder(directives.Length); + + foreach (var directive in directives) + { + if (directive is CSharpDirective.IncludeOrExclude includeOrExcludeDirective) + { + var evaluated = includeOrExcludeDirective.WithDeterminedItemType(ErrorReporters.IgnoringReporter, mapping); + evaluatedBuilder.Add(evaluated); + } + else + { + evaluatedBuilder.Add(directive); + } + } + + var evaluatedDirectives = evaluatedBuilder.DrainToImmutable(); + + var projectWriter = new System.IO.StringWriter(); + VirtualProjectBuilder.WriteProjectFile( + projectWriter, + evaluatedDirectives, + VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFrameworkVersion), + isVirtualProject: true, + entryPointFilePath: programPath, + artifactsPath: builder.ArtifactsPath); + + var actualProject = projectWriter.ToString(); + + Log.WriteLine(actualProject); + + actualProject.Should().Contain(""""""); + + actualProject.Should().NotContain(".proto"); + } + + [Fact] + public void IncludeDirective_DefaultMapping_InSync() + { + var parsed = CSharpDirective.IncludeOrExclude.ParseMapping(CSharpDirective.IncludeOrExclude.DefaultMappingString, + sourceFile: default, + VirtualProjectBuildingCommand.ThrowingReporter); + parsed.Should().BeEquivalentTo(CSharpDirective.IncludeOrExclude.DefaultMapping); + } + + [Theory] // https://github.com/dotnet/aspnetcore/issues/63440 + [InlineData(true, null)] + [InlineData(false, null)] + [InlineData(true, "test-id")] + [InlineData(false, "test-id")] + public void UserSecrets(bool useIdArg, string? userSecretsId) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + string code = $""" + #:package Microsoft.Extensions.Configuration.UserSecrets@{CSharpCompilerCommand.RuntimeVersion} + {(userSecretsId is null ? "" : $"#:property UserSecretsId={userSecretsId}")} + + using Microsoft.Extensions.Configuration; + + IConfigurationRoot config = new ConfigurationBuilder() + .AddUserSecrets() + .Build(); + + Console.WriteLine("v1"); + Console.WriteLine(config.GetDebugView()); + """; + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, code); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + if (useIdArg) + { + if (userSecretsId == null) + { + var result = new DotnetCommand(Log, "build", "-getProperty:UserSecretsId", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute(); + result.Should().Pass(); + userSecretsId = result.StdOut!.Trim(); + } + + new DotnetCommand(Log, "user-secrets", "set", "MySecret", "MyValue", "--id", userSecretsId) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + } + else + { + new DotnetCommand(Log, "user-secrets", "set", "MySecret", "MyValue", "--file", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + } + + Build(testInstance, BuildLevel.All, expectedOutput: """ + v1 + MySecret=MyValue (JsonConfigurationProvider for 'secrets.json' (Optional)) + """); + + code = code.Replace("v1", "v2"); + File.WriteAllText(programPath, code); + + Build(testInstance, BuildLevel.Csc, expectedOutput: """ + v2 + MySecret=MyValue (JsonConfigurationProvider for 'secrets.json' (Optional)) + """); + } +} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_General.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_General.cs new file mode 100644 index 000000000000..8ed39476b63e --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_General.cs @@ -0,0 +1,1180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.Cli.Commands; +using Microsoft.DotNet.FileBasedPrograms; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +public sealed class RunFileTests_General(ITestOutputHelper log) : RunFileTestBase(log) +{ + + /// + /// dotnet run file.cs succeeds without a project file. + /// + [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, + "--project")); + } + } + + /// + /// dotnet file.cs is equivalent to dotnet run file.cs. + /// + [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 + """); + + // https://github.com/dotnet/sdk/issues/52108 + new DotnetCommand(Log, "Program.cs", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:Program.cs + Hello from Program + Release config + """); + } + + /// + /// Casing of the argument is used for the output binary name. + /// + [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, + "--project")); + } + } + + /// + /// dotnet run folder/file.cs succeeds without a project file. + /// + [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"); + } + + /// + /// dotnet run --project file.cs fails. + /// + [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); + } + + /// + /// Even if there is a file-based app ./build, dotnet build should not execute that. + /// + [Theory] + // error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file. + [InlineData("build", "MSB1003", false)] + // dotnet watch: Could not find a MSBuild project file in '...'. Specify which project to use with the --project option. + [InlineData("watch", "--project", true)] + public void Precedence_BuiltInCommand(string cmd, string error, bool errorInStdErr) + { + 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 + var failure = new DotnetCommand(Log, cmd) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail(); + + if (errorInStdErr) + { + failure.And.HaveStdErrContaining(error); + } + else + { + failure.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"); + } + + /// + /// Even if there is a file-based app ./test.dll, dotnet test.dll should not execute that. + /// + [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"); + } + + /// + /// dotnet run - reads the C# code from stdin. + /// + [Fact] + public void ReadFromStdin() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(testInstance.Path) + .WithStandardInput(""" + Console.WriteLine("Hello from stdin"); + Console.WriteLine("Read: " + (Console.ReadLine() ?? "null")); + Console.WriteLine("Working directory: " + Environment.CurrentDirectory); + """) + .Execute() + .Should().Pass() + .And.HaveStdOut($""" + Hello from stdin + Read: null + Working directory: {testInstance.Path} + """); + } + + /// + /// Directory.Build.props doesn't have any effect on dotnet run -. + /// + [Fact] + public void ReadFromStdin_BuildProps() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + disable + + + """); + + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(testInstance.Path) + .WithStandardInput(""" + Console.WriteLine("Hello from stdin"); + """) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from stdin"); + + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(testInstance.Path) + .WithStandardInput(""" + #:property ImplicitUsings=disable + Console.WriteLine("Hello from stdin"); + """) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + + /// + /// Directory.Build.props doesn't have any effect on dotnet run -. + /// + [Fact] + public void ReadFromStdin_ProjectReference() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var libDir = Path.Join(testInstance.Path, "lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + + + """); + + 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); + + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(appDir) + .WithStandardInput($""" + #:project $(MSBuildStartupDirectory)/../lib + Console.WriteLine(Lib.LibClass.GetMessage()); + """) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from Lib"); + + // Relative paths are resolved from the isolated temp directory, hence they don't work. + + var errorParts = DirectiveError("app.cs", 1, FileBasedProgramsResources.InvalidProjectDirective, + string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, "{}")).Split("{}"); + errorParts.Should().HaveCount(2); + + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(appDir) + .WithStandardInput($""" + #:project ../lib + Console.WriteLine(Lib.LibClass.GetMessage()); + """) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(errorParts[0]) + .And.HaveStdErrContaining(errorParts[1]); + } + + /// + /// dotnet run - with #:ref uses $(MSBuildStartupDirectory) to resolve paths. + /// Relative paths don't work from stdin since the file is in an isolated temp directory. + /// Analogous to . + /// + [Fact] + public void ReadFromStdin_RefDirective() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var libDir = Path.Join(testInstance.Path, "lib"); + Directory.CreateDirectory(libDir); + + File.WriteAllText(Path.Join(libDir, "mylib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet() => "Hello from lib!"; + } + """); + + var appDir = Path.Join(testInstance.Path, "app"); + Directory.CreateDirectory(appDir); + + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(appDir) + .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") + .WithStandardInput(""" + #:ref $(MSBuildStartupDirectory)/../lib/mylib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from lib!"); + + // Relative paths are resolved from the isolated temp directory, hence they don't work. + + var errorParts = DirectiveError("app.cs", 1, FileBasedProgramsResources.InvalidRefDirective, + string.Format(FileBasedProgramsResources.CouldNotFindRefFile, "{}")).Split("{}"); + errorParts.Should().HaveCount(2); + + new DotnetCommand(Log, "run", "-") + .WithWorkingDirectory(appDir) + .WithEnvironmentVariable(CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, "true") + .WithStandardInput(""" + #:ref ../lib/mylib.cs + Console.WriteLine(MyLib.Greeter.Greet()); + """) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(errorParts[0]) + .And.HaveStdErrContaining(errorParts[1]); + } + + [Fact] + public void ReadFromStdin_NoBuild() + { + new DotnetCommand(Log, "run", "-", "--no-build") + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionForStdin, "--no-build")); + } + + [Fact] + public void ReadFromStdin_LaunchProfile() + { + new DotnetCommand(Log, "run", "-", "--launch-profile=test") + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionForStdin, "--launch-profile")); + } + + /// + /// dotnet run -- - should NOT read the C# file from stdin, + /// the hyphen should be considred an app argument instead since it's after --. + /// + [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, "--project")); + } + + /// + /// dotnet run folder without a project file is not supported. + /// + [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, + "--project")); + } + + /// + /// dotnet run app.csproj fails if app.csproj does not exist. + /// + [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, + "--project")); + } + + /// + /// dotnet run app.csproj where app.csproj exists + /// runs the project and passes 'app.csproj' as an argument. + /// + [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.NotHaveStdErr() + .And.HaveStdOut(""" + 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.NotHaveStdErr() + .And.HaveStdOut("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.NotHaveStdErr() + .And.HaveStdOut("Hello from Program"); + } + + /// + /// dotnet run --project App.csproj Program.cs does not warn + /// because --project was explicitly specified. + /// + [Fact] + public void ProjectInCurrentDirectory_ProjectOption_NoWarning() + { + 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", "--project", "App.csproj", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.NotHaveStdErr() + .And.HaveStdOut(""" + echo args:Program.cs + Hello from App + """); + } + + /// + /// dotnet run file.cs in a directory with a project file warns + /// because file.cs is passed as an application argument to the project instead of running as a file-based program. + /// + [Fact] + public void ProjectInCurrentDirectory_Warns() + { + 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", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:Program.cs + Hello from App + """) + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, + "Program.cs", + Path.Join(testInstance.Path, "App.csproj"))); + } + + /// + /// dotnet run nonexistent.cs in a directory with a project file warns + /// even though the file does not exist, because the .cs extension suggests it was intended as a file-based program. + /// + [Fact] + public void ProjectInCurrentDirectory_NonExistentCsFile_Warns() + { + 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", "nonexistent.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:nonexistent.cs + Hello from App + """) + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.RunCommandWarningCsFileArgumentPassedToProject, + "nonexistent.cs", + Path.Join(testInstance.Path, "App.csproj"))); + } + + /// + /// dotnet run -- file.cs in a directory with a project file does not warn + /// because -- signals that the arguments are intentional. + /// + [Fact] + public void ProjectInCurrentDirectory_DoubleDash_NoWarning() + { + 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", "--", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:Program.cs + Hello from App + """) + .And.NotHaveStdErr(); + } + + /// + /// dotnet run file.cs -- other still warns because file.cs appears before --. + /// + [Fact] + public void ProjectInCurrentDirectory_DoubleDashAfterFile_Warns() + { + 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", "Program.cs", "--", "otherArg") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:Program.cs;otherArg + Hello from App + """) + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, + "Program.cs", + Path.Join(testInstance.Path, "App.csproj"))); + } + + /// + /// dotnet run someArg file.cs in a directory with a project warns + /// when an unrecognized argument prevents file.cs from being treated as a file-based program entry point. + /// + [Fact] + public void ProjectInCurrentDirectory_UnrecognizedArg_Warns() + { + 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", "someArg", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:someArg;Program.cs + Hello from App + """) + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, + "Program.cs", + Path.Join(testInstance.Path, "App.csproj"))); + } + + /// + /// dotnet run -c Release Program.cs in a directory with a project warns because + /// known options like -c don't suppress the warning; only --project, --file, or -- do. + /// + [Fact] + public void ProjectInCurrentDirectory_KnownOption_Warns() + { + 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", "-c", "Release", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:Program.cs + Hello from App + Release config + """) + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, + "Program.cs", + Path.Join(testInstance.Path, "App.csproj"))); + } + + /// + /// dotnet run someArg -- file.cs does not warn because the .cs file is after --. + /// + [Fact] + public void ProjectInCurrentDirectory_UnrecognizedArg_DoubleDash_NoWarning() + { + 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", "someArg", "--", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + echo args:someArg;Program.cs + Hello from App + """) + .And.NotHaveStdErr(); + } + + /// + /// dotnet build someArg Program.cs warns because 'Program.cs' is a valid file-based entry point + /// but additional positional arguments cause it to fall back to MSBuild. + /// + [Theory] + [InlineData("build", "someArg", "Program.cs")] + [InlineData("clean", "someArg", "Program.cs")] + [InlineData("publish", "someArg", "Program.cs")] + [InlineData("build", "Program.cs", "-consoleLoggerParameters:NoSummary")] + public void ExtraArgWithFileEntryPoint_Warns(string command, string arg1, string arg2) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); + + new DotnetCommand(Log, command, arg1, arg2) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.WarningFileArgumentPassedToMSBuild, + "Program.cs", + command)); + } + + /// + /// dotnet build nonexistent.cs warns because the .cs extension suggests it was intended as a file-based program. + /// + [Theory] + [InlineData("build")] + [InlineData("clean")] + [InlineData("publish")] + public void NonExistentCsFile_Warns(string command) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + new DotnetCommand(Log, command, "nonexistent.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(string.Format( + CliCommandStrings.WarningCsFileArgumentPassedToMSBuild, + "nonexistent.cs", + command)); + } + + /// + /// dotnet build --no-incremental Program.cs is handled as file-based (known option + single positional arg) and does not warn. + /// + [Theory] + [InlineData("Program.cs")] + [InlineData("--no-incremental", "Program.cs")] + public void SingleFileEntryPoint_NoWarning(params string[] extraArgs) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); + + new DotnetCommand(Log, ["build", .. extraArgs]) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.NotHaveStdErr(); + } + + /// + /// When a file is not a .cs file, we probe the first characters of the file for #!, and + /// execute as a single file program if we find them. + /// + [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"); + } + + /// + /// When a file is not a .cs file, we probe the first characters of the file for #!, and + /// fall back to normal dotnet run behavior if we don't find them. + /// + [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, + "--project")); + } + + [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"); + } + + /// + /// When the entry-point file does not exist, fallback to normal dotnet run behavior. + /// + [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, + "--project")); + } + + /// + /// Cannot run a non-entry-point file. + /// + [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 + } + + /// + /// When the entry-point file does not exist, fallback to normal dotnet run behavior. + /// + [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, + "--project")); + } + + /// + /// Other files in the folder are not part of the compilation. + /// See . + /// + [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"), $""" + #!/usr/bin/env dotnet + #:property EnableDefaultCompileItems=true + {s_programDependingOnUtil} + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + } + + /// + /// Setting EnableDefaultCompileItems=true via Directory.Build.props should not cause CS2002 warning. + /// See . + /// + [Fact] + public void MultipleFiles_EnableDefaultCompileItemsViaDirectoryBuildProps() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #!/usr/bin/env dotnet + {s_programDependingOnUtil} + """); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + true + + + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + } + + /// + /// Directives in other files are considered even if those files are included via manual MSBuild rather than #:include. + /// + [Fact] + public void MultipleFiles_DirectivesInOtherFiles() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "A.cs"), """ + #!/usr/bin/env dotnet + Console.WriteLine(B.M()); + #if !DEBUG + Console.WriteLine("Release config"); + #endif + """); + File.WriteAllText(Path.Join(testInstance.Path, "B.cs"), """ + #:property Configuration=Release + public static class B + { + public static string M() => "String from Util"; + } + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" + + + + + + """); + + new DotnetCommand(Log, "run", "A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + String from Util + Release config + """); + } + + /// + /// dotnet run util.cs fails if util.cs is not the entry-point. + /// + [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 + } + + /// + /// If there are nested project files like + /// + /// app/file.cs + /// app/nested/x.csproj + /// app/nested/another.cs + /// + /// executing dotnet run app/file.cs will include the nested .cs 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. + /// + [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"); + } + + /// + /// dotnet run folder/app.csproj -> the argument is not recognized as an entry-point file + /// (it does not have .cs file extension), so this fallbacks to normal dotnet run behavior. + /// + [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, + "--project")); + } +} From f23cc8af89ba0ee92c4ace0c88282724846db0d9 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 22 Apr 2026 14:33:31 +0200 Subject: [PATCH 2/3] Add RunFileTestFixture to RunFileTestBase matching main branch Adds the fixture class that: - Copies NuGet.config to the runfile base directory - Ensures a simple app runs fully with MSBuild before other tests so packages like ILLink.Tasks are restored and csc-only optimization works Adapted for xUnit v2 (Task instead of ValueTask). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CommandTests/Run/RunFileTestBase.cs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs index 71d0e567721c..92748afb5994 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs @@ -5,13 +5,34 @@ using Microsoft.Build.Logging.StructuredLogger; using Microsoft.DotNet.Cli.Commands; using Microsoft.DotNet.Cli.Commands.Run; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.FileBasedPrograms; using Microsoft.DotNet.ProjectTools; namespace Microsoft.DotNet.Cli.Run.Tests; -public abstract class RunFileTestBase(ITestOutputHelper log) : SdkTest(log) +public sealed class RunFileTestFixture(IMessageSink sink) : IAsyncLifetime +{ + public System.Threading.Tasks.Task InitializeAsync() + { + RunFileTestBase.CopyNuGetConfigToRunfileDirectory(); + + // Ensure a simple app runs fully with MSBuild before running other csc-only tests + // so we have packages like ILLink.Tasks restored and csc-only optimization can kick in. + new DotnetCommand(new SharedTestOutputHelper(sink), "run", "-") + .WithStandardInput(""" + Console.WriteLine("Hello"); + """) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello"); + + return System.Threading.Tasks.Task.CompletedTask; + } + + public System.Threading.Tasks.Task DisposeAsync() => System.Threading.Tasks.Task.CompletedTask; +} + +public abstract class RunFileTestBase(ITestOutputHelper log) : SdkTest(log), IClassFixture { internal static string s_includeExcludeDefaultKnownExtensions => field ??= string.Join(", ", CSharpDirective.IncludeOrExclude.DefaultMapping.Select(static e => e.Extension)); @@ -134,6 +155,20 @@ private static string PrepareOutOfTreeBaseDirectory() return outOfTreeBaseDirectory; } + /// + /// Copies NuGet.config to the runfile base directory so virtual projects created by + /// dotnet run - (stdin) can resolve packages from test feeds. The virtual project + /// is created under this directory, and NuGet walks up from the project location to + /// find config files. + /// + internal static void CopyNuGetConfigToRunfileDirectory() + { + var sourceNuGetConfig = Path.Join(SdkTestContext.Current.TestExecutionDirectory, "NuGet.config"); + var runfileDir = VirtualProjectBuilder.GetTempSubdirectory(); + Directory.CreateDirectory(runfileDir); + File.Copy(sourceNuGetConfig, Path.Join(runfileDir, "NuGet.config"), overwrite: true); + } + internal static string DirectiveError(string path, int line, string messageFormat, params ReadOnlySpan args) { return $"{path}({line}): {FileBasedProgramsResources.DirectiveError}: {string.Format(messageFormat, args)}"; From 19da28b2679f9b603756090d7aa2c62a6e77e81e Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 22 Apr 2026 15:21:12 +0200 Subject: [PATCH 3/3] Fix Pack tests: set NUGET_PACKAGES to avoid cache interference Port fix from main branch - the tool exec commands in Pack and Pack_CustomPath tests need an isolated NUGET_PACKAGES directory to avoid picking up cached packages from other test runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs index 2e4d17f9d313..3f7b981df166 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs @@ -675,6 +675,7 @@ public void Pack() // Run the packed tool. new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", packageDir.FullName) + .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages")) .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() @@ -718,6 +719,7 @@ public void Pack_CustomPath() // Run the packed tool. new DotnetCommand(Log, "tool", "exec", "MyFileBasedTool", "--yes", "--add-source", outputDir) + .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages")) .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass()