diff --git a/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs b/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs index 695031d496..675c1a1475 100644 --- a/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs +++ b/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs @@ -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; @@ -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(); + var analyzerManager = new Mock(); + + 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(); + var analyzerManager = new Mock(); + + 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(); + var analyzerManager = new Mock(); + + 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(y => y.Mode == SonarLintMode.Connected && y.Project == boundSonarQubeProject))); + } [TestMethod] public void ActiveSolutionBoundTracker_Changes() { var configScopeUpdaterMock = new Mock(); + var analyzerManager = new Mock(); ConfigureService(isConnected: false); ConfigureSolutionBinding(boundSonarQubeProject); @@ -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 @@ -185,6 +249,7 @@ public void ActiveSolutionBoundTracker_Changes() // Case 1: Clear bound project ConfigureSolutionBinding(null); + activeSolutionTracker.CurrentSolutionName = "solution1"; // Act testSubject.HandleBindingChange(true); @@ -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()), Times.Exactly(1)); + configScopeUpdaterMock.Verify(x => x.UpdateConfigScopeForCurrentSolution(null), Times.Exactly(1)); + VerifyAnalyzerUpdatedAndClearMock(analyzerManager, "solution1", false); VerifyAndResetBoundSolutionUiContextMock(isActive: false); VerifyServiceDisconnect(Times.Never()); @@ -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()), Times.Exactly(2)); + VerifyAnalyzerUpdatedAndClearMock(analyzerManager, "solution1", true); VerifyAndResetBoundSolutionUiContextMock(isActive: true); // Notifications from the Team Explorer should not trigger connect/disconnect @@ -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()), Times.Exactly(3)); + VerifyAnalyzerUpdatedAndClearMock(analyzerManager, null, false); VerifyAndResetBoundSolutionUiContextMock(isActive: false); // Closing an unbound solution should not call disconnect/connect @@ -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"); @@ -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()), Times.Exactly(4)); + VerifyAnalyzerUpdatedAndClearMock(analyzerManager, "solution2", true); VerifyAndResetBoundSolutionUiContextMock(isActive: true); // Loading a bound solution should call connect @@ -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()), Times.Exactly(5)); + VerifyAnalyzerUpdatedAndClearMock(analyzerManager, null, false); VerifyAndResetBoundSolutionUiContextMock(isActive: false); // SonarQubeService.Disconnect should be called since the WPF DisconnectCommand is not available @@ -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()), 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 analyzerManager, string solutionName, bool bound) + { + if (bound) + { + analyzerManager.Verify(x => x.OnSolutionChanged(solutionName, + It.Is(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() { @@ -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(); logger ??= new TestLogger(logToConsole: true); gitEvents ??= Mock.Of(); sonarQubeService ??= Mock.Of(); - return new ActiveSolutionBoundTracker(serviceProvider, solutionTracker, configScopeUpdater, logger, gitEvents, configurationProvider, sonarQubeService); + solutionRoslynAnalyzerManager ??= Mock.Of(); + return new ActiveSolutionBoundTracker(serviceProvider, solutionTracker, configScopeUpdater, solutionRoslynAnalyzerManager, logger, gitEvents, configurationProvider, sonarQubeService); } private void ConfigureService(bool isConnected) diff --git a/src/Integration/MefServices/ActiveSolutionBoundTracker.cs b/src/Integration/MefServices/ActiveSolutionBoundTracker.cs index 762bea9ee3..2fd479ed1d 100644 --- a/src/Integration/MefServices/ActiveSolutionBoundTracker.cs +++ b/src/Integration/MefServices/ActiveSolutionBoundTracker.cs @@ -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; @@ -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; @@ -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, @@ -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(); @@ -97,7 +102,7 @@ public void HandleBindingChange(bool isBindingCleared) return; } - this.RaiseAnalyzersChangedIfBindingChanged(GetBindingConfiguration(), isBindingCleared); + this.RaiseAnalyzersChangedIfBindingChanged(GetBindingConfiguration(), solutionTracker.CurrentSolutionName, isBindingCleared); } private BindingConfiguration GetBindingConfiguration() @@ -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)) { @@ -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)) {