Merge pull request #3061 from sharwell/formatting

Use the real HTML formatter
This commit is contained in:
Sam Harwell 2021-02-16 11:06:48 -08:00 коммит произвёл GitHub
Родитель fc1a75e4a2 b605a2a9ef
Коммит c9f93d4d10
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
74 изменённых файлов: 1416 добавлений и 760 удалений

Просмотреть файл

@ -83,6 +83,12 @@
Versions below this comment are not managed by automation and can be changed as needed.
-->
<PropertyGroup>
<!-- Several packages from the editor are used for testing HTML support, and share the following version. -->
<Tooling_HtmlEditorPackageVersion>16.10.57-preview1</Tooling_HtmlEditorPackageVersion>
<!-- Several packages share the MS.CA.Testing version -->
<Tooling_MicrosoftCodeAnalysisTestingVersion>1.0.1-beta1.21103.2</Tooling_MicrosoftCodeAnalysisTestingVersion>
</PropertyGroup>
<PropertyGroup Label="Manual">
<MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>5.0.0-preview.4.20205.1</MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>
<BenchmarkDotNetPackageVersion>0.12.1</BenchmarkDotNetPackageVersion>
@ -93,15 +99,16 @@
<MicrosoftInternalVisualStudioShellEmbeddablePackageVersion>16.4.29305.180</MicrosoftInternalVisualStudioShellEmbeddablePackageVersion>
<MicrosoftNETCoreApp50PackageVersion>$(MicrosoftNETCoreAppRuntimewinx64PackageVersion)</MicrosoftNETCoreApp50PackageVersion>
<!-- Packages from dotnet/roslyn -->
<MicrosoftCodeAnalysisAnalyzerTestingPackageVersion>1.0.1-beta1.21103.2</MicrosoftCodeAnalysisAnalyzerTestingPackageVersion>
<MicrosoftCodeAnalysisAnalyzerTestingPackageVersion>$(Tooling_MicrosoftCodeAnalysisTestingVersion)</MicrosoftCodeAnalysisAnalyzerTestingPackageVersion>
<MicrosoftCodeAnalysisTestingVerifiersXunitPackageVersion>$(Tooling_MicrosoftCodeAnalysisTestingVersion)</MicrosoftCodeAnalysisTestingVerifiersXunitPackageVersion>
<MicrosoftNetCompilersToolsetPackageVersion>3.9.0-2.20573.10</MicrosoftNetCompilersToolsetPackageVersion>
<MicrosoftServiceHubFrameworkPackageVersion>2.7.313-preview</MicrosoftServiceHubFrameworkPackageVersion>
<MicrosoftVisualStudioCoreUtilityPackageVersion>16.8.272</MicrosoftVisualStudioCoreUtilityPackageVersion>
<MicrosoftVisualStudioCoreUtilityPackageVersion>16.10.8</MicrosoftVisualStudioCoreUtilityPackageVersion>
<MicrosoftVisualStudioComponentModelHostPackageVersion>16.0.467</MicrosoftVisualStudioComponentModelHostPackageVersion>
<MicrosoftVisualStudioImageCatalogPackageVersion>16.8.30406.65-pre</MicrosoftVisualStudioImageCatalogPackageVersion>
<MicrosoftVisualStudioEditorPackageVersion>16.8.272</MicrosoftVisualStudioEditorPackageVersion>
<MicrosoftVisualStudioLanguagePackageVersion>16.8.272</MicrosoftVisualStudioLanguagePackageVersion>
<MicrosoftVisualStudioLanguageIntellisensePackageVersion>16.8.272</MicrosoftVisualStudioLanguageIntellisensePackageVersion>
<MicrosoftVisualStudioLanguagePackageVersion>16.10.8</MicrosoftVisualStudioLanguagePackageVersion>
<MicrosoftVisualStudioLanguageIntellisensePackageVersion>16.10.8</MicrosoftVisualStudioLanguageIntellisensePackageVersion>
<MicrosoftVisualStudioLanguageServerClientImplementationPackageVersion>16.9.150</MicrosoftVisualStudioLanguageServerClientImplementationPackageVersion>
<MicrosoftVisualStudioPackageLanguageService150PackageVersion>16.7.30204.53-pre</MicrosoftVisualStudioPackageLanguageService150PackageVersion>
<MicrosoftVisualStudioLiveSharePackageVersion>0.3.1074</MicrosoftVisualStudioLiveSharePackageVersion>
@ -113,9 +120,17 @@
<MicrosoftVisualStudioShellInterop163DesignTimePackageVersion>16.3.29316.127</MicrosoftVisualStudioShellInterop163DesignTimePackageVersion>
<MicrosoftVisualStudioShell150PackageVersion>16.7.30204.53-pre</MicrosoftVisualStudioShell150PackageVersion>
<MicrosoftVisualStudioShellInteropPackageVersion>7.10.6073</MicrosoftVisualStudioShellInteropPackageVersion>
<MicrosoftVisualStudioTextDataPackageVersion>16.8.272</MicrosoftVisualStudioTextDataPackageVersion>
<MicrosoftVisualStudioTextUIPackageVersion>16.8.272</MicrosoftVisualStudioTextUIPackageVersion>
<MicrosoftVisualStudioThreadingPackageVersion>16.8.55</MicrosoftVisualStudioThreadingPackageVersion>
<MicrosoftVisualStudioTextDataPackageVersion>16.10.8</MicrosoftVisualStudioTextDataPackageVersion>
<MicrosoftVisualStudioTextImplementationPackageVersion>16.10.8</MicrosoftVisualStudioTextImplementationPackageVersion>
<MicrosoftVisualStudioTextLogicPackageVersion>16.10.8</MicrosoftVisualStudioTextLogicPackageVersion>
<MicrosoftVisualStudioTextUIPackageVersion>16.10.8</MicrosoftVisualStudioTextUIPackageVersion>
<MicrosoftVisualStudioThreadingPackageVersion>16.9.45-alpha</MicrosoftVisualStudioThreadingPackageVersion>
<MicrosoftVisualStudioWebPackageVersion>16.10.0-preview-1-31008-014</MicrosoftVisualStudioWebPackageVersion>
<MicrosoftWebToolsLanguagesHtmlPackageVersion>$(Tooling_HtmlEditorPackageVersion)</MicrosoftWebToolsLanguagesHtmlPackageVersion>
<MicrosoftWebToolsLanguagesLanguageServerServerPackageVersion>$(Tooling_HtmlEditorPackageVersion)</MicrosoftWebToolsLanguagesLanguageServerServerPackageVersion>
<MicrosoftWebToolsLanguagesSharedPackageVersion>$(Tooling_HtmlEditorPackageVersion)</MicrosoftWebToolsLanguagesSharedPackageVersion>
<MicrosoftWebToolsLanguagesSharedEditorPackageVersion>$(Tooling_HtmlEditorPackageVersion)</MicrosoftWebToolsLanguagesSharedEditorPackageVersion>
<MicrosoftWebToolsSharedPackageVersion>$(Tooling_HtmlEditorPackageVersion)</MicrosoftWebToolsSharedPackageVersion>
<MonoAddinsPackageVersion>1.3.8</MonoAddinsPackageVersion>
<MonoDevelopSdkPackageVersion>1.0.15</MonoDevelopSdkPackageVersion>
<MoqPackageVersion>4.16.0</MoqPackageVersion>
@ -123,7 +138,7 @@
<NewtonsoftJsonPackageVersion>12.0.2</NewtonsoftJsonPackageVersion>
<OmniSharpExtensionsLanguageServerPackageVersion>0.18.1</OmniSharpExtensionsLanguageServerPackageVersion>
<OmniSharpMSBuildPackageVersion>1.33.0</OmniSharpMSBuildPackageVersion>
<StreamJsonRpcPackageVersion>2.7.66-alpha</StreamJsonRpcPackageVersion>
<StreamJsonRpcPackageVersion>2.7.72</StreamJsonRpcPackageVersion>
<SystemPrivateUriPackageVersion>4.3.2</SystemPrivateUriPackageVersion>
<SystemCompositionPackageVersion>1.0.31.0</SystemCompositionPackageVersion>
<SystemCollectionsImmutablePackageVersion>5.0.0</SystemCollectionsImmutablePackageVersion>

Просмотреть файл

@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="$(Tooling_MicrosoftCodeAnalysisEditorFeaturesTextPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Language.Intellisense" Version="$(MicrosoftVisualStudioLanguageIntellisensePackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Language" Version="$(MicrosoftVisualStudioLanguagePackageVersion)" />
<PackageReference Include="StreamJsonRpc" Version="$(StreamJsonRpcPackageVersion)" />
<PackageReference Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutablePackageVersion)" />
<PackageReference Include="System.Private.Uri" Version="$(SystemPrivateUriPackageVersion)" />
</ItemGroup>

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using Microsoft.AspNetCore.Razor.Test.Common;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Test
{
public static class EditorTestCompositions
{
public static readonly TestComposition Editor = TestComposition.Empty
.AddAssemblies(
// Microsoft.VisualStudio.Text.Implementation.dll:
Assembly.Load("Microsoft.VisualStudio.Text.Implementation, Version=16.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"))
.AddParts(
typeof(TestExportJoinableTaskContext));
}
}

