File: RoslynBuildTaskTests.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Build.Tests\Microsoft.NET.Build.Tests.csproj (Microsoft.NET.Build.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using System.Runtime.CompilerServices;
using Basic.CompilerLog.Util;
using Microsoft.Build.Logging.StructuredLogger;
using Microsoft.CodeAnalysis;
 
namespace Microsoft.NET.Build.Tests;
 
public sealed class RoslynBuildTaskTests(ITestOutputHelper log) : SdkTest(log)
{
    private static string CompilerFileNameWithoutExtension(Language language) => language switch
    {
        Language.CSharp => "csc",
        Language.VisualBasic => "vbc",
        _ => throw new ArgumentOutOfRangeException(paramName: nameof(language)),
    };
 
    private static string CoreCompilerFileName(Language language) => CompilerFileNameWithoutExtension(language) + ".dll";
 
    private static string FxCompilerFileName(Language language) => CompilerFileNameWithoutExtension(language) + ".exe";
 
    [FullMSBuildOnlyTheory, CombinatorialData]
    public void FullMSBuild_SdkStyle(bool useSharedCompilation, Language language)
    {
        var testAsset = CreateProject(useSharedCompilation, language);
        var buildCommand = BuildAndRunUsingMSBuild(testAsset);
        VerifyCompiler(buildCommand, CoreCompilerFileName(language), useSharedCompilation);
    }
 
    [FullMSBuildOnlyTheory, CombinatorialData]
    public void FullMSBuild_SdkStyle_OptOut(bool useSharedCompilation, Language language)
    {
        var testAsset = CreateProject(useSharedCompilation, language).WithProjectChanges(static doc =>
        {
            doc.Root!.Element("PropertyGroup")!.Add(new XElement("RoslynCompilerType", "Framework"));
        });
        var buildCommand = BuildAndRunUsingMSBuild(testAsset);
        VerifyCompiler(buildCommand, FxCompilerFileName(language), useSharedCompilation);
    }
 
    [FullMSBuildOnlyTheory, CombinatorialData]
    public void FullMSBuild_NonSdkStyle(bool useSharedCompilation, Language language)
    {
        var testAsset = CreateProject(useSharedCompilation, language, static project =>
        {
            project.IsSdkProject = false;
            project.TargetFrameworkVersion = "v4.7.2";
        });
        var buildCommand = BuildAndRunUsingMSBuild(testAsset);
        VerifyCompiler(buildCommand, FxCompilerFileName(language), useSharedCompilation);
    }
 
    [FullMSBuildOnlyTheory, CombinatorialData]
    public void FullMSBuild_SdkStyle_ToolsetPackage(bool useSharedCompilation, Language language)
    {
        var testAsset = CreateProject(useSharedCompilation, language, AddCompilersToolsetPackage);
        var buildCommand = BuildAndRunUsingMSBuild(testAsset);
        VerifyCompiler(buildCommand, FxCompilerFileName(language), useSharedCompilation, toolsetPackage: true);
    }
 
    [Theory, CombinatorialData]
    public void DotNet(bool useSharedCompilation, Language language)
    {
        var testAsset = CreateProject(useSharedCompilation, language);
        var buildCommand = BuildAndRunUsingDotNet(testAsset);
        VerifyCompiler(buildCommand, CoreCompilerFileName(language), useSharedCompilation);
    }
 
    //  https://github.com/dotnet/sdk/issues/49665
    [PlatformSpecificTheory(TestPlatforms.Any & ~TestPlatforms.OSX), CombinatorialData]
    public void DotNet_ToolsetPackage(bool useSharedCompilation, Language language)
    {
        var testAsset = CreateProject(useSharedCompilation, language, AddCompilersToolsetPackage);
        var buildCommand = BuildAndRunUsingDotNet(testAsset);
        VerifyCompiler(buildCommand, CoreCompilerFileName(language), useSharedCompilation, toolsetPackage: true);
    }
 
    private TestAsset CreateProject(bool useSharedCompilation, Language language, Action<TestProject>? configure = null, [CallerMemberName] string callingMethod = "")
    {
        var (projExtension, sourceName, sourceText) = language switch
        {
            Language.CSharp => (".csproj", "Program.cs", """
                class Program
                {
                    static void Main()
                    {
                        System.Console.WriteLine(40 + 2);
                    }
                }
                """),
            Language.VisualBasic => (".vbproj", "Program.vb", """
                Module Program
                    Sub Main()
                        System.Console.WriteLine(40 + 2)
                    End Sub
                End Module
                """),
            _ => throw new ArgumentOutOfRangeException(paramName: nameof(language)),
        };
 
        var project = new TestProject
        {
            Name = "App1",
            IsExe = true,
            TargetExtension = projExtension,
            SourceFiles =
            {
                [sourceName] = sourceText,
            },
        };
 
        // UseSharedCompilation should be the default, so set it only if false.
        if (!useSharedCompilation)
        {
            project.AdditionalProperties["UseSharedCompilation"] = "false";
        }
 
        configure?.Invoke(project);
        return _testAssetsManager.CreateTestProject(project, callingMethod: callingMethod);
    }
 
    private static void AddCompilersToolsetPackage(TestProject project)
    {
        string roslynVersion = typeof(Compilation).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0];
        Assert.False(string.IsNullOrEmpty(roslynVersion));
        project.PackageReferences.Add(new TestPackageReference("Microsoft.Net.Compilers.Toolset", roslynVersion));
    }
 
    private TestCommand BuildAndRunUsingMSBuild(TestAsset testAsset)
    {
        var buildCommand = new MSBuildCommand(testAsset, "Build");
        buildCommand.WithWorkingDirectory(testAsset.Path)
            .Execute("-bl").Should().Pass();
 
        Run(buildCommand.GetOutputDirectory().File(testAsset.TestProject!.GetOutputFileName()));
 
        return buildCommand;
    }
 
    private TestCommand BuildAndRunUsingDotNet(TestAsset testAsset)
    {
        var buildCommand = new DotnetBuildCommand(testAsset);
        buildCommand.Execute("-bl").Should().Pass();
 
        Run(buildCommand.GetOutputDirectory().File(testAsset.TestProject!.GetOutputFileName()));
 
        return buildCommand;
    }
 
    private void Run(FileInfo outputFile)
    {
        var runCommand = new RunExeCommand(Log, outputFile.FullName);
        runCommand.Execute().Should().Pass()
            .And.HaveStdOut("42");
    }
 
    private static void VerifyCompiler(TestCommand buildCommand, string compilerFileName, bool usedCompilerServer, bool toolsetPackage = false)
    {
        var binaryLogPath = Path.Join(buildCommand.WorkingDirectory, "msbuild.binlog");
        using (var reader = BinaryLogReader.Create(binaryLogPath))
        {
            var call = reader.ReadAllCompilerCalls().Should().ContainSingle().Subject;
            Path.GetFileName(call.CompilerFilePath).Should().Be(compilerFileName);
 
            const string toolsetPackageName = "microsoft.net.compilers.toolset";
            if (toolsetPackage)
            {
                call.CompilerFilePath.Should().Contain(toolsetPackageName);
            }
            else
            {
                call.CompilerFilePath.Should().NotContain(toolsetPackageName);
            }
        }
 
        // Verify compiler server message.
        var compilerServerMesssages = BinaryLog.ReadBuild(binaryLogPath).FindChildrenRecursive<Message>(
            static message => message.Text.StartsWith("CompilerServer:", StringComparison.Ordinal));
        compilerServerMesssages.Should().ContainSingle().Which.Text.Should().StartWith(usedCompilerServer
            ? "CompilerServer: server - server processed compilation - "
            : "CompilerServer: tool - using command line tool by design");
    }
 
    public enum Language
    {
        CSharp,
        VisualBasic,
    }
}