diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
index 54b265c2350f..6848047eba4f 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs
@@ -16,1024 +16,524 @@ 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()
+ public void UpToDate()
{
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);
+ 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(entryPointPath);
+ var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs"));
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}");
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "Hello v1");
- // 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);
+ Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1");
- // 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.
+ Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1");
- using System.Text.Json;
+ // Change the source file (a rebuild is necessary).
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
- namespace Microsoft.DotNet.Cli.Commands.Run;
+ Build(testInstance, BuildLevel.Csc);
- // 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;
- }
+ Build(testInstance, BuildLevel.None);
- // We don't need to generate a ref assembly.
- if (arg.StartsWith("/refout:", StringComparison.Ordinal))
- {
- continue;
- }
+ // Change an unrelated source file (no rebuild necessary).
+ File.WriteAllText(Path.Join(testInstance.Path, "Program2.cs"), "test");
- // There should be no source link arguments.
- if (arg.StartsWith("/sourcelink:", StringComparison.Ordinal))
- {
- Assert.Fail($"Unexpected source link argument: {arg}");
- }
+ Build(testInstance, BuildLevel.None);
- // PreferredUILang is normally not set by default but can be in builds, so ignore it.
- if (arg.StartsWith("/preferreduilang:", StringComparison.Ordinal))
- {
- continue;
- }
+ // Add an implicit build file (a rebuild is necessary).
+ string buildPropsFile = Path.Join(testInstance.Path, "Directory.Build.props");
+ File.WriteAllText(buildPropsFile, """
+
+
+ $(DefineConstants);CUSTOM_DEFINE
+
+
+ """);
- bool needsInterpolation = false;
- bool fromNuGetPackage = false;
+ Build(testInstance, BuildLevel.All, expectedOutput: """
+ Hello from Program
+ Custom define
+ """);
- // Normalize slashes in paths.
- string rewritten = NormalizePathArg(arg);
+ Build(testInstance, BuildLevel.None, expectedOutput: """
+ Hello from Program
+ Custom define
+ """);
- // Remove quotes.
- rewritten = RemoveQuotes(rewritten);
+ // Change the implicit build file (a rebuild is necessary).
+ string importedFile = Path.Join(testInstance.Path, "Settings.props");
+ File.WriteAllText(importedFile, """
+
+
+ """);
+ File.WriteAllText(buildPropsFile, """
+
+
+
+ """);
- string msbuildArgToVerify = rewritten;
+ Build(testInstance, BuildLevel.All);
- // Use variable SDK path.
- if (rewritten.Contains(sdkPath, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(sdkPath, "{SdkPath}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ // Change the imported build file (this is not recognized).
+ File.WriteAllText(importedFile, """
+
+
+ $(DefineConstants);CUSTOM_DEFINE
+
+
+ """);
- // Use variable .NET root path.
- if (rewritten.Contains(dotNetRootPath, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(dotNetRootPath, "{DotNetRootPath}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ Build(testInstance, BuildLevel.None);
- // Use variable NuGet cache path.
- if (rewritten.Contains(nuGetCachePath, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(nuGetCachePath, "{NuGetCachePath}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- fromNuGetPackage = true;
- }
+ // Force rebuild.
+ Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: """
+ Hello from Program
+ Custom define
+ """);
- // 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}";
+ // Remove an implicit build file (a rebuild is necessary).
+ File.Delete(buildPropsFile);
+ Build(testInstance, BuildLevel.Csc);
- if (isOut)
- {
- msbuildArgToVerify = msbuildArgToVerify.Replace("/obj/", "/bin/", StringComparison.OrdinalIgnoreCase);
- }
+ // Force rebuild.
+ Build(testInstance, BuildLevel.All, args: ["--no-cache"]);
- rewritten = rewritten.Replace(objPath, replacement, StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ Build(testInstance, BuildLevel.None);
- // Use variable file path.
- if (rewritten.Contains(entryPointPathNormalized, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(entryPointPathNormalized, "{" + nameof(CSharpCompilerCommand.EntryPointFileFullPath) + "}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ // Pass argument (no rebuild necessary).
+ Build(testInstance, BuildLevel.None, args: ["--", "test-arg"], expectedOutput: """
+ echo args:test-arg
+ Hello from Program
+ """);
- // Use variable file name.
- if (rewritten.Contains(fileName, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(fileName, "{FileName}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ // Change config (a rebuild is necessary).
+ Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: """
+ Hello from Program
+ Release config
+ """);
- // Use variable program name.
- if (rewritten.Contains(programName, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(programName, "{FileNameWithoutExtension}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ // Keep changed config (no rebuild necessary).
+ Build(testInstance, BuildLevel.None, args: ["-c", "Release"], expectedOutput: """
+ Hello from Program
+ Release config
+ """);
- // Use variable runtime version.
- if (rewritten.Contains(CSharpCompilerCommand.RuntimeVersion, StringComparison.OrdinalIgnoreCase))
- {
- rewritten = rewritten.Replace(CSharpCompilerCommand.RuntimeVersion, "{" + nameof(CSharpCompilerCommand.RuntimeVersion) + "}", StringComparison.OrdinalIgnoreCase);
- needsInterpolation = true;
- }
+ // Change config back (a rebuild is necessary).
+ Build(testInstance, BuildLevel.Csc);
- // Ignore `/analyzerconfig` which is not variable (so it comes from the machine or sdk repo).
- if (!needsInterpolation && arg.StartsWith("/analyzerconfig", StringComparison.Ordinal))
- {
- continue;
- }
+ // 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.
- // Use GetFrameworkReferenceArguments() for framework references instead of hard-coding them.
- if (arg.StartsWith("/reference:", StringComparison.Ordinal))
- {
- if (!referenceSpreadInserted)
- {
- code.AppendLine("""
- .. GetFrameworkReferenceArguments(),
- """);
- referenceSpreadInserted = true;
- }
+ // A rebuild is necessary since the last build failed.
+ Build(testInstance, BuildLevel.Csc);
+ }
- msbuildArgsToVerify.Add(msbuildArgToVerify);
- continue;
- }
+ [Fact]
+ public void UpToDate_InvalidOptions()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
- // 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;
- }
+ 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"));
+ }
- string prefix = needsInterpolation ? "$" : string.Empty;
+ ///
+ /// optimization should see through symlinks.
+ /// See .
+ ///
+ [Fact]
+ public void UpToDate_SymbolicLink()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
- code.AppendLine($"""
- {prefix}"{rewritten}",
- """);
+ 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);
- msbuildArgsToVerify.Add(msbuildArgToVerify);
+ var programFileName = "linked";
+ var programPath = Path.Join(testInstance.Path, programFileName);
- if (fromNuGetPackage)
- {
- nuGetPackageFilePaths.Add(CSharpCompilerCommand.IsPathOption(rewritten, out int colonIndex)
- ? rewritten.Substring(colonIndex + 1)
- : rewritten);
- }
- }
- code.AppendLine("""
- ];
- }
+ File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
- ///
- /// 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("""
- ];
- }
- """);
+ // Remove artifacts from possible previous runs of this test.
+ var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath);
+ if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
- // 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;
- }
+ Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
- var emittedFileContent = File.ReadAllText(emittedFile);
+ Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
- string interpolatedString = emittedFileContent;
- string interpolationPrefix;
+ code = code.Replace("v1", "v2");
+ File.WriteAllText(originalPath, code, utf8NoBom);
- 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 + "}");
- }
- }
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ }
- if (interpolatedString == emittedFileContent)
- {
- interpolationPrefix = "";
- }
+ ///
+ /// Similar to but with a chain of symlinks.
+ ///
+ [Fact]
+ public void UpToDate_SymbolicLink2()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
- code.AppendLine($$""""
+ 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);
- private string Get{{generatedMethodName}}Content()
- {
- return {{interpolationPrefix}}"""
- {{interpolatedString}}
- """;
- }
- """");
- }
+ var intermediateFileName = "linked1";
+ var intermediatePath = Path.Join(testInstance.Path, intermediateFileName);
- code.AppendLine("""
- }
- """);
+ File.CreateSymbolicLink(path: intermediatePath, pathToTarget: originalPath);
- // 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}");
- }
- }
- }
+ var programFileName = "linked2";
+ var programPath = Path.Join(testInstance.Path, programFileName);
- // 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}
- """);
+ File.CreateSymbolicLink(path: programPath, pathToTarget: intermediatePath);
- // Read args from csc.rsp file.
- var rspFilePath = Path.Join(artifactsDir, "csc.rsp");
- var cscOnlyCallArgs = File.ReadAllLines(rspFilePath);
- var cscOnlyCallArgsString = string.Join(' ', cscOnlyCallArgs);
+ // Remove artifacts from possible previous runs of this test.
+ var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath);
+ if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
- // 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));
+ Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
- // 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");
+ Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
- static CompilerCall FindCompilerCall(string binaryLogPath)
- {
- using var reader = BinaryLogReader.Create(binaryLogPath);
- return reader.ReadAllCompilerCalls().Should().ContainSingle().Subject;
- }
+ code = code.Replace("v1", "v2");
+ File.WriteAllText(originalPath, code, utf8NoBom);
- 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);
- }
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ }
- static string NormalizePath(string path)
- {
- return PathUtility.GetPathWithForwardSlashes(TestPathUtility.ResolveTempPrefixLink(path));
- }
+ ///
+ /// 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();
- static string RemoveQuotes(string arg)
- {
- return arg.Replace("\"", string.Empty);
- }
+ var libDir = Path.Join(testInstance.Path, "Lib");
+ Directory.CreateDirectory(libDir);
- static string? GetGeneratedMethodName(string assetFileName)
- {
- return assetFileName switch
+ File.WriteAllText(Path.Join(libDir, "Lib.csproj"), $"""
+
+
+ {ToolsetInfo.CurrentTargetFramework}
+
+
+ """);
+
+ var libPath = Path.Join(libDir, "Lib.cs");
+ var libCode = """
+ namespace Lib;
+ public class LibClass
{
- $".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,
- };
- }
- }
+ public static string GetMessage() => "Hello from Lib v1";
+ }
+ """;
+ File.WriteAllText(libPath, libCode);
- ///
- /// 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}
- """);
+ var appDir = Path.Join(testInstance.Path, "App");
+ Directory.CreateDirectory(appDir);
- string programName = Path.GetFileNameWithoutExtension(fileName);
+ 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(entryPointPath);
+ var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath);
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);
+ var programFileName = "App/Program.cs";
- // Build using MSBuild.
- new DotnetCommand(Log, "run", fileName, "-bl", "--no-cache")
- .WithWorkingDirectory(testInstance.Path)
- .Execute()
- .Should().Pass()
- .And.HaveStdOut($"Hello from {programName}");
+ Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName);
- // 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);
+ // We cannot detect changes in referenced projects, so we always rebuild.
+ Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName);
- if (!File.Exists(msbuildFile))
- {
- throw new InvalidOperationException($"File exists in CSC-only run but not in MSBuild run: {cscOnlyFile}");
- }
+ libCode = libCode.Replace("v1", "v2");
+ File.WriteAllText(libPath, libCode);
- 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");
+ 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()
+ public void UpToDate_RefDirectives()
{
- var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
- File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
- Console.WriteLine("Hello v1");
- """);
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ EnableRefDirective(testInstance);
- // 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);
+ 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);
- Build(testInstance, BuildLevel.Csc, expectedOutput: "Hello v1");
+ var programCode = """
+ #:ref lib.cs
+ Console.WriteLine("Hello " + MyLib.Greeter.Greet());
+ """;
- Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1");
+ var programPath = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(programPath, programCode);
- Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1");
+ // Remove artifacts from possible previous runs of this test.
+ var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath);
+ if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
- // Change the source file (a rebuild is necessary).
- File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
+ Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1");
- Build(testInstance, BuildLevel.Csc);
+ // We cannot detect changes in referenced files, so we always rebuild.
+ Build(testInstance, BuildLevel.All, expectedOutput: "Hello v1");
- Build(testInstance, BuildLevel.None);
+ libCode = libCode.Replace("v1", "v2");
+ File.WriteAllText(libPath, libCode);
- // Change an unrelated source file (no rebuild necessary).
- File.WriteAllText(Path.Join(testInstance.Path, "Program2.cs"), "test");
+ Build(testInstance, BuildLevel.All, expectedOutput: "Hello v2");
+ }
- Build(testInstance, BuildLevel.None);
+ ///
+ /// 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);
- // 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: "Resource not found");
- Build(testInstance, BuildLevel.All, expectedOutput: """
- Hello from Program
- Custom define
- """);
+ File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx);
- Build(testInstance, BuildLevel.None, expectedOutput: """
- Hello from Program
- Custom define
- """);
+ 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]");
+ }
- // Change the implicit build file (a rebuild is necessary).
- string importedFile = Path.Join(testInstance.Path, "Settings.props");
- File.WriteAllText(importedFile, """
-
-
- """);
- File.WriteAllText(buildPropsFile, """
-
-
-
- """);
+ // Update the RESX file.
+ File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue"));
- Build(testInstance, BuildLevel.All);
+ Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, UpdatedValue]");
- // Change the imported build file (this is not recognized).
- File.WriteAllText(importedFile, """
-
-
- $(DefineConstants);CUSTOM_DEFINE
-
-
- """);
+ // Update the C# file.
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v2\n" + code);
- Build(testInstance, BuildLevel.None);
+ Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: "[MyString, UpdatedValue]");
- // Force rebuild.
- Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: """
- Hello from Program
- Custom define
- """);
+ // 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"));
- // Remove an implicit build file (a rebuild is necessary).
- File.Delete(buildPropsFile);
- Build(testInstance, BuildLevel.Csc);
+ Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, UpdatedValue2]");
+ }
- // 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
+ ///
+ /// 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");
""");
- // Change config back (a rebuild is necessary).
- Build(testInstance, BuildLevel.Csc);
+ var razorFilePath = Path.Join(testInstance.Path, "MyCoolApp.razor");
+ File.WriteAllText(razorFilePath, "");
- // 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.
+ Build(testInstance, BuildLevel.All, programFileName: programFileName);
- // A rebuild is necessary since the last build failed.
- Build(testInstance, BuildLevel.Csc);
- }
+ Build(testInstance, BuildLevel.None, programFileName: programFileName);
- [Fact]
- public void UpToDate_InvalidOptions()
- {
- var testInstance = _testAssetsManager.CreateTestDirectory();
- File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program);
+ File.Delete(razorFilePath);
- new DotnetCommand(Log, "run", "Program.cs", "--no-cache", "--no-build")
+ new DotnetCommand(Log, "run", programFileName)
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Fail()
- .And.HaveStdErrContaining(string.Format(CliCommandStrings.CannotCombineOptions, "--no-cache", "--no-build"));
+ // error CS0246: The type or namespace name 'MyRazorApp' could not be found
+ .And.HaveStdOutContaining("error CS0246");
}
- ///
- /// optimization should see through symlinks.
- /// See .
- ///
[Fact]
- public void UpToDate_SymbolicLink()
+ public void CscOnly()
{
- var testInstance = _testAssetsManager.CreateTestDirectory();
+ var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
- var originalPath = Path.Join(testInstance.Path, "original.cs");
- var code = """
- #!/usr/bin/env dotnet
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
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);
+ var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs"));
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
- Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v1");
- Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
+ Console.WriteLine("v2");
+ #if !DEBUG
+ Console.WriteLine("Release config");
+ #endif
+ """);
- code = code.Replace("v1", "v2");
- File.WriteAllText(originalPath, code, utf8NoBom);
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2");
- Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ // Customizing a property forces MSBuild to be used.
+ Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: """
+ v2
+ Release config
+ """);
}
- ///
- /// Similar to but with a chain of symlinks.
- ///
[Fact]
- public void UpToDate_SymbolicLink2()
+ public void CscOnly_CompilationDiagnostics()
{
- 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);
+ var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
- Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
+ string x = null;
+ Console.WriteLine("ran" + x);
+ """);
- Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
+ 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");
- code = code.Replace("v1", "v2");
- File.WriteAllText(originalPath, code, utf8NoBom);
+ File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
+ Console.Write
+ """);
- Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ 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);
}
///
- /// optimization currently does not support #:project references and hence is disabled if those are present.
- /// See .
+ /// Checks that the DOTNET_ROOT env var is set the same in csc mode as in msbuild mode.
///
[Fact]
- public void UpToDate_ProjectReferences()
+ public void CscOnly_DotNetRoot()
{
- 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
+ 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")))
{
- public static string GetMessage() => "Hello from Lib v1";
+ Console.WriteLine($"{entry.Key}={entry.Value}");
}
- """;
- 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);
+ var expectedDotNetRoot = SdkTestContext.Current.ToolsetUnderTest.DotNetRoot;
- // Remove artifacts from possible previous runs of this test.
- var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath);
- if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
+ var cscResult = new DotnetCommand(Log, "run", "Program.cs", "-bl")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute();
- var programFileName = "App/Program.cs";
+ cscResult.Should().Pass()
+ .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
+ .And.HaveStdOutContaining("DOTNET_ROOT")
+ .And.HaveStdOutContaining($"={expectedDotNetRoot}");
- Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName);
+ // Add an implicit build file to force use of msbuild instead of csc.
+ File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), "");
- // We cannot detect changes in referenced projects, so we always rebuild.
- Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v1", programFileName: programFileName);
+ var msbuildResult = new DotnetCommand(Log, "run", "Program.cs", "-bl")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute();
- libCode = libCode.Replace("v1", "v2");
- File.WriteAllText(libPath, libCode);
+ msbuildResult.Should().Pass()
+ .And.NotHaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc)
+ .And.HaveStdOutContaining("DOTNET_ROOT")
+ .And.HaveStdOutContaining($"={expectedDotNetRoot}");
- Build(testInstance, BuildLevel.All, expectedOutput: "v1 Hello from Lib v2", programFileName: programFileName);
+ // 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);
}
///
- /// optimization currently does not support #:ref references and hence is disabled if those are present.
- /// Analogous to .
+ /// 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 UpToDate_RefDirectives()
+ public void CscOnly_IntermediateFiles()
{
- 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);
- """);
+ 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"));
@@ -2432,143 +1932,642 @@ public void MSBuildGet_Consistent(bool success, string subcommand, params string
""");
- var fileBasedResult = new DotnetCommand(Log, [subcommand, "Program.cs", .. args])
+ 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");
+ }
+
+ ///
+ /// 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();
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut($"""
+ {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc}
+ Hello from {programName}
+ """);
- var fileBasedFiles = ReadFiles();
+ // Read args from csc.rsp file.
+ var rspFilePath = Path.Join(artifactsDir, "csc.rsp");
+ var cscOnlyCallArgs = File.ReadAllLines(rspFilePath);
+ var cscOnlyCallArgsString = string.Join(' ', cscOnlyCallArgs);
- File.WriteAllText(Path.Join(testInstance.Path, "Program.csproj"), s_consoleProject);
+ // 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));
- var projectBasedResult = new DotnetCommand(Log, [subcommand, .. args])
- .WithWorkingDirectory(testInstance.Path)
- .Execute();
+ // 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");
- var projectBasedFiles = ReadFiles();
+ static CompilerCall FindCompilerCall(string binaryLogPath)
+ {
+ using var reader = BinaryLogReader.Create(binaryLogPath);
+ return reader.ReadAllCompilerCalls().Should().ContainSingle().Subject;
+ }
- 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);
+ 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);
+ }
- Dictionary ReadFiles()
+ static string NormalizePath(string path)
{
- 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));
+ return PathUtility.GetPathWithForwardSlashes(TestPathUtility.ResolveTempPrefixLink(path));
+ }
- foreach (var (file, text) in result)
- {
- Log.WriteLine($"File '{file}':");
- Log.WriteLine(text);
- File.Delete(Path.Join(testInstance.Path, file));
- }
+ static string RemoveQuotes(string arg)
+ {
+ return arg.Replace("\"", string.Empty);
+ }
- return result;
+ 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,
+ };
}
}
///
- /// 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".
+ /// Verifies that csc-only runs emit auxiliary files equivalent to msbuild-based runs.
///
- [Fact]
- public void VirtualProject_SurvivesGCDuringRestore()
+ [Theory]
+ [InlineData("Program.cs")]
+ [InlineData("test.cs")]
+ [InlineData("noext")]
+ public void CscVsMSBuild(string fileName)
{
- var testInstance = _testAssetsManager.CreateTestDirectory();
- File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
- Console.WriteLine("Hello from virtual project");
+ var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
+ string entryPointPath = Path.Join(testInstance.Path, fileName);
+ File.WriteAllText(entryPointPath, $"""
+ #!/test
+ {s_program}
""");
- // 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 />
-
-
- """);
+ string programName = Path.GetFileNameWithoutExtension(fileName);
- 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")
+ // 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("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());
- """);
+ .And.HaveStdOut($"""
+ {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc}
+ Hello from {programName}
+ """);
- // 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 />
-
-
- """);
+ // Backup the artifacts directory.
+ Directory.Move(artifactsDir, artifactsBackupDir);
- 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")
+ // Build using MSBuild.
+ new DotnetCommand(Log, "run", fileName, "-bl", "--no-cache")
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Pass()
- .And.HaveStdOut("Hello from ref");
+ .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");
}
}