Просмотреть файл

@ -21,8 +21,7 @@ expected: @"
@{
if (true) { }
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -42,8 +41,7 @@ expected: @"
{
}
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -75,8 +73,7 @@ expected: @"
{
}
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -92,8 +89,7 @@ expected: @"
@{
var x = 'foo';
}
",
triggerCharacter: ";");
");
}
[Fact]
@ -111,8 +107,7 @@ expected: @"
var x = @""
foo"";
}
",
triggerCharacter: ";");
");
}
}
}

Просмотреть файл

@ -20,8 +20,7 @@ input: @"
}
}
",
expected: @"
@code {
expected: @"@code {
public class Foo { }
public interface Bar
{
@ -41,8 +40,7 @@ var x = ""foo"";
<div>
</div>
",
expected: @"
@{
expected: @"@{
var x = ""foo"";
}
<div>
@ -61,8 +59,7 @@ void Method() { <div></div> }
}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
void Method()
@ -85,8 +82,7 @@ void Method() { @DateTime.Now }
}
}
",
expected: @"
@code {
expected: @"@code {
public class Foo
{
void Method()
@ -109,8 +105,7 @@ void Method() { @(DateTime.Now) }
}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
void Method()
@ -138,8 +133,7 @@ void Method() { }
<script></script>
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
void Method() { }
@ -165,8 +159,7 @@ void Method() { }
}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
@* This is a Razor Comment *@
@ -187,8 +180,7 @@ input: @"
}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
@* This is a Razor Comment *@
@ -258,8 +250,7 @@ Hello World
}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo { }
public interface Bar
{
@ -294,8 +285,7 @@ public class HelloWorld
public class Bar {}
}
",
expected: @"
Hello World
expected: @"Hello World
@code {
public class HelloWorld
{
@ -317,8 +307,7 @@ input: @"
}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
}
@ -335,8 +324,7 @@ input: @"
public class Foo{
}}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo
{
}
@ -352,8 +340,7 @@ input: @"
@functions {public class Foo{}
}
",
expected: @"
@functions {
expected: @"@functions {
public class Foo { }
}
");
@ -368,9 +355,8 @@ Hello World
@functions {public class Foo{}
}
",
expected: @"
Hello World
@functions {
expected: @"Hello World
@functions {
public class Foo { }
}
");
@ -385,10 +371,9 @@ input: @"
public class Foo{}
}
",
expected: @"
@functions {
expected: @" @functions {
public class Foo { }
}
}
");
}
@ -424,8 +409,7 @@ void Method(){
}
}
",
expected: @"
@using System.Buffers
expected: @"@using System.Buffers
@functions{
public class Foo
{
@ -470,8 +454,7 @@ input: @"
}
}
",
expected: @"
@code {
expected: @"@code {
public class Foo { }
void Method()
{
@ -492,8 +475,7 @@ input: @"
}
}
",
expected: @"
@code {
expected: @"@code {
public class Foo { }
void Method()
{
@ -537,8 +519,7 @@ input: @"
}
}
",
expected: @"
@code {
expected: @"@code {
public class Foo { }
void Method()
{

Просмотреть файл

@ -21,8 +21,7 @@ expected: @"
@code {
public class Foo { }
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -41,8 +40,7 @@ expected: @"
{
}
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -58,8 +56,7 @@ expected: @"
@code {
public void Foo { }
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -78,8 +75,7 @@ expected: @"
{
}
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -95,8 +91,7 @@ expected: @"
@code {
public string Foo { get; set; }
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -116,8 +111,7 @@ expected: @"
get; set;
}
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -132,8 +126,7 @@ expected: @"
@code {
public string Foo { get; set; }
}
",
triggerCharacter: "}");
");
}
[Fact]
@ -149,8 +142,7 @@ expected: @"
@code {
public class Foo { private int _hello = 0; }
}
",
triggerCharacter: ";");
");
}
[Fact]
@ -168,8 +160,7 @@ expected: @"
public class Foo{
private int _hello = 0; }
}
",
triggerCharacter: ";");
");
}
[Fact]
@ -191,8 +182,7 @@ expected: @"
var hello = 0;
}
}
",
triggerCharacter: ";");
");
}
[Fact]
@ -211,9 +201,7 @@ expected: @"
{
}
}
",
triggerCharacter: @"
"); // Newline
");
}
}
}

Просмотреть файл

