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

SLVS-1537 Add ISolutionRoslynAnalyzerManager integration to ActiveSolutionBoundTracker #5768

Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Moq;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Infrastructure.VS.Roslyn;
using SonarLint.VisualStudio.TestInfrastructure;
using SonarQube.Client;
using SonarQube.Client.Models;
Expand Down Expand Up @@ -158,11 +159,73 @@ public void ActiveSolutionBoundTracker_UnBoundProject_NullPassedToConfigScopeUpd
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(null));
configScopeUpdaterMock.VerifyNoOtherCalls();
}

[TestMethod]
public void Ctor_NoSolution_CallsAnalyzerManager()
{
var configScopeUpdaterMock = new Mock<IConfigScopeUpdater>();
var analyzerManager = new Mock<ISolutionRoslynAnalyzerManager>();

activeSolutionTracker.CurrentSolutionName = null;
ConfigureService(isConnected: false);

var testSubject = CreateTestSubject(
activeSolutionTracker,
configProvider,
loggerMock.Object,
configScopeUpdater: configScopeUpdaterMock.Object,
sonarQubeService: sonarQubeServiceMock.Object,
solutionRoslynAnalyzerManager: analyzerManager.Object);

analyzerManager.Verify(x => x.OnSolutionChanged(null, BindingConfiguration.Standalone));
}

[TestMethod]
public void Ctor_StandaloneSolution_CallsAnalyzerManager()
{
var configScopeUpdaterMock = new Mock<IConfigScopeUpdater>();
var analyzerManager = new Mock<ISolutionRoslynAnalyzerManager>();

activeSolutionTracker.CurrentSolutionName = "solution123";
ConfigureService(isConnected: false);

var testSubject = CreateTestSubject(
activeSolutionTracker,
configProvider,
loggerMock.Object,
configScopeUpdater: configScopeUpdaterMock.Object,
sonarQubeService: sonarQubeServiceMock.Object,
solutionRoslynAnalyzerManager: analyzerManager.Object);

analyzerManager.Verify(x => x.OnSolutionChanged("solution123", BindingConfiguration.Standalone));
}

[TestMethod]
public void Ctor_BoundSolution_CallsAnalyzerManager()
{
var configScopeUpdaterMock = new Mock<IConfigScopeUpdater>();
var analyzerManager = new Mock<ISolutionRoslynAnalyzerManager>();

activeSolutionTracker.CurrentSolutionName = "solution123";
ConfigureService(isConnected: false);
ConfigureSolutionBinding(boundSonarQubeProject);

var testSubject = CreateTestSubject(
activeSolutionTracker,
configProvider,
loggerMock.Object,
configScopeUpdater: configScopeUpdaterMock.Object,
sonarQubeService: sonarQubeServiceMock.Object,
solutionRoslynAnalyzerManager: analyzerManager.Object);

analyzerManager.Verify(x => x.OnSolutionChanged("solution123", It.Is<BindingConfiguration>(y => y.Mode == SonarLintMode.Connected && y.Project == boundSonarQubeProject)));
}

[TestMethod]
public void ActiveSolutionBoundTracker_Changes()
{
var configScopeUpdaterMock = new Mock<IConfigScopeUpdater>();
var analyzerManager = new Mock<ISolutionRoslynAnalyzerManager>();

ConfigureService(isConnected: false);
ConfigureSolutionBinding(boundSonarQubeProject);
Expand All @@ -172,7 +235,8 @@ public void ActiveSolutionBoundTracker_Changes()
configProvider,
loggerMock.Object,
configScopeUpdater: configScopeUpdaterMock.Object,
sonarQubeService: sonarQubeServiceMock.Object);
sonarQubeService: sonarQubeServiceMock.Object,
solutionRoslynAnalyzerManager: analyzerManager.Object);
var eventCounter = new EventCounter(testSubject);

// Sanity
Expand All @@ -185,6 +249,7 @@ public void ActiveSolutionBoundTracker_Changes()

// Case 1: Clear bound project
ConfigureSolutionBinding(null);
activeSolutionTracker.CurrentSolutionName = "solution1";

// Act
testSubject.HandleBindingChange(true);
Expand All @@ -195,7 +260,8 @@ public void ActiveSolutionBoundTracker_Changes()
eventCounter.SolutionBindingChangedCount.Should().Be(1, "Unbind should trigger reanalysis");
eventCounter.PreSolutionBindingUpdatedCount.Should().Be(0, "Unbind should not trigger change");
eventCounter.SolutionBindingUpdatedCount.Should().Be(0, "Unbind should not trigger change");
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(It.IsAny<BoundServerProject>()), Times.Exactly(1));
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(null), Times.Exactly(1));
VerifyAnalyzerUpdatedAndClearMock(analyzerManager, "solution1", false);
VerifyAndResetBoundSolutionUiContextMock(isActive: false);

