-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Invoke dotnet run-api
to obtain virtual project
#78648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6e51543
a06c028
adebf2b
f9c91c5
edaadf3
c50833a
6cbbdf0
1159e30
1febb54
0160546
648aa23
f455432
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
// 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. | ||
|
||
// Uncomment this to test run-api locally. | ||
// Eventually when a new enough SDK is adopted in-repo we can remove this | ||
//#define RoslynTestRunApi | ||
|
||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; | ||
using Microsoft.Extensions.Logging; | ||
using Roslyn.LanguageServer.Protocol; | ||
using Roslyn.Test.Utilities; | ||
using Xunit.Abstractions; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; | ||
|
||
/// <summary> | ||
/// Goal of these tests: | ||
/// - Ensure that the various request/response forms work as expected in basic scenarios. | ||
/// - Ensure that various properties on the response are populated in a reasonable way. | ||
/// Non-goals: | ||
/// - Thorough behavioral testing. | ||
/// - Testing of more intricate behaviors which are subject to change. | ||
/// </summary> | ||
public sealed class VirtualProjectXmlProviderTests : AbstractLanguageServerHostTests | ||
{ | ||
public VirtualProjectXmlProviderTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) | ||
{ | ||
} | ||
|
||
private class EnableRunApiTests : ExecutionCondition | ||
{ | ||
public override bool ShouldSkip => | ||
#if RoslynTestRunApi | ||
false; | ||
#else | ||
true; | ||
#endif | ||
|
||
public override string SkipReason => $"Compilation symbol 'RoslynTestRunApi' is not defined."; | ||
} | ||
|
||
private async Task<VirtualProjectXmlProvider> GetProjectXmlProviderAsync() | ||
{ | ||
var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync( | ||
LoggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, extensionPaths: null); | ||
return exportProvider.GetExportedValue<VirtualProjectXmlProvider>(); | ||
} | ||
|
||
[Fact] | ||
public async Task GetProjectXml_FileBasedProgram_SdkTooOld_01() | ||
{ | ||
var projectProvider = await GetProjectXmlProviderAsync(); | ||
|
||
var tempDir = TempRoot.CreateDirectory(); | ||
var appFile = tempDir.CreateFile("app.cs"); | ||
await appFile.WriteAllTextAsync(""" | ||
Console.WriteLine("Hello, world!"); | ||
"""); | ||
|
||
var globalJsonFile = tempDir.CreateFile("global.json"); | ||
globalJsonFile.WriteAllBytes(Encoding.UTF8.GetBytes(""" | ||
{ | ||
"sdk": { | ||
"version": "9.0.105" | ||
} | ||
} | ||
""")); | ||
|
||
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None); | ||
Assert.Null(contentNullable); | ||
} | ||
|
||
[ConditionalFact(typeof(EnableRunApiTests))] | ||
public async Task GetProjectXml_FileBasedProgram_01() | ||
{ | ||
var projectProvider = await GetProjectXmlProviderAsync(); | ||
|
||
var tempDir = TempRoot.CreateDirectory(); | ||
var appFile = tempDir.CreateFile("app.cs"); | ||
await appFile.WriteAllTextAsync(""" | ||
Console.WriteLine("Hello, world!"); | ||
"""); | ||
|
||
var globalJsonFile = tempDir.CreateFile("global.json"); | ||
await globalJsonFile.WriteAllTextAsync(""" | ||
{ | ||
"sdk": { | ||
"version": "10.0.100-preview.5.25265.12" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these tests should fail in CI (and look like they are). We would need to figure out some way of ensuring the preview SDK is available in helix machines. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opted to keep things moving forward by just skipping the tests requiring preview SDK in CI. |
||
} | ||
} | ||
"""); | ||
|
||
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None); | ||
var content = contentNullable.Value; | ||
var virtualProjectXml = content.VirtualProjectXml; | ||
LoggerFactory.CreateLogger<VirtualProjectXmlProviderTests>().LogTrace(virtualProjectXml); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use the xunit logging support? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The base type is adding a logger provider which wraps the xunit logger. It didn't feel necessary to directly use the xunit logger given that. |
||
|
||
Assert.Contains("<TargetFramework>net10.0</TargetFramework>", virtualProjectXml); | ||
Assert.Contains("<ArtifactsPath>", virtualProjectXml); | ||
Assert.Empty(content.Diagnostics); | ||
} | ||
|
||
[ConditionalFact(typeof(EnableRunApiTests))] | ||
public async Task GetProjectXml_NonFileBasedProgram_01() | ||
{ | ||
var projectProvider = await GetProjectXmlProviderAsync(); | ||
|
||
var tempDir = TempRoot.CreateDirectory(); | ||
var appFile = tempDir.CreateFile("app.cs"); | ||
await appFile.WriteAllTextAsync(""" | ||
public class C | ||
{ | ||
} | ||
"""); | ||
|
||
var globalJsonFile = tempDir.CreateFile("global.json"); | ||
await globalJsonFile.WriteAllTextAsync(""" | ||
{ | ||
"sdk": { | ||
"version": "10.0.100-preview.5.25265.12" | ||
} | ||
} | ||
"""); | ||
|
||
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None); | ||
var content = contentNullable.Value; | ||
LoggerFactory.CreateLogger<VirtualProjectXmlProviderTests>().LogTrace(content.VirtualProjectXml); | ||
|
||
Assert.Contains("<TargetFramework>net10.0</TargetFramework>", content.VirtualProjectXml); | ||
Assert.Contains("<ArtifactsPath>", content.VirtualProjectXml); | ||
Assert.Empty(content.Diagnostics); | ||
} | ||
|
||
[ConditionalFact(typeof(EnableRunApiTests))] | ||
public async Task GetProjectXml_BadPath_01() | ||
{ | ||
var projectProvider = await GetProjectXmlProviderAsync(); | ||
|
||
var tempDir = TempRoot.CreateDirectory(); | ||
|
||
var globalJsonFile = tempDir.CreateFile("global.json"); | ||
await globalJsonFile.WriteAllTextAsync(""" | ||
{ | ||
"sdk": { | ||
"version": "10.0.100-preview.5.25265.12" | ||
} | ||
} | ||
"""); | ||
|
||
var content = await projectProvider.GetVirtualProjectContentAsync(Path.Combine(tempDir.Path, "BAD"), CancellationToken.None); | ||
Assert.Null(content); | ||
} | ||
|
||
[ConditionalFact(typeof(EnableRunApiTests))] | ||
public async Task GetProjectXml_BadDirective_01() | ||
{ | ||
var projectProvider = await GetProjectXmlProviderAsync(); | ||
|
||
var tempDir = TempRoot.CreateDirectory(); | ||
var appFile = tempDir.CreateFile("app.cs"); | ||
await appFile.WriteAllTextAsync(""" | ||
#:package [email protected] | ||
#:BAD | ||
Console.WriteLine("Hello, world!"); | ||
"""); | ||
|
||
var globalJsonFile = tempDir.CreateFile("global.json"); | ||
await globalJsonFile.WriteAllTextAsync(""" | ||
{ | ||
"sdk": { | ||
"version": "10.0.100-preview.5.25265.12" | ||
} | ||
} | ||
"""); | ||
|
||
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None); | ||
var content = contentNullable.Value; | ||
var diagnostic = content.Diagnostics.Single(); | ||
Assert.Contains("Unrecognized directive 'BAD'", diagnostic.Message); | ||
Assert.Equal(appFile.Path, diagnostic.Location.Path); | ||
|
||
// LinePositionSpan is not deserializing properly. | ||
// Address when implementing editor squiggles. https://github.com/dotnet/roslyn/issues/78688 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like we should fix that now, since otherwise we're reporting bad locations to the error list? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing is including the diagnostics from run-api in the error list currently. |
||
Assert.Equal("(0,0)-(0,0)", diagnostic.Location.Span.ToString()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,14 +68,15 @@ private async Task<string> GetDotnetSdkFolderFromDotnetExecutableAsync(string pr | |
return dotnetSdkFolderPath; | ||
} | ||
|
||
public Process Run(string[] arguments, string? workingDirectory, bool shouldLocalizeOutput) | ||
public Process Run(string[] arguments, string? workingDirectory, bool shouldLocalizeOutput, bool redirectStandardInput = false) | ||
{ | ||
_logger.LogDebug($"Running dotnet CLI command at {_dotnetExecutablePath.Value} in directory {workingDirectory} with arguments {arguments}"); | ||
|
||
var startInfo = new ProcessStartInfo(_dotnetExecutablePath.Value) | ||
{ | ||
CreateNoWindow = true, | ||
UseShellExecute = false, | ||
RedirectStandardInput = redirectStandardInput, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this frankly always be true as well? Because it seems either that:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case (2), I think it would essentially read the stdin of the parent process, which it seems like we wouldn't want. |
||
RedirectStandardOutput = true, | ||
RedirectStandardError = true, | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,10 @@ | |
|
||
using System.Collections.Immutable; | ||
using System.Security; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Features.Workspaces; | ||
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; | ||
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; | ||
using Microsoft.CodeAnalysis.MetadataAsSource; | ||
using Microsoft.CodeAnalysis.MSBuild; | ||
|
@@ -22,18 +24,20 @@ | |
using Roslyn.Utilities; | ||
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; | ||
namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; | ||
|
||
/// <summary>Handles loading both miscellaneous files and file-based program projects.</summary> | ||
internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider | ||
{ | ||
private readonly ILspServices _lspServices; | ||
private readonly ILogger<FileBasedProgramsProjectSystem> _logger; | ||
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService; | ||
private readonly VirtualProjectXmlProvider _projectXmlProvider; | ||
|
||
public FileBasedProgramsProjectSystem( | ||
ILspServices lspServices, | ||
IMetadataAsSourceFileService metadataAsSourceFileService, | ||
VirtualProjectXmlProvider projectXmlProvider, | ||
LanguageServerWorkspaceFactory workspaceFactory, | ||
IFileChangeWatcher fileChangeWatcher, | ||
IGlobalOptionService globalOptionService, | ||
|
@@ -57,6 +61,7 @@ public FileBasedProgramsProjectSystem( | |
_lspServices = lspServices; | ||
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>(); | ||
_metadataAsSourceFileService = metadataAsSourceFileService; | ||
_projectXmlProvider = projectXmlProvider; | ||
} | ||
|
||
public Workspace Workspace => ProjectFactory.Workspace; | ||
|
@@ -124,20 +129,33 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool | |
await UnloadProjectAsync(documentPath); | ||
} | ||
|
||
protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync( | ||
protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync( | ||
BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) | ||
{ | ||
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; | ||
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken); | ||
var content = await _projectXmlProvider.GetVirtualProjectContentAsync(documentPath, cancellationToken); | ||
if (content is not var (virtualProjectContent, diagnostics)) | ||
{ | ||
// 'GetVirtualProjectContentAsync' will log errors when it fails | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be clearer then to pass in the ILogger so that way it's clear it's expected to log to this rather than having it's own logger. |
||
return null; | ||
} | ||
|
||
var loader = ProjectFactory.CreateFileTextLoader(documentPath); | ||
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken); | ||
var (virtualProjectContent, isFileBasedProgram) = VirtualCSharpFileBasedProgramProject.MakeVirtualProjectContent(documentPath, textAndVersion.Text); | ||
foreach (var diagnostic in diagnostics) | ||
{ | ||
// https://github.com/dotnet/roslyn/issues/78688: Surface diagnostics in editor | ||
_logger.LogError($"{diagnostic.Location.Path}{diagnostic.Location.Span.Start}: {diagnostic.Message}"); | ||
} | ||
|
||
// When loading a virtual project, the path to the on-disk source file is not used. Instead the path is adjusted to end with .csproj. | ||
// This is necessary in order to get msbuild to apply the standard c# props/targets to the project. | ||
var virtualProjectPath = VirtualCSharpFileBasedProgramProject.GetVirtualProjectPath(documentPath); | ||
var virtualProjectPath = VirtualProjectXmlProvider.GetVirtualProjectPath(documentPath); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the name of the file also come from the API too? |
||
|
||
var loader = ProjectFactory.CreateFileTextLoader(documentPath); | ||
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken); | ||
var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text); | ||
|
||
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; | ||
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken); | ||
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken); | ||
return (loadedFile, hasAllInformation: isFileBasedProgram, preferred: buildHostKind, actual: buildHostKind); | ||
return new RemoteProjectLoadResult(loadedFile, HasAllInformation: isFileBasedProgram, Preferred: buildHostKind, Actual: buildHostKind); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a bug tracking enabling this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#78879