@ -4,25 +4,34 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Microsoft.Web.Editor;
using Microsoft.WebTools.Languages.Shared.ContentTypes;
using Microsoft.WebTools.Languages.Shared.Editor.Text;
using Microsoft.WebTools.Shared;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.JsonRpc;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Progress;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using OmniSharp.Extensions.LanguageServer.Protocol.Server.WorkDone;
using Xunit.Sdk;
using Xunit;
using FormattingOptions = OmniSharp.Extensions.LanguageServer.Protocol.Models.FormattingOptions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
@ -40,12 +49,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
_baselineFileName = fileName;
}
#if GENERATE_BASELINES
protected bool GenerateBaselines { get; } = true;
#else
protected bool GenerateBaselines { get; } = false;
#endif
public IProgressManager ProgressManager => throw new NotImplementedException();
public IServerWorkDoneManager WorkDoneManager => throw new NotImplementedException();
@ -104,62 +107,84 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
var codeDocument = _documents[@params.HostDocumentFilePath];
var generatedHtml = codeDocument.GetHtmlDocument().GeneratedHtml;
generatedHtml = generatedHtml.Replace("\r", "", StringComparison.Ordinal).Replace("\n", "\r\n", StringComparison.Ordinal);
var generatedHtmlSource = SourceText.From(generatedHtml, Encoding.UTF8);
// Get formatted baseline file
var baselineInputFileName = Path.ChangeExtension(_baselineFileName, ".input.html");
var baselineOutputFileName = Path.ChangeExtension(_baselineFileName, ".output.html");
var editHandlerAssembly = Assembly.Load("Microsoft.WebTools.Languages.LanguageServer.Server, Version=16.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
var editHandlerType = editHandlerAssembly.GetType("Microsoft.WebTools.Languages.LanguageServer.Server.Html.OperationHandlers.ApplyFormatEditsHandler", throwOnError: true);
var bufferManagerType = editHandlerAssembly.GetType("Microsoft.WebTools.Languages.LanguageServer.Server.Shared.Buffer.BufferManager", throwOnError: true);
var baselineInputFile = TestFile.Create(baselineInputFileName, GetType().GetTypeInfo().Assembly);
var baselineOutputFile = TestFile.Create(baselineOutputFileName, GetType().GetTypeInfo().Assembly);
var exportProvider = EditorTestCompositions.Editor.ExportProviderFactory.CreateExportProvider();
var contentTypeService = exportProvider.GetExportedValue<IContentTypeRegistryService>();
if (GenerateBaselines)
{
if (baselineInputFile.Exists())
{
// If it already exists, we only want to update if the input is different.
var inputContent = baselineInputFile.ReadAllText();
if (string.Equals(inputContent, generatedHtml, StringComparison.Ordinal))
{
return response;
}
}
contentTypeService.AddContentType(HtmlContentTypeDefinition.HtmlContentType, new[] { StandardContentTypeNames.Text });
var baselineInputFilePath = Path.Combine(_projectPath, baselineInputFileName);
File.WriteAllText(baselineInputFilePath, generatedHtml);
var textBufferFactoryService = exportProvider.GetExportedValue<ITextBufferFactoryService>();
var textBufferListeners = Array.Empty<Lazy<IWebTextBufferListener, IOrderedComponentContentTypes>>();
var bufferManager = Activator.CreateInstance(bufferManagerType, new object[] { contentTypeService, textBufferFactoryService, textBufferListeners });
var joinableTaskFactoryThreadSwitcher = typeof(IdAttribute).Assembly.GetType("Microsoft.WebTools.Shared.Threading.JoinableTaskFactoryThreadSwitcher", throwOnError: true);
var threadSwitcher = (IThreadSwitcher)Activator.CreateInstance(joinableTaskFactoryThreadSwitcher, new object[] { new JoinableTaskContext().Factory });
var applyFormatEditsHandler = Activator.CreateInstance(editHandlerType, new object[] { bufferManager, threadSwitcher, textBufferFactoryService });
var baselineOutputFilePath = Path.Combine(_projectPath, baselineOutputFileName);
File.WriteAllText(baselineOutputFilePath, generatedHtml);
// Make sure the buffer manager knows about the source document
var documentUri = DocumentUri.From($"file:///{@params.HostDocumentFilePath}");
var contentTypeName = HtmlContentTypeDefinition.HtmlContentType;
var initialContent = generatedHtml;
var snapshotVersionFromLSP = 0;
Assert.IsAssignableFrom<ITextSnapshot>(bufferManager.GetType().GetMethod("CreateBuffer").Invoke(bufferManager, new object[] { documentUri, contentTypeName, initialContent, snapshotVersionFromLSP }));
return response;
}
if (!baselineInputFile.Exists())
{
throw new XunitException($"The resource {baselineInputFileName} was not found.");
}
if (!baselineOutputFile.Exists())
{
throw new XunitException($"The resource {baselineOutputFileName} was not found.");
}
var baselineInputHtml = baselineInputFile.ReadAllText();
if (!string.Equals(baselineInputHtml, generatedHtml, StringComparison.Ordinal))
{
throw new XunitException($"The baseline for {_baselineFileName} is out of date.");
}
var baselineOutputHtml = baselineOutputFile.ReadAllText();
var baselineInputText = SourceText.From(baselineInputHtml);
var baselineOutputText = SourceText.From(baselineOutputHtml);
var changes = SourceTextDiffer.GetMinimalTextChanges(baselineInputText, baselineOutputText, lineDiffOnly: false);
var edits = changes.Select(c => c.AsTextEdit(baselineInputText)).ToArray();
response.Edits = edits;
var requestType = editHandlerAssembly.GetType("Microsoft.WebTools.Languages.LanguageServer.Server.ContainedLanguage.ApplyFormatEditsParamForOmniSharp", throwOnError: true);
var serializedValue = $@"{{
""Options"": {{
""UseSpaces"": {(@params.Options.InsertSpaces ? "true" : "false")},
""TabSize"": {@params.Options.TabSize},
""IndentSize"": {@params.Options.TabSize}
}},
""SpanToFormat"": {{
""start"": {@params.ProjectedRange.AsTextSpan(generatedHtmlSource).Start},
""length"": {@params.ProjectedRange.AsTextSpan(generatedHtmlSource).Length},
}},
""Uri"": ""file:///{@params.HostDocumentFilePath}"",
""GeneratedChanges"": [
]
}}
";
var request = JsonConvert.DeserializeObject(serializedValue, requestType);
var resultTask = (Task)applyFormatEditsHandler.GetType().GetRuntimeMethod("Handle", new Type[] { requestType, typeof(CancellationToken) }).Invoke(applyFormatEditsHandler, new object[] { request, CancellationToken.None });
var result = resultTask.GetType().GetProperty(nameof(Task<int>.Result)).GetValue(resultTask);
var rawTextChanges = result.GetType().GetProperty("TextChanges").GetValue(result);
var serializedTextChanges = JsonConvert.SerializeObject(rawTextChanges, Newtonsoft.Json.Formatting.Indented);
var textChanges = JsonConvert.DeserializeObject<HtmlFormatterTextEdit[]>(serializedTextChanges);
response.Edits = textChanges.Select(change => change.AsTextEdit(SourceText.From(generatedHtml))).ToArray();
}
return response;
}
private struct HtmlFormatterTextEdit
{
#pragma warning disable CS0649 // Field 'name' is never assigned to, and will always have its default value
public int Position;
public int Length;
public string NewText;
#pragma warning restore CS0649 // Field 'name' is never assigned to, and will always have its default value
public TextEdit AsTextEdit(SourceText sourceText)
{
var startLinePosition = sourceText.Lines.GetLinePosition(Position);
var endLinePosition = sourceText.Lines.GetLinePosition(Position + Length);
return new TextEdit
{
Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range()
{
Start = new Position(startLinePosition.Line, startLinePosition.Character),
End = new Position(endLinePosition.Line, endLinePosition.Character),
},
NewText = NewText,
};
}
}
private static Document GetCSharpDocument(RazorCodeDocument codeDocument, FormattingOptions options)
{
var adhocWorkspace = new AdhocWorkspace();

Просмотреть файл

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.Language.IntegrationTests;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Moq;
@ -87,14 +88,10 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
var edited = ApplyEdits(source, edits);
var actual = edited.ToString();
#if GENERATE_BASELINES
Assert.False(true, "GENERATE_BASELINES is set to true.");
#else
Assert.Equal(expected, actual, ignoreLineEndingDifferences: true);
#endif
new XUnitVerifier().EqualOrDiff(expected, actual);
}
protected async Task RunOnTypeFormattingTestAsync(string input, string expected, string triggerCharacter, int tabSize = 4, bool insertSpaces = true, string fileKind = null)
protected async Task RunOnTypeFormattingTestAsync(string input, string expected, int tabSize = 4, bool insertSpaces = true, string fileKind = null)
{
// Arrange
fileKind ??= FileKinds.Component;
@ -122,11 +119,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
var edited = ApplyEdits(source, edits);
var actual = edited.ToString();
#if GENERATE_BASELINES
Assert.False(true, "GENERATE_BASELINES is set to true.");
#else
Assert.Equal(expected, actual);
#endif
new XUnitVerifier().EqualOrDiff(expected, actual);
}
private (RazorLanguageKind, TextEdit[]) GetFormattedEdits(RazorCodeDocument codeDocument, string expected, int positionBeforeTriggerChar)

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~ ~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~ ~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~ ~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~ ~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~ ~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~ ~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,27 +0,0 @@
~~~~~~ ~~~~~~~~~~~~~~
~~~~~~~~~~~
~~~~~~ ~~~~~ ~~~
~
~~~~~~ ~~~~~
~
~~~ ~~~ ~ ~~~ ~~~~~~~ ~ ~
~~~~~~ ~~~~~~
~~~~~~~
~~
~~~ ~~~ ~ ~~
~~~~ ~~~~~~
~~~
~~ ~~~~~~~~~
~~
~
~~~~~~ ~~~ ~~~~~~~~~~ ~ ~~~
~
~~~~~~ ~ ~
~ ~~~ ~~ ~
~~~~ ~~~~~~~~~
~
~
~

Просмотреть файл

@ -1,27 +0,0 @@
~~~~~~ ~~~~~~~~~~~~~~
~~~~~~~~~~~
~~~~~~ ~~~~~ ~~~
~
~~~~~~ ~~~~~
~
~~~ ~~~ ~ ~~~ ~~~~~~~ ~ ~
~~~~~~ ~~~~~~
~~~~~~~
~~
~~~ ~~~ ~ ~~
~~~~ ~~~~~~
~~~
~~ ~~~~~~~~~
~~
~
~~~~~~ ~~~ ~~~~~~~~~~ ~ ~~~
~
~~~~~~ ~ ~
~ ~~~ ~~ ~
~~~~ ~~~~~~~~~
~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ ~~~~~~~~~~~~~~~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ ~~~~~~~~~~~~~~~ ~
~
~

Просмотреть файл

@ -1,7 +0,0 @@
<div>Foo</div>
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~

Просмотреть файл

@ -1,7 +0,0 @@
<div>Foo</div>
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~

Просмотреть файл

@ -1,10 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ ~
~
~
~~~~~~~~ Scripts {
<script></script>
}

Просмотреть файл

@ -1,10 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ ~
~
~
~~~~~~~~ Scripts {
<script></script>
}

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ ~~~~~~~~~~~~~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ ~~~~~~~~~~~~~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ <div></div> ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~~~ ~~~~~~~~ ~ <div></div> ~
~
~

Просмотреть файл

@ -1,7 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~ ~~~~ ~~ ~ ~~~~~ ~~~~~~~ ~~
~~~~ ~~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,7 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~ ~~~~ ~~ ~ ~~~~~ ~~~~~~~ ~~
~~~~ ~~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~ ~~~~ ~~ ~ ~~~~~ ~~~~~~~ ~~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~
~~ ~~~~ ~~ ~ ~~~~~ ~~~~~~~ ~~
~
~

Просмотреть файл

@ -1,4 +0,0 @@
Hello World
~~~~~~~~~~ ~~~~~~~ ~~~~~ ~~~~~
~

Просмотреть файл

@ -1,4 +0,0 @@
Hello World
~~~~~~~~~~ ~~~~~~~ ~~~~~ ~~~~~
~

Просмотреть файл

@ -1,13 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~
Hello World
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~ ~
~~~~ ~~~~~~ ~ ~
~ ~
~
~

Просмотреть файл

@ -1,13 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~
Hello World
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~ ~
~~~~ ~~~~~~ ~ ~
~ ~
~
~

Просмотреть файл

@ -1,12 +0,0 @@
Hello World
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~~~~~~
~
~
~
~~~~~~~~~~~
~~~~~~ ~~~~~ ~~~ ~~
~

Просмотреть файл

@ -1,12 +0,0 @@
Hello World
~~~~~ ~
~~~~~~ ~~~~~ ~~~~~~~~~~
~
~
~
~~~~~~~~~~~
~~~~~~ ~~~~~ ~~~ ~~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~

Просмотреть файл

@ -1,6 +0,0 @@
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~~~
~~~~~~ ~~~~~~~~~ ~~~ ~
~
~

Просмотреть файл

@ -1,35 +0,0 @@
~~~~~ ~~~
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="FF"
id="ERT">
asdf
<div class="3"
id="3">
~~~~~~~~~~<p></p>~
</div>
</div>
~~
<div class="FF"
id="ERT">
asdf
<div class="3"
id="3">
~~~~~~~~~~<p></p>~
</div>
</div>
~
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~
~
~~ ~~~~ ~~ ~ ~~~~~ ~~~~~~~ ~~
~~~~ ~~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,35 +0,0 @@
~~~~~ ~~~
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="FF"
id="ERT">
asdf
<div class="3"
id="3">
~~~~~~~~~~<p></p>~
</div>
</div>
~~
<div class="FF"
id="ERT">
asdf
<div class="3"
id="3">
~~~~~~~~~~<p></p>~
</div>
</div>
~
~~~~~~~~~~ ~
~~~~~~ ~~~~~ ~~~
~
~~ ~~~~ ~~ ~ ~~~~~ ~~~~~~~ ~~
~~~~ ~~~~~~~~ ~ ~
~
~

