File: TestUtilities\IntegrationTestBase.cs
Web Access
Project: src\src\Compilers\Core\MSBuildTaskTests\Microsoft.Build.Tasks.CodeAnalysis.UnitTests.csproj (Microsoft.Build.Tasks.CodeAnalysis.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests;
 
public abstract class IntegrationTestBase : TestBase
{
    protected static readonly string? s_msbuildDirectory;
 
#if NET472
    static IntegrationTestBase()
    {
        s_msbuildDirectory = DesktopTestHelpers.GetMSBuildDirectory();
    }
#endif
 
    protected readonly ITestOutputHelper _output;
    protected readonly string? _msbuildExecutable;
    protected readonly TempDirectory _tempDirectory;
    protected string _buildTaskDll;
 
    protected IntegrationTestBase(ITestOutputHelper output)
    {
        if (s_msbuildDirectory == null)
        {
            output.WriteLine("Could not locate MSBuild");
        }
 
        _output = output;
        _msbuildExecutable = s_msbuildDirectory == null ? null : Path.Combine(s_msbuildDirectory, "MSBuild.exe");
        _tempDirectory = Temp.CreateDirectory();
        _buildTaskDll = typeof(ManagedCompiler).Assembly.Location;
    }
 
    private static IEnumerable<KeyValuePair<string, string>> AddForLoggingEnvironmentVars(IEnumerable<KeyValuePair<string, string>>? vars)
    {
        vars = vars ?? new KeyValuePair<string, string>[] { };
        if (!vars.Where(kvp => kvp.Key == "RoslynCommandLineLogFile").Any())
        {
            var list = vars.ToList();
            list.Add(new KeyValuePair<string, string>(
                "RoslynCommandLineLogFile",
                typeof(IntegrationTestBase).Assembly.Location + ".client-server.log"));
            return list;
        }
        return vars;
    }
 
    protected static ProcessResult RunCommandLineCompiler(
        string compilerPath,
        string arguments,
        string currentDirectory,
        IEnumerable<KeyValuePair<string, string>>? additionalEnvironmentVars = null)
    {
        return ProcessUtilities.Run(
            compilerPath,
            arguments,
            currentDirectory,
            additionalEnvironmentVars: AddForLoggingEnvironmentVars(additionalEnvironmentVars));
    }
 
    protected static ProcessResult RunCommandLineCompiler(
        string compilerPath,
        string arguments,
        TempDirectory currentDirectory,
        IEnumerable<KeyValuePair<string, string>> filesInDirectory,
        IEnumerable<KeyValuePair<string, string>>? additionalEnvironmentVars = null)
    {
        foreach (var pair in filesInDirectory)
        {
            TempFile file = currentDirectory.CreateFile(pair.Key);
            file.WriteAllText(pair.Value);
        }
 
        return RunCommandLineCompiler(
            compilerPath,
            arguments,
            currentDirectory.Path,
            additionalEnvironmentVars: AddForLoggingEnvironmentVars(additionalEnvironmentVars));
    }
 
    protected ProcessResult? RunMsbuild(
        string arguments,
        TempDirectory currentDirectory,
        IEnumerable<KeyValuePair<string, string>> filesInDirectory,
        IEnumerable<KeyValuePair<string, string>>? additionalEnvironmentVars = null)
    {
        if (_msbuildExecutable != null)
        {
            return RunCommandLineCompiler(
                _msbuildExecutable,
                arguments,
                currentDirectory,
                filesInDirectory,
                additionalEnvironmentVars);
        }
 
        if (ExecutionConditionUtil.IsDesktop)
        {
            _output.WriteLine("Skipping because Framework MSBuild is missing, this is a desktop test, " +
                "and we cannot use the desktop Csc/Vbc task from 'dotnet msbuild', i.e., Core MSBuild.");
            return null;
        }
 
        return RunCommandLineCompiler(
            "dotnet",
            $"msbuild {arguments}",
            currentDirectory,
            filesInDirectory,
            additionalEnvironmentVars);
    }
 
    /// <param name="overrideToolExe">
    /// Setting ToolExe to "csc.exe" should use the built-in compiler regardless of apphost being used or not.
    /// </param>
    [Theory, CombinatorialData]
    [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2615118")]
    public void SdkBuild_Csc(bool useSharedCompilation, bool overrideToolExe, bool useAppHost)
    {
        if (!ManagedToolTask.IsBuiltinToolRunningOnCoreClr && !useAppHost)
        {
            _output.WriteLine("Skipping test case: netfx compiler always uses apphost.");
            return;
        }
 
        var originalAppHost = Path.Combine(ManagedToolTask.GetToolDirectory(), $"csc{PlatformInformation.ExeExtension}");
        var backupAppHost = originalAppHost + ".bak";
        if (!useAppHost)
        {
            _output.WriteLine($"Apphost: {originalAppHost}");
            File.Move(originalAppHost, backupAppHost);
        }
 
        ProcessResult? result;
 
        try
        {
            result = RunMsbuild(
                "/v:n /m /nr:false /t:Build /restore Test.csproj" +
                    (overrideToolExe ? $" /p:CscToolExe=csc{PlatformInformation.ExeExtension}" : ""),
                _tempDirectory,
                new Dictionary<string, string>
                {
                    { "File.cs", """
                        class Program { static void Main() { System.Console.WriteLine("Hello from file"); } }
                        """ },
                    { "Test.csproj", $"""
                        <Project Sdk="Microsoft.NET.Sdk">
                            <UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Csc" AssemblyFile="{_buildTaskDll}" />
                            <PropertyGroup>
                                <TargetFramework>netstandard2.0</TargetFramework>
                                <UseSharedCompilation>{useSharedCompilation}</UseSharedCompilation>
                            </PropertyGroup>
                        </Project>
                        """ },
                });
        }
        finally
        {
            if (!useAppHost)
            {
                File.Move(backupAppHost, originalAppHost);
            }
        }
 
        if (result == null) return;
 
        _output.WriteLine(result.Output);
 
        Assert.Equal(0, result.ExitCode);
        Assert.Contains(useSharedCompilation ? "server processed compilation" : "using command line tool by design", result.Output);
 
        if (useAppHost)
        {
            Assert.DoesNotContain("csc.dll", result.Output);
            Assert.Contains($"csc{PlatformInformation.ExeExtension} ", result.Output);
        }
        else
        {
            Assert.Contains("csc.dll", result.Output);
            Assert.DoesNotContain($"csc{PlatformInformation.ExeExtension} ", result.Output);
        }
    }
 
    /// <param name="overrideToolExe">
    /// Setting ToolExe to "vbc.exe" should use the built-in compiler regardless of apphost being used or not.
    /// </param>
    [Theory, CombinatorialData]
    [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2615118")]
    public void SdkBuild_Vbc(bool useSharedCompilation, bool overrideToolExe, bool useAppHost)
    {
        if (!ManagedToolTask.IsBuiltinToolRunningOnCoreClr && !useAppHost)
        {
            _output.WriteLine("Skipping test case: netfx compiler always uses apphost.");
            return;
        }
 
        var originalAppHost = Path.Combine(ManagedToolTask.GetToolDirectory(), $"vbc{PlatformInformation.ExeExtension}");
        var backupAppHost = originalAppHost + ".bak";
        if (!useAppHost)
        {
            File.Move(originalAppHost, backupAppHost);
        }
 
        ProcessResult? result;
 
        try
        {
            result = RunMsbuild(
                "/v:n /m /nr:false /t:Build /restore Test.vbproj" +
                    (overrideToolExe ? $" /p:VbcToolExe=vbc{PlatformInformation.ExeExtension}" : ""),
                _tempDirectory,
                new Dictionary<string, string>
                {
                    { "File.vb", """
                        Public Module Program
                            Public Sub Main()
                                System.Console.WriteLine("Hello from file")
                            End Sub
                        End Module
                        """ },
                    { "Test.vbproj", $"""
                        <Project Sdk="Microsoft.NET.Sdk">
                            <UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Vbc" AssemblyFile="{_buildTaskDll}" />
                            <PropertyGroup>
                                <TargetFramework>netstandard2.0</TargetFramework>
                                <UseSharedCompilation>{useSharedCompilation}</UseSharedCompilation>
                            </PropertyGroup>
                        </Project>
                        """ },
                });
        }
        finally
        {
            if (!useAppHost)
            {
                File.Move(backupAppHost, originalAppHost);
            }
        }
 
        if (result == null) return;
 
        _output.WriteLine(result.Output);
 
        Assert.Equal(0, result.ExitCode);
        Assert.Contains(useSharedCompilation ? "server processed compilation" : "using command line tool by design", result.Output);
 
        if (useAppHost)
        {
            Assert.DoesNotContain("vbc.dll", result.Output);
            Assert.Contains($"vbc{PlatformInformation.ExeExtension} ", result.Output);
        }
        else
        {
            Assert.Contains("vbc.dll", result.Output);
            Assert.DoesNotContain($"vbc{PlatformInformation.ExeExtension} ", result.Output);
        }
    }
 
    [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/79907")]
    public void StdLib_Csc(bool useSharedCompilation, bool disableSdkPath, bool noConfig)
    {
        if (_msbuildExecutable == null) return;
 
        var result = RunCommandLineCompiler(
            _msbuildExecutable,
            "/m /nr:false /t:CustomTarget Test.csproj",
            _tempDirectory,
            new Dictionary<string, string>
            {
                { "File.cs", """
                    using System.Linq;
                    System.Console.WriteLine("Hello from file");
                    """ },
                { "Test.csproj", $"""
                    <Project>
                        <UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Csc" AssemblyFile="{_buildTaskDll}" />
                        <Target Name="CustomTarget">
                            <Csc Sources="File.cs" UseSharedCompilation="{useSharedCompilation}" DisableSdkPath="{disableSdkPath}" NoConfig="{noConfig}" />
                        </Target>
                    </Project>
                    """ },
            });
        _output.WriteLine(result.Output);
 
        if (disableSdkPath || noConfig)
        {
            Assert.NotEqual(0, result.ExitCode);
            if (disableSdkPath && noConfig)
            {
                // error CS0246: The type or namespace name 'System' could not be found
                Assert.Contains("error CS0246", result.Output);
            }
            else if (disableSdkPath)
            {
                // error CS0006: Metadata file could not be found
                Assert.Contains("error CS0006", result.Output);
            }
            else
            {
                // error CS0234: The type or namespace name 'Linq' does not exist in the namespace 'System'
                Assert.Contains("error CS0234", result.Output);
            }
        }
        else
        {
            Assert.Equal(0, result.ExitCode);
        }
 
        Assert.Contains(useSharedCompilation ? "server processed compilation" : "using command line tool by design", result.Output);
    }
 
    [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/79907")]
    public void StdLib_Vbc(bool useSharedCompilation, bool disableSdkPath, bool noConfig)
    {
        if (_msbuildExecutable == null) return;
 
        var result = RunCommandLineCompiler(
            _msbuildExecutable,
            "/m /nr:false /t:CustomTarget Test.vbproj",
            _tempDirectory,
            new Dictionary<string, string>
            {
                { "File.vb", """
                    Public Module Program
                        Public Sub Main()
                            Console.WriteLine("Hello from file")
                        End Sub
                    End Module
                    """ },
                { "Test.vbproj", $"""
                    <Project>
                        <UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Vbc" AssemblyFile="{_buildTaskDll}" />
                        <Target Name="CustomTarget">
                            <Vbc Sources="File.vb" UseSharedCompilation="{useSharedCompilation}" DisableSdkPath="{disableSdkPath}" NoConfig="{noConfig}" />
                        </Target>
                    </Project>
                    """ },
            });
        _output.WriteLine(result.Output);
 
        if (disableSdkPath || noConfig)
        {
            Assert.NotEqual(0, result.ExitCode);
            if (disableSdkPath)
            {
                // error BC2017: could not find library 'Microsoft.VisualBasic.dll'
                Assert.Contains("error BC2017", result.Output);
            }
            else
            {
                Assert.True(noConfig);
                // error BC30451: 'Console' is not declared. It may be inaccessible due to its protection level.
                Assert.Contains("error BC30451", result.Output);
            }
        }
        else
        {
            Assert.Equal(0, result.ExitCode);
        }
 
        Assert.Contains(useSharedCompilation ? "server processed compilation" : "using command line tool by design", result.Output);
    }
 
    /// <summary>
    /// Verifies that both RSPs are included: the default <c>csc.rsp</c> (which has <c>/r:System.Data.OracleClient</c>),
    /// and the custom RSP (which has <c>/warnaserror+</c> so we get an error for using an obsolete type).
    /// </summary>
    [Theory, CombinatorialData]
    public void CustomRsp_Csc(bool includeCustomRsp, bool useSharedCompilation, bool noConfig)
    {
        if (_msbuildExecutable == null) return;
 
        var result = RunCommandLineCompiler(
            _msbuildExecutable,
            "/m /nr:false /t:CustomTarget Test.csproj",
            _tempDirectory,
            new Dictionary<string, string>
            {
                { "File.cs", """
                    new System.Data.OracleClient.OracleConnection("");
                    """ },
                { "custom.rsp", """
                    /warnaserror+
                    """ },
                { "Test.csproj", $"""
                    <Project>
                        <UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Csc" AssemblyFile="{_buildTaskDll}" />
                        <Target Name="CustomTarget">
                            <Csc Sources="File.cs" UseSharedCompilation="{useSharedCompilation}" ResponseFiles="{(includeCustomRsp ? "custom.rsp" : "")}" NoConfig="{noConfig}" />
                        </Target>
                    </Project>
                    """ },
            });
        _output.WriteLine(result.Output);
 
        Assert.Equal(!includeCustomRsp && !noConfig, 0 == result.ExitCode);
        if (noConfig)
        {
            // error CS0234: The type or namespace name 'Data' does not exist in the namespace 'System'
            Assert.Contains("error CS0234", result.Output);
        }
        else
        {
            // warning CS0618: The type is obsolete
            Assert.Contains($"{(includeCustomRsp ? "error" : "warning")} CS0618", result.Output);
        }
 
        Assert.Contains(useSharedCompilation ? "server processed compilation" : "using command line tool by design", result.Output);
    }
 
    /// <inheritdoc cref="CustomRsp_Csc"/>
    [Theory, CombinatorialData]
    public void CustomRsp_Vbc(bool includeCustomRsp, bool useSharedCompilation, bool noConfig)
    {
        if (_msbuildExecutable == null) return;
 
        var result = RunCommandLineCompiler(
            _msbuildExecutable,
            "/m /nr:false /t:CustomTarget Test.vbproj",
            _tempDirectory,
            new Dictionary<string, string>
            {
                { "File.vb", """
                    Public Module Program
                        Public Sub Main()
                            Dim x As Object = New System.Data.OracleClient.OracleConnection("")
                        End Sub
                    End Module
                    """ },
                { "custom.rsp", """
                    /warnaserror+
                    """ },
                { "Test.vbproj", $"""
                    <Project>
                        <UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Vbc" AssemblyFile="{_buildTaskDll}" />
                        <Target Name="CustomTarget">
                            <Vbc Sources="File.vb" UseSharedCompilation="{useSharedCompilation}" ResponseFiles="{(includeCustomRsp ? "custom.rsp" : "")}" NoConfig="{noConfig}" />
                        </Target>
                    </Project>
                    """ },
            });
        _output.WriteLine(result.Output);
 
        Assert.Equal(!includeCustomRsp && !noConfig, 0 == result.ExitCode);
        if (noConfig)
        {
            // error BC30002: Type 'System.Data.OracleClient.OracleConnection' is not defined.
            Assert.Contains("error BC30002", result.Output);
        }
        else
        {
            // warning BC40000: The type is obsolete
            Assert.Contains($"{(includeCustomRsp ? "error" : "warning")} BC40000", result.Output);
        }
 
        Assert.Contains(useSharedCompilation ? "server processed compilation" : "using command line tool by design", result.Output);
    }
}