VerifyServiceDisconnect(Times.Never());
Expand All @@ -213,6 +279,7 @@ public void ActiveSolutionBoundTracker_Changes()
eventCounter.PreSolutionBindingUpdatedCount.Should().Be(0, "Bind should not trigger update event");
eventCounter.SolutionBindingUpdatedCount.Should().Be(0, "Bind should not trigger update event");
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(It.IsAny<BoundServerProject>()), Times.Exactly(2));
VerifyAnalyzerUpdatedAndClearMock(analyzerManager, "solution1", true);
VerifyAndResetBoundSolutionUiContextMock(isActive: true);

// Notifications from the Team Explorer should not trigger connect/disconnect
Expand All @@ -231,6 +298,7 @@ public void ActiveSolutionBoundTracker_Changes()
eventCounter.PreSolutionBindingUpdatedCount.Should().Be(0, "Solution change should not trigger update event");
eventCounter.SolutionBindingUpdatedCount.Should().Be(0, "Solution change should not trigger update event");
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(It.IsAny<BoundServerProject>()), Times.Exactly(3));
VerifyAnalyzerUpdatedAndClearMock(analyzerManager, null, false);
VerifyAndResetBoundSolutionUiContextMock(isActive: false);

// Closing an unbound solution should not call disconnect/connect
Expand All @@ -240,7 +308,7 @@ public void ActiveSolutionBoundTracker_Changes()
// Case 4: Load a bound solution
ConfigureSolutionBinding(boundSonarQubeProject);
// Act
activeSolutionTracker.SimulateActiveSolutionChanged(isSolutionOpen: true, "solution");
activeSolutionTracker.SimulateActiveSolutionChanged(isSolutionOpen: true, "solution2");

// Assert
testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Connected, "Bound respond to solution change event and report bound");
Expand All @@ -249,6 +317,7 @@ public void ActiveSolutionBoundTracker_Changes()
eventCounter.PreSolutionBindingUpdatedCount.Should().Be(0, "Bind should not trigger update event");
eventCounter.SolutionBindingUpdatedCount.Should().Be(0, "Bind should not trigger update event");
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(It.IsAny<BoundServerProject>()), Times.Exactly(4));
VerifyAnalyzerUpdatedAndClearMock(analyzerManager, "solution2", true);
VerifyAndResetBoundSolutionUiContextMock(isActive: true);

// Loading a bound solution should call connect
Expand All @@ -266,6 +335,7 @@ public void ActiveSolutionBoundTracker_Changes()
eventCounter.PreSolutionBindingUpdatedCount.Should().Be(0, "Solution change should not trigger update event");
eventCounter.SolutionBindingUpdatedCount.Should().Be(0, "Solution change should not trigger update event");
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(It.IsAny<BoundServerProject>()), Times.Exactly(5));
VerifyAnalyzerUpdatedAndClearMock(analyzerManager, null, false);
VerifyAndResetBoundSolutionUiContextMock(isActive: false);

// SonarQubeService.Disconnect should be called since the WPF DisconnectCommand is not available
Expand All @@ -282,11 +352,27 @@ public void ActiveSolutionBoundTracker_Changes()
eventCounter.PreSolutionBindingChangedCount.Should().Be(5, "Once disposed should stop raising the event");
eventCounter.SolutionBindingChangedCount.Should().Be(5, "Once disposed should stop raising the event");
configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(It.IsAny<BoundServerProject>()), Times.Exactly(5));
analyzerManager.Invocations.Should().BeEmpty();
// SonarQubeService.Disconnect should be called since the WPF DisconnectCommand is not available
VerifyServiceDisconnect(Times.Once());
VerifyServiceConnect(Times.Once());
}

private void VerifyAnalyzerUpdatedAndClearMock(Mock<ISolutionRoslynAnalyzerManager> analyzerManager, string solutionName, bool bound)
{
if (bound)
{
analyzerManager.Verify(x => x.OnSolutionChanged(solutionName,
It.Is<BindingConfiguration>(y => y.Mode == SonarLintMode.Connected && y.Project == boundSonarQubeProject)),
Times.Exactly(1));
}
else
{
analyzerManager.Verify(x => x.OnSolutionChanged(solutionName, BindingConfiguration.Standalone), Times.Exactly(1));
}
analyzerManager.Invocations.Clear();
}