Просмотреть файл

@ -1,14 +0,0 @@

<Counter>
~~~~~~~~~~
<p>~~~~~~~~~~~~~</p>
~
</Counter>
<GridTable>
~~~~~~~~ ~~~~ ~~~ ~~ ~~~~~~
<GridRow @onclick="SelectRow(row)">
~~~~~~~~ ~~~~ ~~~~ ~~ ~~~~~
<GridCell>~~~~~</GridCell>~</GridRow>
~
</GridTable>

Просмотреть файл

@ -1,15 +0,0 @@

<Counter>
~~~~~~~~~~
<p>~~~~~~~~~~~~~</p>
~
</Counter>
<GridTable>
~~~~~~~~ ~~~~ ~~~ ~~ ~~~~~~
<GridRow @onclick="SelectRow(row)">
~~~~~~~~ ~~~~ ~~~~ ~~ ~~~~~
<GridCell>~~~~~</GridCell>~
</GridRow>
~
</GridTable>

Просмотреть файл

@ -1,27 +0,0 @@
~~~~~ ~~~~~~~
<div
attr='val'
class=~~~~~~~~~~>Some Text</div>
~~
~~ Hi!
~~~ ~ ~ ~~~~~~~~
~~~~~~~~~~~~~~~~
<p>
~~~ ~~~~~~ ~
~~~ ~ ~ ~~
~
</p>
~
~~~~~~~~~~
~~~~
~~~~~~~~~~~~
~~
~~~~~~~~~~~~~~~~~~~ ~~
~
~~~~~~ ~~~~~~~~~~~~~
~~
~

Просмотреть файл

@ -1,28 +0,0 @@
~~~~~ ~~~~~~~
<div attr='val'
class=~~~~~~~~~~>
Some Text
</div>
~~
~~ Hi!
~~~ ~ ~ ~~~~~~~~
~~~~~~~~~~~~~~~~
<p>
~~~ ~~~~~~ ~
~~~ ~ ~ ~~
~
</p>
~
~~~~~~~~~~
~~~~
~~~~~~~~~~~~
~~
~~~~~~~~~~~~~~~~~~~ ~~
~
~~~~~~ ~~~~~~~~~~~~~
~~
~

Просмотреть файл

@ -1,21 +0,0 @@
~~~~~ ~~~~~~~
~~
<p>
~~
~~~ ~ ~ ~~
~~ ~~~~~~
~
~
~
</p>
<div>
~~
<div>
<div>
This is heavily nested
</div>
</div>
~
</div>
~

Просмотреть файл

@ -1,21 +0,0 @@
~~~~~ ~~~~~~~
~~
<p>
~~
~~~ ~ ~ ~~
~~ ~~~~~~
~
~
~
</p>
<div>
~~
<div>
<div>
This is heavily nested
</div>
</div>
~
</div>
~

Просмотреть файл

@ -1,20 +0,0 @@
~~~~~ ~~~~~~~
<div class=~~~~~~~~~~>Some Text</div>
~~
~~ Hi!
~~~ ~ ~ ~~~~
<p>
~~~ ~~~~~~ ~
~~~ ~ ~ ~~
~~ ~~~~~~
~
<div>~~~~~~~~~~~~~</div>
~
~~~~~~~~~~~~~
~
~
</p>
~

Просмотреть файл

@ -1,20 +0,0 @@
~~~~~ ~~~~~~~
<div class=~~~~~~~~~~>Some Text</div>
~~
~~ Hi!
~~~ ~ ~ ~~~~
<p>
~~~ ~~~~~~ ~
~~~ ~ ~ ~~
~~ ~~~~~~
~
<div>~~~~~~~~~~~~~</div>
~
~~~~~~~~~~~~~
~
~
</p>
~

Просмотреть файл

@ -1,22 +0,0 @@
~~~~~ ~~~~~~~~
<h1 class=
"text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.
</strong>
<div>
<div>
<div>
<div>
This is heavily nested
</div>
</div>
</div>
</div>
</p>

Просмотреть файл

@ -1,25 +0,0 @@
~~~~~ ~~~~~~~~
<h1 class="text-danger">
Error.
</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>
The Development environment shouldn't be enabled for deployed applications.
</strong>
<div>
<div>
<div>
<div>
This is heavily nested
</div>
</div>
</div>
</div>
</p>

Просмотреть файл

@ -1,8 +0,0 @@

<html>
<head>
<title>Hello</title></head>
<body><div>
</div>
</body>
</html>

Просмотреть файл

@ -1,10 +0,0 @@

<html>
<head>
<title>Hello</title>
</head>
<body>
<div>
</div>
</body>
</html>

Просмотреть файл

@ -12,13 +12,24 @@
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WebTools.Languages.Html" Version="$(MicrosoftWebToolsLanguagesHtmlPackageVersion)" NoWarn="NU1701" />
<PackageReference Include="Microsoft.WebTools.Languages.LanguageServer.Server" Version="$(MicrosoftWebToolsLanguagesLanguageServerServerPackageVersion)" NoWarn="NU1701" />
<PackageReference Include="Microsoft.WebTools.Languages.Shared" Version="$(MicrosoftWebToolsLanguagesSharedPackageVersion)" NoWarn="NU1701" />
<PackageReference Include="Microsoft.WebTools.Languages.Shared.Editor" Version="$(MicrosoftWebToolsLanguagesSharedEditorPackageVersion)" NoWarn="NU1701" />
<PackageReference Include="Microsoft.VisualStudio.Text.Data" Version="$(MicrosoftVisualStudioTextDataPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Text.Implementation" Version="$(MicrosoftVisualStudioTextImplementationPackageVersion)" NoWarn="NU1701" />
<PackageReference Include="Microsoft.VisualStudio.Text.Logic" Version="$(MicrosoftVisualStudioTextLogicPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Web" Version="$(MicrosoftVisualStudioWebPackageVersion)" NoWarn="NU1701" />
<PackageReference Include="Microsoft.WebTools.Shared" Version="$(MicrosoftWebToolsSharedPackageVersion)" NoWarn="NU1701" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.LanguageServer\Microsoft.AspNetCore.Razor.LanguageServer.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.LanguageServer.Test.Common\Microsoft.AspNetCore.Razor.LanguageServer.Test.Common.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Formatting\TestFiles\**\*" />
<EmbeddedResource Include="Semantic\TestFiles\**\*" />
</ItemGroup>

Просмотреть файл

