Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IMuteIssuesService #5068

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SonarLint.VisualStudio.ConnectedMode.Suppressions;
using SonarLint.VisualStudio.ConnectedMode.Transition;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Core.Transition;
using SonarLint.VisualStudio.TestInfrastructure;
using SonarQube.Client;
using SonarQube.Client.Models;

namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Transition
{
[TestClass]
public class MuteIssuesServiceTests
{
[TestMethod]
public void MefCtor_CheckIsExported()
{
MefTestHelpers.CheckTypeCanBeImported<MuteIssuesService, IMuteIssuesService>(
MefTestHelpers.CreateExport<IConfigurationProvider>(),
MefTestHelpers.CreateExport<ILogger>(),
MefTestHelpers.CreateExport<IMuteIssuesWindowService>(),
MefTestHelpers.CreateExport<ISonarQubeService>(),
MefTestHelpers.CreateExport<IServerIssuesStoreWriter>());
}

[TestMethod]
public void MefCtor_CheckIsSingleton()
{
MefTestHelpers.CheckIsSingletonMefComponent<MuteIssuesService>();
}

[TestMethod]
public async Task Mute_NotInConnectedMode_Logs()
{
var configurationProvider = CreateConfigurationProvider(false);
var logger = new Mock<ILogger>();

var testSubject = CreateTestSubject(configurationProvider: configurationProvider, logger: logger.Object);

await testSubject.Mute("anyKEy", CancellationToken.None);

logger.Verify(l => l.LogVerbose("[Transition]Issue muting is only supported in connected mode"), Times.Once);
}

[TestMethod]
public async Task Mute_WindowOK_CallService()
{
var muteIssuesWindowService = CreateMuteIssuesWindowService("issueKey", true, SonarQubeIssueTransition.FalsePositive, "some comment");

var sonarQubeService = new Mock<ISonarQubeService>();

sonarQubeService.Setup(s => s.TransitionIssueAsync(It.IsAny<string>(), It.IsAny<SonarQubeIssueTransition>(), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(SonarQubeIssueTransitionResult.FailedToTransition);
sonarQubeService.Setup(s => s.TransitionIssueAsync("issueKey", SonarQubeIssueTransition.FalsePositive, "some comment", CancellationToken.None)).ReturnsAsync(SonarQubeIssueTransitionResult.Success);

var serverIssuesStore = new Mock<IServerIssuesStoreWriter>();

var testSubject = CreateTestSubject(muteIssuesWindowService: muteIssuesWindowService.Object, sonarQubeService: sonarQubeService.Object, serverIssuesStore: serverIssuesStore.Object);

await testSubject.Mute("issueKey", CancellationToken.None);

muteIssuesWindowService.Verify(s => s.Show("issueKey"), Times.Once);
sonarQubeService.Verify(s => s.TransitionIssueAsync("issueKey", SonarQubeIssueTransition.FalsePositive, "some comment", CancellationToken.None), Times.Once);
serverIssuesStore.Verify(s => s.UpdateIssues(true, It.Is<IEnumerable<string>>(p => p.SequenceEqual(new[] { "issueKey" }))), Times.Once);
}

[TestMethod]
public async Task Mute_WindowCancel_DontCallService()
{
var muteIssuesWindowService = CreateMuteIssuesWindowService("issueKey", false, SonarQubeIssueTransition.FalsePositive, "some comment");

var sonarQubeService = new Mock<ISonarQubeService>();

var testSubject = CreateTestSubject(muteIssuesWindowService: muteIssuesWindowService.Object, sonarQubeService: sonarQubeService.Object);

await testSubject.Mute("issueKey", CancellationToken.None);

muteIssuesWindowService.Verify(s => s.Show("issueKey"), Times.Once);
sonarQubeService.VerifyNoOtherCalls();
}

private Mock<IMuteIssuesWindowService> CreateMuteIssuesWindowService(string issueKey, bool result, SonarQubeIssueTransition transition = default, string comment = default)
{
var muteIssuesWindowResponse = CreateMuteIssuesWindowResponse(result, transition, comment);

var service = new Mock<IMuteIssuesWindowService>();
service.Setup(s => s.Show(issueKey)).Returns(muteIssuesWindowResponse);

return service;

static MuteIssuesWindowResponse CreateMuteIssuesWindowResponse(bool result, SonarQubeIssueTransition transition, string comment)
{
return new MuteIssuesWindowResponse
{
Result = result,
IssueTransition = transition,
Comment = comment
};
}
}

private Mock<IThreadHandling> CreateThreadHandling()
{
var threadHandling = new Mock<IThreadHandling>();
threadHandling
.Setup(x => x.RunOnUIThreadAsync(It.IsAny<Action>()))
.Callback<Action>(callbackAction =>
{
callbackAction();
});

return threadHandling;
}

private IConfigurationProvider CreateConfigurationProvider(bool isConnectedMode = true)
{
var configurationProvider = new ConfigurableConfigurationProvider();
configurationProvider.ModeToReturn = isConnectedMode ? SonarLintMode.Connected : SonarLintMode.Standalone;
configurationProvider.ProjectToReturn = new BoundSonarQubeProject();
configurationProvider.FolderPathToReturn = "someFolder";

return configurationProvider;
}

private MuteIssuesService CreateTestSubject(IConfigurationProvider configurationProvider = null,
ILogger logger = null,
IMuteIssuesWindowService muteIssuesWindowService = null,
ISonarQubeService sonarQubeService = null,
IServerIssuesStoreWriter serverIssuesStore = null,
IThreadHandling threadHandling = null)
{
configurationProvider ??= CreateConfigurationProvider();
logger ??= Mock.Of<ILogger>();
muteIssuesWindowService ??= Mock.Of<IMuteIssuesWindowService>();
sonarQubeService ??= Mock.Of<ISonarQubeService>();
serverIssuesStore ??= Mock.Of<IServerIssuesStoreWriter>();
threadHandling ??= CreateThreadHandling().Object;

return new MuteIssuesService(configurationProvider, logger, muteIssuesWindowService, sonarQubeService, serverIssuesStore, threadHandling);
}
}
}
9 changes: 9 additions & 0 deletions src/ConnectedMode/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/ConnectedMode/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,7 @@
<data name="SharedBindingConfigProvider_SharedFolderNotFound" xml:space="preserve">
<value>[SharedBindingConfigProvider] SonarLint shared folder was not found</value>
</data>
<data name="MuteWindowService_NotInConnectedMode" xml:space="preserve">
<value>[Transition]Issue muting is only supported in connected mode</value>
</data>
</root>
89 changes: 89 additions & 0 deletions src/ConnectedMode/Transition/MuteIssuesService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using SonarLint.VisualStudio.ConnectedMode.Suppressions;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Core.Transition;
using SonarLint.VisualStudio.Infrastructure.VS;
using SonarQube.Client;
using SonarQube.Client.Models;

namespace SonarLint.VisualStudio.ConnectedMode.Transition
{
[Export(typeof(IMuteIssuesService))]
[PartCreationPolicy(CreationPolicy.Shared)]
internal class MuteIssuesService : IMuteIssuesService
{
private readonly IConfigurationProvider configurationProvider;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did the same mistake in one of my PRs, but only realized it just now - I think we shouldn't use configuration provider directly, but instead use the cached configuration from IActiveSolutionBoundTracker for performance reasons

private readonly ILogger logger;
private readonly IMuteIssuesWindowService muteIssuesWindowService;
private readonly IThreadHandling threadHandling;
private readonly ISonarQubeService sonarQubeService;
private readonly IServerIssuesStoreWriter serverIssuesStore;

[ImportingConstructor]
public MuteIssuesService(IConfigurationProvider configurationProvider, ILogger logger, IMuteIssuesWindowService muteIssuesWindowService, ISonarQubeService sonarQubeService, IServerIssuesStoreWriter serverIssuesStore)
: this(configurationProvider, logger, muteIssuesWindowService, sonarQubeService, serverIssuesStore, ThreadHandling.Instance)
{ }

internal MuteIssuesService(IConfigurationProvider configurationProvider,
ILogger logger,
IMuteIssuesWindowService muteIssuesWindowService,
ISonarQubeService sonarQubeService,
IServerIssuesStoreWriter serverIssuesStore,
IThreadHandling threadHandling)
{
this.configurationProvider = configurationProvider;
this.logger = logger;
this.muteIssuesWindowService = muteIssuesWindowService;
this.threadHandling = threadHandling;
this.sonarQubeService = sonarQubeService;
this.serverIssuesStore = serverIssuesStore;
}

public async Task Mute(string issueKey, CancellationToken token)
{
if (!configurationProvider.GetConfiguration().Mode.IsInAConnectedMode())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding throwifnotonbackgroundthread or an explicit switch to it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: Just throwing if on UI thread should be enough

{
logger.LogVerbose(Resources.MuteWindowService_NotInConnectedMode);
return;
}

MuteIssuesWindowResponse windowResponse = default;

await threadHandling.RunOnUIThreadAsync(() => windowResponse = muteIssuesWindowService.Show(issueKey));

//TODO: Failure will be handled later
if (windowResponse.Result)
{
var serviceResult = await sonarQubeService.TransitionIssueAsync(issueKey, windowResponse.IssueTransition, windowResponse.Comment, token);

if (serviceResult == SonarQubeIssueTransitionResult.Success)
{
serverIssuesStore.UpdateIssues(true, new[] { issueKey });
}
}
}
}
}
30 changes: 30 additions & 0 deletions src/Core/Transition/IMuteIssuesService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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;
using System.Threading.Tasks;

namespace SonarLint.VisualStudio.Core.Transition
{
public interface IMuteIssuesService
{
Task Mute(string issueKey, CancellationToken token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
*/

using System;
using FluentAssertions;
using SonarLint.VisualStudio.Core.Binding;

namespace SonarLint.VisualStudio.TestInfrastructure
Expand All @@ -38,7 +37,6 @@ public BindingConfiguration GetConfiguration()
: BindingConfiguration.CreateBoundConfiguration(ProjectToReturn, ModeToReturn, FolderPathToReturn);
}


#region Test helpers

public BoundSonarQubeProject ProjectToReturn { get; set; }
Expand Down