diff --git a/src/Integration.TeamExplorer.UnitTests/SonarQubeNavigationItemTests.cs b/src/Integration.TeamExplorer.UnitTests/SonarQubeNavigationItemTests.cs index 1fe0e88c95..8b40acff75 100644 --- a/src/Integration.TeamExplorer.UnitTests/SonarQubeNavigationItemTests.cs +++ b/src/Integration.TeamExplorer.UnitTests/SonarQubeNavigationItemTests.cs @@ -20,7 +20,9 @@ using System; using FluentAssertions; +using Microsoft.TeamFoundation.Controls; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using SonarLint.VisualStudio.Integration.Resources; using SonarLint.VisualStudio.Integration.TeamExplorer; using SonarLint.VisualStudio.TestInfrastructure; @@ -30,29 +32,49 @@ namespace SonarLint.VisualStudio.Integration.UnitTests.TeamExplorer [TestClass] public class SonarQubeNavigationItemTests { + [TestMethod] + [Ignore] // Object is created successfully, but an exception when + // the MEF composition contained is disposed causing the test to fail + public void MefCtor_CheckIsExported() + => MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void CheckIsNonSharedMefComponent() + => MefTestHelpers.CheckIsNonSharedMefComponent(); + + [TestMethod] + public void MefCtor_DoesNotCallAnyServices() + { + var controller = new Mock(); + + _ = new SonarQubeNavigationItem(controller.Object); + + // The MEF constructor should be free-threaded, which it will be if + // it doesn't make any external calls. + controller.Invocations.Should().BeEmpty(); + } + [TestMethod] public void SonarQubeNavigationItem_Execute() { // Arrange - var controller = new ConfigurableTeamExplorerController(); + var controller = new Mock(); - var testSubject = new SonarQubeNavigationItem(controller); + var testSubject = CreateTestSubject(controller.Object); // Act testSubject.Execute(); // Assert - controller.ShowConnectionsPageCallsCount.Should().Be(1); + controller.Verify(x => x.ShowSonarQubePage(), Times.Once); } [TestMethod] public void SonarQubeNavigationItem_Ctor() { - // Arrange - var controller = new ConfigurableTeamExplorerController(); - - // Act - var testSubject = new SonarQubeNavigationItem(controller); + // Arrange & Act + var testSubject = CreateTestSubject(); // Assert testSubject.IsVisible.Should().BeTrue("Nav item should be visible"); @@ -67,5 +89,8 @@ public void SonarQubeNavigationItem_Ctor_NullArgChecks() { Exceptions.Expect(() => new SonarQubeNavigationItem(null)); } + + private static SonarQubeNavigationItem CreateTestSubject(ITeamExplorerController controller = null) + => new SonarQubeNavigationItem(controller ?? Mock.Of()); } } diff --git a/src/Integration.TeamExplorer/SonarQubeNavigationItem.cs b/src/Integration.TeamExplorer/SonarQubeNavigationItem.cs index 0a0ac27517..b121f2c3af 100644 --- a/src/Integration.TeamExplorer/SonarQubeNavigationItem.cs +++ b/src/Integration.TeamExplorer/SonarQubeNavigationItem.cs @@ -28,6 +28,7 @@ namespace SonarLint.VisualStudio.Integration.TeamExplorer { [TeamExplorerNavigationItem(SonarQubeNavigationItem.ItemId, SonarQubeNavigationItem.Priority, TargetPageId = SonarQubePage.PageId)] + [PartCreationPolicy(CreationPolicy.NonShared)] // The VS navigations in MS.TeamFoundation.TeamExplorer.Navigation are non-shared internal class SonarQubeNavigationItem : TeamExplorerNavigationItemBase { public const string ItemId = "172AF455-5F42-46FC-BFE6-23227A05806B"; @@ -49,6 +50,12 @@ internal SonarQubeNavigationItem([Import] ITeamExplorerController controller) this.IsVisible = true; this.IsEnabled = true; + // Note on threading: + // MEF constructors must be free-threaded i.e. capable of running to completion the calling thread. + // There's no public documentation on how these whether it's ok to create WPF artefacts like brushes + // and icons in the constructor. However, this is what the VS Team Explorer implementations of this + // class are doing, so we assume it's ok. + // See [VS installation directory]\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Microsoft.TeamFoundation.TeamExplorer.Navigation.dll var image = ResourceHelper.Get("SonarQubeServerIcon"); this.m_icon = image != null ? new DrawingBrush(image.Drawing) : null;