From c03b84b422508810d8d3c66c23c0a02a9458aa1b Mon Sep 17 00:00:00 2001 From: Vasileios Naskos Date: Wed, 29 Jan 2025 14:48:58 +0100 Subject: [PATCH 1/2] SLVS-1786 Promote t-sql to standalone users --- .../Promote/PromoteGoldBarTests.cs | 155 ++++++++++++++++++ src/ConnectedMode/Promote/PromoteGoldBar.cs | 100 +++++++++++ src/ConnectedMode/Resources.Designer.cs | 36 ++++ src/ConnectedMode/Resources.resx | 12 ++ src/Core/DocumentationLinks.cs | 1 + .../Implementation/PromoteListenerTests.cs | 58 +++++++ .../Implementation/PromoteListener.cs | 38 +++++ ...bledLanguagesInConnectedModeParamsTests.cs | 49 ++++++ .../Listener/Promote/IPromoteListener.cs | 28 ++++ ...raEnabledLanguagesInConnectedModeParams.cs | 25 +++ 10 files changed, 502 insertions(+) create mode 100644 src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs create mode 100644 src/ConnectedMode/Promote/PromoteGoldBar.cs create mode 100644 src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs create mode 100644 src/SLCore.Listeners/Implementation/PromoteListener.cs create mode 100644 src/SLCore.UnitTests/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParamsTests.cs create mode 100644 src/SLCore/Listener/Promote/IPromoteListener.cs create mode 100644 src/SLCore/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParams.cs diff --git a/src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs b/src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs new file mode 100644 index 0000000000..61f0c8ac4c --- /dev/null +++ b/src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs @@ -0,0 +1,155 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 SonarLint.VisualStudio.ConnectedMode.Promote; +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.Notifications; +using SonarLint.VisualStudio.Core.Telemetry; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Promote; + +[TestClass] +public class PromoteGoldBarTests +{ + private const string LanguageToPromote = "TSQL"; + + private INotificationService notificationService; + private IDoNotShowAgainNotificationAction doNotShowAgainNotificationAction; + private IActiveSolutionBoundTracker activeSolutionBoundTracker; + private IBrowserService browserService; + private ITelemetryManager telemetryManager; + private IConnectedModeUIManager connectedModeUiManager; + private PromoteGoldBar testSubject; + + [TestInitialize] + public void TestInitialize() + { + notificationService = Substitute.For(); + doNotShowAgainNotificationAction = Substitute.For(); + activeSolutionBoundTracker = Substitute.For(); + browserService = Substitute.For(); + telemetryManager = Substitute.For(); + connectedModeUiManager = Substitute.For(); + testSubject = new PromoteGoldBar(notificationService, doNotShowAgainNotificationAction, activeSolutionBoundTracker, browserService, telemetryManager, connectedModeUiManager); + } + + [TestMethod] + public void MefCtor_CheckExports() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void PromoteConnectedMode_ShowsNotification_WithId() + { + testSubject.PromoteConnectedMode(LanguageToPromote); + + notificationService.Received(1).ShowNotification(Arg.Is(n => n.Id == "PromoteNotification")); + } + + [TestMethod] + public void PromoteConnectedMode_ShowsNotification_WithMessageThatContainsTheLanguageToPromote() + { + testSubject.PromoteConnectedMode(LanguageToPromote); + + notificationService.Received(1).ShowNotification(Arg.Is(n => + n.Message == string.Format(Resources.PromoteConnectedModeLanguagesMessage, LanguageToPromote) + )); + } + + [TestMethod] + public void PromoteConnectedMode_ShowsNotification_WithCorrectActions() + { + testSubject.PromoteConnectedMode(LanguageToPromote); + + notificationService.Received(1).ShowNotification(Arg.Is(n => + n.Actions.ToList().Count == 4 && + n.Actions.ToList()[0].CommandText == Resources.PromoteBind && + n.Actions.ToList()[1].CommandText == Resources.PromoteSonarQubeCloud && + n.Actions.ToList()[2].CommandText == Resources.PromoteLearnMore && + n.Actions.ToList()[3] == doNotShowAgainNotificationAction + )); + } + + [TestMethod] + public void PromoteConnectedMode_BindAction_ShowsManageBindingDialog() + { + testSubject.PromoteConnectedMode(LanguageToPromote); + var notification = (Notification)notificationService.ReceivedCalls().Single().GetArguments()[0]; + var bindAction = notification.Actions.First(a => a.CommandText == Resources.PromoteBind); + + bindAction.Action(null); + + connectedModeUiManager.Received(1).ShowManageBindingDialog(); + } + + [TestMethod] + public void PromoteConnectedMode_SonarQubeCloudAction_NavigatesToCorrectUrl() + { + testSubject.PromoteConnectedMode(LanguageToPromote); + var notification = (Notification) notificationService.ReceivedCalls().Single().GetArguments()[0]; + var sonarQubeCloudAction = notification.Actions.First(a => a.CommandText == Resources.PromoteSonarQubeCloud); + + sonarQubeCloudAction.Action(null); + + browserService.Received(1).Navigate(TelemetryLinks.LinkIdToUrls[TelemetryLinks.SonarQubeCloudFreeSignUpId]); + } + + [TestMethod] + public void PromoteConnectedMode_LearnMoreAction_NavigatesToCorrectUrl() + { + testSubject.PromoteConnectedMode(LanguageToPromote); + var notification = (Notification) notificationService.ReceivedCalls().Single().GetArguments()[0]; + var learnMoreAction = notification.Actions.First(a => a.CommandText == Resources.PromoteLearnMore); + + learnMoreAction.Action(null); + + browserService.Received(1).Navigate(DocumentationLinks.ConnectedModeBenefits); + } + + [TestMethod] + public void OnActiveSolutionBindingChanged_ConnectedMode_ClosesNotification() + { + var eventArgs = new ActiveSolutionBindingEventArgs(new BindingConfiguration(null, SonarLintMode.Connected, null)); + + testSubject.PromoteConnectedMode(LanguageToPromote); + activeSolutionBoundTracker.SolutionBindingChanged += Raise.EventWith(this, eventArgs); + + notificationService.Received(1).CloseNotification(); + } + + [TestMethod] + public void Dispose_UnsubscribesFromAllEvents() + { + testSubject.Dispose(); + + activeSolutionBoundTracker.Received(1).SolutionBindingChanged -= Arg.Any>(); + } +} diff --git a/src/ConnectedMode/Promote/PromoteGoldBar.cs b/src/ConnectedMode/Promote/PromoteGoldBar.cs new file mode 100644 index 0000000000..fcf93e0a35 --- /dev/null +++ b/src/ConnectedMode/Promote/PromoteGoldBar.cs @@ -0,0 +1,100 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.Notifications; +using SonarLint.VisualStudio.Core.Telemetry; + +namespace SonarLint.VisualStudio.ConnectedMode.Promote; + +public interface IPromoteGoldBar +{ + void PromoteConnectedMode(string languagesToPromote); +} + +[Export(typeof(IPromoteGoldBar))] +[PartCreationPolicy(CreationPolicy.Shared)] +public sealed class PromoteGoldBar : IPromoteGoldBar, IDisposable +{ + private readonly INotificationService notificationService; + private readonly IDoNotShowAgainNotificationAction doNotShowAgainNotificationAction; + private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; + private readonly IBrowserService browserService; + private readonly ITelemetryManager telemetryManager; + private readonly IConnectedModeUIManager connectedModeUiManager; + + [ImportingConstructor] + public PromoteGoldBar( + INotificationService notificationService, + IDoNotShowAgainNotificationAction doNotShowAgainNotificationAction, + IActiveSolutionBoundTracker activeSolutionBoundTracker, + IBrowserService browserService, + ITelemetryManager telemetryManager, + IConnectedModeUIManager connectedModeUiManager) + { + this.notificationService = notificationService; + this.doNotShowAgainNotificationAction = doNotShowAgainNotificationAction; + this.activeSolutionBoundTracker = activeSolutionBoundTracker; + this.browserService = browserService; + this.telemetryManager = telemetryManager; + this.connectedModeUiManager = connectedModeUiManager; + + this.activeSolutionBoundTracker.SolutionBindingChanged += OnActiveSolutionBindingChanged; + } + + public void PromoteConnectedMode(string languagesToPromote) + { + var notification = new Notification( + id: "PromoteNotification", + message: string.Format(Resources.PromoteConnectedModeLanguagesMessage, languagesToPromote), + actions: + [ + new NotificationAction(Resources.PromoteBind, _ => OnBind(), false), + new NotificationAction(Resources.PromoteSonarQubeCloud, _ => OnTrySonarQubeCloud(), false), + new NotificationAction(Resources.PromoteLearnMore, _ => OnLearnMore(), false), + doNotShowAgainNotificationAction + ]); + + notificationService.ShowNotification(notification); + } + + public void Dispose() => activeSolutionBoundTracker.SolutionBindingChanged -= OnActiveSolutionBindingChanged; + + private void OnActiveSolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs e) + { + if (e.Configuration.Mode == SonarLintMode.Connected) + { + notificationService.CloseNotification(); + } + } + + private void OnBind() => connectedModeUiManager.ShowManageBindingDialog(); + + private void OnTrySonarQubeCloud() + { + browserService.Navigate(TelemetryLinks.LinkIdToUrls[TelemetryLinks.SonarQubeCloudFreeSignUpId]); + telemetryManager.LinkClicked(TelemetryLinks.SonarQubeCloudFreeSignUpId); + } + + private void OnLearnMore() => browserService.Navigate(DocumentationLinks.ConnectedModeBenefits); +} diff --git a/src/ConnectedMode/Resources.Designer.cs b/src/ConnectedMode/Resources.Designer.cs index f4388b1c3f..967ca2147a 100644 --- a/src/ConnectedMode/Resources.Designer.cs +++ b/src/ConnectedMode/Resources.Designer.cs @@ -455,6 +455,42 @@ internal static string Package_Initializing { } } + /// + /// Looks up a localized string similar to Bind. + /// + internal static string PromoteBind { + get { + return ResourceManager.GetString("PromoteBind", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider connecting to SonarQube to enable analysis on the following languages: {0}. + /// + internal static string PromoteConnectedModeLanguagesMessage { + get { + return ResourceManager.GetString("PromoteConnectedModeLanguagesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Learn more. + /// + internal static string PromoteLearnMore { + get { + return ResourceManager.GetString("PromoteLearnMore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Try SonarQube Cloud for free. + /// + internal static string PromoteSonarQubeCloud { + get { + return ResourceManager.GetString("PromoteSonarQubeCloud", resourceCulture); + } + } + /// /// Looks up a localized string similar to [SharedBindingConfigProvider] There's no .sonarlint shared folder or solution is not under git. /// diff --git a/src/ConnectedMode/Resources.resx b/src/ConnectedMode/Resources.resx index e7fa031a60..c36eb5091f 100644 --- a/src/ConnectedMode/Resources.resx +++ b/src/ConnectedMode/Resources.resx @@ -334,4 +334,16 @@ [ConnectedMode/DotnetAnalyzerIndicator] {0} + + Learn more + + + Consider connecting to SonarQube to enable analysis on the following languages: {0} + + + Bind + + + Try SonarQube Cloud for free + \ No newline at end of file diff --git a/src/Core/DocumentationLinks.cs b/src/Core/DocumentationLinks.cs index 6a4776d944..7def0a9087 100644 --- a/src/Core/DocumentationLinks.cs +++ b/src/Core/DocumentationLinks.cs @@ -32,6 +32,7 @@ public static class DocumentationLinks public const string MigrateToConnectedModeV7_NotesForTfvcUsers = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/team-features/migrate-connected-mode-to-v7/#notes-for-tfvc-users"; public const string ConnectedMode = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/team-features/connected-mode/"; + public const string ConnectedModeBenefits = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/team-features/connected-mode#benefits"; public const string TaintVulnerabilities = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/using-sonarlint/taint-vulnerabilities/"; public const string DisablingARule = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/using-sonarlint/rules/#disabling-a-rule"; public const string UseSharedBinding = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/team-features/connected-mode-setup/#bind-using-shared-configuration"; diff --git a/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs new file mode 100644 index 0000000000..63cdfd1c55 --- /dev/null +++ b/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs @@ -0,0 +1,58 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 SonarLint.VisualStudio.ConnectedMode.Promote; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Promote; + +namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation; + +[TestClass] +public class PromoteListenerTests +{ + private IPromoteGoldBar promoteGoldBar; + private PromoteListener testSubject; + + [TestInitialize] + public void TestInitialize() + { + promoteGoldBar = Substitute.For(); + testSubject = new PromoteListener(promoteGoldBar); + } + + [TestMethod] + public void MefCtor_CheckExports() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void PromoteExtraEnabledLanguagesInConnectedMode_DisplaysGoldBarWithCommaSeparatedLanguages() + { + var parameters = new PromoteExtraEnabledLanguagesInConnectedModeParams("CONFIGURATION_SCOPE_ID", [Language.TSQL, Language.PLSQL]); + + testSubject.PromoteExtraEnabledLanguagesInConnectedMode(parameters); + + promoteGoldBar.Received().PromoteConnectedMode("TSQL, PLSQL"); + } +} diff --git a/src/SLCore.Listeners/Implementation/PromoteListener.cs b/src/SLCore.Listeners/Implementation/PromoteListener.cs new file mode 100644 index 0000000000..35ba4e9d54 --- /dev/null +++ b/src/SLCore.Listeners/Implementation/PromoteListener.cs @@ -0,0 +1,38 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 SonarLint.VisualStudio.ConnectedMode.Promote; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Promote; + +namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation; + +[Export(typeof(ISLCoreListener))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +public class PromoteListener(IPromoteGoldBar promoteGoldBar) : IPromoteListener +{ + public void PromoteExtraEnabledLanguagesInConnectedMode(PromoteExtraEnabledLanguagesInConnectedModeParams parameters) + { + var languagesToPromote = string.Join(", ", parameters.languagesToPromote); + promoteGoldBar.PromoteConnectedMode(languagesToPromote); + } +} diff --git a/src/SLCore.UnitTests/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParamsTests.cs b/src/SLCore.UnitTests/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParamsTests.cs new file mode 100644 index 0000000000..7ccc0775f7 --- /dev/null +++ b/src/SLCore.UnitTests/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParamsTests.cs @@ -0,0 +1,49 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Listener.Promote; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Promote; + +[TestClass] +public class PromoteExtraEnabledLanguagesInConnectedModeParamsTests +{ + [TestMethod] + public void Deserialize_AsExpected() + { + var expectedObject = new PromoteExtraEnabledLanguagesInConnectedModeParams("CONFIG_SCOPE_ID", [Language.TSQL]); + + const string serializedParams = """ + { + "configurationScopeId": "CONFIG_SCOPE_ID", + "languagesToPromote": [ + "TSQL" + ] + } + """; + + var deserializedObject = JsonConvert.DeserializeObject(serializedParams); + + deserializedObject.configurationScopeId.Should().Be(expectedObject.configurationScopeId); + deserializedObject.languagesToPromote.Should().BeEquivalentTo(expectedObject.languagesToPromote); + } +} diff --git a/src/SLCore/Listener/Promote/IPromoteListener.cs b/src/SLCore/Listener/Promote/IPromoteListener.cs new file mode 100644 index 0000000000..2eacdcb48f --- /dev/null +++ b/src/SLCore/Listener/Promote/IPromoteListener.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 SonarLint.VisualStudio.SLCore.Core; + +namespace SonarLint.VisualStudio.SLCore.Listener.Promote; + +public interface IPromoteListener : ISLCoreListener +{ + void PromoteExtraEnabledLanguagesInConnectedMode(PromoteExtraEnabledLanguagesInConnectedModeParams parameters); +} diff --git a/src/SLCore/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParams.cs b/src/SLCore/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParams.cs new file mode 100644 index 0000000000..3def028d87 --- /dev/null +++ b/src/SLCore/Listener/Promote/PromoteExtraEnabledLanguagesInConnectedModeParams.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 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 SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.SLCore.Listener.Promote; + +public record PromoteExtraEnabledLanguagesInConnectedModeParams(string configurationScopeId, List languagesToPromote); From 4ab347b5c3584fef89092f383265fe52a7964491 Mon Sep 17 00:00:00 2001 From: Vasileios Naskos Date: Thu, 30 Jan 2025 15:22:09 +0100 Subject: [PATCH 2/2] Implement PR feedback --- ...arTests.cs => PromoteNotificationTests.cs} | 72 ++++++++++++++----- ...omoteGoldBar.cs => PromoteNotification.cs} | 36 +++++++--- .../Implementation/PromoteListenerTests.cs | 16 +++-- .../Implementation/PromoteListener.cs | 9 ++- 4 files changed, 98 insertions(+), 35 deletions(-) rename src/ConnectedMode.UnitTests/Promote/{PromoteGoldBarTests.cs => PromoteNotificationTests.cs} (65%) rename src/ConnectedMode/Promote/{PromoteGoldBar.cs => PromoteNotification.cs} (72%) diff --git a/src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs b/src/ConnectedMode.UnitTests/Promote/PromoteNotificationTests.cs similarity index 65% rename from src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs rename to src/ConnectedMode.UnitTests/Promote/PromoteNotificationTests.cs index 61f0c8ac4c..661c055480 100644 --- a/src/ConnectedMode.UnitTests/Promote/PromoteGoldBarTests.cs +++ b/src/ConnectedMode.UnitTests/Promote/PromoteNotificationTests.cs @@ -22,6 +22,7 @@ using SonarLint.VisualStudio.ConnectedMode.UI; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.TestInfrastructure; @@ -29,9 +30,10 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Promote; [TestClass] -public class PromoteGoldBarTests +public class PromoteNotificationTests { - private const string LanguageToPromote = "TSQL"; + private const string DefaultConfigurationScopeId = "CONFIG_SCOPE_ID"; + private readonly List languageToPromote = [Language.TSql]; private INotificationService notificationService; private IDoNotShowAgainNotificationAction doNotShowAgainNotificationAction; @@ -39,7 +41,8 @@ public class PromoteGoldBarTests private IBrowserService browserService; private ITelemetryManager telemetryManager; private IConnectedModeUIManager connectedModeUiManager; - private PromoteGoldBar testSubject; + private IActiveConfigScopeTracker activeConfigScopeTracker; + private PromoteNotification testSubject; [TestInitialize] public void TestInitialize() @@ -50,44 +53,81 @@ public void TestInitialize() browserService = Substitute.For(); telemetryManager = Substitute.For(); connectedModeUiManager = Substitute.For(); - testSubject = new PromoteGoldBar(notificationService, doNotShowAgainNotificationAction, activeSolutionBoundTracker, browserService, telemetryManager, connectedModeUiManager); + activeConfigScopeTracker = Substitute.For(); + + activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope(DefaultConfigurationScopeId)); + activeSolutionBoundTracker.CurrentConfiguration.Returns(BindingConfiguration.Standalone); + + testSubject = new PromoteNotification( + notificationService, + doNotShowAgainNotificationAction, + activeSolutionBoundTracker, + browserService, + telemetryManager, + connectedModeUiManager, + activeConfigScopeTracker); } [TestMethod] public void MefCtor_CheckExports() => - MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void PromoteConnectedMode_WhenConfigScopeMissMatch_DoesNotShowNotification() + { + using var _ = new AssertIgnoreScope(); + testSubject.PromoteConnectedMode("ANOTHER_CONFIG_SCOPE_ID", languageToPromote); + + notificationService.DidNotReceive().ShowNotification(Arg.Any()); + } + + [TestMethod] + public void PromoteConnectedMode_WhenInConnectedMode_DoesNotShowNotification() + { + using var _ = new AssertIgnoreScope(); + var inConnectedMode = new BindingConfiguration( + new BoundServerProject("test", "test", new ServerConnection.SonarQube(new Uri("https://localhost:9000"))), + SonarLintMode.Connected, + "C:\\path"); + activeSolutionBoundTracker.CurrentConfiguration.Returns(inConnectedMode); + + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); + + notificationService.DidNotReceive().ShowNotification(Arg.Any()); + } [TestMethod] public void PromoteConnectedMode_ShowsNotification_WithId() { - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); - notificationService.Received(1).ShowNotification(Arg.Is(n => n.Id == "PromoteNotification")); + notificationService.Received(1).ShowNotification(Arg.Is(n => n.Id == $"PromoteNotification.{languageToPromote[0].Id}")); } [TestMethod] public void PromoteConnectedMode_ShowsNotification_WithMessageThatContainsTheLanguageToPromote() { - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); notificationService.Received(1).ShowNotification(Arg.Is(n => - n.Message == string.Format(Resources.PromoteConnectedModeLanguagesMessage, LanguageToPromote) + n.Message == string.Format(Resources.PromoteConnectedModeLanguagesMessage, languageToPromote[0].Name) )); } [TestMethod] public void PromoteConnectedMode_ShowsNotification_WithCorrectActions() { - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); notificationService.Received(1).ShowNotification(Arg.Is(n => n.Actions.ToList().Count == 4 && @@ -101,7 +141,7 @@ public void PromoteConnectedMode_ShowsNotification_WithCorrectActions() [TestMethod] public void PromoteConnectedMode_BindAction_ShowsManageBindingDialog() { - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); var notification = (Notification)notificationService.ReceivedCalls().Single().GetArguments()[0]; var bindAction = notification.Actions.First(a => a.CommandText == Resources.PromoteBind); @@ -113,7 +153,7 @@ public void PromoteConnectedMode_BindAction_ShowsManageBindingDialog() [TestMethod] public void PromoteConnectedMode_SonarQubeCloudAction_NavigatesToCorrectUrl() { - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); var notification = (Notification) notificationService.ReceivedCalls().Single().GetArguments()[0]; var sonarQubeCloudAction = notification.Actions.First(a => a.CommandText == Resources.PromoteSonarQubeCloud); @@ -125,7 +165,7 @@ public void PromoteConnectedMode_SonarQubeCloudAction_NavigatesToCorrectUrl() [TestMethod] public void PromoteConnectedMode_LearnMoreAction_NavigatesToCorrectUrl() { - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); var notification = (Notification) notificationService.ReceivedCalls().Single().GetArguments()[0]; var learnMoreAction = notification.Actions.First(a => a.CommandText == Resources.PromoteLearnMore); @@ -139,7 +179,7 @@ public void OnActiveSolutionBindingChanged_ConnectedMode_ClosesNotification() { var eventArgs = new ActiveSolutionBindingEventArgs(new BindingConfiguration(null, SonarLintMode.Connected, null)); - testSubject.PromoteConnectedMode(LanguageToPromote); + testSubject.PromoteConnectedMode(DefaultConfigurationScopeId, languageToPromote); activeSolutionBoundTracker.SolutionBindingChanged += Raise.EventWith(this, eventArgs); notificationService.Received(1).CloseNotification(); diff --git a/src/ConnectedMode/Promote/PromoteGoldBar.cs b/src/ConnectedMode/Promote/PromoteNotification.cs similarity index 72% rename from src/ConnectedMode/Promote/PromoteGoldBar.cs rename to src/ConnectedMode/Promote/PromoteNotification.cs index fcf93e0a35..a75a1639df 100644 --- a/src/ConnectedMode/Promote/PromoteGoldBar.cs +++ b/src/ConnectedMode/Promote/PromoteNotification.cs @@ -22,19 +22,20 @@ using SonarLint.VisualStudio.ConnectedMode.UI; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.Core.Telemetry; namespace SonarLint.VisualStudio.ConnectedMode.Promote; -public interface IPromoteGoldBar +public interface IPromoteNotification { - void PromoteConnectedMode(string languagesToPromote); + void PromoteConnectedMode(string configurationScopeId, List languagesToPromote); } -[Export(typeof(IPromoteGoldBar))] +[Export(typeof(IPromoteNotification))] [PartCreationPolicy(CreationPolicy.Shared)] -public sealed class PromoteGoldBar : IPromoteGoldBar, IDisposable +public sealed class PromoteNotification : IPromoteNotification, IDisposable { private readonly INotificationService notificationService; private readonly IDoNotShowAgainNotificationAction doNotShowAgainNotificationAction; @@ -42,15 +43,17 @@ public sealed class PromoteGoldBar : IPromoteGoldBar, IDisposable private readonly IBrowserService browserService; private readonly ITelemetryManager telemetryManager; private readonly IConnectedModeUIManager connectedModeUiManager; + private readonly IActiveConfigScopeTracker activeConfigScopeTracker; [ImportingConstructor] - public PromoteGoldBar( + public PromoteNotification( INotificationService notificationService, IDoNotShowAgainNotificationAction doNotShowAgainNotificationAction, IActiveSolutionBoundTracker activeSolutionBoundTracker, IBrowserService browserService, ITelemetryManager telemetryManager, - IConnectedModeUIManager connectedModeUiManager) + IConnectedModeUIManager connectedModeUiManager, + IActiveConfigScopeTracker activeConfigScopeTracker) { this.notificationService = notificationService; this.doNotShowAgainNotificationAction = doNotShowAgainNotificationAction; @@ -58,15 +61,30 @@ public PromoteGoldBar( this.browserService = browserService; this.telemetryManager = telemetryManager; this.connectedModeUiManager = connectedModeUiManager; + this.activeConfigScopeTracker = activeConfigScopeTracker; this.activeSolutionBoundTracker.SolutionBindingChanged += OnActiveSolutionBindingChanged; } - public void PromoteConnectedMode(string languagesToPromote) + public void PromoteConnectedMode(string configurationScopeId, List languagesToPromote) { + var currentConfigScope = activeConfigScopeTracker.Current; + + if (currentConfigScope is null || currentConfigScope.Id != configurationScopeId) + { + Debug.Fail($"[Promote] Config scope miss match: {currentConfigScope} does not match {configurationScopeId}"); + return; + } + + if (activeSolutionBoundTracker.CurrentConfiguration.Mode == SonarLintMode.Connected) + { + Debug.Fail("Cannot promote extra language when already in connected"); + return; + } + var notification = new Notification( - id: "PromoteNotification", - message: string.Format(Resources.PromoteConnectedModeLanguagesMessage, languagesToPromote), + id: $"PromoteNotification.{string.Join(".", languagesToPromote.Select(x => x.Id))}", + message: string.Format(Resources.PromoteConnectedModeLanguagesMessage, string.Join(", ", languagesToPromote.Select(x => x.Name))), actions: [ new NotificationAction(Resources.PromoteBind, _ => OnBind(), false), diff --git a/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs index 63cdfd1c55..411a9b079b 100644 --- a/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/Implementation/PromoteListenerTests.cs @@ -28,31 +28,33 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation; [TestClass] public class PromoteListenerTests { - private IPromoteGoldBar promoteGoldBar; + private IPromoteNotification promoteNotification; private PromoteListener testSubject; [TestInitialize] public void TestInitialize() { - promoteGoldBar = Substitute.For(); - testSubject = new PromoteListener(promoteGoldBar); + promoteNotification = Substitute.For(); + testSubject = new PromoteListener(promoteNotification); } [TestMethod] public void MefCtor_CheckExports() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void PromoteExtraEnabledLanguagesInConnectedMode_DisplaysGoldBarWithCommaSeparatedLanguages() + public void PromoteExtraEnabledLanguagesInConnectedMode_DisplaysGoldBar() { - var parameters = new PromoteExtraEnabledLanguagesInConnectedModeParams("CONFIGURATION_SCOPE_ID", [Language.TSQL, Language.PLSQL]); + var parameters = new PromoteExtraEnabledLanguagesInConnectedModeParams("CONFIGURATION_SCOPE_ID", [Language.TSQL]); testSubject.PromoteExtraEnabledLanguagesInConnectedMode(parameters); - promoteGoldBar.Received().PromoteConnectedMode("TSQL, PLSQL"); + promoteNotification.Received().PromoteConnectedMode( + Arg.Is("CONFIGURATION_SCOPE_ID"), + Arg.Is>(x => x.Count == 1 && x[0] == VisualStudio.Core.Language.TSql)); } } diff --git a/src/SLCore.Listeners/Implementation/PromoteListener.cs b/src/SLCore.Listeners/Implementation/PromoteListener.cs index 35ba4e9d54..1d49adcc48 100644 --- a/src/SLCore.Listeners/Implementation/PromoteListener.cs +++ b/src/SLCore.Listeners/Implementation/PromoteListener.cs @@ -20,6 +20,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.ConnectedMode.Promote; +using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Promote; @@ -28,11 +29,13 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation; [Export(typeof(ISLCoreListener))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -public class PromoteListener(IPromoteGoldBar promoteGoldBar) : IPromoteListener +public class PromoteListener(IPromoteNotification promoteNotification) : IPromoteListener { public void PromoteExtraEnabledLanguagesInConnectedMode(PromoteExtraEnabledLanguagesInConnectedModeParams parameters) { - var languagesToPromote = string.Join(", ", parameters.languagesToPromote); - promoteGoldBar.PromoteConnectedMode(languagesToPromote); + var languagesToPromote = parameters.languagesToPromote + .Select(x => x.ConvertToCoreLanguage()) + .ToList(); + promoteNotification.PromoteConnectedMode(parameters.configurationScopeId, languagesToPromote); } }