From 005c87f1c41da3f1b2b35f32c98e3d3132df99a3 Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Sat, 24 Apr 2021 17:07:50 -0400 Subject: [PATCH] WIP tracing support (#553) * WIP tracing support * Added unit tests, * Moved integration tests to allow for modifying xunit behavior --- LSP.sln | 15 + build.sh | 0 src/Dap.Protocol/DapReceiver.cs | 30 +- .../DapClientNotificationConverter.cs | 10 + .../DapClientRequestConverter.cs | 10 + src/Dap.Shared/DebugAdapterRequestRouter.cs | 5 +- src/Directory.Build.props | 69 ++-- src/JsonRpc/Client/OutgoingNotification.cs | 16 +- src/JsonRpc/Client/OutgoingRequest.cs | 14 +- src/JsonRpc/IActivityTracingStrategy.cs | 353 ++++++++++++++++++ src/JsonRpc/JsonRpc.csproj | 1 + src/JsonRpc/JsonRpcServerOptionsBase.cs | 7 + src/JsonRpc/OutputHandler.cs | 23 +- src/JsonRpc/Receiver.cs | 17 +- src/JsonRpc/RequestRouter.cs | 5 +- src/JsonRpc/RequestRouterBase.cs | 147 ++++---- .../Converters/ClientNotificationConverter.cs | 10 + .../Converters/ClientRequestConverter.cs | 10 + src/JsonRpc/Server/Notification.cs | 12 +- src/JsonRpc/Server/Request.cs | 12 +- src/Shared/LspRequestRouter.cs | 5 +- test/Directory.Build.targets | 69 ++-- .../ActivityTracingTests.cs | 64 ++++ .../ConnectionAndDisconnectionTests.cs | 0 .../CustomRequestsTests.cs | 0 .../DisableDefaultsTests.cs | 0 .../DynamicRegistrationTests.cs | 0 .../ErroringHandlingTests.cs | 0 .../EventingTests.cs | 0 .../ExecuteCommandTests.cs | 0 .../ExecuteTypedCommandTests.cs | 0 .../ExtensionTests.cs | 0 .../FileOperationTests.cs | 0 .../Fixtures/DefaultClient.cs | 0 .../Fixtures/DefaultOptions.cs | 0 .../Fixtures/DefaultServer.cs | 0 .../Fixtures/ExampleExtensions.cs | 0 .../IConfigureLanguageClientOptions.cs | 0 .../IConfigureLanguageProtocolFixture.cs | 0 .../IConfigureLanguageServerOptions.cs | 0 .../Fixtures/LanguageProtocolFixture.cs | 0 .../Fixtures/LanguageProtocolFixtureTest.cs | 0 .../FluentAssertionsExtensions.cs | 50 +++ .../HandlersManagerIntegrationTests.cs | 0 .../InitializationTests.cs | 0 .../IntegrationTestingExtensions.cs | 0 .../LanguageServerConfigurationTests.cs | 22 +- .../LanguageServerLoggingTests.cs | 7 + .../LogMessageTests.cs | 0 .../Lsp.Integration.Tests.csproj | 24 ++ .../MonikerTests.cs | 0 .../OverrideHandlerTests.cs | 0 .../PartialItemTests.cs | 0 .../PartialItemsTests.cs | 0 .../ProgressTests.cs | 0 test/Lsp.Integration.Tests/Properties.cs | 3 + .../ProposalTests.cs | 0 test/Lsp.Integration.Tests/Records.cs | 23 ++ .../RecursiveResolutionTests.cs | 0 .../RenameTests.cs | 0 .../RequestCancellationTests.cs | 0 .../ShowMessageTests.cs | 0 .../TypedCallHierarchyTests.cs | 0 .../TypedCodeActionTests.cs | 0 .../TypedCodeLensTests.cs | 0 .../TypedCompletionTests.cs | 0 .../TypedDocumentLinkTests.cs | 0 .../WorkspaceFolderTests.cs | 0 test/Lsp.Tests/CompletionItemKindTests.cs | 1 - 69 files changed, 870 insertions(+), 164 deletions(-) mode change 100644 => 100755 build.sh create mode 100644 src/JsonRpc/IActivityTracingStrategy.cs create mode 100644 test/Lsp.Integration.Tests/ActivityTracingTests.cs rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ConnectionAndDisconnectionTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/CustomRequestsTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/DisableDefaultsTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/DynamicRegistrationTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ErroringHandlingTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/EventingTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ExecuteCommandTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ExecuteTypedCommandTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ExtensionTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/FileOperationTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/DefaultClient.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/DefaultOptions.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/DefaultServer.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/ExampleExtensions.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/IConfigureLanguageClientOptions.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/IConfigureLanguageProtocolFixture.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/IConfigureLanguageServerOptions.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/LanguageProtocolFixture.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/Fixtures/LanguageProtocolFixtureTest.cs (100%) create mode 100644 test/Lsp.Integration.Tests/FluentAssertionsExtensions.cs rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/HandlersManagerIntegrationTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/InitializationTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/IntegrationTestingExtensions.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/LanguageServerConfigurationTests.cs (98%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/LanguageServerLoggingTests.cs (97%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/LogMessageTests.cs (100%) create mode 100644 test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/MonikerTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/OverrideHandlerTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/PartialItemTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/PartialItemsTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ProgressTests.cs (100%) create mode 100644 test/Lsp.Integration.Tests/Properties.cs rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ProposalTests.cs (100%) create mode 100644 test/Lsp.Integration.Tests/Records.cs rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/RecursiveResolutionTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/RenameTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/RequestCancellationTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/ShowMessageTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/TypedCallHierarchyTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/TypedCodeActionTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/TypedCodeLensTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/TypedCompletionTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/TypedDocumentLinkTests.cs (100%) rename test/{Lsp.Tests/Integration => Lsp.Integration.Tests}/WorkspaceFolderTests.cs (100%) diff --git a/LSP.sln b/LSP.sln index 1a5d0701e..7ca6d6531 100644 --- a/LSP.sln +++ b/LSP.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dap.Protocol.Proposals", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Protocol.Proposals", "src\Protocol.Proposals\Protocol.Proposals.csproj", "{201B1CA7-AB12-41AD-9246-BC30F2EBE2DF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lsp.Integration.Tests", "test\Lsp.Integration.Tests\Lsp.Integration.Tests.csproj", "{72A74595-A278-46F0-9C8B-3151C9681B91}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -357,6 +359,18 @@ Global {201B1CA7-AB12-41AD-9246-BC30F2EBE2DF}.Release|x64.Build.0 = Release|Any CPU {201B1CA7-AB12-41AD-9246-BC30F2EBE2DF}.Release|x86.ActiveCfg = Release|Any CPU {201B1CA7-AB12-41AD-9246-BC30F2EBE2DF}.Release|x86.Build.0 = Release|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Debug|x64.ActiveCfg = Debug|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Debug|x64.Build.0 = Debug|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Debug|x86.ActiveCfg = Debug|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Debug|x86.Build.0 = Debug|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Release|Any CPU.Build.0 = Release|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Release|x64.ActiveCfg = Release|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Release|x64.Build.0 = Release|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Release|x86.ActiveCfg = Release|Any CPU + {72A74595-A278-46F0-9C8B-3151C9681B91}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -385,6 +399,7 @@ Global {58E83291-1ED9-4921-A12F-F2450AB17F47} = {2F323ED5-EBF8-45E1-B9D3-C014561B3DDA} {D43637CC-94E6-4ED4-BAA3-E5D1AD3285F5} = {D764E024-3D3F-4112-B932-2DB722A1BACC} {201B1CA7-AB12-41AD-9246-BC30F2EBE2DF} = {D764E024-3D3F-4112-B932-2DB722A1BACC} + {72A74595-A278-46F0-9C8B-3151C9681B91} = {2F323ED5-EBF8-45E1-B9D3-C014561B3DDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D38DD0EC-D095-4BCD-B8AF-2D788AF3B9AE} diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/src/Dap.Protocol/DapReceiver.cs b/src/Dap.Protocol/DapReceiver.cs index 709070725..c7555e100 100644 --- a/src/Dap.Protocol/DapReceiver.cs +++ b/src/Dap.Protocol/DapReceiver.cs @@ -52,6 +52,8 @@ protected virtual IEnumerable GetRenor(JToken @object) var sequence = id.Value(); var messageType = type.Value(); + var properties = request.Properties().ToLookup(z => z.Name, StringComparer.OrdinalIgnoreCase); + if (messageType == "event") { if (!request.TryGetValue("event", out var @event)) @@ -60,7 +62,10 @@ protected virtual IEnumerable GetRenor(JToken @object) yield break; } - yield return new Notification(@event.Value(), request.TryGetValue("body", out var body) ? body : null); + yield return new Notification(@event.Value(), request.TryGetValue("body", out var body) ? body : null) { + TraceState = properties["tracestate"].FirstOrDefault()?.Value(), + TraceParent = properties["traceparent"].FirstOrDefault()?.Value() + }; yield break; } @@ -81,16 +86,29 @@ protected virtual IEnumerable GetRenor(JToken @object) // This makes it so that the cancel handler implementer must still return a positive response even if the request didn't make it through. if (ro.TryGetValue("requestId", out var requestId)) { - yield return new Notification(JsonRpcNames.CancelRequest, JObject.FromObject(new { id = requestId })); + yield return new Notification(JsonRpcNames.CancelRequest, JObject.FromObject(new { id = requestId })) { + TraceState = properties["tracestate"].FirstOrDefault()?.Value(), + TraceParent = properties["traceparent"].FirstOrDefault()?.Value() + }; ro.Remove("requestId"); } + else + { + yield return new Request(sequence, RequestNames.Cancel, ro) { + TraceState = properties["tracestate"].FirstOrDefault()?.Value(), + TraceParent = properties["traceparent"].FirstOrDefault()?.Value() + }; + yield break; + } + } - yield return new Request(sequence, RequestNames.Cancel, ro); + { + yield return new Request(sequence, requestName, requestObject) { + TraceState = properties["tracestate"].FirstOrDefault()?.Value(), + TraceParent = properties["traceparent"].FirstOrDefault()?.Value() + }; yield break; } - - yield return new Request(sequence, requestName, requestObject); - yield break; } if (messageType == "response") diff --git a/src/Dap.Protocol/DebugAdapterConverters/DapClientNotificationConverter.cs b/src/Dap.Protocol/DebugAdapterConverters/DapClientNotificationConverter.cs index c4bd824c9..17daf699a 100644 --- a/src/Dap.Protocol/DebugAdapterConverters/DapClientNotificationConverter.cs +++ b/src/Dap.Protocol/DebugAdapterConverters/DapClientNotificationConverter.cs @@ -33,6 +33,16 @@ public override void WriteJson(JsonWriter writer, OutgoingNotification value, Js writer.WritePropertyName("body"); serializer.Serialize(writer, value.Params); } + if (value.TraceParent != null) + { + writer.WritePropertyName("traceparent"); + writer.WriteValue(value.TraceParent); + if (!string.IsNullOrWhiteSpace(value.TraceState)) + { + writer.WritePropertyName("tracestate"); + writer.WriteValue(value.TraceState); + } + } writer.WriteEndObject(); } diff --git a/src/Dap.Protocol/DebugAdapterConverters/DapClientRequestConverter.cs b/src/Dap.Protocol/DebugAdapterConverters/DapClientRequestConverter.cs index 1b92d21c1..d69f82722 100644 --- a/src/Dap.Protocol/DebugAdapterConverters/DapClientRequestConverter.cs +++ b/src/Dap.Protocol/DebugAdapterConverters/DapClientRequestConverter.cs @@ -28,6 +28,16 @@ public override void WriteJson(JsonWriter writer, OutgoingRequest value, JsonSer writer.WritePropertyName("arguments"); serializer.Serialize(writer, value.Params); } + if (value.TraceParent != null) + { + writer.WritePropertyName("traceparent"); + writer.WriteValue(value.TraceParent); + if (!string.IsNullOrWhiteSpace(value.TraceState)) + { + writer.WritePropertyName("tracestate"); + writer.WriteValue(value.TraceState); + } + } writer.WriteEndObject(); } diff --git a/src/Dap.Shared/DebugAdapterRequestRouter.cs b/src/Dap.Shared/DebugAdapterRequestRouter.cs index 8b9e939bc..0cc429eda 100644 --- a/src/Dap.Shared/DebugAdapterRequestRouter.cs +++ b/src/Dap.Shared/DebugAdapterRequestRouter.cs @@ -12,9 +12,10 @@ internal class DebugAdapterRequestRouter : RequestRouterBase private readonly DebugAdapterHandlerCollection _collection; public DebugAdapterRequestRouter( - DebugAdapterHandlerCollection collection, ISerializer serializer, IServiceScopeFactory serviceScopeFactory, ILogger logger + DebugAdapterHandlerCollection collection, ISerializer serializer, IServiceScopeFactory serviceScopeFactory, ILogger logger, + IActivityTracingStrategy? activityTracingStrategy = null ) - : base(serializer, serviceScopeFactory, logger) => + : base(serializer, serviceScopeFactory, logger, activityTracingStrategy) => _collection = collection; public IDisposable Add(IJsonRpcHandler handler) => _collection.Add(handler); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 48b9b1adf..727ebafee 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,33 +1,36 @@ - - - - $(GitVersion_NuGetVersion) - 0.0.9.9 - $(GitVersion_Major).$(GitVersion_Minor).0.0 - $(GitVersion_AssemblySemVer) - $(GitVersion_InformationalVersion) - true - enable - - - - - <_Parameter1>Client.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>JsonRpc.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>Dap.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>Lsp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>TestingUtils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7 - - - + + + + $(GitVersion_NuGetVersion) + 0.0.9.9 + $(GitVersion_Major).$(GitVersion_Minor).0.0 + $(GitVersion_AssemblySemVer) + $(GitVersion_InformationalVersion) + true + enable + + + + + <_Parameter1>Client.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>JsonRpc.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>Dap.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>Lsp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>Lsp.Integration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>TestingUtils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7 + + + diff --git a/src/JsonRpc/Client/OutgoingNotification.cs b/src/JsonRpc/Client/OutgoingNotification.cs index a1480ae91..9f5696be9 100644 --- a/src/JsonRpc/Client/OutgoingNotification.cs +++ b/src/JsonRpc/Client/OutgoingNotification.cs @@ -1,9 +1,23 @@ +using Newtonsoft.Json; + namespace OmniSharp.Extensions.JsonRpc.Client { - public record OutgoingNotification + public record OutgoingNotification : ITraceData { public string Method { get; set; } = null!; public object? Params { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context traceparent value. + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? TraceParent { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context tracestate value. + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? TraceState { get; set; } } } diff --git a/src/JsonRpc/Client/OutgoingRequest.cs b/src/JsonRpc/Client/OutgoingRequest.cs index 787e00591..e639e8435 100644 --- a/src/JsonRpc/Client/OutgoingRequest.cs +++ b/src/JsonRpc/Client/OutgoingRequest.cs @@ -2,7 +2,7 @@ namespace OmniSharp.Extensions.JsonRpc.Client { - public record OutgoingRequest + public record OutgoingRequest : ITraceData { public object? Id { get; set; } @@ -10,5 +10,17 @@ public record OutgoingRequest [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public object? Params { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context traceparent value. + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? TraceParent { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context tracestate value. + /// + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? TraceState { get; set; } } } diff --git a/src/JsonRpc/IActivityTracingStrategy.cs b/src/JsonRpc/IActivityTracingStrategy.cs new file mode 100644 index 000000000..6a8837a81 --- /dev/null +++ b/src/JsonRpc/IActivityTracingStrategy.cs @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft; +using OmniSharp.Extensions.JsonRpc.Server; + +namespace OmniSharp.Extensions.JsonRpc +{ + /// + /// Based on https://github.com/microsoft/vs-streamjsonrpc IActivityTracingStrategy + /// + public interface IActivityTracingStrategy + { + void ApplyOutgoing(ITraceData data); + IDisposable? ApplyInbound(ITraceData data); + } + internal static class Hex + { + private static readonly byte[] HexBytes = new byte[] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f' }; + private static readonly byte[] ReverseHexDigits = BuildReverseHexDigits(); + + internal static void Encode(ReadOnlySpan src, ref Span dest) + { + Span bytes = MemoryMarshal.Cast(dest); + + // Inspired by http://stackoverflow.com/questions/623104/c-byte-to-hex-string/3974535#3974535 + int lengthInNibbles = src.Length * 2; + + for (int i = 0; i < (lengthInNibbles & -2); i++) + { + int index0 = +i >> 1; + var b = (byte)(src[index0] >> 4); + bytes[(2 * i) + 1] = 0; + bytes[2 * i++] = HexBytes[b]; + + b = (byte)(src[index0] & 0x0F); + bytes[(2 * i) + 1] = 0; + bytes[2 * i] = HexBytes[b]; + } + + dest = dest.Slice(lengthInNibbles); + } + + internal static void Decode(ReadOnlySpan value, Span bytes) + { + for (int i = 0; i < value.Length; i++) + { + int c1 = ReverseHexDigits[value[i++] - '0'] << 4; + int c2 = ReverseHexDigits[value[i] - '0']; + + bytes[i >> 1] = (byte)(c1 + c2); + } + } + + private static byte[] BuildReverseHexDigits() + { + var bytes = new byte['f' - '0' + 1]; + + for (int i = 0; i < 10; i++) + { + bytes[i] = (byte)i; + } + + for (int i = 10; i < 16; i++) + { + bytes[i + 'a' - '0' - 0x0a] = (byte)i; + bytes[i + 'A' - '0' - 0x0a] = (byte)i; + } + + return bytes; + } + } + + internal unsafe struct TraceParent + { + internal const int VersionByteCount = 1; + internal const int ParentIdByteCount = 8; + internal const int TraceIdByteCount = 16; + internal const int FlagsByteCount = 1; + + internal byte Version; + + internal fixed byte TraceId[TraceIdByteCount]; + + internal fixed byte ParentId[ParentIdByteCount]; + + internal TraceFlags Flags; + + internal TraceParent(string? traceparent) + { + if (traceparent is null) + { + this.Version = 0; + this.Flags = TraceFlags.None; + return; + } + + ReadOnlySpan traceparentChars = traceparent.AsSpan(); + + // Decode version + ReadOnlySpan slice = Consume(ref traceparentChars, VersionByteCount * 2); + fixed (byte* pVersion = &this.Version) + { + Hex.Decode(slice, new Span(pVersion, 1)); + } + + ConsumeHyphen(ref traceparentChars); + + // Decode traceid + slice = Consume(ref traceparentChars, TraceIdByteCount * 2); + fixed (byte* pTraceId = this.TraceId) + { + Hex.Decode(slice, new Span(pTraceId, TraceIdByteCount)); + } + + ConsumeHyphen(ref traceparentChars); + + // Decode parentid + slice = Consume(ref traceparentChars, ParentIdByteCount * 2); + fixed (byte* pParentId = this.ParentId) + { + Hex.Decode(slice, new Span(pParentId, ParentIdByteCount)); + } + + ConsumeHyphen(ref traceparentChars); + + // Decode flags + slice = Consume(ref traceparentChars, FlagsByteCount * 2); + fixed (TraceFlags* pFlags = &this.Flags) + { + Hex.Decode(slice, new Span(pFlags, 1)); + } + + static void ConsumeHyphen(ref ReadOnlySpan value) + { + if (value[0] != '-') + { + Requires.Fail("Invalid format."); + } + + value = value.Slice(1); + } + + ReadOnlySpan Consume(ref ReadOnlySpan buffer, int length) + { + ReadOnlySpan result = buffer.Slice(0, length); + buffer = buffer.Slice(length); + return result; + } + } + + [Flags] + internal enum TraceFlags : byte + { + /// + /// No flags. + /// + None = 0x0, + + /// + /// The parent is tracing their action. + /// + Sampled = 0x1, + } + + internal Guid TraceIdGuid + { + get + { + fixed (byte* pTraceId = this.TraceId) + { + return CopyBufferToGuid(new ReadOnlySpan(pTraceId, TraceIdByteCount)); + } + } + } + + public override string ToString() + { + // When calculating the number of characters required, double each 'byte' we have to encode since we're using hex. + Span traceparent = stackalloc char[(VersionByteCount * 2) + 1 + (TraceIdByteCount * 2) + 1 + (ParentIdByteCount * 2) + 1 + (FlagsByteCount * 2)]; + Span traceParentRemaining = traceparent; + + fixed (byte* pVersion = &this.Version) + { + Hex.Encode(new ReadOnlySpan(pVersion, 1), ref traceParentRemaining); + } + + AddHyphen(ref traceParentRemaining); + + fixed (byte* pTraceId = this.TraceId) + { + Hex.Encode(new ReadOnlySpan(pTraceId, TraceIdByteCount), ref traceParentRemaining); + } + + AddHyphen(ref traceParentRemaining); + + fixed (byte* pParentId = this.ParentId) + { + Hex.Encode(new ReadOnlySpan(pParentId, ParentIdByteCount), ref traceParentRemaining); + } + + AddHyphen(ref traceParentRemaining); + + fixed (TraceFlags* pFlags = &this.Flags) + { + Hex.Encode(new ReadOnlySpan(pFlags, 1), ref traceParentRemaining); + } + + Debug.Assert(traceParentRemaining.Length == 0, "Characters were not initialized."); + + fixed (char* pValue = traceparent) + { + return new string(pValue, 0, traceparent.Length); + } + + static void AddHyphen(ref Span value) + { + value[0] = '-'; + value = value.Slice(1); + } + } + + private static unsafe Guid CopyBufferToGuid(ReadOnlySpan buffer) + { + Debug.Assert(buffer.Length == 16, "Guid buffer length mismatch."); + fixed (byte* pBuffer = buffer) + { + return *(Guid*)pBuffer; + } + } + } + + public sealed class CorrelationManagerTracingStrategy : IActivityTracingStrategy + { + private static readonly AsyncLocal TraceStateAsyncLocal = new AsyncLocal(); + + /// + /// Gets or sets the contextual tracestate value. + /// + public static string? TraceState + { + get => TraceStateAsyncLocal.Value; + set => TraceStateAsyncLocal.Value = value; + } + + /// + /// Gets or sets the that will receive the activity transfer, start and stop events . + /// + public TraceSource? TraceSource { get; set; } + + public unsafe void ApplyOutgoing(ITraceData data) + { + if (Trace.CorrelationManager.ActivityId != Guid.Empty) + { + var traceparent = default(TraceParent); + + FillRandomBytes(new Span(traceparent.ParentId, TraceParent.ParentIdByteCount)); + CopyGuidToBuffer(Trace.CorrelationManager.ActivityId, new Span(traceparent.TraceId, TraceParent.TraceIdByteCount)); + + if (this.TraceSource is object && (this.TraceSource.Switch.Level & SourceLevels.ActivityTracing) == SourceLevels.ActivityTracing && this.TraceSource.Listeners.Count > 0) + { + traceparent.Flags |= TraceParent.TraceFlags.Sampled; + } + + data.TraceParent = traceparent.ToString(); + data.TraceState = TraceState; + } + } + + /// + public unsafe IDisposable? ApplyInbound(ITraceData request) + { + var traceparent = new TraceParent(request.TraceParent); + Guid childActivityId = Guid.NewGuid(); + string? activityName = request is IMethodWithParams p ? p.Method : null; + + return new ActivityState(request, this.TraceSource, activityName, traceparent.TraceIdGuid, childActivityId); + } + + private static void FillRandomBytes(Span buffer) => CopyGuidToBuffer(Guid.NewGuid(), buffer); + + private unsafe static void CopyGuidToBuffer(Guid guid, Span buffer) + { + ReadOnlySpan guidBytes = new ReadOnlySpan(&guid, sizeof(Guid)); + guidBytes.Slice(0, buffer.Length).CopyTo(buffer); + } + + private class ActivityState : IDisposable + { + private readonly TraceSource? traceSource; + private readonly Guid originalActivityId; + private readonly string? originalTraceState; + private readonly string? activityName; + private readonly Guid parentTraceId; + + internal ActivityState(ITraceData request, TraceSource? traceSource, string? activityName, Guid parentTraceId, Guid childTraceId) + { + this.originalActivityId = Trace.CorrelationManager.ActivityId; + this.originalTraceState = TraceState; + this.activityName = activityName; + this.parentTraceId = parentTraceId; + + if (traceSource is object && parentTraceId != Guid.Empty) + { + // We set ActivityId to a short-lived value here for the sake of the TraceTransfer call that comes next. + // TraceTransfer goes from the current activity to the one passed as an argument. + // Without a traceSource object, there's no transfer and thus no need to set this temporary ActivityId. + Trace.CorrelationManager.ActivityId = parentTraceId; + traceSource.TraceTransfer(0, nameof(TraceEventType.Transfer), childTraceId); + } + + Trace.CorrelationManager.ActivityId = childTraceId; + TraceState = request.TraceState; + + traceSource?.TraceEvent(TraceEventType.Start, 0, this.activityName); + + this.traceSource = traceSource; + } + + public void Dispose() + { + this.traceSource?.TraceEvent(TraceEventType.Stop, 0, this.activityName); + + if (this.parentTraceId != Guid.Empty) + { + this.traceSource?.TraceTransfer(0, nameof(TraceEventType.Transfer), this.parentTraceId); + } + + Trace.CorrelationManager.ActivityId = this.originalActivityId; + TraceState = this.originalTraceState; + } + } + } + + public interface ITraceData + { + + + /// + /// Gets or sets the data for the W3C Trace Context traceparent value. + /// + string? TraceParent { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context tracestate value. + /// + string? TraceState { get; set; } + } +} diff --git a/src/JsonRpc/JsonRpc.csproj b/src/JsonRpc/JsonRpc.csproj index 084fab058..f62bc92b6 100644 --- a/src/JsonRpc/JsonRpc.csproj +++ b/src/JsonRpc/JsonRpc.csproj @@ -5,6 +5,7 @@ OmniSharp.Extensions.JsonRpc OmniSharp.Extensions.JsonRpc Primitives for working with JsonRpc. This library is used as the base for communication with language servers + true diff --git a/src/JsonRpc/JsonRpcServerOptionsBase.cs b/src/JsonRpc/JsonRpcServerOptionsBase.cs index ad3d787fe..22847aec2 100644 --- a/src/JsonRpc/JsonRpcServerOptionsBase.cs +++ b/src/JsonRpc/JsonRpcServerOptionsBase.cs @@ -144,5 +144,12 @@ public T WithLink(string fromMethod, string toMethod) Handlers.Add(JsonRpcHandlerDescription.Link(fromMethod, toMethod)); return (T) (object) this; } + + public T WithActivityTracingStrategy(IActivityTracingStrategy activityTracingStrategy) + { + Services.RemoveAll(typeof(IActivityTracingStrategy)); + Services.AddSingleton(activityTracingStrategy); + return (T) (object) this; + } } } diff --git a/src/JsonRpc/OutputHandler.cs b/src/JsonRpc/OutputHandler.cs index ef852869e..f20d06d3b 100644 --- a/src/JsonRpc/OutputHandler.cs +++ b/src/JsonRpc/OutputHandler.cs @@ -22,6 +22,7 @@ public class OutputHandler : IOutputHandler private readonly ISerializer _serializer; private readonly IEnumerable _outputFilters; private readonly ILogger _logger; + private readonly IActivityTracingStrategy? _activityTracingStrategy; private readonly ChannelReader _queue; @@ -38,13 +39,15 @@ public OutputHandler( ISerializer serializer, IEnumerable outputFilters, IScheduler scheduler, - ILogger logger + ILogger logger, + IActivityTracingStrategy? activityTracingStrategy = null ) { _pipeWriter = pipeWriter; _serializer = serializer; _outputFilters = outputFilters.ToArray(); _logger = logger; + _activityTracingStrategy = activityTracingStrategy; _delayedQueue = new Queue(); _outputIsFinished = new TaskCompletionSource(); @@ -130,7 +133,11 @@ private async Task ProcessOutputStream(CancellationToken cancellationToken) do { var value = await _queue.ReadAsync(cancellationToken); -// _logger.LogTrace("Writing out {@Value}", value); + if (value is ITraceData traceData) + { + _activityTracingStrategy?.ApplyOutgoing(traceData); + } + // TODO: this will be part of the serialization refactor to make streaming first class var content = _serializer.SerializeObject(value); var contentBytes = Encoding.UTF8.GetBytes(content).AsMemory(); @@ -144,6 +151,11 @@ private async Task ProcessOutputStream(CancellationToken cancellationToken) _logger.LogTrace(ex, "Cancellation happened"); Error(ex); } + catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken) + { + _logger.LogTrace(ex, "Cancellation happened"); + Cancel(); + } catch (Exception e) { _logger.LogTrace(e, "Could not write to output handler, perhaps serialization failed?"); @@ -160,6 +172,13 @@ private void Error(Exception ex) _disposable.Dispose(); } + private void Cancel() + { + _outputIsFinished.TrySetCanceled(); + _writer.TryComplete(); + _disposable.Dispose(); + } + public void Dispose() { _outputIsFinished.TrySetResult(null); diff --git a/src/JsonRpc/Receiver.cs b/src/JsonRpc/Receiver.cs index af6700d8e..9b7c3dcee 100644 --- a/src/JsonRpc/Receiver.cs +++ b/src/JsonRpc/Receiver.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; @@ -97,14 +98,24 @@ protected virtual Renor GetRenor(JToken @object) @params = new JObject(); } + var properties = request.Properties().ToLookup(z => z.Name, StringComparer.OrdinalIgnoreCase); + // id == request // !id == notification if (!hasRequestId) { - return new Notification(method!, @params); + return new Notification(method!, @params) { + TraceState = properties["tracestate"].FirstOrDefault()?.Value(), + TraceParent = properties["traceparent"].FirstOrDefault()?.Value() + }; + } + else + { + return new Request(requestId!, method!, @params) { + TraceState = properties["tracestate"].FirstOrDefault()?.Value(), + TraceParent = properties["traceparent"].FirstOrDefault()?.Value() + }; } - - return new Request(requestId!, method!, @params); } public bool ShouldOutput(object value) => _initialized; diff --git a/src/JsonRpc/RequestRouter.cs b/src/JsonRpc/RequestRouter.cs index 12a0ddb23..cf52940e3 100644 --- a/src/JsonRpc/RequestRouter.cs +++ b/src/JsonRpc/RequestRouter.cs @@ -13,9 +13,10 @@ public RequestRouter( IHandlersManager collection, ISerializer serializer, IServiceScopeFactory serviceScopeFactory, - ILogger logger + ILogger logger, + IActivityTracingStrategy? activityTracingStrategy = null ) - : base(serializer, serviceScopeFactory, logger) => + : base(serializer, serviceScopeFactory, logger, activityTracingStrategy) => _collection = collection; private IHandlerDescriptor FindDescriptor(IMethodWithParams instance) => _collection.Descriptors.FirstOrDefault(x => x.Method == instance.Method); diff --git a/src/JsonRpc/RequestRouterBase.cs b/src/JsonRpc/RequestRouterBase.cs index 0470fc30b..dcfafde60 100644 --- a/src/JsonRpc/RequestRouterBase.cs +++ b/src/JsonRpc/RequestRouterBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reactive.Disposables; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -22,41 +23,52 @@ public abstract class RequestRouterBase : IRequestRouter descriptors, Notification notification, CancellationToken token) { - using var debug = _logger.TimeDebug("Routing Notification {Method}", notification.Method); - using var _ = _logger.BeginScope( - new[] { - new KeyValuePair("Method", notification.Method), - new KeyValuePair("Params", notification.Params?.ToString()) - } - ); - - object? @params = null; - if (!( descriptors.Default?.Params is null )) + using (_activityTracingStrategy?.ApplyInbound(notification)?? Disposable.Empty) { - if (descriptors.Default.IsDelegatingHandler) - { - _logger.LogTrace("Converting params for Notification {Method} to {Type}", notification.Method, descriptors.Default.Params.GetGenericArguments()[0].FullName); - var o = notification.Params?.ToObject(descriptors.Default.Params.GetGenericArguments()[0], _serializer.JsonSerializer); - @params = Activator.CreateInstance(descriptors.Default.Params, o); - } - else + using var debug = _logger.TimeDebug("Routing Notification {Method}", notification.Method); + using var _ = _logger.BeginScope( + new[] { + new KeyValuePair("Method", notification.Method), + new KeyValuePair("Params", notification.Params?.ToString()) + } + ); + + object? @params = null; + if (!( descriptors.Default?.Params is null )) { - _logger.LogTrace("Converting params for Notification {Method} to {Type}", notification.Method, descriptors.Default.Params.FullName); - @params = notification.Params?.ToObject(descriptors.Default.Params, _serializer.JsonSerializer); + if (descriptors.Default.IsDelegatingHandler) + { + _logger.LogTrace( + "Converting params for Notification {Method} to {Type}", notification.Method, descriptors.Default.Params.GetGenericArguments()[0].FullName + ); + var o = notification.Params?.ToObject(descriptors.Default.Params.GetGenericArguments()[0], _serializer.JsonSerializer); + @params = Activator.CreateInstance(descriptors.Default.Params, o); + } + else + { + _logger.LogTrace("Converting params for Notification {Method} to {Type}", notification.Method, descriptors.Default.Params.FullName); + @params = notification.Params?.ToObject(descriptors.Default.Params, _serializer.JsonSerializer); + } } - } - await Task.WhenAll(descriptors.Select(descriptor => InnerRoute(_serviceScopeFactory, descriptor, @params, token))).ConfigureAwait(false); + await Task.WhenAll(descriptors.Select(descriptor => InnerRoute(_serviceScopeFactory, descriptor, @params, token))).ConfigureAwait(false); + } static async Task InnerRoute(IServiceScopeFactory serviceScopeFactory, TDescriptor descriptor, object? @params, CancellationToken token) { @@ -74,60 +86,65 @@ public virtual async Task RouteRequest(IRequestDescriptor("Id", request.Id.ToString()), - new KeyValuePair("Method", request.Method), - new KeyValuePair("Params", request.Params?.ToString()) - } - ); - - object? @params; - try + using (_activityTracingStrategy?.ApplyInbound(request) ?? Disposable.Empty) { - if (descriptors.Default!.IsDelegatingHandler) + using var debug = _logger.TimeDebug("Routing Request ({Id}) {Method}", request.Id, request.Method); + using var _ = _logger.BeginScope( + new[] { + new KeyValuePair("Id", request.Id.ToString()), + new KeyValuePair("Method", request.Method), + new KeyValuePair("Params", request.Params?.ToString()) + } + ); + + object? @params; + try { - _logger.LogTrace( - "Converting params for Request ({Id}) {Method} to {Type}", request.Id, request.Method, - descriptors.Default!.Params!.GetGenericArguments()[0].FullName - ); - var o = request.Params?.ToObject(descriptors.Default!.Params!.GetGenericArguments()[0], _serializer.JsonSerializer); - @params = Activator.CreateInstance(descriptors.Default!.Params, o); + if (descriptors.Default!.IsDelegatingHandler) + { + _logger.LogTrace( + "Converting params for Request ({Id}) {Method} to {Type}", request.Id, request.Method, + descriptors.Default!.Params!.GetGenericArguments()[0].FullName + ); + var o = request.Params?.ToObject(descriptors.Default!.Params!.GetGenericArguments()[0], _serializer.JsonSerializer); + @params = Activator.CreateInstance(descriptors.Default!.Params, o); + } + else + { + _logger.LogTrace("Converting params for Request ({Id}) {Method} to {Type}", request.Id, request.Method, descriptors.Default!.Params!.FullName); + _logger.LogTrace("Converting params for Notification {Method} to {Type}", request.Method, descriptors.Default!.Params.FullName); + @params = request.Params?.ToObject(descriptors.Default!.Params, _serializer.JsonSerializer); + } } - else + catch (Exception cannotDeserializeRequestParams) { - _logger.LogTrace("Converting params for Request ({Id}) {Method} to {Type}", request.Id, request.Method, descriptors.Default!.Params!.FullName); - _logger.LogTrace("Converting params for Notification {Method} to {Type}", request.Method, descriptors.Default!.Params.FullName); - @params = request.Params?.ToObject(descriptors.Default!.Params, _serializer.JsonSerializer); + _logger.LogError(new EventId(-32602), cannotDeserializeRequestParams, "Failed to deserialize request parameters."); + return new InvalidParams(request.Id, request.Method); } - } - catch (Exception cannotDeserializeRequestParams) - { - _logger.LogError(new EventId(-32602), cannotDeserializeRequestParams, "Failed to deserialize request parameters."); - return new InvalidParams(request.Id, request.Method); - } - using var scope = _serviceScopeFactory.CreateScope(); - // TODO: Do we want to support more handlers as "aggregate"? - if (typeof(IEnumerable).IsAssignableFrom(descriptors.Default!.Response) && typeof(string) != descriptors.Default!.Response && !typeof(JToken).IsAssignableFrom(descriptors.Default!.Response)) - { - var responses = await Task.WhenAll(descriptors.Select(descriptor => InnerRoute(_serviceScopeFactory, request, descriptor, @params, token, _logger))).ConfigureAwait(false); - var errorResponse = responses.FirstOrDefault(x => x.IsError); - if (errorResponse.IsError) return errorResponse; - if (responses.Length == 1) + using var scope = _serviceScopeFactory.CreateScope(); + // TODO: Do we want to support more handlers as "aggregate"? + if (typeof(IEnumerable).IsAssignableFrom(descriptors.Default!.Response) && typeof(string) != descriptors.Default!.Response + && !typeof(JToken).IsAssignableFrom(descriptors.Default!.Response)) { - return responses[0]; + var responses = await Task.WhenAll(descriptors.Select(descriptor => InnerRoute(_serviceScopeFactory, request, descriptor, @params, token, _logger))) + .ConfigureAwait(false); + var errorResponse = responses.FirstOrDefault(x => x.IsError); + if (errorResponse.IsError) return errorResponse; + if (responses.Length == 1) + { + return responses[0]; + } + + var response = Activator.CreateInstance( + typeof(AggregateResponse<>).MakeGenericType(descriptors.Default!.Response!), responses.Select(z => z.Response!.Result) + ); + return new OutgoingResponse(request.Id, response, request); } - var response = Activator.CreateInstance( - typeof(AggregateResponse<>).MakeGenericType(descriptors.Default!.Response!), responses.Select(z => z.Response!.Result) - ); - return new OutgoingResponse(request.Id, response, request); + return await InnerRoute(_serviceScopeFactory, request, descriptors.Default!, @params, token, _logger).ConfigureAwait(false); } - return await InnerRoute(_serviceScopeFactory, request, descriptors.Default!, @params, token, _logger).ConfigureAwait(false); - static async Task InnerRoute( IServiceScopeFactory serviceScopeFactory, Request request, TDescriptor descriptor, object? @params, CancellationToken token, ILogger logger diff --git a/src/JsonRpc/Serialization/Converters/ClientNotificationConverter.cs b/src/JsonRpc/Serialization/Converters/ClientNotificationConverter.cs index 14561083c..3e2c72ade 100644 --- a/src/JsonRpc/Serialization/Converters/ClientNotificationConverter.cs +++ b/src/JsonRpc/Serialization/Converters/ClientNotificationConverter.cs @@ -26,6 +26,16 @@ public override void WriteJson(JsonWriter writer, OutgoingNotification value, Js writer.WritePropertyName("params"); serializer.Serialize(writer, value.Params); } + if (value.TraceParent != null) + { + writer.WritePropertyName("traceparent"); + writer.WriteValue(value.TraceParent); + if (!string.IsNullOrWhiteSpace(value.TraceState)) + { + writer.WritePropertyName("tracestate"); + writer.WriteValue(value.TraceState); + } + } writer.WriteEndObject(); } diff --git a/src/JsonRpc/Serialization/Converters/ClientRequestConverter.cs b/src/JsonRpc/Serialization/Converters/ClientRequestConverter.cs index edf7863f0..cd62617cf 100644 --- a/src/JsonRpc/Serialization/Converters/ClientRequestConverter.cs +++ b/src/JsonRpc/Serialization/Converters/ClientRequestConverter.cs @@ -28,6 +28,16 @@ public override void WriteJson(JsonWriter writer, OutgoingRequest value, JsonSer writer.WritePropertyName("params"); serializer.Serialize(writer, value.Params); } + if (value.TraceParent != null) + { + writer.WritePropertyName("traceparent"); + writer.WriteValue(value.TraceParent); + if (!string.IsNullOrWhiteSpace(value.TraceState)) + { + writer.WritePropertyName("tracestate"); + writer.WriteValue(value.TraceState); + } + } writer.WriteEndObject(); } diff --git a/src/JsonRpc/Server/Notification.cs b/src/JsonRpc/Server/Notification.cs index b86d3b446..072489a02 100644 --- a/src/JsonRpc/Server/Notification.cs +++ b/src/JsonRpc/Server/Notification.cs @@ -2,7 +2,7 @@ namespace OmniSharp.Extensions.JsonRpc.Server { - public class Notification : IMethodWithParams + public class Notification : IMethodWithParams, ITraceData { public Notification( string method, @@ -16,5 +16,15 @@ public Notification( public string Method { get; } public JToken? Params { get; } + + /// + /// Gets or sets the data for the W3C Trace Context traceparent value. + /// + public string? TraceParent { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context tracestate value. + /// + public string? TraceState { get; set; } } } diff --git a/src/JsonRpc/Server/Request.cs b/src/JsonRpc/Server/Request.cs index d5c680890..9cd23e59e 100644 --- a/src/JsonRpc/Server/Request.cs +++ b/src/JsonRpc/Server/Request.cs @@ -2,7 +2,7 @@ namespace OmniSharp.Extensions.JsonRpc.Server { - public class Request : IMethodWithParams + public class Request : IMethodWithParams, ITraceData { public Request( object id, @@ -20,5 +20,15 @@ public Request( public string Method { get; } public JToken? Params { get; } + + /// + /// Gets or sets the data for the W3C Trace Context traceparent value. + /// + public string? TraceParent { get; set; } + + /// + /// Gets or sets the data for the W3C Trace Context tracestate value. + /// + public string? TraceState { get; set; } } } diff --git a/src/Shared/LspRequestRouter.cs b/src/Shared/LspRequestRouter.cs index ad5d1dbe6..2d979c00a 100644 --- a/src/Shared/LspRequestRouter.cs +++ b/src/Shared/LspRequestRouter.cs @@ -23,9 +23,10 @@ public LspRequestRouter( ILogger logger, IEnumerable handlerMatchers, ISerializer serializer, - IServiceScopeFactory serviceScopeFactory + IServiceScopeFactory serviceScopeFactory, + IActivityTracingStrategy? activityTracingStrategy = null ) : - base(serializer, serviceScopeFactory, logger) + base(serializer, serviceScopeFactory, logger, activityTracingStrategy) { _collection = collection; _handlerMatchers = new HashSet(handlerMatchers); diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index f84bb6488..2e1c2229c 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,33 +1,36 @@ - - - - - - - - - - - - - - - - - - - - <_Parameter1>Client.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>JsonRpc.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>Dap.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - <_Parameter1>Lsp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - - - - + + + + + + + + + + + + + + + + + + + + <_Parameter1>Client.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>JsonRpc.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>Dap.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>Lsp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>Lsp.Integration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + + diff --git a/test/Lsp.Integration.Tests/ActivityTracingTests.cs b/test/Lsp.Integration.Tests/ActivityTracingTests.cs new file mode 100644 index 000000000..6e9547cc9 --- /dev/null +++ b/test/Lsp.Integration.Tests/ActivityTracingTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Testing; +using OmniSharp.Extensions.LanguageProtocol.Testing; +using Serilog.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Lsp.Tests.Integration +{ + public class ActivityTracingTests : LanguageProtocolTestBase + { + public ActivityTracingTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(outputHelper, LogEventLevel.Verbose)) + { + } + + [Fact] + public async Task Should_Have_Activity_Information() + { + var clientStub = Substitute.For(); + clientStub.ApplyInbound(Arg.Any()).Returns(System.Reactive.Disposables.Disposable.Empty); + var serverStub = Substitute.For(); + serverStub.ApplyInbound(Arg.Any()).Returns(System.Reactive.Disposables.Disposable.Empty); + + var (client, server) = await Initialize( + options => options.WithActivityTracingStrategy(clientStub), + options => options.WithActivityTracingStrategy(serverStub) + ); + + serverStub.Received().ApplyInbound(Arg.Any()); + clientStub.Received().ApplyOutgoing(Arg.Any()); + } + + [Fact] + public async Task Should_Have_Activity_Information_Exchanging_Data() + { + var clientStub = Substitute.For(); + clientStub.ApplyInbound(Arg.Any()).Returns(System.Reactive.Disposables.Disposable.Empty); + var serverStub = Substitute.For(); + serverStub.ApplyInbound(Arg.Any()).Returns(System.Reactive.Disposables.Disposable.Empty); + + var (client, server) = await Initialize( + options => options + .WithActivityTracingStrategy(clientStub) + .OnRequest("test", (Func) ( ct => Task.CompletedTask )), + options => options + .WithActivityTracingStrategy(serverStub) + .OnRequest("test", (Func) ( ct => Task.CompletedTask )) + ); + + await client.SendRequest("test").ReturningVoid(CancellationToken); + await server.SendRequest("test").ReturningVoid(CancellationToken); + + serverStub.Received().ApplyInbound(Arg.Any()); + clientStub.Received().ApplyOutgoing(Arg.Any()); + serverStub.Received().ApplyOutgoing(Arg.Any()); + clientStub.Received().ApplyInbound(Arg.Any()); + } + } +} diff --git a/test/Lsp.Tests/Integration/ConnectionAndDisconnectionTests.cs b/test/Lsp.Integration.Tests/ConnectionAndDisconnectionTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ConnectionAndDisconnectionTests.cs rename to test/Lsp.Integration.Tests/ConnectionAndDisconnectionTests.cs diff --git a/test/Lsp.Tests/Integration/CustomRequestsTests.cs b/test/Lsp.Integration.Tests/CustomRequestsTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/CustomRequestsTests.cs rename to test/Lsp.Integration.Tests/CustomRequestsTests.cs diff --git a/test/Lsp.Tests/Integration/DisableDefaultsTests.cs b/test/Lsp.Integration.Tests/DisableDefaultsTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/DisableDefaultsTests.cs rename to test/Lsp.Integration.Tests/DisableDefaultsTests.cs diff --git a/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs b/test/Lsp.Integration.Tests/DynamicRegistrationTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/DynamicRegistrationTests.cs rename to test/Lsp.Integration.Tests/DynamicRegistrationTests.cs diff --git a/test/Lsp.Tests/Integration/ErroringHandlingTests.cs b/test/Lsp.Integration.Tests/ErroringHandlingTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ErroringHandlingTests.cs rename to test/Lsp.Integration.Tests/ErroringHandlingTests.cs diff --git a/test/Lsp.Tests/Integration/EventingTests.cs b/test/Lsp.Integration.Tests/EventingTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/EventingTests.cs rename to test/Lsp.Integration.Tests/EventingTests.cs diff --git a/test/Lsp.Tests/Integration/ExecuteCommandTests.cs b/test/Lsp.Integration.Tests/ExecuteCommandTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ExecuteCommandTests.cs rename to test/Lsp.Integration.Tests/ExecuteCommandTests.cs diff --git a/test/Lsp.Tests/Integration/ExecuteTypedCommandTests.cs b/test/Lsp.Integration.Tests/ExecuteTypedCommandTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ExecuteTypedCommandTests.cs rename to test/Lsp.Integration.Tests/ExecuteTypedCommandTests.cs diff --git a/test/Lsp.Tests/Integration/ExtensionTests.cs b/test/Lsp.Integration.Tests/ExtensionTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ExtensionTests.cs rename to test/Lsp.Integration.Tests/ExtensionTests.cs diff --git a/test/Lsp.Tests/Integration/FileOperationTests.cs b/test/Lsp.Integration.Tests/FileOperationTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/FileOperationTests.cs rename to test/Lsp.Integration.Tests/FileOperationTests.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/DefaultClient.cs b/test/Lsp.Integration.Tests/Fixtures/DefaultClient.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/DefaultClient.cs rename to test/Lsp.Integration.Tests/Fixtures/DefaultClient.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/DefaultOptions.cs b/test/Lsp.Integration.Tests/Fixtures/DefaultOptions.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/DefaultOptions.cs rename to test/Lsp.Integration.Tests/Fixtures/DefaultOptions.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/DefaultServer.cs b/test/Lsp.Integration.Tests/Fixtures/DefaultServer.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/DefaultServer.cs rename to test/Lsp.Integration.Tests/Fixtures/DefaultServer.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/ExampleExtensions.cs b/test/Lsp.Integration.Tests/Fixtures/ExampleExtensions.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/ExampleExtensions.cs rename to test/Lsp.Integration.Tests/Fixtures/ExampleExtensions.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageClientOptions.cs b/test/Lsp.Integration.Tests/Fixtures/IConfigureLanguageClientOptions.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageClientOptions.cs rename to test/Lsp.Integration.Tests/Fixtures/IConfigureLanguageClientOptions.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageProtocolFixture.cs b/test/Lsp.Integration.Tests/Fixtures/IConfigureLanguageProtocolFixture.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageProtocolFixture.cs rename to test/Lsp.Integration.Tests/Fixtures/IConfigureLanguageProtocolFixture.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageServerOptions.cs b/test/Lsp.Integration.Tests/Fixtures/IConfigureLanguageServerOptions.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageServerOptions.cs rename to test/Lsp.Integration.Tests/Fixtures/IConfigureLanguageServerOptions.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixture.cs b/test/Lsp.Integration.Tests/Fixtures/LanguageProtocolFixture.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixture.cs rename to test/Lsp.Integration.Tests/Fixtures/LanguageProtocolFixture.cs diff --git a/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixtureTest.cs b/test/Lsp.Integration.Tests/Fixtures/LanguageProtocolFixtureTest.cs similarity index 100% rename from test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixtureTest.cs rename to test/Lsp.Integration.Tests/Fixtures/LanguageProtocolFixtureTest.cs diff --git a/test/Lsp.Integration.Tests/FluentAssertionsExtensions.cs b/test/Lsp.Integration.Tests/FluentAssertionsExtensions.cs new file mode 100644 index 000000000..6c12fb871 --- /dev/null +++ b/test/Lsp.Integration.Tests/FluentAssertionsExtensions.cs @@ -0,0 +1,50 @@ +using System; +using FluentAssertions.Equivalency; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using NSubstitute; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; + +namespace Lsp.Tests +{ + public static class FluentAssertionsExtensions + { + public static EquivalencyAssertionOptions ConfigureForSupports(this EquivalencyAssertionOptions options, ILogger? logger = null) => + options + .WithTracing(new TraceWriter(logger ?? NullLogger.Instance)) + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>() + .ComparingByMembers>(); + } +} diff --git a/test/Lsp.Tests/Integration/HandlersManagerIntegrationTests.cs b/test/Lsp.Integration.Tests/HandlersManagerIntegrationTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/HandlersManagerIntegrationTests.cs rename to test/Lsp.Integration.Tests/HandlersManagerIntegrationTests.cs diff --git a/test/Lsp.Tests/Integration/InitializationTests.cs b/test/Lsp.Integration.Tests/InitializationTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/InitializationTests.cs rename to test/Lsp.Integration.Tests/InitializationTests.cs diff --git a/test/Lsp.Tests/Integration/IntegrationTestingExtensions.cs b/test/Lsp.Integration.Tests/IntegrationTestingExtensions.cs similarity index 100% rename from test/Lsp.Tests/Integration/IntegrationTestingExtensions.cs rename to test/Lsp.Integration.Tests/IntegrationTestingExtensions.cs diff --git a/test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs b/test/Lsp.Integration.Tests/LanguageServerConfigurationTests.cs similarity index 98% rename from test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs rename to test/Lsp.Integration.Tests/LanguageServerConfigurationTests.cs index af31698b0..684b95d3d 100644 --- a/test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs +++ b/test/Lsp.Integration.Tests/LanguageServerConfigurationTests.cs @@ -32,7 +32,7 @@ public LanguageServerConfigurationTests(ITestOutputHelper outputHelper) : base(n { } - [RetryFact] + [Fact] public async Task Should_Not_Support_Configuration_It_Not_Configured() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, o => { }); @@ -46,7 +46,7 @@ public async Task Should_Not_Support_Configuration_It_Not_Configured() server.Configuration.AsEnumerable().Should().BeEmpty(); } - [RetryFact] + [Fact] public async Task Should_Allow_Null_Response() { var (client, server) = await Initialize( @@ -60,7 +60,7 @@ public async Task Should_Allow_Null_Response() a.Should().NotThrow(); } - [RetryFact] + [Fact] public async Task Should_Update_Configuration_On_Server() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); @@ -75,7 +75,7 @@ public async Task Should_Update_Configuration_On_Server() server.Configuration["othersection:value"].Should().Be("key"); } - [RetryFact] + [Fact] public async Task Should_Update_Configuration_On_Server_After_Starting() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, options => {}); @@ -91,7 +91,7 @@ public async Task Should_Update_Configuration_On_Server_After_Starting() server.Configuration["othersection:value"].Should().Be("key"); } - [RetryFact] + [Fact] public async Task Should_Update_Configuration_Should_Stop_Watching_Sections() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); @@ -113,7 +113,7 @@ public async Task Should_Update_Configuration_Should_Stop_Watching_Sections() server.Configuration["othersection:value"].Should().BeNull(); } - [RetryFact] + [Fact] public async Task Should_Update_Scoped_Configuration() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); @@ -135,7 +135,7 @@ public async Task Should_Update_Scoped_Configuration() scopedConfiguration["othersection:value"].Should().Be("scopedkey"); } - [RetryFact] + [Fact] public async Task Should_Fallback_To_Original_Configuration() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); @@ -167,7 +167,7 @@ public async Task Should_Fallback_To_Original_Configuration() scopedConfiguration["othersection:value"].Should().Be("key"); } - [RetryFact] + [Fact] public async Task Should_Only_Update_Configuration_Items_That_Are_Defined() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); @@ -185,7 +185,7 @@ public async Task Should_Only_Update_Configuration_Items_That_Are_Defined() server.Configuration["notmysection:key"].Should().BeNull(); } - [RetryFact] + [Fact] public async Task Should_Support_Configuration_Binding() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); @@ -209,7 +209,7 @@ public async Task Should_Support_Configuration_Binding() data.Port.Should().Be(80); } - [RetryFact] + [Fact] public async Task Should_Support_Options() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, options => { @@ -234,7 +234,7 @@ public async Task Should_Support_Options() options.Value.Port.Should().Be(443); } - [RetryFact] + [Fact] public async Task Should_Support_Options_Monitor() { var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, options => { diff --git a/test/Lsp.Tests/Integration/LanguageServerLoggingTests.cs b/test/Lsp.Integration.Tests/LanguageServerLoggingTests.cs similarity index 97% rename from test/Lsp.Tests/Integration/LanguageServerLoggingTests.cs rename to test/Lsp.Integration.Tests/LanguageServerLoggingTests.cs index 346bfd8f3..273dcffef 100644 --- a/test/Lsp.Tests/Integration/LanguageServerLoggingTests.cs +++ b/test/Lsp.Integration.Tests/LanguageServerLoggingTests.cs @@ -54,6 +54,7 @@ public async Task Logs_Are_Sent_To_Client_From_Server() logger.LogDebug("Just gotta let you debug!"); await logs.DelayUntilCount(6, CancellationToken); + await SettleNext(); var items = logs.Take(6).ToList(); items.Should().HaveCount(6); @@ -92,6 +93,7 @@ public async Task Logs_Are_Sent_To_Client_From_Server_Respecting_SetMinimumLevel logger.LogDebug("Just gotta let you debug!"); await logs.DelayUntilCount(3, CancellationToken); + await SettleNext(); var items = logs.Take(3).ToList(); items.Should().HaveCount(3); @@ -130,6 +132,7 @@ public async Task Logs_Are_Sent_To_Client_From_Server_Respecting_TraceLevel() logger.LogDebug("Just gotta let you debug!"); await logs.DelayUntilCount(4, CancellationToken); + await SettleNext(); var items = logs.Take(4).ToList(); items.Should().HaveCount(4); @@ -170,6 +173,7 @@ public async Task Client_Can_Dynamically_Change_Server_Trace_Level_Off_To_Verbos await logs.DelayUntilCount(3, CancellationToken); { var items = logs.Take(3).ToList(); + await SettleNext(); ; items.Should().HaveCount(3); @@ -193,6 +197,7 @@ public async Task Client_Can_Dynamically_Change_Server_Trace_Level_Off_To_Verbos await logs.DelayUntilCount(6, CancellationToken); { + await SettleNext(); var items = logs.Take(6).ToList(); ; @@ -236,6 +241,7 @@ public async Task Client_Can_Dynamically_Change_Server_Trace_Level_Verbose_To_Of await logs.DelayUntilCount(6, CancellationToken); { + await SettleNext(); var items = logs.Take(6).ToList(); items.Should().HaveCount(6); @@ -259,6 +265,7 @@ public async Task Client_Can_Dynamically_Change_Server_Trace_Level_Verbose_To_Of await logs.DelayUntilCount(3, CancellationToken); { + await SettleNext(); var items = logs.Take(3).ToList(); items.Should().HaveCount(3); diff --git a/test/Lsp.Tests/Integration/LogMessageTests.cs b/test/Lsp.Integration.Tests/LogMessageTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/LogMessageTests.cs rename to test/Lsp.Integration.Tests/LogMessageTests.cs diff --git a/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj b/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj new file mode 100644 index 000000000..46970d637 --- /dev/null +++ b/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj @@ -0,0 +1,24 @@ + + + net5.0;netcoreapp3.1;netcoreapp2.1 + true + AnyCPU + + + + + + + + + + + + + + + + + + + diff --git a/test/Lsp.Tests/Integration/MonikerTests.cs b/test/Lsp.Integration.Tests/MonikerTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/MonikerTests.cs rename to test/Lsp.Integration.Tests/MonikerTests.cs diff --git a/test/Lsp.Tests/Integration/OverrideHandlerTests.cs b/test/Lsp.Integration.Tests/OverrideHandlerTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/OverrideHandlerTests.cs rename to test/Lsp.Integration.Tests/OverrideHandlerTests.cs diff --git a/test/Lsp.Tests/Integration/PartialItemTests.cs b/test/Lsp.Integration.Tests/PartialItemTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/PartialItemTests.cs rename to test/Lsp.Integration.Tests/PartialItemTests.cs diff --git a/test/Lsp.Tests/Integration/PartialItemsTests.cs b/test/Lsp.Integration.Tests/PartialItemsTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/PartialItemsTests.cs rename to test/Lsp.Integration.Tests/PartialItemsTests.cs diff --git a/test/Lsp.Tests/Integration/ProgressTests.cs b/test/Lsp.Integration.Tests/ProgressTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ProgressTests.cs rename to test/Lsp.Integration.Tests/ProgressTests.cs diff --git a/test/Lsp.Integration.Tests/Properties.cs b/test/Lsp.Integration.Tests/Properties.cs new file mode 100644 index 000000000..80a070195 --- /dev/null +++ b/test/Lsp.Integration.Tests/Properties.cs @@ -0,0 +1,3 @@ +using System; + +[assembly: Xunit.CollectionBehavior(MaxParallelThreads = 10)] diff --git a/test/Lsp.Tests/Integration/ProposalTests.cs b/test/Lsp.Integration.Tests/ProposalTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ProposalTests.cs rename to test/Lsp.Integration.Tests/ProposalTests.cs diff --git a/test/Lsp.Integration.Tests/Records.cs b/test/Lsp.Integration.Tests/Records.cs new file mode 100644 index 000000000..207d2246e --- /dev/null +++ b/test/Lsp.Integration.Tests/Records.cs @@ -0,0 +1,23 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_RECORD_ATTRIBUTES +#if NETSTANDARD || NETCOREAPP +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] +#if INTERNAL_RECORD_ATTRIBUTES + internal +#else + public +#endif + static class IsExternalInit + { + } +} +#endif diff --git a/test/Lsp.Tests/Integration/RecursiveResolutionTests.cs b/test/Lsp.Integration.Tests/RecursiveResolutionTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/RecursiveResolutionTests.cs rename to test/Lsp.Integration.Tests/RecursiveResolutionTests.cs diff --git a/test/Lsp.Tests/Integration/RenameTests.cs b/test/Lsp.Integration.Tests/RenameTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/RenameTests.cs rename to test/Lsp.Integration.Tests/RenameTests.cs diff --git a/test/Lsp.Tests/Integration/RequestCancellationTests.cs b/test/Lsp.Integration.Tests/RequestCancellationTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/RequestCancellationTests.cs rename to test/Lsp.Integration.Tests/RequestCancellationTests.cs diff --git a/test/Lsp.Tests/Integration/ShowMessageTests.cs b/test/Lsp.Integration.Tests/ShowMessageTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/ShowMessageTests.cs rename to test/Lsp.Integration.Tests/ShowMessageTests.cs diff --git a/test/Lsp.Tests/Integration/TypedCallHierarchyTests.cs b/test/Lsp.Integration.Tests/TypedCallHierarchyTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/TypedCallHierarchyTests.cs rename to test/Lsp.Integration.Tests/TypedCallHierarchyTests.cs diff --git a/test/Lsp.Tests/Integration/TypedCodeActionTests.cs b/test/Lsp.Integration.Tests/TypedCodeActionTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/TypedCodeActionTests.cs rename to test/Lsp.Integration.Tests/TypedCodeActionTests.cs diff --git a/test/Lsp.Tests/Integration/TypedCodeLensTests.cs b/test/Lsp.Integration.Tests/TypedCodeLensTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/TypedCodeLensTests.cs rename to test/Lsp.Integration.Tests/TypedCodeLensTests.cs diff --git a/test/Lsp.Tests/Integration/TypedCompletionTests.cs b/test/Lsp.Integration.Tests/TypedCompletionTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/TypedCompletionTests.cs rename to test/Lsp.Integration.Tests/TypedCompletionTests.cs diff --git a/test/Lsp.Tests/Integration/TypedDocumentLinkTests.cs b/test/Lsp.Integration.Tests/TypedDocumentLinkTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/TypedDocumentLinkTests.cs rename to test/Lsp.Integration.Tests/TypedDocumentLinkTests.cs diff --git a/test/Lsp.Tests/Integration/WorkspaceFolderTests.cs b/test/Lsp.Integration.Tests/WorkspaceFolderTests.cs similarity index 100% rename from test/Lsp.Tests/Integration/WorkspaceFolderTests.cs rename to test/Lsp.Integration.Tests/WorkspaceFolderTests.cs diff --git a/test/Lsp.Tests/CompletionItemKindTests.cs b/test/Lsp.Tests/CompletionItemKindTests.cs index 27581d245..08bd91c48 100644 --- a/test/Lsp.Tests/CompletionItemKindTests.cs +++ b/test/Lsp.Tests/CompletionItemKindTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using Lsp.Tests.Integration.Fixtures; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models;