Skip to content

Commit

Permalink
Add SLCoreJsonRpc wrapper for JsonRpc
Browse files Browse the repository at this point in the history
  • Loading branch information
georgii-borovinskikh-sonarsource committed Dec 27, 2023
1 parent 50ff0a1 commit 22ce6e5
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 5 deletions.
119 changes: 119 additions & 0 deletions src/SLCore.UnitTests/Core/SLCoreJsonRpcTests.cs
Original file line number Diff line number Diff line change
@@ -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<ITestSLCoreService>();
clientMock.Setup(x => x.Attach<ITestSLCoreService>(It.IsAny<JsonRpcProxyOptions>())).Returns(service);

var createdService = testSubject.CreateService<ITestSLCoreService>();

createdService.Should().BeSameAs(service);
clientMock.VerifyGet(x => x.Completion, Times.Once);
clientMock.Verify(x =>
x.Attach<ITestSLCoreService>(It.Is<JsonRpcProxyOptions>(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<JsonRpcTargetOptions>()));

testSubject.AttachListener(listener);

clientMock.VerifyGet(x => x.Completion, Times.Once);
clientMock.Verify(x => x.AddLocalRpcTarget(listener, It.Is<JsonRpcTargetOptions>(options =>
options.MethodNameTransform == CommonMethodNameTransforms.CamelCase && options.UseSingleObjectParameterDeserialization)),
Times.Once);
clientMock.VerifyNoOtherCalls();
}

private static SLCoreJsonRpc CreateTestSubject(out Mock<IJsonRpc> clientMock, out TaskCompletionSource<bool> clientCompletionSource)
{
(clientMock, clientCompletionSource) = TestJsonRpcFactory.Create();
return new SLCoreJsonRpc(clientMock.Object);
}

public interface ITestSLCoreService : ISLCoreService {}

public class TestSLCoreListener : ISLCoreListener {}
}
2 changes: 1 addition & 1 deletion src/SLCore.UnitTests/Core/SLCoreServiceProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public void SetCurrentConnection_ClearsAllCachedServices()
requestedService3.Should().BeSameAs(service3New).And.NotBeSameAs(service3);
}

private static void SetUpServiceCreation<T>(Mock<ISLCoreJsonRpc> rpcMock, T service) where T : ISLCoreService
private static void SetUpServiceCreation<T>(Mock<ISLCoreJsonRpc> rpcMock, T service) where T : class, ISLCoreService
{
rpcMock.Setup(x => x.CreateService<T>()).Returns(service);
}
Expand Down
35 changes: 35 additions & 0 deletions src/SLCore.UnitTests/Helpers/TestJsonRpcFactory.cs
Original file line number Diff line number Diff line change
@@ -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<IJsonRpc> clientMock, TaskCompletionSource<bool> clientCompletionSource) Create()
{
var mock = new Mock<IJsonRpc>();
var tcs = new TaskCompletionSource<bool>();
mock.SetupGet(x => x.Completion).Returns(tcs.Task);
return (mock, tcs);
}
}
50 changes: 50 additions & 0 deletions src/SLCore/Core/IJsonRpc.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A testable wrapper for JsonRpc.
/// </summary>
internal interface IJsonRpc
{
T Attach<T>(JsonRpcProxyOptions options) where T : class;

void AddLocalRpcTarget(object target, JsonRpcTargetOptions options);

Task Completion { get; }
}

/// <summary>
/// Wrapper for <see cref="JsonRpc"/> that implements <see cref="IJsonRpc"/>
/// </summary>
[ExcludeFromCodeCoverage]
internal class JsonRpcWrapper : JsonRpc, IJsonRpc
{
public JsonRpcWrapper(Stream sendingStream, Stream receivingStream) : base(sendingStream, receivingStream)
{
}
}
}
72 changes: 70 additions & 2 deletions src/SLCore/Core/ISLCoreJsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// A wrapper for JsonRpc connection.
/// A friendly wrapper for JsonRpc connection.
/// </summary>
public interface ISLCoreJsonRpc
{
TService CreateService<TService>() where TService : ISLCoreService;
TService CreateService<TService>() 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<TService>() where TService : class, ISLCoreService
{
lock (lockObject)
{
return rpc.Attach<TService>(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;
}
}
}
}
4 changes: 2 additions & 2 deletions src/SLCore/Core/ISLCoreServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public interface ISLCoreServiceProvider
/// </summary>
/// <typeparam name="TService">An interface inherited from <see cref="ISLCoreService"/></typeparam>
/// <returns>True if the underlying connection is alive, False if the connection is unavailable at the moment</returns>
bool TryGetTransientService<TService>(out TService service) where TService : ISLCoreService;
bool TryGetTransientService<TService>(out TService service) where TService : class, ISLCoreService;
}

public interface ISLCoreServiceProviderWriter
Expand All @@ -52,7 +52,7 @@ public class SLCoreServiceProvider : ISLCoreServiceProvider, ISLCoreServiceProvi
private readonly object cacheLock = new object();
private ISLCoreJsonRpc jsonRpc;

public bool TryGetTransientService<TService>(out TService service) where TService : ISLCoreService
public bool TryGetTransientService<TService>(out TService service) where TService : class, ISLCoreService
{
service = default;

Expand Down

0 comments on commit 22ce6e5

Please sign in to comment.