File: MSBuildWorkspaceTestBase.cs
Web Access
Project: src\src\Workspaces\MSBuild\Test\Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild.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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.UnitTests;
using Microsoft.CodeAnalysis.UnitTests.TestFiles;
using Microsoft.Extensions.Logging;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.CodeAnalysis.MSBuild.UnitTests.SolutionGeneration;
using CS = Microsoft.CodeAnalysis.CSharp;
using VB = Microsoft.CodeAnalysis.VisualBasic;
 
namespace Microsoft.CodeAnalysis.MSBuild.UnitTests;
 
public abstract class MSBuildWorkspaceTestBase : WorkspaceTestBase
{
    protected readonly ITestOutputHelper TestOutput;
    protected readonly ILoggerFactory LoggerFactory;
 
    protected MSBuildWorkspaceTestBase(ITestOutputHelper testOutput)
    {
        TestOutput = testOutput;
        LoggerFactory = new LoggerFactory([new TestOutputLoggerProvider(testOutput)]);
    }
 
    protected const string MSBuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";
 
    protected static void AssertFailures(MSBuildWorkspace workspace, params string[] expectedFailures)
    {
        AssertEx.Equal(expectedFailures, workspace.Diagnostics.Where(d => d.Kind == WorkspaceDiagnosticKind.Failure).Select(d => d.Message));
    }
 
    protected async Task AssertCSCompilationOptionsAsync<T>(T expected, Func<CS.CSharpCompilationOptions, T> actual)
    {
        var options = await LoadCSharpCompilationOptionsAsync();
        Assert.Equal(expected, actual(options));
    }
 
    protected async Task AssertCSParseOptionsAsync<T>(T expected, Func<CS.CSharpParseOptions, T> actual)
    {
        var options = await LoadCSharpParseOptionsAsync();
        Assert.Equal(expected, actual(options));
    }
 
    protected async Task AssertVBCompilationOptionsAsync<T>(T expected, Func<VB.VisualBasicCompilationOptions, T> actual)
    {
        var options = await LoadVisualBasicCompilationOptionsAsync();
        Assert.Equal(expected, actual(options));
    }
 
    protected async Task AssertVBParseOptionsAsync<T>(T expected, Func<VB.VisualBasicParseOptions, T> actual)
    {
        var options = await LoadVisualBasicParseOptionsAsync();
        Assert.Equal(expected, actual(options));
    }
 
    protected async Task<CS.CSharpCompilationOptions> LoadCSharpCompilationOptionsAsync()
    {
        var solutionFilePath = GetSolutionFileName("TestSolution.sln");
        using var workspace = CreateMSBuildWorkspace();
        var sol = await workspace.OpenSolutionAsync(solutionFilePath);
        var project = sol.Projects.First();
        return (CS.CSharpCompilationOptions)project.CompilationOptions;
    }
 
    protected async Task<CS.CSharpParseOptions> LoadCSharpParseOptionsAsync()
    {
        var solutionFilePath = GetSolutionFileName("TestSolution.sln");
        using var workspace = CreateMSBuildWorkspace();
        var sol = await workspace.OpenSolutionAsync(solutionFilePath);
        var project = sol.Projects.First();
        return (CS.CSharpParseOptions)project.ParseOptions;
    }
 
    protected async Task<VB.VisualBasicCompilationOptions> LoadVisualBasicCompilationOptionsAsync()
    {
        var solutionFilePath = GetSolutionFileName("TestSolution.sln");
        using var workspace = CreateMSBuildWorkspace();
        var sol = await workspace.OpenSolutionAsync(solutionFilePath);
        var project = sol.GetProjectsByName("VisualBasicProject").FirstOrDefault();
        return (VB.VisualBasicCompilationOptions)project.CompilationOptions;
    }
 
    protected async Task<VB.VisualBasicParseOptions> LoadVisualBasicParseOptionsAsync()
    {
        var solutionFilePath = GetSolutionFileName("TestSolution.sln");
        using var workspace = CreateMSBuildWorkspace();
        var sol = await workspace.OpenSolutionAsync(solutionFilePath);
        var project = sol.GetProjectsByName("VisualBasicProject").FirstOrDefault();
        return (VB.VisualBasicParseOptions)project.ParseOptions;
    }
 