@ -0,0 +1,292 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Composition;
namespace Microsoft.AspNetCore.Razor.Test.Common
{
public static class ExportProviderCache
{
private static readonly PartDiscovery s_partDiscovery = CreatePartDiscovery(Resolver.DefaultInstance);
private static readonly TestComposition s_defaultHostExportProviderComposition = TestComposition.Empty
.AddAssemblies(MefHostServices.DefaultAssemblies);
private static bool _enabled;
private static readonly ConcurrentDictionary<string, Scope> _scopes = new ConcurrentDictionary<string, Scope>();
private static readonly string DefaultScope = "default";
internal static bool Enabled => _enabled;
internal static ExportProvider[] ExportProvidersForCleanup
{
get
{
var scopes = _scopes.Values.ToArray();
var defaultScope = scopes.Where(scope => scope.Name == DefaultScope);
var allButDefault = scopes.Where(scope => scope.Name != DefaultScope);
// Make sure to return the default scope as the last element
return allButDefault.Concat(defaultScope)
.Where(scope => scope.CurrentExportProvider is { })
.Select(scope => scope.CurrentExportProvider!)
.ToArray();
}
}
internal static void SetEnabled_OnlyUseExportProviderAttributeCanCall(bool value)
{
_enabled = value;
if (!_enabled)
{
foreach (var scope in _scopes.Values.ToArray())
{
scope.Clear();
}
}
}
/// <summary>
/// Use to create <see cref="IExportProviderFactory"/> for default instances of <see cref="MefHostServices"/>.
/// </summary>
public static IExportProviderFactory GetOrCreateExportProviderFactory(IEnumerable<Assembly> assemblies)
{
if (assemblies is ImmutableArray<Assembly> assembliesArray &&
assembliesArray == MefHostServices.DefaultAssemblies)
{
return s_defaultHostExportProviderComposition.ExportProviderFactory;
}
return CreateExportProviderFactory(CreateAssemblyCatalog(assemblies), scopeName: DefaultScope);
}
public static ComposableCatalog CreateAssemblyCatalog(IEnumerable<Assembly> assemblies, Resolver? resolver = null)
{
var discovery = resolver == null ? s_partDiscovery : CreatePartDiscovery(resolver);
// If we run CreatePartsAsync on the test thread we may deadlock since it'll schedule stuff back
// on the thread.
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
var parts = Task.Run(async () => await discovery.CreatePartsAsync(assemblies).ConfigureAwait(false)).Result;
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
return ComposableCatalog.Create(resolver ?? Resolver.DefaultInstance).AddParts(parts);
}
public static ComposableCatalog CreateTypeCatalog(IEnumerable<Type> types, Resolver? resolver = null)
{
var discovery = resolver == null ? s_partDiscovery : CreatePartDiscovery(resolver);
// If we run CreatePartsAsync on the test thread we may deadlock since it'll schedule stuff back
// on the thread.
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
var parts = Task.Run(async () => await discovery.CreatePartsAsync(types).ConfigureAwait(false)).Result;
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
return ComposableCatalog.Create(resolver ?? Resolver.DefaultInstance).AddParts(parts);
}
public static Resolver CreateResolver()
{
// simple assembly loader is stateless, so okay to share
return new Resolver(SimpleAssemblyLoader.Instance);
}
public static PartDiscovery CreatePartDiscovery(Resolver resolver)
=> PartDiscovery.Combine(new AttributedPartDiscoveryV1(resolver), new AttributedPartDiscovery(resolver, isNonPublicSupported: true));
public static ComposableCatalog WithParts(this ComposableCatalog catalog, IEnumerable<Type> types)
=> catalog.AddParts(CreateTypeCatalog(types).DiscoveredParts);
/// <summary>
/// Creates a <see cref="ComposableCatalog"/> derived from <paramref name="catalog"/>, but with all exported
/// parts assignable to any type in <paramref name="types"/> removed from the catalog.
/// </summary>
public static ComposableCatalog WithoutPartsOfTypes(this ComposableCatalog catalog, IEnumerable<Type> types)
{
var parts = catalog.Parts.Where(composablePartDefinition => !IsExcludedPart(composablePartDefinition));
return ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts);
bool IsExcludedPart(ComposablePartDefinition part)
{
return types.Any(excludedType => excludedType.IsAssignableFrom(part.Type));
}
}
public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog, string? scopeName = null)
{
var scope = _scopes.GetOrAdd(scopeName ?? DefaultScope, scopeName => new Scope(scopeName));
var configuration = CompositionConfiguration.Create(catalog.WithCompositionService());
var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
var exportProviderFactory = runtimeComposition.CreateExportProviderFactory();
return new SingleExportProviderFactory(scope, catalog, configuration, exportProviderFactory);
}
private sealed class SingleExportProviderFactory : IExportProviderFactory
{
private readonly Scope _scope;
private readonly ComposableCatalog _catalog;
private readonly CompositionConfiguration _configuration;
private readonly IExportProviderFactory _exportProviderFactory;
public SingleExportProviderFactory(Scope scope, ComposableCatalog catalog, CompositionConfiguration configuration, IExportProviderFactory exportProviderFactory)
{
_scope = scope;
_catalog = catalog;
_configuration = configuration;
_exportProviderFactory = exportProviderFactory;
}
public ExportProvider GetOrCreateExportProvider()
{
if (!Enabled)
{
// The [UseExportProvider] attribute on tests ensures that the pre- and post-conditions of methods
// in this type are met during test conditions.
throw new InvalidOperationException($"{nameof(ExportProviderCache)} may only be used from tests marked with {nameof(UseExportProviderAttribute)}");
}
var expectedCatalog = Interlocked.CompareExchange(ref _scope.ExpectedCatalog, _catalog, null) ?? _catalog;
RequireForSingleExportProvider(expectedCatalog == _catalog);
var expected = _scope.ExpectedProviderForCatalog;
if (expected == null)
{
foreach (var errorCollection in _configuration.CompositionErrors)
{
foreach (var error in errorCollection)
{
foreach (var part in error.Parts)
{
foreach (var pair in part.SatisfyingExports)
{
var (importBinding, exportBindings) = (pair.Key, pair.Value);
if (exportBindings.Count <= 1)
{
// Ignore composition errors for missing parts
continue;
}
if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore)
{
// This failure occurs when a binding fails because multiple exports were
// provided but only a single one (at most) is expected. This typically occurs
// when a test ExportProvider is created with a mock implementation without
// first removing a value provided by default.
throw new InvalidOperationException(
"Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine
+ error.Message);
}
}
}
}
}
expected = _exportProviderFactory.CreateExportProvider();
expected = Interlocked.CompareExchange(ref _scope.ExpectedProviderForCatalog, expected, null) ?? expected;
Interlocked.CompareExchange(ref _scope.CurrentExportProvider, expected, null);
}
var exportProvider = _scope.CurrentExportProvider;
RequireForSingleExportProvider(exportProvider == expected);
return exportProvider!;
}
ExportProvider IExportProviderFactory.CreateExportProvider()
{
// Currently this implementation deviates from the typical behavior of IExportProviderFactory. For the
// duration of a single test, an instance of SingleExportProviderFactory will continue returning the
// same ExportProvider instance each time this method is called.
//
// It may be clearer to refactor the implementation to only allow one call to CreateExportProvider in
// the context of a single test. https://github.com/dotnet/roslyn/issues/25863
return GetOrCreateExportProvider();
}
private void RequireForSingleExportProvider([DoesNotReturnIf(false)] bool condition)
{
if (!condition)
{
// The ExportProvider provides services that act as singleton instances in the context of an
// application (this include cases of multiple exports, where the 'singleton' is the list of all
// exports matching the contract). When reasoning about the behavior of test code, it is valuable to
// know service instances will be used in a consistent manner throughout the execution of a test,
// regardless of whether they are passed as arguments or obtained through requests to the
// ExportProvider.
//
// Restricting a test to a single ExportProvider guarantees that objects that *look* like singletons
// will *behave* like singletons for the duration of the test. Each test is expected to create and
// use its ExportProvider in a consistent manner.
//
// A test that validates remote services is allowed to create a couple of ExportProviders:
// one for local workspace and the other for the remote one.
//
// When this exception is thrown by a test, it typically means one of the following occurred:
//
// * A test failed to pass an ExportProvider via an optional argument to a method, resulting in the
// method attempting to create a default ExportProvider which did not match the one assigned to
// the test.
// * A test attempted to perform multiple test sequences in the context of a single test method,
// rather than break up the test into distinct tests for each case.
// * A test referenced different predefined ExportProvider instances within the context of a test.
// Each test is expected to use the same ExportProvider throughout the test.
throw new InvalidOperationException($"Only one {_scope.Name} {nameof(ExportProvider)} can be created in the context of a single test.");
}
}
}
private sealed class Scope
{
public readonly string Name;
public ExportProvider? CurrentExportProvider;
public ComposableCatalog? ExpectedCatalog;
public ExportProvider? ExpectedProviderForCatalog;
public Scope(string name)
{
Name = name;
}
public void Clear()
{
CurrentExportProvider = null;
ExpectedCatalog = null;
ExpectedProviderForCatalog = null;
}
}
private sealed class SimpleAssemblyLoader : IAssemblyLoader
{
public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader();
public Assembly LoadAssembly(AssemblyName assemblyName)
=> Assembly.Load(assemblyName);
public Assembly LoadAssembly(string assemblyFullName, string codeBasePath)
{
var assemblyName = new AssemblyName(assemblyFullName);
if (!string.IsNullOrEmpty(codeBasePath))
{
assemblyName.CodeBase = codeBasePath;
}
return this.LoadAssembly(assemblyName);
}
}
}
}

Просмотреть файл

@ -497,7 +497,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
{
var span = spans[i];
var sourceSpan = span.GetSourceSpan(codeDocument.Source);
if (sourceSpan == null)
if (sourceSpan == SourceSpan.Undefined)
{
// Not in the main file, skip.
continue;

Просмотреть файл

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
@ -18,6 +19,7 @@ using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
{
[UseExportProvider]
public class RazorIntegrationTestBase
{
internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test";

Просмотреть файл

@ -4,7 +4,7 @@
<!-- To generate baselines, run tests with /p:GenerateBaselines=true -->
<DefineConstants Condition="'$(GenerateBaselines)'=='true'">$(DefineConstants);GENERATE_BASELINES</DefineConstants>
<DefineConstants>$(DefineConstants);__RemoveThisBitTo__GENERATE_BASELINES</DefineConstants>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<TargetFrameworks>net5.0;netstandard2.0;net461</TargetFrameworks>
<IsShipping>false</IsShipping>
</PropertyGroup>
@ -18,10 +18,16 @@
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="$(MicrosoftCodeAnalysisAnalyzerTestingPackageVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(Tooling_MicrosoftCodeAnalysisCSharpPackageVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Razor" Version="$(MicrosoftCodeAnalysisRazorPackageVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Testing.Verifiers.XUnit" Version="$(MicrosoftCodeAnalysisTestingVerifiersXunitPackageVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(Tooling_MicrosoftCodeAnalysisWorkspacesCommonPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="$(MicrosoftVisualStudioThreadingPackageVersion)" />
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersPackageVersion)" />
<PackageReference Include="Xunit.Combinatorial" Version="$(XunitCombinatorialPackageVersion)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
<Reference Include="WindowsBase" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,151 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable
// This was copied from https://github.com/dotnet/runtime/blob/39b9607807f29e48cae4652cd74735182b31182e/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
// and updated to have the scope of the attributes be internal.
namespace System.Diagnostics.CodeAnalysis
{
#if !NETCOREAPP
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class AllowNullAttribute : Attribute { }
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class DisallowNullAttribute : Attribute { }
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class MaybeNullAttribute : Attribute { }
/// <summary>Specifies that an output will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class NotNullAttribute : Attribute { }
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter may be null.
/// </param>
public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
internal sealed class NotNullIfNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with the associated parameter name.</summary>
/// <param name="parameterName">
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
/// </param>
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
/// <summary>Gets the associated parameter name.</summary>
public string ParameterName { get; }
}
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute { }
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified parameter value.</summary>
/// <param name="parameterValue">
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
/// the associated parameter matches this value.
/// </param>
public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
/// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; }
}
#endif
#if !NETCOREAPP || NETCOREAPP3_1
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullAttribute(string member) => Members = new[] { member };
/// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullAttribute(params string[] members) => Members = members;
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.</summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = new[] { member };
}
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
#endif
}

