diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
index fdf1036f70c7..80c286978143 100644
--- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
+++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
@@ -203,6 +203,456 @@ public static void M()
""");
}
+ [Fact]
+ public void RefDirective()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ 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"));
+ """);
+
+ var expectedOutput = "Hello, World!";
+
+ new DotnetCommand(Log, "run", "app.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ // #:ref lib.cs should become a ProjectReference to ../lib/lib.csproj
+ File.ReadAllText(Path.Join(outputDirFullPath, "app", "app.csproj"))
+ .Should().Contain($"""
+
+ """);
+
+ // The referenced library should have been converted too.
+ var libProjectDir = Path.Join(outputDirFullPath, "lib");
+ File.Exists(Path.Join(libProjectDir, "lib.csproj")).Should().BeTrue();
+ File.Exists(Path.Join(libProjectDir, "lib.cs")).Should().BeTrue();
+ File.ReadAllText(Path.Join(libProjectDir, "lib.csproj"))
+ .Should().Contain("Library");
+
+ // The converted project should build and produce the same output.
+ new DotnetCommand(Log, "run")
+ .WithWorkingDirectory(Path.Join(outputDirFullPath, "app"))
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
+ }
+
+ [Fact]
+ public void RefDirective_Transitive_Convert()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "lib2.cs"), """
+ #:property OutputType=Library
+ namespace Lib2;
+ public static class Helper
+ {
+ public static string Get() => "from lib2";
+ }
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "lib1.cs"), """
+ #:property OutputType=Library
+ #:ref lib2.cs
+ namespace Lib1;
+ public static class Facade
+ {
+ public static string Get() => $"from lib1 and {Lib2.Helper.Get()}";
+ }
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """
+ #:ref lib1.cs
+ Console.WriteLine(Lib1.Facade.Get());
+ """);
+
+ var expectedOutput = "from lib1 and from lib2";
+
+ new DotnetCommand(Log, "run", "app.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ // All three projects should exist.
+ File.Exists(Path.Join(outputDirFullPath, "app", "app.csproj")).Should().BeTrue();
+ File.Exists(Path.Join(outputDirFullPath, "lib1", "lib1.csproj")).Should().BeTrue();
+ File.Exists(Path.Join(outputDirFullPath, "lib2", "lib2.csproj")).Should().BeTrue();
+
+ // lib1.csproj should reference lib2.
+ File.ReadAllText(Path.Join(outputDirFullPath, "lib1", "lib1.csproj"))
+ .Should().Contain($"""
+
+ """);
+
+ // The converted project should build and produce the same output.
+ new DotnetCommand(Log, "run")
+ .WithWorkingDirectory(Path.Join(outputDirFullPath, "app"))
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(expectedOutput);
+ }
+
+ [Fact]
+ public void RefDirective_DuplicateFolderName()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ Directory.CreateDirectory(Path.Join(testInstance.Path, "a"));
+ File.WriteAllText(Path.Join(testInstance.Path, "a", "lib.cs"), """
+ #:property OutputType=Library
+ namespace A;
+ public static class Lib { public static string Get() => "a"; }
+ """);
+
+ Directory.CreateDirectory(Path.Join(testInstance.Path, "b"));
+ File.WriteAllText(Path.Join(testInstance.Path, "b", "lib.cs"), """
+ #:property OutputType=Library
+ namespace B;
+ public static class Lib { public static string Get() => "b"; }
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """
+ #:ref a/lib.cs
+ #:ref b/lib.cs
+ Console.WriteLine(A.Lib.Get() + B.Lib.Get());
+ """);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ var duplicateTargetDirectory = Path.Join(outputDirFullPath, "lib");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(CliCommandStrings.ProjectConvertDuplicateRefFolderName, duplicateTargetDirectory));
+
+ // Nothing should have been converted.
+ Directory.Exists(outputDirFullPath).Should().BeFalse();
+
+ new DirectoryInfo(testInstance.Path)
+ .EnumerateFileSystemInfos().Select(d => d.Name).Order()
+ .Should().BeEquivalentTo(["a", "app.cs", "b", "Directory.Build.props"]);
+ }
+
+ [Fact]
+ public void RefDirective_DuplicateFolderName_Transitive()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ // a/lib.cs is referenced by mid.cs
+ Directory.CreateDirectory(Path.Join(testInstance.Path, "a"));
+ File.WriteAllText(Path.Join(testInstance.Path, "a", "lib.cs"), """
+ #:property OutputType=Library
+ namespace A;
+ public static class Lib { public static string Get() => "a"; }
+ """);
+
+ // mid.cs references a/lib.cs
+ File.WriteAllText(Path.Join(testInstance.Path, "mid.cs"), """
+ #:property OutputType=Library
+ #:ref a/lib.cs
+ namespace Mid;
+ public static class Mid { public static string Get() => A.Lib.Get(); }
+ """);
+
+ // b/lib.cs would conflict with a/lib.cs (both "lib")
+ Directory.CreateDirectory(Path.Join(testInstance.Path, "b"));
+ File.WriteAllText(Path.Join(testInstance.Path, "b", "lib.cs"), """
+ #:property OutputType=Library
+ namespace B;
+ public static class Lib { public static string Get() => "b"; }
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """
+ #:ref mid.cs
+ #:ref b/lib.cs
+ Console.WriteLine(Mid.Mid.Get() + B.Lib.Get());
+ """);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ var duplicateTargetDirectory = Path.Join(outputDirFullPath, "lib");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(CliCommandStrings.ProjectConvertDuplicateRefFolderName, duplicateTargetDirectory));
+
+ // Nothing should have been converted.
+ Directory.Exists(outputDirFullPath).Should().BeFalse();
+ }
+
+ [Fact]
+ public void RefDirective_DuplicateFolderName_ViaInclude()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ // a/lib.cs is referenced by the app directly
+ Directory.CreateDirectory(Path.Join(testInstance.Path, "a"));
+ File.WriteAllText(Path.Join(testInstance.Path, "a", "lib.cs"), """
+ #:property OutputType=Library
+ namespace A;
+ public static class Lib { public static string Get() => "a"; }
+ """);
+
+ // b/lib.cs would conflict (same name "lib") - referenced via #:include-d file
+ Directory.CreateDirectory(Path.Join(testInstance.Path, "b"));
+ File.WriteAllText(Path.Join(testInstance.Path, "b", "lib.cs"), """
+ #:property OutputType=Library
+ namespace B;
+ public static class Lib { public static string Get() => "b"; }
+ """);
+
+ // extra.cs is included and references b/lib.cs
+ File.WriteAllText(Path.Join(testInstance.Path, "extra.cs"), """
+ #:ref b/lib.cs
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """
+ #:ref a/lib.cs
+ #:include extra.cs
+ Console.WriteLine(A.Lib.Get() + B.Lib.Get());
+ """);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ var duplicateTargetDirectory = Path.Join(outputDirFullPath, "lib");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(CliCommandStrings.ProjectConvertDuplicateRefFolderName, duplicateTargetDirectory));
+
+ // Nothing should have been converted.
+ Directory.Exists(outputDirFullPath).Should().BeFalse();
+ }
+
+ ///
+ /// Verifies that default items (e.g., appsettings.json) in a #:ref'd file's directory
+ /// are copied to the converted project output directory.
+ ///
+ [Fact]
+ public void RefDirective_IncludedItemsCopied()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ var libDir = Path.Join(testInstance.Path, "lib");
+ Directory.CreateDirectory(libDir);
+
+ File.WriteAllText(Path.Join(libDir, "mylib.cs"), """
+ #:property OutputType=Library
+ #:property EnableDefaultNoneItems=true
+ namespace MyLib;
+ public static class Greeter
+ {
+ public static string Greet() => "Hello!";
+ }
+ """);
+
+ // A non-code file next to the library that should be picked up as a default item.
+ File.WriteAllText(Path.Join(libDir, "data.json"), """{ "key": "value" }""");
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """
+ #:ref lib/mylib.cs
+ Console.WriteLine(MyLib.Greeter.Greet());
+ """);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ // The library's included item (data.json) should be copied to the ref output directory.
+ var libOutputDir = Path.Join(outputDirFullPath, "mylib");
+ File.Exists(Path.Join(libOutputDir, "mylib.cs")).Should().BeTrue();
+ File.Exists(Path.Join(libOutputDir, "mylib.csproj")).Should().BeTrue();
+ File.Exists(Path.Join(libOutputDir, "data.json")).Should().BeTrue();
+ }
+
+ ///
+ /// Verifies that --delete-source also deletes included items of #:ref'd files.
+ ///
+ [Fact]
+ public void RefDirective_IncludedItemsDeleted()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ var libDir = Path.Join(testInstance.Path, "lib");
+ Directory.CreateDirectory(libDir);
+
+ File.WriteAllText(Path.Join(libDir, "mylib.cs"), """
+ #:property OutputType=Library
+ #:property EnableDefaultNoneItems=true
+ namespace MyLib;
+ public static class Greeter
+ {
+ public static string Greet() => "Hello!";
+ }
+ """);
+
+ File.WriteAllText(Path.Join(libDir, "config.json"), """{ "setting": true }""");
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """
+ #:ref lib/mylib.cs
+ Console.WriteLine(MyLib.Greeter.Greet());
+ """);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath, "--delete-source")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ // Source files should be deleted.
+ File.Exists(Path.Join(testInstance.Path, "app.cs")).Should().BeFalse();
+ File.Exists(Path.Join(libDir, "mylib.cs")).Should().BeFalse();
+ File.Exists(Path.Join(libDir, "config.json")).Should().BeFalse();
+
+ // Converted files should exist.
+ var libOutputDir = Path.Join(outputDirFullPath, "mylib");
+ File.Exists(Path.Join(libOutputDir, "mylib.cs")).Should().BeTrue();
+ File.Exists(Path.Join(libOutputDir, "mylib.csproj")).Should().BeTrue();
+ File.Exists(Path.Join(libOutputDir, "config.json")).Should().BeTrue();
+ }
+
+ ///
+ /// Converting one app that #:refs a library does not affect other apps that also reference the same library.
+ ///
+ [Fact]
+ public void RefDirective_ConvertScope()
+ {
+ var testInstance = TestAssetsManager.CreateTestDirectory();
+
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
+
+
+ <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ 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, "app1.cs"), """
+ #:ref lib.cs
+ Console.WriteLine(MyLib.Greeter.Greet());
+ """);
+
+ File.WriteAllText(Path.Join(testInstance.Path, "app2.cs"), """
+ #:ref lib.cs
+ Console.WriteLine(MyLib.Greeter.Greet());
+ """);
+
+ var unrelatedDir = Path.Join(testInstance.Path, "unrelated");
+ Directory.CreateDirectory(unrelatedDir);
+ File.WriteAllText(Path.Join(unrelatedDir, "app3.cs"), """
+ #:ref ../lib.cs
+ Console.WriteLine(MyLib.Greeter.Greet());
+ """);
+
+ var outputDirFullPath = Path.Join(testInstance.Path, "Project");
+ new DotnetCommand(Log, "project", "convert", "app1.cs", "-o", outputDirFullPath)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ // app1 should be converted.
+ File.Exists(Path.Join(outputDirFullPath, "app1", "app1.csproj")).Should().BeTrue();
+ File.Exists(Path.Join(outputDirFullPath, "lib", "lib.csproj")).Should().BeTrue();
+
+ // app2 and app3 should be unaffected (still exist as .cs files with their directives intact).
+ File.ReadAllText(Path.Join(testInstance.Path, "app2.cs")).Should().Contain("#:ref lib.cs");
+ File.ReadAllText(Path.Join(unrelatedDir, "app3.cs")).Should().Contain("#:ref ../lib.cs");
+ }
+
[Fact]
public void ProjectReference_FullPath_WithVars()
{
@@ -1237,15 +1687,6 @@ public void Directives_IncludeExclude()
{
var testInstance = TestAssetsManager.CreateTestDirectory();
- File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
-
-
- true
- true
-
-
- """);
-
VerifyConversion(
baseDirectory: testInstance.Path,
evaluateDirectives: true,
@@ -1290,8 +1731,6 @@ public void Directives_IncludeExclude_FilesCopied()
{
var testInstance = TestAssetsManager.CreateTestDirectory();
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
- #:property ExperimentalFileBasedProgramEnableIncludeDirective=true
- #:property ExperimentalFileBasedProgramEnableExcludeDirective=true
#:include **/*.cs
#:include *.json
#:exclude my.json
@@ -1950,7 +2389,7 @@ private static void Convert(
builder.CreateProjectInstance(
new ProjectCollection(),
errorReporter,
- out _,
+ project: out _,
projectRootElement: out _,
out directives);
}
@@ -2210,7 +2649,6 @@ public void DeleteSource_WithIncludeDirective_NotDeleted()
// Create entry point file with #:include directive
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
- #:property ExperimentalFileBasedProgramEnableIncludeDirective=true
#:include Util.cs
Console.WriteLine("Test");
""");
@@ -2241,7 +2679,6 @@ public void DeleteSource_WithIncludeDirective_MultipleFiles()
// Create entry point file with multiple #:include directives
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
- #:property ExperimentalFileBasedProgramEnableIncludeDirective=true
#:include Util.cs
#:include Helper.cs
#:include config.json
@@ -2279,8 +2716,6 @@ public void DeleteSource_WithIncludeDirective_Transitive()
// Create entry point file with #:include directive
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
- #:property ExperimentalFileBasedProgramEnableIncludeDirective=true
- #:property ExperimentalFileBasedProgramEnableTransitiveDirectives=true
#:include Util.cs
Console.WriteLine("Test");
""");
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs
index 91e58c83c1e0..794c62fd380b 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTestBase.cs
@@ -176,6 +176,24 @@ internal static string DirectiveError(string path, int line, string messageForma
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{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+ }
+
internal static void VerifyBinLogEvaluationDataCount(string binaryLogPath, int expectedCount)
{
var records = BinaryLog.ReadRecords(binaryLogPath).ToList();
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs
index ad5224a00f76..9552b75f6d17 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs
@@ -922,5 +922,4 @@ 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
index 14ed5a0473bc..260fcffa2a97 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs
@@ -636,7 +636,6 @@ public void BinaryLog_EvaluationData()
VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: 3);
}
-
///
/// Binary logs from our in-memory projects should have evaluation data.
///
@@ -645,14 +644,6 @@ public void BinaryLog_EvaluationData_MultiFile()
{
var testInstance = TestAssetsManager.CreateTestDirectory();
- File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
-
-
- true
-
-
- """);
-
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"),
$"""
#!/usr/bin/env dotnet
@@ -813,6 +804,116 @@ public void Verbosity_CompilationDiagnostics()
.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.
///
@@ -900,5 +1001,4 @@ public void EmbeddedResource_AlongsideProj([CombinatorialValues("sln", "slnx", "
.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
index 35a845b28917..dfdec8997a9e 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
@@ -3,7 +3,9 @@
using System.Text.Json;
using Basic.CompilerLog.Util;
+using Microsoft.Build.Evaluation;
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;
@@ -282,6 +284,50 @@ public class LibClass
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.
@@ -989,6 +1035,52 @@ public class LibClass
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.
@@ -1325,14 +1417,6 @@ public void Api_Evaluation()
{
var testInstance = TestAssetsManager.CreateTestDirectory();
- File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
-
-
- true
-
-
- """);
-
var programPath = Path.Join(testInstance.Path, "A.cs");
File.WriteAllText(programPath, """
#:property P1=cs
@@ -1603,6 +1687,57 @@ public void Api_RunCommand()
""");
}
+ [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)
{
@@ -1833,6 +1968,109 @@ Dictionary ReadFiles()
}
}
+ ///
+ /// 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");
+ }
+
///
/// Verifies that msbuild-based runs use CSC args equivalent to csc-only runs.
/// Can regenerate CSC arguments template in .
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs
index 06ff227764d0..95a32f497891 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_Directives.cs
@@ -314,6 +314,687 @@ public void ProjectReference_Duplicate(string? subdir)
.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{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ 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{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ 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{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ // 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{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>
+
+
+ """);
+
+ 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,
@@ -730,31 +1411,6 @@ class UtilClass
Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput, workDir: appDir);
}
- [Fact]
- public void IncludeDirective_FeatureFlags()
- {
- var testInstance = TestAssetsManager.CreateTestDirectory();
-
- var programPath = Path.Join(testInstance.Path, "Program.cs");
- File.WriteAllText(programPath, $"""
- #!/usr/bin/env dotnet
- #:include *.cs
- {s_programDependingOnUtil}
- """);
-
- var utilPath = Path.Join(testInstance.Path, "Util.cs");
- File.WriteAllText(utilPath, $"""
- #:exclude Other.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_CustomMapping()
{
@@ -762,6 +1418,7 @@ public void IncludeDirective_CustomMapping()
var programPath = Path.Join(testInstance.Path, "Program.cs");
File.WriteAllText(programPath, $"""
+ #!/usr/bin/env dotnet
#:property FileBasedProgramsItemMapping=.json=Content
#:include *.cs
{s_programDependingOnUtil}
@@ -775,12 +1432,13 @@ public void IncludeDirective_CustomMapping()
.Execute()
.Should().Fail()
.And.HaveStdErr($"""
- {DirectiveError(programPath, 2, FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", ".json")}
+ {DirectiveError(programPath, 3, FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", ".json")}
{CliCommandStrings.RunCommandException}
""");
File.WriteAllText(programPath, $"""
+ #!/usr/bin/env dotnet
#:property FileBasedProgramsItemMapping=.cs=Content
#:include *.cs
{s_programDependingOnUtil}
@@ -1024,5 +1682,4 @@ public void UserSecrets(bool useIdArg, string? userSecretsId)
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
index 1e2b5664f9c7..27ea8456623a 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_General.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_General.cs
@@ -453,6 +453,61 @@ public class LibClass
.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()
{
@@ -546,7 +601,8 @@ public void ProjectPath_Exists()
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Pass()
- .And.HaveStdOutContaining("""
+ .And.NotHaveStdErr()
+ .And.HaveStdOut("""
echo args:./App.csproj
Hello from App
""");
@@ -565,9 +621,8 @@ public void ProjectInCurrentDirectory_NoRunVerb()
.WithWorkingDirectory(Path.Join(testInstance.Path, "proj"))
.Execute()
.Should().Pass()
- .And.HaveStdOutContaining("""
- Hello from Program
- """);
+ .And.NotHaveStdErr()
+ .And.HaveStdOut("Hello from Program");
}
[Fact]
@@ -583,11 +638,263 @@ public void ProjectInCurrentDirectory_FileOption()
.WithWorkingDirectory(Path.Join(testInstance.Path, "proj"))
.Execute()
.Should().Pass()
- .And.HaveStdOutContaining("""
- Hello from Program
+ .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.
@@ -789,9 +1096,6 @@ public static class B
""");
File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
-
- true
-