diff --git a/src/SLCore.UnitTests/Core/SLCoreJsonRpcTests.cs b/src/SLCore.UnitTests/Core/SLCoreJsonRpcTests.cs new file mode 100644 index 0000000000..e1ebe8f710 --- /dev/null +++ b/src/SLCore.UnitTests/Core/SLCoreJsonRpcTests.cs @@ -0,0 +1,119 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System; +using System.Threading.Tasks; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.UnitTests.Helpers; +using StreamJsonRpc; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Core; + +[TestClass] +public class SLCoreJsonRpcTests +{ + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public void IsAlive_UpdatesOnCompletion(bool triggerCompletion) + { + var testSubject = CreateTestSubject(out _, out var completionSource); + + testSubject.IsAlive.Should().BeTrue(); + + if (triggerCompletion) + { + completionSource.TrySetResult(true); + testSubject.IsAlive.Should().BeFalse(); + } + else + { + testSubject.IsAlive.Should().BeTrue(); + } + } + + [TestMethod] + public void IsAlive_TaskCanceled_NoException() + { + var testSubject = CreateTestSubject(out _, out var completionSource); + + testSubject.IsAlive.Should().BeTrue(); + + completionSource.TrySetCanceled(); + + testSubject.IsAlive.Should().BeFalse(); + } + + [TestMethod] + public void IsAlive_TaskThrows_NoException() + { + var testSubject = CreateTestSubject(out _, out var completionSource); + + testSubject.IsAlive.Should().BeTrue(); + + completionSource.TrySetException(new Exception()); + + testSubject.IsAlive.Should().BeFalse(); + } + + [TestMethod] + public void CreateService_CallsAttachWithCorrectOptions() + { + var testSubject = CreateTestSubject(out var clientMock, out _); + var service = Mock.Of(); + clientMock.Setup(x => x.Attach(It.IsAny())).Returns(service); + + var createdService = testSubject.CreateService(); + + createdService.Should().BeSameAs(service); + clientMock.VerifyGet(x => x.Completion, Times.Once); + clientMock.Verify(x => + x.Attach(It.Is(options => + options.MethodNameTransform == CommonMethodNameTransforms.CamelCase)), // todo: https://github.com/SonarSource/sonarlint-visualstudio/issues/5140 + Times.Once); + clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void AttachListener_CallsAddLocalRpcTargetWithCorrectOptions() + { + var testSubject = CreateTestSubject(out var clientMock, out _); + var listener = new TestSLCoreListener(); + clientMock.Setup(x => x.AddLocalRpcTarget(listener, It.IsAny())); + + testSubject.AttachListener(listener); + + clientMock.VerifyGet(x => x.Completion, Times.Once); + clientMock.Verify(x => x.AddLocalRpcTarget(listener, It.Is(options => + options.MethodNameTransform == CommonMethodNameTransforms.CamelCase && options.UseSingleObjectParameterDeserialization)), + Times.Once); + clientMock.VerifyNoOtherCalls(); + } + + private static SLCoreJsonRpc CreateTestSubject(out Mock clientMock, out TaskCompletionSource clientCompletionSource) + { + (clientMock, clientCompletionSource) = TestJsonRpcFactory.Create(); + return new SLCoreJsonRpc(clientMock.Object); + } + + public interface ITestSLCoreService : ISLCoreService {} + + public class TestSLCoreListener : ISLCoreListener {} +} diff --git a/src/SLCore.UnitTests/Core/SLCoreServiceProviderTests.cs b/src/SLCore.UnitTests/Core/SLCoreServiceProviderTests.cs index 51de0f62cb..77196511fa 100644 --- a/src/SLCore.UnitTests/Core/SLCoreServiceProviderTests.cs +++ b/src/SLCore.UnitTests/Core/SLCoreServiceProviderTests.cs @@ -166,7 +166,7 @@ public void SetCurrentConnection_ClearsAllCachedServices() requestedService3.Should().BeSameAs(service3New).And.NotBeSameAs(service3); } - private static void SetUpServiceCreation(Mock rpcMock, T service) where T : ISLCoreService + private static void SetUpServiceCreation(Mock rpcMock, T service) where T : class, ISLCoreService { rpcMock.Setup(x => x.CreateService()).Returns(service); } diff --git a/src/SLCore.UnitTests/Helpers/TestJsonRpcFactory.cs b/src/SLCore.UnitTests/Helpers/TestJsonRpcFactory.cs new file mode 100644 index 0000000000..6ef43714a7 --- /dev/null +++ b/src/SLCore.UnitTests/Helpers/TestJsonRpcFactory.cs @@ -0,0 +1,35 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Threading.Tasks; +using SonarLint.VisualStudio.SLCore.Core; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Helpers; + +internal static class TestJsonRpcFactory +{ + public static (Mock clientMock, TaskCompletionSource clientCompletionSource) Create() + { + var mock = new Mock(); + var tcs = new TaskCompletionSource(); + mock.SetupGet(x => x.Completion).Returns(tcs.Task); + return (mock, tcs); + } +} diff --git a/src/SLCore/Core/IJsonRpc.cs b/src/SLCore/Core/IJsonRpc.cs new file mode 100644 index 0000000000..0648a227a8 --- /dev/null +++ b/src/SLCore/Core/IJsonRpc.cs @@ -0,0 +1,50 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using StreamJsonRpc; + +namespace SonarLint.VisualStudio.SLCore.Core +{ + /// + /// A testable wrapper for JsonRpc. + /// + internal interface IJsonRpc + { + T Attach(JsonRpcProxyOptions options) where T : class; + + void AddLocalRpcTarget(object target, JsonRpcTargetOptions options); + + Task Completion { get; } + } + + /// + /// Wrapper for that implements + /// + [ExcludeFromCodeCoverage] + internal class JsonRpcWrapper : JsonRpc, IJsonRpc + { + public JsonRpcWrapper(Stream sendingStream, Stream receivingStream) : base(sendingStream, receivingStream) + { + } + } +} diff --git a/src/SLCore/Core/ISLCoreJsonRpc.cs b/src/SLCore/Core/ISLCoreJsonRpc.cs index e5e07fc153..b6c05412d2 100644 --- a/src/SLCore/Core/ISLCoreJsonRpc.cs +++ b/src/SLCore/Core/ISLCoreJsonRpc.cs @@ -18,17 +18,85 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; +using StreamJsonRpc; + namespace SonarLint.VisualStudio.SLCore.Core { /// - /// A wrapper for JsonRpc connection. + /// A friendly wrapper for JsonRpc connection. /// public interface ISLCoreJsonRpc { - TService CreateService() where TService : ISLCoreService; + TService CreateService() where TService : class, ISLCoreService; void AttachListener(ISLCoreListener listener); bool IsAlive { get; } } + + internal class SLCoreJsonRpc : ISLCoreJsonRpc + { + private readonly object lockObject = new object(); + private readonly IJsonRpc rpc; + private bool isAlive = true; + + public SLCoreJsonRpc(IJsonRpc jsonRpc) + { + rpc = jsonRpc; + AwaitRpcCompletionAsync().Forget(); + } + + public TService CreateService() where TService : class, ISLCoreService + { + lock (lockObject) + { + return rpc.Attach(new JsonRpcProxyOptions + { MethodNameTransform = CommonMethodNameTransforms.CamelCase }); // todo: https://github.com/SonarSource/sonarlint-visualstudio/issues/5140 + } + } + + public void AttachListener(ISLCoreListener listener) + { + lock (lockObject) + { + rpc.AddLocalRpcTarget(listener, + new JsonRpcTargetOptions + { + MethodNameTransform = CommonMethodNameTransforms.CamelCase, + UseSingleObjectParameterDeserialization = true + }); + } + } + + public bool IsAlive + { + get + { + lock (lockObject) + { + return isAlive; + } + } + } + + private async Task AwaitRpcCompletionAsync() + { + try + { + await rpc.Completion; + } + catch (Exception) + { + // we want to set isAlive to false on any exception here, including TaskCanceledException + } + + lock (lockObject) + { + isAlive = false; + } + } + } } diff --git a/src/SLCore/Core/ISLCoreServiceProvider.cs b/src/SLCore/Core/ISLCoreServiceProvider.cs index 175b95110b..7fb5a5d030 100644 --- a/src/SLCore/Core/ISLCoreServiceProvider.cs +++ b/src/SLCore/Core/ISLCoreServiceProvider.cs @@ -32,7 +32,7 @@ public interface ISLCoreServiceProvider /// /// An interface inherited from /// True if the underlying connection is alive, False if the connection is unavailable at the moment - bool TryGetTransientService(out TService service) where TService : ISLCoreService; + bool TryGetTransientService(out TService service) where TService : class, ISLCoreService; } public interface ISLCoreServiceProviderWriter @@ -52,7 +52,7 @@ public class SLCoreServiceProvider : ISLCoreServiceProvider, ISLCoreServiceProvi private readonly object cacheLock = new object(); private ISLCoreJsonRpc jsonRpc; - public bool TryGetTransientService(out TService service) where TService : ISLCoreService + public bool TryGetTransientService(out TService service) where TService : class, ISLCoreService { service = default;