/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Workspaces/MSBuild/Test/MSBuildWorkspaceTestBase.cs
244 строки
10 KB
Jason Malinowski
Add additional logging of workspace diagnostics
01 май 2026, 02:36
01 май 2026, 02:36
ba3471b
Код
Авторство
О чём код?
// 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 { private readonly ITestOutputHelper _testOutputHelper; private readonly TestOutputLoggerProvider _testOutputLoggerProvider; protected readonly ILoggerFactory LoggerFactory; protected MSBuildWorkspaceTestBase(ITestOutputHelper testOutput) { _testOutputHelper = testOutput; _testOutputLoggerProvider = new TestOutputLoggerProvider(testOutput); LoggerFactory = new LoggerFactory([_testOutputLoggerProvider]); } public override void Dispose() { // Dispose our LoggingFactory and providers. xunit validates that we don't write anything to ITestOutputHelper after a test is done, // so we want to ensure our providers are all disposed so a broken test doesn't cause the entire test run to fail -- our implementation of // TestOutputLoggerProvider stops forwarding messages once it's disposed. // // LoggerFactory's handling of lifetime is subtle -- providers passed to the LoggerFactorys' constructor are not owned and we have to dispose them; // but providers added via AddLoggerProvider are owned and will be disposed by the LoggerFactory. Thus we need to dispose both here. LoggerFactory.Dispose(); // We'll call ValidateNotAlreadyDisposedAndDispose() as a way to ensure this wasn't disposed prematurely, which would cause us to lose log output. _testOutputLoggerProvider.ValidateNotAlreadyDisposedAndDispose(); base.Dispose(); } 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(_testOutputHelper)); if (throwOnWorkspaceFailed) { _ = workspace.RegisterWorkspaceFailedHandler((e) => { var message = $"MSBuildWorkspace raised WorkspaceFailed with kind {e.Diagnostic.Kind}: {e.Diagnostic.Message}"; var logger = LoggerFactory.CreateLogger(nameof(workspace.WorkspaceFailed)); if (e.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure) { logger.LogError(message); } else { logger.LogWarning(message); } throw new Exception(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 } }