[TestMethod]
public void UpdateConnection_WasDisconnected_NewSolutionIsUnbound_NoConnectOrDisconnectCalls()
{
Expand Down Expand Up @@ -487,13 +573,15 @@ private ActiveSolutionBoundTracker CreateTestSubject(
ILogger logger = null,
IBoundSolutionGitMonitor gitEvents = null,
IConfigScopeUpdater configScopeUpdater = null,
ISonarQubeService sonarQubeService = null)
ISonarQubeService sonarQubeService = null,
ISolutionRoslynAnalyzerManager solutionRoslynAnalyzerManager = null)
{
configScopeUpdater ??= Mock.Of<IConfigScopeUpdater>();
logger ??= new TestLogger(logToConsole: true);
gitEvents ??= Mock.Of<IBoundSolutionGitMonitor>();
sonarQubeService ??= Mock.Of<ISonarQubeService>();
return new ActiveSolutionBoundTracker(serviceProvider, solutionTracker, configScopeUpdater, logger, gitEvents, configurationProvider, sonarQubeService);
solutionRoslynAnalyzerManager ??= Mock.Of<ISolutionRoslynAnalyzerManager>();
return new ActiveSolutionBoundTracker(serviceProvider, solutionTracker, configScopeUpdater, solutionRoslynAnalyzerManager, logger, gitEvents, configurationProvider, sonarQubeService);
}

private void ConfigureService(bool isConnected)
Expand Down
12 changes: 9 additions & 3 deletions src/Integration/MefServices/ActiveSolutionBoundTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Microsoft.VisualStudio.Shell.Interop;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Infrastructure.VS.Roslyn;
using SonarQube.Client;
using ErrorHandler = Microsoft.VisualStudio.ErrorHandler;
using Task = System.Threading.Tasks.Task;
Expand All @@ -48,6 +49,7 @@ internal sealed class ActiveSolutionBoundTracker : IActiveSolutionBoundTracker,
private readonly IVsMonitorSelection vsMonitorSelection;
private readonly IBoundSolutionGitMonitor gitEventsMonitor;
private readonly IConfigScopeUpdater configScopeUpdater;
private readonly ISolutionRoslynAnalyzerManager solutionRoslynAnalyzerManager;
private readonly ILogger logger;
private readonly uint boundSolutionContextCookie;
private bool disposed;
Expand All @@ -62,6 +64,7 @@ internal sealed class ActiveSolutionBoundTracker : IActiveSolutionBoundTracker,
public ActiveSolutionBoundTracker([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
IActiveSolutionTracker activeSolutionTracker,
IConfigScopeUpdater configScopeUpdater,
ISolutionRoslynAnalyzerManager solutionRoslynAnalyzerManager,
ILogger logger,
IBoundSolutionGitMonitor gitEventsMonitor,
IConfigurationProvider configurationProvider,
Expand All @@ -79,11 +82,13 @@ public ActiveSolutionBoundTracker([Import(typeof(SVsServiceProvider))] IServiceP
this.configurationProvider = configurationProvider;
this.sonarQubeService = sonarQubeService;
this.configScopeUpdater = configScopeUpdater;
this.solutionRoslynAnalyzerManager = solutionRoslynAnalyzerManager;

// The solution changed inside the IDE
solutionTracker.ActiveSolutionChanged += OnActiveSolutionChanged;

CurrentConfiguration = GetBindingConfiguration();
solutionRoslynAnalyzerManager.OnSolutionChanged(activeSolutionTracker.CurrentSolutionName, CurrentConfiguration);

SetBoundSolutionUIContext();

Expand All @@ -97,7 +102,7 @@ public void HandleBindingChange(bool isBindingCleared)
return;
}

this.RaiseAnalyzersChangedIfBindingChanged(GetBindingConfiguration(), isBindingCleared);
this.RaiseAnalyzersChangedIfBindingChanged(GetBindingConfiguration(), solutionTracker.CurrentSolutionName, isBindingCleared);
}

private BindingConfiguration GetBindingConfiguration()
Expand Down Expand Up @@ -127,7 +132,7 @@ private async void OnActiveSolutionChanged(object sender, ActiveSolutionChangedE

gitEventsMonitor.Refresh();

this.RaiseAnalyzersChangedIfBindingChanged(newBindingConfiguration);
this.RaiseAnalyzersChangedIfBindingChanged(newBindingConfiguration, args.SolutionName);
}
catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex))
{
Expand Down Expand Up @@ -155,9 +160,10 @@ await WebServiceHelper.SafeServiceCallAsync(() => sonarQubeService.ConnectAsync(
}
}

private void RaiseAnalyzersChangedIfBindingChanged(BindingConfiguration newBindingConfiguration, bool? isBindingCleared = null)
private void RaiseAnalyzersChangedIfBindingChanged(BindingConfiguration newBindingConfiguration, string solutionName, bool? isBindingCleared = null)
{
configScopeUpdater.UpdateConfigScopeForCurrentSolution(newBindingConfiguration.Project);
solutionRoslynAnalyzerManager.OnSolutionChanged(solutionName, newBindingConfiguration);

if (!CurrentConfiguration.Equals(newBindingConfiguration))
{
Expand Down