    protected static int GetMethodInsertionPoint(VB.Syntax.ClassBlockSyntax classBlock)
    {
        if (classBlock.Implements.Count > 0)
        {
            return classBlock.Implements[^1].FullSpan.End;
        }
        else if (classBlock.Inherits.Count > 0)
        {
            return classBlock.Inherits[^1].FullSpan.End;
        }
        else
        {
            return classBlock.BlockStatement.FullSpan.End;
        }
    }
 
    protected async Task PrepareCrossLanguageProjectWithEmittedMetadataAsync()
    {
        // Now try variant of CSharpProject that has an emitted assembly 
        CreateFiles(GetMultiProjectSolutionFiles()
            .WithFile(@"CSharpProject\CSharpProject.csproj", Resources.ProjectFiles.CSharp.ForEmittedOutput));
 
        var solutionFilePath = GetSolutionFileName("TestSolution.sln");
        using var workspace = CreateMSBuildWorkspace();
        var sol = await workspace.OpenSolutionAsync(solutionFilePath);
        var p1 = sol.Projects.First(p => p.Language == LanguageNames.CSharp);
        var p2 = sol.Projects.First(p => p.Language == LanguageNames.VisualBasic);
 
        Assert.NotNull(p1.OutputFilePath);
        Assert.Equal("EmittedCSharpProject.dll", Path.GetFileName(p1.OutputFilePath));
 
        // if the assembly doesn't already exist, emit it now
        if (!File.Exists(p1.OutputFilePath))
        {
            var c1 = await p1.GetCompilationAsync();
            var result = c1.Emit(p1.OutputFilePath);
            Assert.True(result.Success);
        }
    }
 
    protected async Task<Solution> SolutionAsync(params IBuilder[] inputs)
    {
        var files = GetSolutionFiles(inputs);
        CreateFiles(files);
        var solutionFileName = files.First(t => t.fileName.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)).fileName;
        solutionFileName = GetSolutionFileName(solutionFileName);
        using var workspace = CreateMSBuildWorkspace();
        return await workspace.OpenSolutionAsync(solutionFileName);
    }
 
    protected MSBuildWorkspace CreateMSBuildWorkspace(params (string key, string value)[] additionalProperties)
        => CreateMSBuildWorkspace(throwOnWorkspaceFailed: true, skipUnrecognizedProjects: false, additionalProperties: additionalProperties);
 
    protected MSBuildWorkspace CreateMSBuildWorkspace(
        bool throwOnWorkspaceFailed = true,
        bool skipUnrecognizedProjects = false,
        (string key, string value)[] additionalProperties = null)
    {
        additionalProperties ??= [];
        var workspace = MSBuildWorkspace.Create(CreateProperties(additionalProperties));
        workspace.AddLoggerProvider(new TestOutputLoggerProvider(TestOutput));
 
        if (throwOnWorkspaceFailed)
        {
            workspace.WorkspaceFailed += (s, e) => throw new Exception($"Workspace failure {e.Diagnostic.Kind}:{e.Diagnostic.Message}");
        }
 
        if (skipUnrecognizedProjects)
        {
            workspace.SkipUnrecognizedProjects = true;
        }
 
        return workspace;
    }
 
    protected static MSBuildWorkspace CreateMSBuildWorkspace(HostServices hostServices, params (string key, string value)[] additionalProperties)
    {
        return MSBuildWorkspace.Create(CreateProperties(additionalProperties), hostServices);
    }
 
    private static Dictionary<string, string> CreateProperties((string key, string value)[] additionalProperties)
    {
        var properties = new Dictionary<string, string>();
 
        foreach (var (k, v) in additionalProperties)
        {
            properties.Add(k, v);
        }
 
        return properties;
    }
 
    protected static async Task AssertThrowsExceptionForInvalidPath(Func<Task> testCode)
    {
#if NET

        // On .NET Core, invalid file paths don't throw exceptions when calling Path manipulation APIs, they just throw FileNotFound once you
        // actually try to use the path
        await Assert.ThrowsAsync<FileNotFoundException>(testCode);
 
#else
 
        // On .NET Framework, invalid file paths throw exceptions that we caught as IOExceptions we re-raise an InvalidOperationException.
        // We'll assert the paths we test with contain "Invalid" to have some confidence this isn't an unrelated exception being thrown.
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(testCode);
        Assert.Contains("Invalid", exception.Message);
 
#endif
    }
}