Просмотреть файл

@ -0,0 +1,243 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
namespace Microsoft.AspNetCore.Razor.Test.Common
{
/// <summary>
/// Represents a MEF composition used for testing.
/// </summary>
public sealed class TestComposition
{
public static readonly TestComposition Empty = new TestComposition(ImmutableHashSet<Assembly>.Empty, ImmutableHashSet<Type>.Empty, ImmutableHashSet<Type>.Empty, scope: null);
private static readonly Dictionary<CacheKey, IExportProviderFactory> s_factoryCache = new Dictionary<CacheKey, IExportProviderFactory>();
private readonly struct CacheKey : IEquatable<CacheKey>
{
private readonly ImmutableArray<Assembly> _assemblies;
private readonly ImmutableArray<Type> _parts;
private readonly ImmutableArray<Type> _excludedPartTypes;
public CacheKey(ImmutableHashSet<Assembly> assemblies, ImmutableHashSet<Type> parts, ImmutableHashSet<Type> excludedPartTypes)
{
_assemblies = assemblies.OrderBy(a => a.FullName, StringComparer.Ordinal).ToImmutableArray();
_parts = parts.OrderBy(a => a.FullName, StringComparer.Ordinal).ToImmutableArray();
_excludedPartTypes = excludedPartTypes.OrderBy(a => a.FullName, StringComparer.Ordinal).ToImmutableArray();
}
public override bool Equals(object? obj)
=> obj is CacheKey key && Equals(key);
public bool Equals(CacheKey other)
=> _parts.SequenceEqual(other._parts) &&
_excludedPartTypes.SequenceEqual(other._excludedPartTypes) &&
_assemblies.SequenceEqual(other._assemblies);
public override int GetHashCode()
{
var hashCode = -744873704;
foreach (var assembly in _assemblies)
hashCode = hashCode * -1521134295 + assembly.GetHashCode();
foreach (var part in _parts)
hashCode = hashCode * -1521134295 + part.GetHashCode();
foreach (var excludedPartType in _excludedPartTypes)
hashCode = hashCode * -1521134295 + excludedPartType.GetHashCode();
return hashCode;
}
public static bool operator ==(CacheKey left, CacheKey right)
=> left.Equals(right);
public static bool operator !=(CacheKey left, CacheKey right)
=> !(left == right);
}
/// <summary>
/// Assemblies to include in the composition.
/// </summary>
public readonly ImmutableHashSet<Assembly> Assemblies;
/// <summary>
/// Types to exclude from the composition.
/// All subtypes of types specified in <see cref="ExcludedPartTypes"/> and defined in <see cref="Assemblies"/> are excluded before <see cref="Parts"/> are added.
/// </summary>
public readonly ImmutableHashSet<Type> ExcludedPartTypes;
/// <summary>
/// Additional part types to add to the composition.
/// </summary>
public readonly ImmutableHashSet<Type> Parts;
/// <summary>
/// The scope in which to create the export provider, or <see langword="null"/> to use the default scope.
/// </summary>
public readonly string? Scope;
private readonly Lazy<IExportProviderFactory> _exportProviderFactory;
private TestComposition(ImmutableHashSet<Assembly> assemblies, ImmutableHashSet<Type> parts, ImmutableHashSet<Type> excludedPartTypes, string? scope)
{
Assemblies = assemblies;
Parts = parts;
ExcludedPartTypes = excludedPartTypes;
Scope = scope;
_exportProviderFactory = new Lazy<IExportProviderFactory>(GetOrCreateFactory);
}
#if false
/// <summary>
/// Returns a new instance of <see cref="HostServices"/> for the composition. This will either be a MEF composition or VS MEF composition host,
/// depending on what layer the composition is for. Editor Features and VS layers use VS MEF composition while anything else uses System.Composition.
/// </summary>
public HostServices GetHostServices()
=> VisualStudioMefHostServices.Create(ExportProviderFactory.CreateExportProvider());
#endif
/// <summary>
/// VS MEF <see cref="ExportProvider"/>.
/// </summary>
public IExportProviderFactory ExportProviderFactory => _exportProviderFactory.Value;
private IExportProviderFactory GetOrCreateFactory()
{
var key = new CacheKey(Assemblies, Parts, ExcludedPartTypes);
lock (s_factoryCache)
{
if (s_factoryCache.TryGetValue(key, out var existing))
{
return existing;
}
}
var newFactory = ExportProviderCache.CreateExportProviderFactory(GetCatalog(), Scope);
lock (s_factoryCache)
{
if (s_factoryCache.TryGetValue(key, out var existing))
{
return existing;
}
s_factoryCache.Add(key, newFactory);
}
return newFactory;
}
private ComposableCatalog GetCatalog()
=> ExportProviderCache.CreateAssemblyCatalog(Assemblies, ExportProviderCache.CreateResolver()).WithoutPartsOfTypes(ExcludedPartTypes).WithParts(Parts);
public TestComposition Add(TestComposition composition)
=> AddAssemblies(composition.Assemblies).AddParts(composition.Parts).AddExcludedPartTypes(composition.ExcludedPartTypes);
public TestComposition AddAssemblies(params Assembly[]? assemblies)
=> AddAssemblies((IEnumerable<Assembly>?)assemblies);
public TestComposition AddAssemblies(IEnumerable<Assembly>? assemblies)
=> WithAssemblies(Assemblies.Union(assemblies ?? Array.Empty<Assembly>()));
public TestComposition AddParts(IEnumerable<Type>? types)
=> WithParts(Parts.Union(types ?? Array.Empty<Type>()));
public TestComposition AddParts(params Type[]? types)
=> AddParts((IEnumerable<Type>?)types);
public TestComposition AddExcludedPartTypes(IEnumerable<Type>? types)
=> WithExcludedPartTypes(ExcludedPartTypes.Union(types ?? Array.Empty<Type>()));
public TestComposition AddExcludedPartTypes(params Type[]? types)
=> AddExcludedPartTypes((IEnumerable<Type>?)types);
public TestComposition Remove(TestComposition composition)
=> RemoveAssemblies(composition.Assemblies).RemoveParts(composition.Parts).RemoveExcludedPartTypes(composition.ExcludedPartTypes);
public TestComposition RemoveAssemblies(params Assembly[]? assemblies)
=> RemoveAssemblies((IEnumerable<Assembly>?)assemblies);
public TestComposition RemoveAssemblies(IEnumerable<Assembly>? assemblies)
=> WithAssemblies(Assemblies.Except(assemblies ?? Array.Empty<Assembly>()));
public TestComposition RemoveParts(IEnumerable<Type>? types)
=> WithParts(Parts.Except(types ?? Array.Empty<Type>()));
public TestComposition RemoveParts(params Type[]? types)
=> RemoveParts((IEnumerable<Type>?)types);
public TestComposition RemoveExcludedPartTypes(IEnumerable<Type>? types)
=> WithExcludedPartTypes(ExcludedPartTypes.Except(types ?? Array.Empty<Type>()));
public TestComposition RemoveExcludedPartTypes(params Type[]? types)
=> RemoveExcludedPartTypes((IEnumerable<Type>?)types);
public TestComposition WithAssemblies(ImmutableHashSet<Assembly> assemblies)
{
if (assemblies == Assemblies)
{
return this;
}
var testAssembly = assemblies.FirstOrDefault(IsTestAssembly);
Verify.Operation(testAssembly == null, $"Test assemblies are not allowed in test composition: {testAssembly}. Specify explicit test parts instead.");
return new TestComposition(assemblies, Parts, ExcludedPartTypes, Scope);
static bool IsTestAssembly(Assembly assembly)
{
var name = assembly.GetName().Name!;
return
name.EndsWith(".Test", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith(".Tests", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith(".UnitTests", StringComparison.OrdinalIgnoreCase) ||
name.IndexOf("Test.Utilities", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("Test.Common", StringComparison.OrdinalIgnoreCase) >= 0;
}
}
public TestComposition WithParts(ImmutableHashSet<Type> parts)
=> (parts == Parts) ? this : new TestComposition(Assemblies, parts, ExcludedPartTypes, Scope);
public TestComposition WithExcludedPartTypes(ImmutableHashSet<Type> excludedPartTypes)
=> (excludedPartTypes == ExcludedPartTypes) ? this : new TestComposition(Assemblies, Parts, excludedPartTypes, Scope);
public TestComposition WithScope(string? scope)
=> scope == Scope ? this : new TestComposition(Assemblies, Parts, ExcludedPartTypes, scope);
/// <summary>
/// Use for VS MEF composition troubleshooting.
/// </summary>
/// <returns>All composition error messages.</returns>
internal string GetCompositionErrorLog()
{
var configuration = CompositionConfiguration.Create(GetCatalog());
var sb = new StringBuilder();
foreach (var errorGroup in configuration.CompositionErrors)
{
foreach (var error in errorGroup)
{
sb.Append(error.Message);
sb.AppendLine();
}
}
return sb.ToString();
}
}
}

Просмотреть файл

@ -0,0 +1,148 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable
using System;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading;
namespace Microsoft.AspNetCore.Razor.Test.Common
{
internal partial class TestExportJoinableTaskContext
{
/// <summary>
/// Defines a <see cref="SynchronizationContext"/> for use in cases where the synchronization context should not
/// be used for execution of code. Attempting to execute code through this synchronization context will record
/// an exception (with stack trace) for the first occurrence, which can be re-thrown by calling
/// <see cref="ThrowIfSwitchOccurred"/> at an appropriate time.
/// </summary>
/// <remarks>
/// <para>This synchronization context is used in cases where code expects a synchronization context with
/// specific properties (e.g. code is executed on a "main thread"), but no synchronization context with those
/// properties is available. Due to the positioning of synchronization contexts within asynchronous code
/// patterns, detection of misused synchronization contexts without crashes is often challenging. Test cases can
/// use this synchronization context in place of a mock to capture </para>
///
/// <note type="important">
/// <para>This synchronization context will not directly block the execution of scheduled tasks, and will fall
/// back to execution on an underlying synchronization context (typically the thread pool). Tests using this
/// synchronization context should verify the synchronization context was not used by calling
/// <see cref="ThrowIfSwitchOccurred"/> at the end of the test.</para>
/// </note>
/// </remarks>
internal sealed class DenyExecutionSynchronizationContext : SynchronizationContext
{
/// <summary>
/// Records the first case where the synchronization context was used for scheduling an operation.
/// </summary>
/// <remarks>
/// <para>The <see cref="StrongBox{T}"/> wrapper allows copies of the synchronization context (created by
/// <see cref="CreateCopy"/>) to record information about incorrect synchronization context in the original
/// synchronization context from which it was created.</para>
/// </remarks>
private readonly StrongBox<ExceptionDispatchInfo> _failedTransfer;
/// <summary>
/// Initializes a new instance of the <see cref="DenyExecutionSynchronizationContext"/> class.
/// </summary>
/// <param name="underlyingContext">The fallback synchronization context to use for scheduling operations
/// posted to this synchronization context.</param>
public DenyExecutionSynchronizationContext(SynchronizationContext? underlyingContext)
: this(underlyingContext, mainThread: null, failedTransfer: null)
{
}
private DenyExecutionSynchronizationContext(SynchronizationContext? underlyingContext, Thread? mainThread, StrongBox<ExceptionDispatchInfo>? failedTransfer)
{
UnderlyingContext = underlyingContext ?? new SynchronizationContext();
MainThread = mainThread ?? new Thread(MainThreadStart);
_failedTransfer = failedTransfer ?? new StrongBox<ExceptionDispatchInfo>();
}
internal event EventHandler? InvalidSwitch;
private SynchronizationContext UnderlyingContext
{
get;
}
/// <summary>
/// Gets the <see cref="Thread"/> to treat as the main thread.
/// </summary>
/// <remarks>
/// <para>This thread will never be started, and will never have work scheduled for execution. The value is
/// used for checking if <see cref="Thread.CurrentThread"/> matches a known "main thread", and ensures that
/// the comparison will always result in a failure (not currently on the main thread).</para>
/// </remarks>
internal Thread MainThread
{
get;
}
private void MainThreadStart() => throw new InvalidOperationException("This location is thought to be unreachable");
/// <summary>
/// Verifies that the current synchronization context has not been used for scheduling work. If the
/// synchronization was used, an exception is thrown with information about the first such case.
/// </summary>
internal void ThrowIfSwitchOccurred()
{
if (_failedTransfer.Value == null)
{
return;
}
_failedTransfer.Value.Throw();
}
public override void Post(SendOrPostCallback d, object? state)
{
try
{
if (_failedTransfer.Value == null)
{
ThrowFailedTransferExceptionForCapture();
}
}
catch (InvalidOperationException e)
{
_failedTransfer.Value = ExceptionDispatchInfo.Capture(e);
InvalidSwitch?.Invoke(this, EventArgs.Empty);
}
#pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs
UnderlyingContext.Post(d, state);
#pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs
}
public override void Send(SendOrPostCallback d, object? state)
{
try
{
if (_failedTransfer.Value == null)
{
ThrowFailedTransferExceptionForCapture();
}
}
catch (InvalidOperationException e)
{
_failedTransfer.Value = ExceptionDispatchInfo.Capture(e);
InvalidSwitch?.Invoke(this, EventArgs.Empty);
}
#pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs
UnderlyingContext.Send(d, state);
#pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs
}
public override SynchronizationContext CreateCopy()
=> new DenyExecutionSynchronizationContext(UnderlyingContext.CreateCopy(), MainThread, _failedTransfer);
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowFailedTransferExceptionForCapture()
=> throw new InvalidOperationException($"Code cannot switch to the main thread without configuring the threading context.");
}
}
}

Просмотреть файл

@ -0,0 +1,163 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable
using System.ComponentModel.Composition;
using System.Threading;
using Microsoft.VisualStudio.Threading;
using Xunit.Sdk;
#if NETFRAMEWORK
using System.Windows.Threading;
#endif
#if false
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
#endif
namespace Microsoft.AspNetCore.Razor.Test.Common
{
// Starting with 15.3 the editor took a dependency on JoinableTaskContext
// in Text.Logic and IntelliSense layers as an editor host provided service.
[Export]
internal partial class TestExportJoinableTaskContext
{
[ImportingConstructor]
//[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TestExportJoinableTaskContext()
{
var synchronizationContext = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(GetEffectiveSynchronizationContext());
(JoinableTaskContext, SynchronizationContext) = CreateJoinableTaskContext();
#if false
ResetThreadAffinity(JoinableTaskContext.Factory);
#endif
}
finally
{
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
}
}
private static (JoinableTaskContext joinableTaskContext, SynchronizationContext synchronizationContext) CreateJoinableTaskContext()
{
Thread mainThread;
SynchronizationContext synchronizationContext;
#if NETFRAMEWORK
if (SynchronizationContext.Current is DispatcherSynchronizationContext)
{
// The current thread is the main thread, and provides a suitable synchronization context
mainThread = Thread.CurrentThread;
synchronizationContext = SynchronizationContext.Current;
}
else
#endif
{
// The current thread is not known to be the main thread; we have no way to know if the
// synchronization context of the current thread will behave in a manner consistent with main thread
// synchronization contexts, so we use DenyExecutionSynchronizationContext to track any attempted
// use of it.
var denyExecutionSynchronizationContext = new DenyExecutionSynchronizationContext(SynchronizationContext.Current);
mainThread = denyExecutionSynchronizationContext.MainThread;
synchronizationContext = denyExecutionSynchronizationContext;
}
return (new JoinableTaskContext(mainThread, synchronizationContext), synchronizationContext);
}
[Export]
private JoinableTaskContext JoinableTaskContext
{
get;
}
internal SynchronizationContext SynchronizationContext
{
get;
}
internal static SynchronizationContext? GetEffectiveSynchronizationContext()
{
if (SynchronizationContext.Current is AsyncTestSyncContext asyncTestSyncContext)
{
SynchronizationContext? innerSynchronizationContext = null;
asyncTestSyncContext.Send(
_ =>
{
innerSynchronizationContext = SynchronizationContext.Current;
},
null);
return innerSynchronizationContext;
}
else
{
return SynchronizationContext.Current;
}
}
#if false
/// <summary>
/// Reset the thread affinity, in particular the designated foreground thread, to the active
/// thread.
/// </summary>
internal static void ResetThreadAffinity(JoinableTaskFactory joinableTaskFactory)
{
// HACK: When the platform team took over several of our components they created a copy
// of ForegroundThreadAffinitizedObject. This needs to be reset in the same way as our copy
// does. Reflection is the only choice at the moment.
var thread = joinableTaskFactory.Context.MainThread;
var taskScheduler = new JoinableTaskFactoryTaskScheduler(joinableTaskFactory);
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
{
var type = assembly.GetType("Microsoft.VisualStudio.Language.Intellisense.Implementation.ForegroundThreadAffinitizedObject", throwOnError: false);
if (type != null)
{
type.GetField("foregroundThread", BindingFlags.Static | BindingFlags.NonPublic)!.SetValue(null, thread);
type.GetField("ForegroundTaskScheduler", BindingFlags.Static | BindingFlags.NonPublic)!.SetValue(null, taskScheduler);
break;
}
}
}
// HACK: Part of ResetThreadAffinity
private class JoinableTaskFactoryTaskScheduler : TaskScheduler
{
private readonly JoinableTaskFactory _joinableTaskFactory;
public JoinableTaskFactoryTaskScheduler(JoinableTaskFactory joinableTaskFactory)
=> _joinableTaskFactory = joinableTaskFactory;
public override int MaximumConcurrencyLevel => 1;
protected override IEnumerable<Task>? GetScheduledTasks() => null;
protected override void QueueTask(Task task)
{
_joinableTaskFactory.RunAsync(async () =>
{
await _joinableTaskFactory.SwitchToMainThreadAsync();
TryExecuteTask(task);
});
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
if (_joinableTaskFactory.Context.IsOnMainThread)
{
return TryExecuteTask(task);
}
return false;
}
}
#endif
}
}

Просмотреть файл

@ -0,0 +1,235 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable
using System;
using System.Reflection;
using Microsoft.CodeAnalysis.Host;
using Microsoft.VisualStudio.Composition;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Test.Common
{
/// <summary>
/// This attribute supports tests that need to use a MEF container (<see cref="ExportProvider"/>) directly or
/// indirectly during the test sequence. It ensures production code uniformly handles the export provider created
/// during a test, and cleans up the state before the test completes.
/// </summary>
/// <remarks>
/// <para>This attribute serves several important functions for tests that use state variables which are otherwise
/// shared at runtime:</para>
/// <list type="bullet">
/// <item>Ensures <see cref="HostServices"/> implementations all use the same <see cref="ExportProvider"/>, which is
/// the one created by the test.</item>
/// <item>Clears static cached values in production code holding instances of <see cref="HostServices"/>, or any
/// object obtained from it or one of its related interfaces such as <see cref="HostLanguageServices"/>.</item>
/// <item>Isolates tests by waiting for asynchronous operations to complete before a test is considered
/// complete.</item>
/// <item>When required, provides a separate <see cref="ExportProvider"/> for the <see cref="T:RemoteWorkspace"/>
/// executing in the test process. If this provider is created during testing, it is cleaned up with the primary
/// export provider during test teardown.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class UseExportProviderAttribute : BeforeAfterTestAttribute
{
/// <summary>
/// Asynchronous operations are expected to be cancelled at the end of the test that started them. Operations
/// cancelled by the test are cleaned up immediately. The remaining operations are given an opportunity to run
/// to completion. If this timeout is exceeded by the asynchronous operations running after a test completes,
/// the test is failed.
/// </summary>
private static readonly TimeSpan CleanupTimeout = TimeSpan.FromMinutes(1);
#if false
private MefHostServices? _hostServices;
#endif
public override void Before(MethodInfo? methodUnderTest)
{
#if false
MefHostServices.TestAccessor.HookServiceCreation(CreateMefHostServices);
// make sure we enable this for all unit tests
AsynchronousOperationListenerProvider.Enable(enable: true, diagnostics: true);
#endif
ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(true);
}
/// <summary>
/// To the extent reasonably possible, this method resets the state of the test environment to the same state as
/// it started, ensuring that tests running in sequence cannot influence the outcome of later tests.
/// </summary>
/// <remarks>
/// <para>The test cleanup runs in two primary steps:</para>
/// <list type="number">
/// <item>Waiting for asynchronous operations started by the test to complete.</item>
/// <item>Disposing of mutable resources created by the test.</item>
/// <item>Clearing static state variables related to the use of MEF during a test.</item>
/// </list>
/// </remarks>
public override void After(MethodInfo? methodUnderTest)
{
try
{
foreach (var exportProvider in ExportProviderCache.ExportProvidersForCleanup)
{
DisposeExportProvider(exportProvider);
}
}
finally
{
#if false
// Replace hooks with ones that always throw exceptions. These hooks detect cases where code executing
// after the end of a test attempts to create an ExportProvider.
MefHostServices.TestAccessor.HookServiceCreation(DenyMefHostServicesCreationBetweenTests);
#endif
// Reset static state variables.
#if false
_hostServices = null;
#endif
ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(false);
}
}
private static void DisposeExportProvider(ExportProvider? exportProvider)
{
if (exportProvider == null)
{
return;
}
// Dispose of the export provider, including calling Dispose for any IDisposable services created during the test.
using var _ = exportProvider;
#if false
if (exportProvider.GetExportedValues<IAsynchronousOperationListenerProvider>().SingleOrDefault() is { } listenerProvider)
{
if (exportProvider.GetExportedValues<IThreadingContext>().SingleOrDefault()?.HasMainThread ?? false)
{
// Immediately clear items from the foreground notification service for which cancellation is
// requested. This service maintains a queue separately from Tasks, and work items scheduled for
// execution after a delay are not immediately purged when cancellation is requested. This code
// instructs the service to walk the list of queued work items and immediately cancel and purge any
// which are already cancelled.
var foregroundNotificationService = exportProvider.GetExportedValues<IForegroundNotificationService>().SingleOrDefault() as ForegroundNotificationService;
foregroundNotificationService?.ReleaseCancelledItems();
}
// Verify the synchronization context was not used incorrectly
var testExportJoinableTaskContext = exportProvider.GetExportedValues<TestExportJoinableTaskContext>().SingleOrDefault();
var denyExecutionSynchronizationContext = testExportJoinableTaskContext?.SynchronizationContext as TestExportJoinableTaskContext.DenyExecutionSynchronizationContext;
// Join remaining operations with a timeout
using (var timeoutTokenSource = new CancellationTokenSource(CleanupTimeout))
{
if (denyExecutionSynchronizationContext is object)
{
// Immediately cancel the test if the synchronization context is improperly used
denyExecutionSynchronizationContext.InvalidSwitch += delegate { timeoutTokenSource.CancelAfter(0); };
denyExecutionSynchronizationContext.ThrowIfSwitchOccurred();
}
try
{
// This attribute cleans up the in-process and out-of-process export providers separately, so we
// don't need to provide a workspace when waiting for operations to complete.
var waiter = ((AsynchronousOperationListenerProvider)listenerProvider).WaitAllDispatcherOperationAndTasksAsync(workspace: null);
waiter.JoinUsingDispatcher(timeoutTokenSource.Token);
}
catch (OperationCanceledException ex) when (timeoutTokenSource.IsCancellationRequested)
{
// If the failure was caused by an invalid thread change, throw that exception
denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred();
var messageBuilder = new StringBuilder("Failed to clean up listeners in a timely manner.");
foreach (var token in ((AsynchronousOperationListenerProvider)listenerProvider).GetTokens())
{
messageBuilder.AppendLine().Append($" {token}");
}
throw new TimeoutException(messageBuilder.ToString(), ex);
}
}
denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred();
foreach (var testErrorHandler in exportProvider.GetExportedValues<ITestErrorHandler>())
{
var exceptions = testErrorHandler.Exceptions;
if (exceptions.Count > 0)
{
throw new AggregateException("Tests threw unexpected exceptions", exceptions);
}
}
}
#endif
}
#if false
private MefHostServices CreateMefHostServices(IEnumerable<Assembly> assemblies)
{
ExportProvider exportProvider;
if (assemblies is ImmutableArray<Assembly> array &&
array == MefHostServices.DefaultAssemblies &&
ExportProviderCache.LocalExportProviderForCleanup != null)
{
if (_hostServices != null)
{
return _hostServices;
}
exportProvider = ExportProviderCache.LocalExportProviderForCleanup;
}
else
{
exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(assemblies).CreateExportProvider();
}
Interlocked.CompareExchange(
ref _hostServices,
new ExportProviderMefHostServices(exportProvider),
null);
return _hostServices;
}
private static MefHostServices DenyMefHostServicesCreationBetweenTests(IEnumerable<Assembly> assemblies)
{
// If you hit this, one of three situations occurred:
//
// 1. A test method that uses ExportProvider is not marked with UseExportProviderAttribute (can also be
// applied to the containing type or a base type.
// 2. A test attempted to create an ExportProvider during the test cleanup operations after the
// ExportProvider was already disposed.
// 3. A test attempted to use an ExportProvider in the constructor of the test, or during the initialization
// of a field in the test class.
throw new InvalidOperationException("Cannot create host services after test tear down.");
}
private class ExportProviderMefHostServices : MefHostServices, IMefHostExportProvider
{
private readonly VisualStudioMefHostServices _vsHostServices;
public ExportProviderMefHostServices(ExportProvider exportProvider)
: base(new ContainerConfiguration().CreateContainer())
{
_vsHostServices = VisualStudioMefHostServices.Create(exportProvider);
}
protected internal override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace)
=> _vsHostServices.CreateWorkspaceServices(workspace);
IEnumerable<Lazy<TExtension, TMetadata>> IMefHostExportProvider.GetExports<TExtension, TMetadata>()
=> _vsHostServices.GetExports<TExtension, TMetadata>();
IEnumerable<Lazy<TExtension>> IMefHostExportProvider.GetExports<TExtension>()
=> _vsHostServices.GetExports<TExtension>();
}
#endif
}
}