diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index df4d01ad..ad9ac972 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -189,6 +189,9 @@ 13.0.1 + + 3.13.1 + 1.0.0 diff --git a/FineCodeCoverageTests/AppOptionsProvider_Tests.cs b/FineCodeCoverageTests/AppOptionsProvider_Tests.cs index dce37834..8dabaec3 100644 --- a/FineCodeCoverageTests/AppOptionsProvider_Tests.cs +++ b/FineCodeCoverageTests/AppOptionsProvider_Tests.cs @@ -349,7 +349,8 @@ internal void Should_Use_Deseralized_String_From_Store_For_AppOption_Property(Fu {nameof(IAppOptions.ShowLineNewHighlighting),true }, {nameof(IAppOptions.ShowLineNotIncludedHighlighting),true }, {nameof(IAppOptions.UseEnterpriseFontsAndColors),true }, - {nameof(IAppOptions.EditorCoverageColouringMode), EditorCoverageColouringMode.UseRoslynWhenTextChanges } + {nameof(IAppOptions.EditorCoverageColouringMode), EditorCoverageColouringMode.UseRoslynWhenTextChanges }, + {nameof(IAppOptions.BlazorCoverageLinesFromGeneratedSource), true } }; var mockJsonConvertService = autoMocker.GetMock(); mockJsonConvertService.Setup( diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorCoverageContentType_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorCoverageContentType_Tests.cs new file mode 100644 index 00000000..b525ef65 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorCoverageContentType_Tests.cs @@ -0,0 +1,75 @@ +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Options; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class BlazorCoverageContentType_Tests + { + [TestCase("path.razor",false)] + [TestCase("path.cshtml", true)] + [TestCase("path.vbhtml", true)] + public void Should_Include_Razor_Component_Files(string filePath, bool expectedExclude) + { + Assert.That(new BlazorCoverageContentType(null, null).Exclude(filePath), Is.EqualTo(expectedExclude)); + } + + [Test] + public void Should_Line_Exclude_HtmlTags() + { + var lineExcluder = new BlazorCoverageContentType(null, null).LineExcluder; + Assert.True(lineExcluder.ExcludeIfNotCode("<")); + } + + [Test] + public void Should_Line_Exclude_Directives() + { + var lineExcluder = new BlazorCoverageContentType(null, null).LineExcluder; + Assert.True(lineExcluder.ExcludeIfNotCode("@")); + } + + [Test] + public void Should_Line_Exclude_Comments() + { + var lineExcluder = new BlazorCoverageContentType(null, null).LineExcluder; + Assert.True(lineExcluder.ExcludeIfNotCode("//")); + } + + [Test] + public void Should_Line_Exclude_Compiler_Directives() + { + var lineExcluder = new BlazorCoverageContentType(null, null).LineExcluder; + Assert.True(lineExcluder.ExcludeIfNotCode("#")); + } + + [Test] + public void Should_Not_UseFileCodeSpanRangeServiceForChanges() + { + Assert.False(new BlazorCoverageContentType(null, null).UseFileCodeSpanRangeServiceForChanges); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_CoverageFromFileCodeSpanRangeService_From_AppOptions(bool blazorCoverageLinesFromGeneratedSource) + { + var mockAppOptionsProvider = new Mock(); + mockAppOptionsProvider.Setup(a => a.Get()).Returns(new AppOptions { BlazorCoverageLinesFromGeneratedSource = blazorCoverageLinesFromGeneratedSource }); + Assert.That(new BlazorCoverageContentType(null, mockAppOptionsProvider.Object).CoverageOnlyFromFileCodeSpanRangeService, Is.EqualTo(blazorCoverageLinesFromGeneratedSource)); + } + + [Test] + public void Should_Use_BlazorFileCodeSpanRangeService() + { + var blazorFileCodeSpanRangeService = new Mock().Object; + Assert.That(blazorFileCodeSpanRangeService, Is.SameAs(new BlazorCoverageContentType(blazorFileCodeSpanRangeService, null).FileCodeSpanRangeService)); + } + + [Test] + public void Should_Be_For_The_Razor_ContentType() + { + Assert.That("Razor", Is.EqualTo(new BlazorCoverageContentType(null, null).ContentTypeName)); + } + + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorFileCodeSpanRangeService_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorFileCodeSpanRangeService_Tests.cs new file mode 100644 index 00000000..725f6e09 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorFileCodeSpanRangeService_Tests.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.Linq; +using AutoMoq; +using FineCodeCoverage.Core.Utilities.VsThreading; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Editor.DynamicCoverage.Utilities; +using FineCodeCoverage.Editor.Roslyn; +using FineCodeCoverageTests.TestHelpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class BlazorFileCodeSpanRangeService_Tests + { + [Test] + public void Should_Return_Null_If_Cannot_Find_Syntax_Root_Of_Generated_Document() + { + var mockTextSnapshot = new Mock(); + var mockTextBuffer = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.TextBuffer).Returns(mockTextBuffer.Object); + + var autoMoqer = new AutoMoqer(); + var razorGeneratedFilePathMatcher = autoMoqer.GetMock().Object; + autoMoqer.SetInstance(new TestThreadHelper()); + autoMoqer.GetMock().Setup(t => t.GetFilePath(mockTextBuffer.Object)).Returns("path"); + + var mockRazorGeneratedDocumentRootFinder = autoMoqer.GetMock(); + mockRazorGeneratedDocumentRootFinder.Setup( + razorGeneratedDocumentootFinder => razorGeneratedDocumentootFinder.FindSyntaxRootAsync(mockTextBuffer.Object, "path", razorGeneratedFilePathMatcher) + ).ReturnsAsync((SyntaxNode)null); + + var fileCodeSpanRanges = autoMoqer.Create().GetFileCodeSpanRanges(mockTextSnapshot.Object); + + Assert.IsNull(fileCodeSpanRanges); + mockRazorGeneratedDocumentRootFinder.VerifyAll(); + } + + [Test] + public void Should_Return_Null_If_Generated_Document_Has_No_Code_Nodes() + { + var mockTextSnapshot = new Mock(); + var mockTextBuffer = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.TextBuffer).Returns(mockTextBuffer.Object); + + var autoMoqer = new AutoMoqer(); + var mockCSharpCodeCoverageNodeVisitor = autoMoqer.GetMock(); + mockCSharpCodeCoverageNodeVisitor.Setup(cSharpCodeCoverageNodeVisitor => cSharpCodeCoverageNodeVisitor.GetNodes(It.IsAny())) + .Returns(new List()); + var razorGeneratedFilePathMatcher = autoMoqer.GetMock().Object; + autoMoqer.SetInstance(new TestThreadHelper()); + autoMoqer.GetMock().Setup(t => t.GetFilePath(mockTextBuffer.Object)).Returns("path"); + + var rootNode = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + var mockRazorGeneratedDocumentRootFinder = autoMoqer.GetMock(); + mockRazorGeneratedDocumentRootFinder.Setup( + razorGeneratedDocumentootFinder => razorGeneratedDocumentootFinder.FindSyntaxRootAsync(mockTextBuffer.Object, "path", razorGeneratedFilePathMatcher) + ).ReturnsAsync(rootNode); + + var fileCodeSpanRanges = autoMoqer.Create().GetFileCodeSpanRanges(mockTextSnapshot.Object); + + Assert.IsNull(fileCodeSpanRanges); + + mockCSharpCodeCoverageNodeVisitor.VerifyAll(); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Use_The_Generated_Coverage_Syntax_Nodes_Mapped_To_Razor_File_For_The_CodeSpanRange( + bool firstMapsBack + ) + { + var mockTextSnapshot = new Mock(); + var mockTextBuffer = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.TextBuffer).Returns(mockTextBuffer.Object); + + var autoMoqer = new AutoMoqer(); + var razorGeneratedFilePathMatcher = autoMoqer.GetMock().Object; + autoMoqer.SetInstance(new TestThreadHelper()); + autoMoqer.GetMock().Setup(t => t.GetFilePath(mockTextBuffer.Object)).Returns("path"); + + var mockRazorGeneratedDocumentRootFinder = autoMoqer.GetMock(); + SyntaxNode rootSyntaxNode = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + SyntaxNode codeCoverageNode1 = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + SyntaxNode codeCoverageNode2 = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + mockRazorGeneratedDocumentRootFinder.Setup( + razorGeneratedDocumentootFinder => razorGeneratedDocumentootFinder.FindSyntaxRootAsync(mockTextBuffer.Object, "path", razorGeneratedFilePathMatcher) + ).ReturnsAsync(rootSyntaxNode); + + var mockCSharpCodeCoverageNodeVisitor = autoMoqer.GetMock(); + mockCSharpCodeCoverageNodeVisitor.Setup(cSharpCodeCoverageNodeVisitor => cSharpCodeCoverageNodeVisitor.GetNodes(rootSyntaxNode)) + .Returns(new List { codeCoverageNode1, codeCoverageNode2 }); + var mockSyntaxNodeLocationMapper = autoMoqer.GetMock(); + + var linePositionSpan1 = new LinePositionSpan(new LinePosition(1, 1), new LinePosition(2, 1)); + var fileLinePositionSpan1 = new FileLinePositionSpan(firstMapsBack ? "path" : "",linePositionSpan1); + var linePositionSpan2 = new LinePositionSpan(new LinePosition(3, 1), new LinePosition(4, 1)); + var fileLinePositionSpan2 = new FileLinePositionSpan(firstMapsBack ? "" : "path", linePositionSpan2); + var expectedCodeSpanRange = firstMapsBack ? new CodeSpanRange(1, 2) : new CodeSpanRange(3, 4); + + mockSyntaxNodeLocationMapper.Setup(syntaxNodeLocationMapper => syntaxNodeLocationMapper.Map(codeCoverageNode1)) + .Returns(fileLinePositionSpan1); + mockSyntaxNodeLocationMapper.Setup(syntaxNodeLocationMapper => syntaxNodeLocationMapper.Map(codeCoverageNode2)) + .Returns(fileLinePositionSpan2); + + + var fileCodeSpanRanges = autoMoqer.Create().GetFileCodeSpanRanges(mockTextSnapshot.Object); + var fileCodeSpanRange = fileCodeSpanRanges.Single(); + + Assert.That(expectedCodeSpanRange, Is.EqualTo(fileCodeSpanRange)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorGeneratedFilePathMatcher_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorGeneratedFilePathMatcher_Tests.cs new file mode 100644 index 00000000..cdc52a9e --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/BlazorGeneratedFilePathMatcher_Tests.cs @@ -0,0 +1,21 @@ +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class BlazorGeneratedFilePathMatcher_Tests + { + [TestCase("razorpath","razorpath.",true)] + [TestCase("razorpath", "razorpathx.", false)] + public void Should_Be_Generated_If_File_Path_Starts_With_Razor_Path_And_Dot( + string razorFilePath, + string generatedFilePath, + bool expectedIsGenerated + ) + { + var isGenerated = new BlazorGeneratedFilePathMatcher().IsBlazorGeneratedFilePath(razorFilePath, generatedFilePath); + + Assert.That(expectedIsGenerated, Is.EqualTo(isGenerated)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs index 1175fdf0..51498c58 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs @@ -1,9 +1,9 @@ using AutoMoq; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Editor.DynamicCoverage; -using FineCodeCoverage.Editor.Tagging.Base; using FineCodeCoverage.Engine; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Impl; using FineCodeCoverage.Options; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace FineCodeCoverageTests.Editor.DynamicCoverage { @@ -27,8 +28,8 @@ internal class BufferLineCoverage_Tests private Mock mockTextBuffer; private Mock mockTextView; private ITextSnapshot textSnapshot; - private ITextInfo textInfo; - private Mock mockAppOptions; + private Mock mockTextInfo; + private readonly string filePath = "filepath"; private ILine CreateLine() { @@ -37,249 +38,470 @@ private ILine CreateLine() mockLine.SetupGet(line => line.CoverageType).Returns(CoverageType.Partial); return mockLine.Object; } + + private IAppOptions CreateAppOptions(bool off) + { + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode) + .Returns(off ? EditorCoverageColouringMode.Off : EditorCoverageColouringMode.UseRoslynWhenTextChanges); + return mockAppOptions.Object; + } + private void SetupEditorCoverageColouringMode(AutoMoqer autoMoqer,bool off = false) { - mockAppOptions = new Mock(); - mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode).Returns(off ? EditorCoverageColouringMode.Off : EditorCoverageColouringMode.UseRoslynWhenTextChanges); - autoMoqer.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); + autoMoqer.Setup(appOptionsProvider => appOptionsProvider.Get()) + .Returns(CreateAppOptions(off)); } - private void SimpleTextInfoSetUp(bool editorCoverageOff = false,string contentTypeName = "CSharp") + + private void MockTextInfo(Mock mockTextInfo) { - autoMoqer = new AutoMoqer(); - SetupEditorCoverageColouringMode(autoMoqer, editorCoverageOff); + this.mockTextInfo = mockTextInfo; mockTextView = new Mock(); mockTextBuffer = new Mock(); - mockTextBuffer.Setup(textBuffer => textBuffer.ContentType.TypeName).Returns(contentTypeName); mockTextSnapshot = new Mock(); textSnapshot = mockTextSnapshot.Object; mockTextBuffer.Setup(textBuffer => textBuffer.CurrentSnapshot).Returns(textSnapshot); - var mockTextInfo = new Mock(); mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer).Returns(mockTextBuffer.Object); mockTextInfo.SetupGet(textInfo => textInfo.TextView).Returns(mockTextView.Object); - mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("filepath"); - textInfo = mockTextInfo.Object; - autoMoqer.SetInstance(textInfo); + mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns(filePath); } - [TestCase("CSharp", Language.CSharp)] - [TestCase("Basic", Language.VB)] - [TestCase("C/C++", Language.CPP)] - public void Should_Create_Tracked_Lines_From_Existing_Coverage_Based_Upon_The_Content_Type_Language(string contentTypeName, Language expectedLanguage) + private void SimpleSetUp(bool editorCoverageOff) { - SimpleTextInfoSetUp(false,contentTypeName); + autoMoqer = new AutoMoqer(); + SetupEditorCoverageColouringMode(autoMoqer, editorCoverageOff); + MockTextInfo(autoMoqer.GetMock()); + } - var lines = new List { CreateLine()}; - var mockFileLineCoverage = autoMoqer.GetMock(); - mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines("filepath")).Returns(lines); + private void AddLastFileLineCoverage(Mock mockFileLineCoverage) + { + autoMoqer.GetMock() + .SetupGet(lastCoverage => lastCoverage.FileLineCoverage) + .Returns(mockFileLineCoverage.Object); + } + private BufferLineCoverage SetupNoInitialTrackedLines() + { + SimpleSetUp(false); var bufferLineCoverage = autoMoqer.Create(); - - autoMoqer.Verify(trackedLinesFactory => trackedLinesFactory.Create(lines, textSnapshot, expectedLanguage)); + Assert.Null(bufferLineCoverage.TrackedLines); + return bufferLineCoverage; } [Test] - public void Should_Not_Create_TrackedLines_If_EditorCoverageColouringMode_Is_Off() + public void Should_Add_Itself_As_A_Listener() { - SimpleTextInfoSetUp(true); + SimpleSetUp(true); var bufferLineCoverage = autoMoqer.Create(); - autoMoqer.Verify(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); + autoMoqer.Verify(eventAggregator => eventAggregator.AddListener(bufferLineCoverage, null)); } [Test] - public void Should_Not_Create_TrackedLines_From_NewCoverageLinesMessage_If_EditorCoverageColouringMode_Is_Off() + public void Should_Stop_Listening_When_TextView_Closed() { - SimpleTextInfoSetUp(true); + SimpleSetUp(true); var bufferLineCoverage = autoMoqer.Create(); + mockTextView.Setup(textView => textView.TextSnapshot.GetText()).Returns(""); + mockTextView.Raise(textView => textView.Closed += null, EventArgs.Empty); + + autoMoqer.Verify(eventAggregator => eventAggregator.RemoveListener(bufferLineCoverage)); + mockTextView.VerifyRemove(textView => textView.Closed -= It.IsAny(), Times.Once); + mockTextBuffer.VerifyRemove(textBuffer => textBuffer.Changed -= It.IsAny>(), Times.Once); + var mockAppOptionsProvider = autoMoqer.GetMock(); + mockAppOptionsProvider.VerifyRemove(appOptionsProvider => appOptionsProvider.OptionsChanged -= It.IsAny>(), Times.Once); + } + + [Test] + public void Should_Delegate_GetLines_To_Tracked_Lines() + { + SimpleSetUp(false); + var mockTrackedLines = new Mock(); + var dynamicLines = new List { new Mock().Object }; + mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(0, 5)).Returns(dynamicLines); - var newCoverageLinesMessage = new NewCoverageLinesMessage { CoverageLines = new Mock().Object }; - bufferLineCoverage.Handle(newCoverageLinesMessage); + var bufferLineCoverage = autoMoqer.Create(); + bufferLineCoverage.TrackedLines = mockTrackedLines.Object; - autoMoqer.Verify(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); + Assert.That(bufferLineCoverage.GetLines(0, 5), Is.SameAs(dynamicLines)); } [Test] - public void Should_Not_Throw_If_No_Initial_Coverage() + public void Should_Return_Empty_Enumerable_If_No_Tracked_Lines() { - SimpleTextInfoSetUp(); - - new BufferLineCoverage(null, textInfo, new Mock().Object, null, null,new Mock().Object); + SimpleSetUp(false); + + var bufferLineCoverage = autoMoqer.Create(); + + Assert.IsEmpty(bufferLineCoverage.GetLines(0, 5).ToList()); } + #region creating from existing coverage when file opens + [Test] - public void GetLines_Should_Delegate_To_TrackedLines() + public void Should_Create_TrackedLines_From_Serialized_Coverage_If_Present_And_Not_Out_Of_Date() { - SimpleTextInfoSetUp(); + SimpleSetUp(false); - var mockTrackedLines = new Mock(); - var lines = new List(); - mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(2, 5)).Returns(lines); + DateTime lastWriteDate = new DateTime(2024, 5, 8); + DateTime textExecutionStartingDate = new DateTime(2024, 5, 10); + DateTime serializedDate = new DateTime(2024, 5, 8); + + mockTextInfo.Setup(textInfo => textInfo.GetLastWriteTime()).Returns(lastWriteDate); + var mockLastCoverage = autoMoqer.GetMock(); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.TestExecutionStartingDate).Returns(textExecutionStartingDate); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.FileLineCoverage).Returns(new Mock().Object); + autoMoqer.Setup(dynamicCoverageStore => dynamicCoverageStore.GetSerializedCoverage(filePath)) + .Returns(new SerializedCoverageWhen { Serialized = "serialized", When = serializedDate }); + + var trackedLinesFromSerialized = new Mock().Object; autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) - .Returns(mockTrackedLines.Object); + trackedLinesFactory => trackedLinesFactory.Create("serialized", textSnapshot, filePath) + ).Returns(trackedLinesFromSerialized); var bufferLineCoverage = autoMoqer.Create(); - Assert.That(bufferLineCoverage.GetLines(2, 5), Is.SameAs(lines)); + Assert.That(bufferLineCoverage.TrackedLines, Is.SameAs(trackedLinesFromSerialized)); } [Test] - public void Should_Listen_For_Coverage_Changed() + public void Should_Not_Create_TrackedLines_When_Existing_Coverage_When_LastWriteTime_After_LastTestExecutionStarting_When_No_Serialized_Coverage() { - SimpleTextInfoSetUp(); + SimpleSetUp(false); + mockTextInfo.Setup(textInfo => textInfo.GetLastWriteTime()).Returns(new DateTime(2024, 5, 10)); + var mockLastCoverage = autoMoqer.GetMock(); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.TestExecutionStartingDate).Returns(new DateTime(2024, 5, 9)); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.FileLineCoverage).Returns(new Mock().Object); var bufferLineCoverage = autoMoqer.Create(); - autoMoqer.Verify(eventAggregator => eventAggregator.AddListener(bufferLineCoverage, null)); + Assert.IsNull(bufferLineCoverage.TrackedLines); + } + + private BufferLineCoverage SerializedCoverageisOutOfDate() + { + SimpleSetUp(false); + + DateTime lastWriteDate = new DateTime(2024, 5, 9); + DateTime textExecutionStartingDate = new DateTime(2024, 5, 10); + DateTime serializedDate = new DateTime(2024, 5, 8); + + mockTextInfo.Setup(textInfo => textInfo.GetLastWriteTime()).Returns(lastWriteDate); + var mockLastCoverage = autoMoqer.GetMock(); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.TestExecutionStartingDate).Returns(textExecutionStartingDate); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.FileLineCoverage).Returns(new Mock().Object); + + autoMoqer.Setup(dynamicCoverageStore => dynamicCoverageStore.GetSerializedCoverage(filePath)) + .Returns(new SerializedCoverageWhen { Serialized = "serialized", When = serializedDate }); + + return autoMoqer.Create(); } [Test] - public void Should_Have_Empty_Lines_When_Coverage_Cleared() + public void Should_Not_Create_TrackedLines_When_Existing_Coverage_When_Serialized_Coverage_Is_Out_Of_Date() { - SimpleTextInfoSetUp(); + var bufferLineCoverage = SerializedCoverageisOutOfDate(); + + Assert.IsNull(bufferLineCoverage.TrackedLines); + } + + [Test] + public void Should_Log_When_Not_Creating_TrackedLines_As_Out_Of_Date() + { + SerializedCoverageisOutOfDate(); + + autoMoqer.Verify(logger => logger.Log($"Not creating editor marks for {filePath} as coverage is out of date")); + } + [Test] + public void Should_Create_TrackedLines_When_No_Serialized_Coverage_And_Not_Out_Of_Date() + { + SimpleSetUp(false); + + DateTime lastWriteDate = new DateTime(2024, 5, 9); + DateTime textExecutionStartingDate = new DateTime(2024, 5, 10); + + mockTextInfo.Setup(textInfo => textInfo.GetLastWriteTime()).Returns(lastWriteDate); + var mockLastCoverage = autoMoqer.GetMock(); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.TestExecutionStartingDate).Returns(textExecutionStartingDate); + var mockFileLineCoverage = new Mock(); + mockLastCoverage.SetupGet(lastCoverage => lastCoverage.FileLineCoverage).Returns(mockFileLineCoverage.Object); + var lines = new List { new Mock().Object }; + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines(filePath)).Returns(lines); + var trackedLinesFromSerialized = new Mock().Object; autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) - .Returns(new Mock().Object); + trackedLinesFactory => trackedLinesFactory.Create(lines, textSnapshot, filePath) + ).Returns(trackedLinesFromSerialized); + + var bufferLineCoverage = autoMoqer.Create(); + + Assert.AreSame(bufferLineCoverage.TrackedLines, trackedLinesFromSerialized); + } + + [Test] + public void Should_Log_If_Exception_Creating_TrackedLines() + { + var exception = new Exception("exception"); + + SimpleSetUp(false); + + var mockFileLineCoverage = new Mock(); + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines(filePath)).Throws(exception); + AddLastFileLineCoverage(mockFileLineCoverage); + + var bufferLineCoverage = autoMoqer.Create(); + + autoMoqer.Verify(logger => logger.Log($"Error creating tracked lines for {filePath}", exception)); + + } + + [Test] + public void Should_Not_Create_TrackedLines_If_EditorCoverageColouringMode_Is_Off() + { + SimpleSetUp(true); + + var lines = new List { CreateLine() }; + var mockFileLineCoverage = new Mock(); + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines(filePath)).Returns(lines); + AddLastFileLineCoverage(mockFileLineCoverage); + + var trackedLines = new Mock().Object; + var mockTrackedLinesFactory = autoMoqer.GetMock(); + mockTrackedLinesFactory.Setup(trackedLinesFactory => trackedLinesFactory.Create(lines, textSnapshot, filePath)) + .Returns(trackedLines); + + var bufferLineCoverage = autoMoqer.Create(); + Assert.IsNull(bufferLineCoverage.TrackedLines); + } + + [Test] + public void Should_Have_Null_TrackedLines_If_No_Initial_Coverage() + { + + MockTextInfo(new Mock()); + var bufferLineCoverage = new BufferLineCoverage( + null, + mockTextInfo.Object, + new Mock().Object, + null, + null, + new Mock().Object, + null + ); + + Assert.IsNull(bufferLineCoverage.TrackedLines); + } + + + + #endregion + + #region NewCoverageLinesMessge + + [Test] + public void Should_Have_Null_TrackedLines_After_Sending_CoverageChangedMessage_When_Coverage_Cleared() + { + SimpleSetUp(false); var bufferLineCoverage = autoMoqer.Create(); + bufferLineCoverage.TrackedLines = new Mock().Object; + + var mockEventAggregator = autoMoqer.GetMock(); + mockEventAggregator.Setup(eventAggregator => eventAggregator.SendMessage( + new CoverageChangedMessage(bufferLineCoverage,filePath,null), + null + )).Callback(() => Assert.IsNull(bufferLineCoverage.TrackedLines)); bufferLineCoverage.Handle(new NewCoverageLinesMessage()); - var lines = bufferLineCoverage.GetLines(2, 5); - Assert.That(lines, Is.Empty); + mockEventAggregator.VerifyAll(); + } + + [Test] + public void Should_Not_Send_CoverageChangedMessage_For_Coverage_Clearing_If_Not_Tracking() + { + SimpleSetUp(false); + var bufferLineCoverage = autoMoqer.Create(); + bufferLineCoverage.TrackedLines = null; + + bufferLineCoverage.Handle(new NewCoverageLinesMessage()); + + autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage( + It.IsAny(), + null + ),Times.Never()); + } [TestCase(true)] [TestCase(false)] - public void Should_Create_New_TextLines_When_Coverage_Changed_And_Not_Off_In_AppOptions(bool off) - { - var autoMoqer = new AutoMoqer(); - SetupEditorCoverageColouringMode(autoMoqer, off); - var mockTextBuffer = new Mock(); - mockTextBuffer.Setup(textBuffer => textBuffer.ContentType.TypeName).Returns("CSharp"); - var mockCurrentSnapshot = new Mock(); - mockCurrentSnapshot.SetupGet(snapshot => snapshot.LineCount).Returns(10); - mockTextBuffer.SetupSequence(textBuffer => textBuffer.CurrentSnapshot) - .Returns(new Mock().Object) - .Returns(mockCurrentSnapshot.Object); - var mockTextDocument = new Mock(); - mockTextDocument.SetupGet(textDocument => textDocument.FilePath).Returns("filepath"); - var mockTextInfo = new Mock(); - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer).Returns(mockTextBuffer.Object); - mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("filepath"); - mockTextInfo.SetupGet(textInfo => textInfo.TextView).Returns(new Mock().Object); - autoMoqer.SetInstance(mockTextInfo.Object); + public void Should_Create_New_TextLines_From_New_Coverage_And_Not_Off_In_AppOptions(bool off) + { + // no initial tracked lines + SimpleSetUp(true); + var bufferLineCoverage = autoMoqer.Create(); - var lines = new List { CreateLine()}; - var mockNewFileLineCoverage = autoMoqer.GetMock(); - mockNewFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines("filepath")).Returns(lines); + if (!off) + { + var appOptionsOn = CreateAppOptions(false); + autoMoqer.GetMock() + .Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, appOptionsOn); + } - var mockTrackedLines = new Mock(); - var dynamicLines = new List(); - mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(2, 5)).Returns(dynamicLines); + var newCoverageLines = new List { CreateLine() }; autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(lines, mockCurrentSnapshot.Object, Language.CSharp) - ).Returns(mockTrackedLines.Object); + trackedLinesFactory => trackedLinesFactory.Create(newCoverageLines, textSnapshot, filePath) + ).Returns(new Mock().Object); + var mockFileLineCoverage = new Mock(); + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines(filePath)).Returns(newCoverageLines); + (bufferLineCoverage as IListener).Handle( + new NewCoverageLinesMessage { CoverageLines = mockFileLineCoverage.Object }); - var bufferLineCoverage = autoMoqer.Create(); - - bufferLineCoverage.Handle(new NewCoverageLinesMessage { CoverageLines = mockNewFileLineCoverage.Object }); - var bufferLines = bufferLineCoverage.GetLines(2, 5); + if (off) { - Assert.That(bufferLines, Is.Empty); + Assert.That(bufferLineCoverage.TrackedLines, Is.Null); } else { - Assert.That(bufferLines, Is.SameAs(dynamicLines)); + autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage( + new CoverageChangedMessage(bufferLineCoverage,filePath, null), null), Times.Once()); + Assert.That(bufferLineCoverage.TrackedLines, Is.Not.Null); } } - [TestCase(true,true,true,true)] - [TestCase(true, false, true, true)] - [TestCase(false, true, true, true)] - [TestCase(true, false, false, true)] - [TestCase(false, false, false, false)] - public void Should_Send_CoverageChangedMessage_When_Necessary( - bool initialTrackedLines, - bool nextTrackedLines, - bool hasCoverageLines, - bool expectedSends) + [TestCase(true)] + [TestCase(false)] + public void Should_Not_Create_TrackedLines_From_NewCoverageLinesMessage_If_Text_Changed_Since_TestExecutionStartingMessage( + bool textChangedSinceTestExecutionStarting + ) { - SimpleTextInfoSetUp(); - var trackedLines = new Mock().Object; + var bufferLineCoverage = SetupNoInitialTrackedLines(); + var mockTrackedLinesFactory = autoMoqer.GetMock(); - mockTrackedLinesFactory.SetupSequence(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) - .Returns(initialTrackedLines ? trackedLines : null) - .Returns(nextTrackedLines ? trackedLines : null); - var bufferLineCoverage = autoMoqer.Create(); + mockTrackedLinesFactory.Setup(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), textSnapshot, filePath)) + .Returns(new Mock().Object); - var newCoverageLinesMessage = new NewCoverageLinesMessage(); - if(hasCoverageLines) + + void RaiseChangedOnBackground() + { + mockTextBuffer.Raise(textBuffer => textBuffer.ChangedOnBackground += null, new TextContentChangedEventArgs(new Mock().Object, new Mock().Object, new EditOptions(), null)); + } + void RaiseTestExecutionStartingMessage() { - newCoverageLinesMessage.CoverageLines = new Mock().Object; + (bufferLineCoverage as IListener).Handle(new TestExecutionStartingMessage()); } - bufferLineCoverage.Handle(newCoverageLinesMessage); + - autoMoqer.Verify( - eventAggregator => eventAggregator.SendMessage( - It.Is(message => message.AppliesTo == "filepath" && message.BufferLineCoverage == bufferLineCoverage) - , null - ), expectedSends ? Times.Once() : Times.Never()); + if (textChangedSinceTestExecutionStarting) + { + RaiseTestExecutionStartingMessage(); + Thread.Sleep(1); + RaiseChangedOnBackground(); + } else + { + RaiseChangedOnBackground(); + Thread.Sleep(1); + RaiseTestExecutionStartingMessage(); + } + + bufferLineCoverage.Handle(new NewCoverageLinesMessage { CoverageLines = new Mock().Object }); + + + Assert.That(bufferLineCoverage.TrackedLines, textChangedSinceTestExecutionStarting ? Is.Null : Is.Not.Null); + + var expectCoverageChangedMessage = textChangedSinceTestExecutionStarting ? false : true; + autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage( + It.IsAny(), null), + Times.Exactly(expectCoverageChangedMessage ? 1 : 0)); } - [TestCase(true)] - [TestCase(false)] - public void Should_Update_TrackedLines_When_Text_Buffer_ChangedOnBackground(bool linesInRange) + [Test] + public void Should_Log_When_Text_Changed_Since_TestExecutionStartingMessage() { - SimpleTextInfoSetUp(); + var bufferLineCoverage = SetupNoInitialTrackedLines(); - var mockAfterSnapshot = new Mock(); - mockAfterSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(linesInRange ? 100 : 11); + (bufferLineCoverage as IListener).Handle(new TestExecutionStartingMessage()); + Thread.Sleep(1); + mockTextBuffer.Raise(textBuffer => textBuffer.ChangedOnBackground += null, new TextContentChangedEventArgs(new Mock().Object, new Mock().Object, new EditOptions(), null)); - var newSpan = new Span(1, 2); - var mockTrackedLines = new Mock(); - var changedLineNumbers = new List { 11, 12 }; - mockTrackedLines.Setup(trackedLines => trackedLines.GetChangedLineNumbers(mockAfterSnapshot.Object, new List { newSpan })) - .Returns(changedLineNumbers); - autoMoqer.Setup(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) - .Returns(mockTrackedLines.Object); + bufferLineCoverage.Handle(new NewCoverageLinesMessage { CoverageLines = new Mock().Object }); + + autoMoqer.Verify(ILogger => ILogger.Log($"Not creating editor marks for {filePath} as it was changed after test execution started")); + } + #endregion + + + #region updating when text changes on background + [TestCase(10, new int[] { -1, 9, 10 }, new int[] { 9 })] + [TestCase(10, new int[] { 9, 10 }, new int[] {})] + public void Should_Update_TrackedLines_When_Text_Buffer_ChangedOnBackground_And_Send_CoverageChangedMessage_If_Any_Changed_Within_Snapshot( + int afterLineCount, + int[] changedLineNumbers, + int[] expectedMessageChangedLineNumbers + ) + { + SimpleSetUp(false); + var mockAfterSnapshot = new Mock(); + mockAfterSnapshot.SetupGet(afterSnapshot => afterSnapshot.LineCount).Returns(afterLineCount); var bufferLineCoverage = autoMoqer.Create(); + var mockTrackedLines = new Mock(); + var newSpan = new Span(0, 1); + mockTrackedLines.Setup( + trackedLines => trackedLines.GetChangedLineNumbers(mockAfterSnapshot.Object, new List { newSpan}) + ).Returns(changedLineNumbers); + bufferLineCoverage.TrackedLines = mockTrackedLines.Object; + mockTextBuffer.Raise(textBuffer => textBuffer.ChangedOnBackground += null, CreateTextContentChangedEventArgs(mockAfterSnapshot.Object, newSpan)); autoMoqer.Verify( - eventAggregator => eventAggregator.SendMessage( - It.Is(message => message.AppliesTo == "filepath" && message.BufferLineCoverage == bufferLineCoverage && message.ChangedLineNumbers.SequenceEqual(changedLineNumbers)) - , null - ), Times.Exactly(linesInRange ? 1 : 0)); + eventAggregator => eventAggregator.SendMessage( + It.Is(message => + message.AppliesTo == filePath && + message.BufferLineCoverage == bufferLineCoverage && + message.ChangedLineNumbers.SequenceEqual(expectedMessageChangedLineNumbers)) + , null + ), expectedMessageChangedLineNumbers.Length > 0 ? Times.Once() : Times.Never()); } [Test] - public void Should_Not_Throw_When_Text_Buffer_Changed_And_No_Coverage() + public void Should_Log_When_Exception_Updating_TrackedLines_When_Text_Buffer_ChangedOnBackground() { - SimpleTextInfoSetUp(); + SimpleSetUp(false); + + var bufferLineCoverage = autoMoqer.Create(); - autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) - .Returns(new Mock().Object); + var exception = new Exception("message"); + var mockTrackedLines = new Mock(); + mockTrackedLines.Setup( + trackedLines => trackedLines.GetChangedLineNumbers(It.IsAny(), It.IsAny>()) + ).Throws(exception); + bufferLineCoverage.TrackedLines = mockTrackedLines.Object; - var bufferLineCoverage = autoMoqer.Create(); + mockTextBuffer.Raise(textBuffer => textBuffer.ChangedOnBackground += null, CreateTextContentChangedEventArgs(new Mock().Object, new Span(0,1))); - // clear coverage - bufferLineCoverage.Handle(new NewCoverageLinesMessage()); + autoMoqer.Verify(logger => logger.Log($"Error updating tracked lines for {filePath}", exception)); + } + + [Test] + public void Should_Not_Throw_When_Text_Buffer_Changed_And_No_Coverage() + { + SimpleSetUp(true); + + var bufferLineCoverage = autoMoqer.Create(); + bufferLineCoverage.TrackedLines = null; mockTextBuffer.Raise(textBuffer => textBuffer.Changed += null, CreateTextContentChangedEventArgs(new Mock().Object, new Span(0, 0))); } - + #endregion private TextContentChangedEventArgs CreateTextContentChangedEventArgs(ITextSnapshot afterSnapshot, params Span[] newSpans) { var normalizedTextChangeCollection = new NormalizedTextChangeCollection(); @@ -295,154 +517,76 @@ private TextContentChangedEventArgs CreateTextContentChangedEventArgs(ITextSnaps return new TextContentChangedEventArgs(mockBeforeSnapshot.Object, afterSnapshot, EditOptions.None, null); } - [Test] - public void Should_Stop_Listening_When_TextView_Closed() + #region saving serialized coverage + [TestCase(true)] + [TestCase(false)] + public void Should_SaveSerializedCoverage_When_TextView_Closed_And_Tracking_And_File_System_Reflecting_TrackedLines( + bool fileSystemReflectsTrackedLines + ) { - SimpleTextInfoSetUp(); - + SimpleSetUp(false); var bufferLineCoverage = autoMoqer.Create(); + bufferLineCoverage.TrackedLines = new Mock().Object; - mockTextView.Raise(textView => textView.Closed += null, EventArgs.Empty); - - autoMoqer.Verify(eventAggregator => eventAggregator.RemoveListener(bufferLineCoverage)); - mockTextView.VerifyRemove(textView => textView.Closed -= It.IsAny(), Times.Once); - mockTextBuffer.VerifyRemove(textBuffer => textBuffer.Changed -= It.IsAny>(), Times.Once); - var mockAppOptionsProvider = autoMoqer.GetMock(); - mockAppOptionsProvider.VerifyRemove(appOptionsProvider => appOptionsProvider.OptionsChanged -= It.IsAny>(), Times.Once); - } - - [Test] - public void Should_SaveSerializedCoverage_When_TextView_Closed_And_There_Has_Been_Coverage() - { - var autoMoqer = new AutoMoqer(); - SetupEditorCoverageColouringMode(autoMoqer); - var mockTextInfo = autoMoqer.GetMock(); - mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("filepath"); - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer.ContentType.TypeName).Returns("contenttypename"); - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer.CurrentSnapshot).Returns(new Mock().Object); - var mockTextView = new Mock(); - mockTextInfo.SetupGet(textInfo => textInfo.TextView).Returns(mockTextView.Object); - autoMoqer.Setup>( - fileLineCoverage => fileLineCoverage.GetLines(It.IsAny(), It.IsAny(), It.IsAny()) - ).Returns(new List { }); - var trackedLines = new Mock().Object; - autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()) - ).Returns(trackedLines); + var snapshotText = "snapshot text"; autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Serialize(trackedLines) + trackedLinesFactory => trackedLinesFactory.Serialize(bufferLineCoverage.TrackedLines, snapshotText) ).Returns("serialized"); - autoMoqer.Create(); - + mockTextInfo.Setup(textInfo => textInfo.GetFileText()) + .Returns(fileSystemReflectsTrackedLines ? snapshotText : "changes not saved"); + + var mockTextViewCurrentSnapshot = new Mock(); + mockTextViewCurrentSnapshot.Setup(snapshot => snapshot.GetText()).Returns(snapshotText); + mockTextView.Setup(textView => textView.TextSnapshot).Returns(mockTextViewCurrentSnapshot.Object); + mockTextView.Raise(textView => textView.Closed += null, EventArgs.Empty); - autoMoqer.Verify(dynamicCoverageStore => dynamicCoverageStore.SaveSerializedCoverage("filepath", "serialized")); + autoMoqer.Verify( + dynamicCoverageStore => dynamicCoverageStore.SaveSerializedCoverage(filePath, "serialized"), + fileSystemReflectsTrackedLines ? Times.Once() : Times.Never()); + } [Test] public void Should_Remove_Serialized_Coverage_When_TextView_Closed_And_No_TrackedLines() { - var autoMoqer = new AutoMoqer(); - SetupEditorCoverageColouringMode(autoMoqer); - var mockTextInfo = autoMoqer.GetMock(); - mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("filepath"); - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer.ContentType.TypeName).Returns("contenttypename"); - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer.CurrentSnapshot).Returns(new Mock().Object); - var mockTextView = new Mock(); - mockTextInfo.SetupGet(textInfo => textInfo.TextView).Returns(mockTextView.Object); - autoMoqer.Setup>( - fileLineCoverage => fileLineCoverage.GetLines(It.IsAny(), It.IsAny(), It.IsAny()) - ).Returns(new List { }); - var trackedLines = new Mock().Object; - autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()) - ).Returns(trackedLines); - autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Serialize(trackedLines) - ).Returns("serialized"); - + SimpleSetUp(false); var bufferLineCoverage = autoMoqer.Create(); - // clear coverage - bufferLineCoverage.Handle(new NewCoverageLinesMessage()); + bufferLineCoverage.TrackedLines = null; mockTextView.Raise(textView => textView.Closed += null, EventArgs.Empty); - autoMoqer.Verify(dynamicCoverageStore => dynamicCoverageStore.RemoveSerializedCoverage("filepath")); + autoMoqer.Verify(dynamicCoverageStore => dynamicCoverageStore.RemoveSerializedCoverage(filePath)); } + #endregion - [TestCase(true, "CSharp", Language.CSharp)] - [TestCase(true, "Basic", Language.VB)] - [TestCase(true, "C/C++", Language.CPP)] - [TestCase(false,"",Language.CPP)] - public void Should_Create_From_Serialized_Coverage_If_Present(bool hasSerialized,string contentTypeLanguage, Language expectedLanguage) - { - var autoMoqer = new AutoMoqer(); - SetupEditorCoverageColouringMode(autoMoqer); - var mockTextInfo = autoMoqer.GetMock(); - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer.ContentType.TypeName).Returns(contentTypeLanguage); - mockTextInfo.SetupGet(textInfo => textInfo.TextView).Returns(new Mock().Object); - var currentTextSnaphot = new Mock().Object; - mockTextInfo.SetupGet(textInfo => textInfo.TextBuffer.CurrentSnapshot).Returns(currentTextSnaphot); - mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("filepath"); - autoMoqer.Setup(dynamicCoverageStore => dynamicCoverageStore.GetSerializedCoverage("filepath")) - .Returns(hasSerialized ? "serialized" : null); - - var mockTrackedLinesNoSerialized = new Mock(); - autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()) - ).Returns(mockTrackedLinesNoSerialized.Object); - - var mockTrackedLinesFromSerialized = new Mock(); - autoMoqer.Setup( - trackedLinesFactory => trackedLinesFactory.Create("serialized", currentTextSnaphot, expectedLanguage) - ).Returns(mockTrackedLinesFromSerialized.Object); - - - var bufferLineCoverage = autoMoqer.Create(); - - bufferLineCoverage.GetLines(2, 5); - - var expectedMockTrackedLines = hasSerialized ? mockTrackedLinesFromSerialized : mockTrackedLinesNoSerialized; - expectedMockTrackedLines.Verify(trackedLines => trackedLines.GetLines(2, 5), Times.Once); - } + #region app options turn off editor coverage [Test] public void Should_Remove_TrackedLines_When_AppOptions_Changed_And_EditorCoverageColouringMode_Is_Off() { - SimpleTextInfoSetUp(); - var mockTrackedLines = new Mock(); - mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(It.IsAny(), It.IsAny())) - .Returns(new List { new Mock().Object }); - autoMoqer.Setup( - trackedCoverageLinesFactory => trackedCoverageLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()) - ).Returns(mockTrackedLines.Object); - + SimpleSetUp(false); var bufferLineCoverage = autoMoqer.Create(); - Assert.That(bufferLineCoverage.GetLines(2, 5).Count(), Is.EqualTo(1)); + bufferLineCoverage.TrackedLines = new Mock().Object; + + var mockEventAggregator = autoMoqer.GetMock(); + mockEventAggregator.Setup(eventAggregator => eventAggregator.SendMessage(new CoverageChangedMessage(bufferLineCoverage, filePath, null), null)) + .Callback(() => Assert.IsNull(bufferLineCoverage.TrackedLines)); var mockAppOptions = new Mock(); mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode).Returns(EditorCoverageColouringMode.Off); autoMoqer.GetMock().Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, mockAppOptions.Object); - autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage(It.IsAny(), null)); - - Assert.That(bufferLineCoverage.GetLines(2, 5), Is.Empty); + mockEventAggregator.VerifyAll(); } [Test] public void Should_Not_Remove_TrackedLines_When_AppOptions_Changed_And_EditorCoverageColouringMode_Is_Not_Off() { - SimpleTextInfoSetUp(); - var mockTrackedLines = new Mock(); - mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(It.IsAny(), It.IsAny())) - .Returns(new List { new Mock().Object }); - autoMoqer.Setup( - trackedCoverageLinesFactory => trackedCoverageLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()) - ).Returns(mockTrackedLines.Object); - + SimpleSetUp(false); var bufferLineCoverage = autoMoqer.Create(); - Assert.That(bufferLineCoverage.GetLines(2, 5).Count(), Is.EqualTo(1)); + bufferLineCoverage.TrackedLines = new Mock().Object; var mockAppOptions = new Mock(); mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode).Returns(EditorCoverageColouringMode.DoNotUseRoslynWhenTextChanges); @@ -450,8 +594,12 @@ public void Should_Not_Remove_TrackedLines_When_AppOptions_Changed_And_EditorCov autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage(It.IsAny(), null), Times.Never()); - Assert.That(bufferLineCoverage.GetLines(2, 5).Count(), Is.EqualTo(1)); + Assert.IsNotNull(bufferLineCoverage.TrackedLines); } - + + // todo - should also test that uses the changed value + + #endregion + } } diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/CPPCoverageContentType_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/CPPCoverageContentType_Tests.cs new file mode 100644 index 00000000..87ff2836 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/CPPCoverageContentType_Tests.cs @@ -0,0 +1,26 @@ +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class CPPCoverageContentType_Tests + { + [Test] + public void Should_Have_Null_LineExcluder() + { + Assert.That(new CPPCoverageContentType().LineExcluder, Is.Null); + } + + [Test] + public void Should_Have_Null_FileCodeSpanRangeService() + { + Assert.That(new CPPCoverageContentType().FileCodeSpanRangeService, Is.Null); + } + + [Test] + public void Should_Have_CPP_ContentType() + { + Assert.That(new CPPCoverageContentType().ContentTypeName, Is.EqualTo("C/C++")); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/CSharpCoverageContentType_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/CSharpCoverageContentType_Tests.cs new file mode 100644 index 00000000..5fc44473 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/CSharpCoverageContentType_Tests.cs @@ -0,0 +1,59 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class CSharpCoverageContentType_Tests + { + [Test] + public void Should_Have_CSharp_Content_Type() + { + Assert.That(new CSharpCoverageContentType(null).ContentTypeName, Is.EqualTo("CSharp")); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Delegate_To_IRoslynFileCodeSpanRangeService(bool roslynUseFileCodeSpanRangeServiceForChanges) + { + var fileCodeSpanRangeServiceFromRoslyn = new Mock().Object; + var mockRoslynFileCodeSpanRangeService = new Mock(); + mockRoslynFileCodeSpanRangeService.SetupGet(roslynFileCodeSpanRangeService => roslynFileCodeSpanRangeService.FileCodeSpanRangeService) + .Returns(fileCodeSpanRangeServiceFromRoslyn); + + mockRoslynFileCodeSpanRangeService.SetupGet(roslynFileCodeSpanRangeService => roslynFileCodeSpanRangeService.UseFileCodeSpanRangeServiceForChanges) + .Returns(roslynUseFileCodeSpanRangeServiceForChanges); + + var cSharpCoverageContentType = new CSharpCoverageContentType(mockRoslynFileCodeSpanRangeService.Object); + + Assert.That(cSharpCoverageContentType.FileCodeSpanRangeService, Is.SameAs(fileCodeSpanRangeServiceFromRoslyn)); + Assert.That(cSharpCoverageContentType.UseFileCodeSpanRangeServiceForChanges, Is.EqualTo(roslynUseFileCodeSpanRangeServiceForChanges)); + } + + [Test] + public void Should_Allow_For_ILine_Missed_By_Roslyn() + { + Assert.False(new CSharpCoverageContentType(null).CoverageOnlyFromFileCodeSpanRangeService); + } + + [Test] + public void Should_LineExclude_Comments() + { + Assert.True(new CSharpCoverageContentType(null).LineExcluder.ExcludeIfNotCode("//")); + } + + [Test] + public void Should_LineExclude_Usings() + { + Assert.True(new CSharpCoverageContentType(null).LineExcluder.ExcludeIfNotCode("using")); + } + + [Test] + public void Should_LineExclude_Compiler_Directives() + { + Assert.True(new CSharpCoverageContentType(null).LineExcluder.ExcludeIfNotCode("#")); + } + + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs index 6d8a7b29..5a5a55f2 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs @@ -1,13 +1,9 @@ using AutoMoq; using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Core.Utilities.VsThreading; using FineCodeCoverage.Editor.DynamicCoverage; -using FineCodeCoverage.Editor.Roslyn; -using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction; using FineCodeCoverage.Engine.Model; -using FineCodeCoverage.Options; using FineCodeCoverageTests.TestHelpers; -using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Moq; using NUnit.Framework; @@ -20,6 +16,9 @@ namespace FineCodeCoverageTests.Editor.DynamicCoverage { class Line : ILine { + public Line(int number):this(number, CoverageType.Covered) + { + } public Line(int number, CoverageType coverageType) { Number = number; @@ -52,729 +51,1068 @@ public static CodeSpanRange CodeSpanRangeFromLine(ILine line) return CodeSpanRange.SingleLine(line.Number - 1); } } - internal class ContainingCodeTrackedLinesBuilder_CPP_Tests + + internal class ContainingCodeTrackedLinesBuilder_Tests { - [Test] - public void Should_Create_ContainingCodeTracker_For_Each_Line_When_CPP() + [TestCase(true)] + [TestCase(false)] + public void Should_Have_NewCodeTracker_When_CoverageContentType_Has_LineExcluder(bool hasLineExcluder) { - + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName).Returns("contenttypename"); + var autoMoqer = new AutoMoqer(); + var lineExcluder = new Mock().Object; + var newCodeTracker = new Mock().Object; + var mockNewCodeTrackerFactory = autoMoqer.GetMock(); + mockNewCodeTrackerFactory.Setup(newCodeTrackerFactory => newCodeTrackerFactory.Create(lineExcluder)).Returns(newCodeTracker); - var textSnapshot = new Mock().Object; - var lines = new List - { - new Mock().Object, - new Mock().Object - }; - var containingCodeTrackers = new List - { - new Mock().Object, - new Mock().Object - }; - var firstLine = lines[0]; - var firstCodeSpanRange = TestHelper.CodeSpanRangeFromLine(firstLine); - var secondLine = lines[1]; - var secondCodeSpanRange = TestHelper.CodeSpanRangeFromLine(secondLine); - var mockContainingCodeTrackerFactory = autoMoqer.GetMock(); - mockContainingCodeTrackerFactory.Setup(containingCodeTrackerFactory => - containingCodeTrackerFactory.CreateCoverageLines(textSnapshot, new List { firstLine }, firstCodeSpanRange, SpanTrackingMode.EdgeExclusive) - ).Returns(containingCodeTrackers[0]); - mockContainingCodeTrackerFactory.Setup(containingCodeTrackerFactory => - containingCodeTrackerFactory.CreateCoverageLines(textSnapshot, new List { secondLine }, secondCodeSpanRange, SpanTrackingMode.EdgeExclusive) - ).Returns(containingCodeTrackers[1]); - - var expectedTrackedLines = new TrackedLines(null, null, null); var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); - mockContainingCodeTrackedLinesFactory.Setup(containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create(containingCodeTrackers, null, null) - ).Returns(expectedTrackedLines); + var trackedLinesFromFactory = new Mock().Object; + + mockContainingCodeTrackedLinesFactory.Setup(containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + It.IsAny>(), + hasLineExcluder ? newCodeTracker : null, + It.IsAny() + )).Returns(trackedLinesFromFactory); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.Setup(coverageContentType => coverageContentType.ContentTypeName).Returns("contenttypename"); + if(hasLineExcluder) + { + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.LineExcluder).Returns(lineExcluder); + } + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - var trackedLines = containingCodeTrackedLinesBuilder.Create(lines, textSnapshot, Language.CPP); - Assert.That(trackedLines, Is.EqualTo(expectedTrackedLines)); + containingCodeTrackedLinesBuilder.Create(new List {}, mockTextSnapshot.Object,""); + mockContainingCodeTrackedLinesFactory.VerifyAll(); } - [Test] - public void Should_Use_CPP_Deserialized_When_CodeSpanRange_Within_Total_Lines() + [TestCase(true)] + [TestCase(false)] + public void Should_Use_CoverageContentType_FileCodeSpanRangeService_When_UseFileCodeSpanRangeServiceForChanges(bool useFileCodeSpanRangeServiceForChanges) { var mockTextSnapshot = new Mock(); - mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(40); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName).Returns("contenttypename"); + var autoMoqer = new AutoMoqer(); - var mockCodeSpanRangeContainingCodeTrackerFactory = autoMoqer.GetMock(); - var coverageLineTracker = new Mock().Object; - mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( - mockTextSnapshot.Object, - new List { new Line(1, CoverageType.Covered) }, - new CodeSpanRange(10, 20), - SpanTrackingMode.EdgeExclusive - ) - ).Returns(coverageLineTracker); - var dirtyLineTracker = new Mock().Object; - mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateDirty( - mockTextSnapshot.Object, - new CodeSpanRange(25, 30), - SpanTrackingMode.EdgeExclusive - ) - ).Returns(dirtyLineTracker); - var mockJsonConvertService = autoMoqer.GetMock(); - var serializedState = new SerializedState(new CodeSpanRange(10, 20), ContainingCodeTrackerType.CoverageLines, new List - { - new DynamicLine(0, DynamicCoverageType.Covered) - }); - var serializedState2 = new SerializedState(new CodeSpanRange(25, 30), ContainingCodeTrackerType.CoverageLines, new List - { - new DynamicLine(3, DynamicCoverageType.Dirty) - }); - var serializedState3 = new SerializedState(new CodeSpanRange(50, 60), ContainingCodeTrackerType.CoverageLines, new List()); - mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject>("serializedState")) - .Returns(new List { serializedState, serializedState2, serializedState3 }); + var fileCodeSpanRangeService = new Mock().Object; + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var trackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup(containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + It.IsAny>(), + It.IsAny(), + useFileCodeSpanRangeServiceForChanges ? fileCodeSpanRangeService : null + )).Returns(trackedLinesFromFactory); + + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.UseFileCodeSpanRangeServiceForChanges).Returns(useFileCodeSpanRangeServiceForChanges); + mockCoverageContentType.Setup(coverageContentType => coverageContentType.ContentTypeName).Returns("contenttypename"); + + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.FileCodeSpanRangeService).Returns(fileCodeSpanRangeService); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); - var expectedTrackedLines = new TrackedLines(null, null, null); - autoMoqer.Setup( - containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( - new List { coverageLineTracker, dirtyLineTracker }, - null, - null - )).Returns(expectedTrackedLines); + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + containingCodeTrackedLinesBuilder.Create(new List { }, mockTextSnapshot.Object, ""); + mockContainingCodeTrackedLinesFactory.VerifyAll(); + } + + private ITrackedLines CoverageLinesNotWithinSnapshot(bool inSnapshot,string filePath = "", Action additionalSetup = null) + { + var line1 = new Line(1); + var line2 = new Line(inSnapshot ? 2 : 100); + + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName).Returns("contenttypename"); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(5); + var autoMoqer = new AutoMoqer(); + additionalSetup?.Invoke(autoMoqer); + var trackedLinesFromFactory = new Mock().Object; + + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName).Returns("contenttypename"); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - var trackedLines = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnapshot.Object, Language.CPP); + return containingCodeTrackedLinesBuilder.Create(new List { line1, line2 }, mockTextSnapshot.Object, filePath); - Assert.That(expectedTrackedLines, Is.SameAs(trackedLines)); } - } - [TestFixture(true)] - [TestFixture(false)] - internal class ContainingCodeTrackedLinesBuilder_Tests - { - private readonly bool isCSharp; + [TestCase(true)] + [TestCase(false)] + public void Should_Create_Null_TrackedLines__When_Coverage_Lines_Not_Within_TextSnapshot(bool inSnapshot) + { + var trackedLines = CoverageLinesNotWithinSnapshot(inSnapshot); + if (inSnapshot) + { + Assert.IsNotNull(trackedLines); + } + else + { + Assert.IsNull(trackedLines); + } + } - public ContainingCodeTrackedLinesBuilder_Tests(bool isCSharp) + [Test] + public void Should_Log_When_Coverage_Lines_Not_Within_TextSnapshot() { - this.isCSharp = isCSharp; + var mockLogger = new Mock(); + + _ = CoverageLinesNotWithinSnapshot(false, "filepath", autoMoqer => autoMoqer.SetInstance(mockLogger.Object)); + + mockLogger.Verify(logger => logger.Log("Not creating editor marks for filepath as some coverage lines are outside the text snapshot")); } - -#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() - internal class TrackerArgs : IContainingCodeTracker -#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + [TestCase(ContainingCodeTrackerType.CoverageLines, 1, DynamicCoverageType.Covered, true, true)] + [TestCase(ContainingCodeTrackerType.NotIncluded, 1, DynamicCoverageType.NotIncluded, false, false)] + public void Should_Serialize_State_From_TrackedLines_ContainingCodeTrackers( + ContainingCodeTrackerType containingCodeTrackerType, + int lineNumber, + DynamicCoverageType coverageType, + bool usedFileCodeSpanRangeService, + bool newLines + ) { - public List LinesInRange { get; } - public CodeSpanRange CodeSpanRange { get; } - public ITextSnapshot Snapshot { get; } - public SpanTrackingMode SpanTrackingMode { get; } - public ContainingCodeTrackerType TrackerType { get; } + var autoMoqer = new AutoMoqer(); + autoMoqer.SetInstance(new ICoverageContentType[0]); + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.SerializeObject(It.IsAny())).Returns("SerializedState"); - public static TrackerArgs ExpectedSingleCoverageLines(ILine line, SpanTrackingMode spanTrackingMode) - { - return ExpectedCoverageLines(new List { line }, TestHelper.CodeSpanRangeFromLine(line), spanTrackingMode); - } + var mockContainingCodeTracker = new Mock(); + var codeSpanRange = new CodeSpanRange(1, 2); + var containingCodeTrackerState = new ContainingCodeTrackerState(containingCodeTrackerType, codeSpanRange, new List { new DynamicLine(lineNumber, coverageType) }); + mockContainingCodeTracker.Setup(containingCodeTracker => containingCodeTracker.GetState()).Returns(containingCodeTrackerState); + var containingCodeTrackers = new List { + mockContainingCodeTracker.Object, + }; - public static TrackerArgs ExpectedCoverageLines(List lines, CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + var mockContainingCodeTrackerTrackedLines = new Mock(); + if(newLines) { - return new TrackerArgs(null, lines, codeSpanRange, spanTrackingMode, ContainingCodeTrackerType.CoverageLines); + var mockNewCodeTracker = new Mock(); + var mockNewLine = new Mock(); + mockNewLine.SetupGet(newLine => newLine.Number).Returns(5); + mockNewCodeTracker.SetupGet(newCodeTracker => newCodeTracker.Lines).Returns(new List { mockNewLine.Object}); + mockContainingCodeTrackerTrackedLines.SetupGet(containingCodeTrackerTrackedLines => containingCodeTrackerTrackedLines.NewCodeTracker) + .Returns(mockNewCodeTracker.Object); } + + mockContainingCodeTrackerTrackedLines.SetupGet(containingCodeTrackerTrackedLines => containingCodeTrackerTrackedLines.ContainingCodeTrackers).Returns(containingCodeTrackers); + var trackedLinesWithState = new ContainingCodeTrackerTrackedLinesWithState(mockContainingCodeTrackerTrackedLines.Object, usedFileCodeSpanRangeService); + var serialized = containingCodeTrackedLinesBuilder.Serialize( + trackedLinesWithState,"text"); - public static TrackerArgs ExpectedOtherLines(CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) - { - return new TrackerArgs(null, null, codeSpanRange, spanTrackingMode, ContainingCodeTrackerType.OtherLines); - } + Assert.That("SerializedState", Is.EqualTo(serialized)); - public static TrackerArgs ExpectedNotIncluded(CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) + var serializedEditorDynamicCoverage = mockJsonConvertService.Invocations.GetMethodInvocationSingleArgument( + nameof(IJsonConvertService.SerializeObject)).Single(); + if (newLines) { - return new TrackerArgs(null, null, codeSpanRange, spanTrackingMode, ContainingCodeTrackerType.NotIncluded); + Assert.That(serializedEditorDynamicCoverage.NewCodeLineNumbers, Is.EqualTo(new List { 5 })); } - public TrackerArgs( - ITextSnapshot textSnapsot, - List lines, - CodeSpanRange codeSpanRange, - SpanTrackingMode spanTrackingMode, - ContainingCodeTrackerType trackerType) + else { - Snapshot = textSnapsot; - LinesInRange = lines; - CodeSpanRange = codeSpanRange; - SpanTrackingMode = spanTrackingMode; - TrackerType = trackerType; + Assert.IsEmpty(serializedEditorDynamicCoverage.NewCodeLineNumbers); } + Assert.That(serializedEditorDynamicCoverage.Text, Is.EqualTo("text")); + var serializedContainingCodeTracker = serializedEditorDynamicCoverage.SerializedContainingCodeTrackers.Single(); + Assert.That(serializedContainingCodeTracker.Type, Is.EqualTo(containingCodeTrackerType)); + Assert.That(serializedContainingCodeTracker.CodeSpanRange, Is.SameAs(codeSpanRange)); + var serializedLine = serializedContainingCodeTracker.Lines.Single(); + Assert.That(serializedLine.Number, Is.EqualTo(lineNumber)); + Assert.That(serializedLine.CoverageType, Is.EqualTo(coverageType)); + Assert.That(serializedEditorDynamicCoverage.UsedFileCodeSpanRangeService, Is.EqualTo(usedFileCodeSpanRangeService)); + } - private static bool LinesEqual(List firstLines, List secondLines) - { - if (firstLines == null && secondLines == null) return true; - var linesEqual = firstLines.Count == secondLines.Count; - if (linesEqual) + private ITrackedLines ChangedOutsideEditor(bool textChanged,string filePath = "", Action additionalSetup = null) + { + var autoMoqer = new AutoMoqer(); + additionalSetup?.Invoke(autoMoqer); + var mockTextSnaphot = new Mock(); + mockTextSnaphot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName) + .Returns("contenttypename"); + mockTextSnaphot.Setup(textSnapshot => textSnapshot.GetText()).Returns(textChanged ? "changedtext" : "text"); + + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName) + .Returns("contenttypename"); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); + + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serializedState")) + .Returns(new SerializedEditorDynamicCoverage { - for (var i = 0; i < firstLines.Count; i++) - { - if (firstLines[i] != secondLines[i]) - { - linesEqual = false; - break; - } - } - } - return linesEqual; - } + Text = "text", + SerializedContainingCodeTrackers = new List() + }); - public override bool Equals(object obj) - { - var otherTrackerArgs = obj as TrackerArgs; - return SpanTrackingMode == otherTrackerArgs.SpanTrackingMode - && TrackerType == otherTrackerArgs.TrackerType - && CodeSpanRange.Equals(otherTrackerArgs.CodeSpanRange) - && LinesEqual(LinesInRange, otherTrackerArgs.LinesInRange); - } + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - public IEnumerable Lines => throw new System.NotImplementedException(); + return containingCodeTrackedLinesBuilder.Create( + "serializedState", mockTextSnaphot.Object, filePath); + } - public IContainingCodeTrackerProcessResult ProcessChanges(ITextSnapshot currentSnapshot, List newSpanChanges) + [TestCase(true)] + [TestCase(false)] + public void Should_Deserialize_AsNull_TrackedLines_If_Text_Has_Changed_Outside_Editor(bool textChanged) + { + var deserializedTrackedLines = ChangedOutsideEditor(textChanged); + + if (textChanged) { - throw new System.NotImplementedException(); + Assert.IsNull(deserializedTrackedLines); } - - public ContainingCodeTrackerState GetState() + else { - throw new NotImplementedException(); + Assert.IsNotNull(deserializedTrackedLines); } + } - class DummyCodeSpanRangeContainingCodeTrackerFactory : ICodeSpanRangeContainingCodeTrackerFactory + [Test] + public void Should_Log_When_Text_Changed_Outside_Editor() { - public IContainingCodeTracker CreateCoverageLines(ITextSnapshot textSnapshot, List lines, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) - { - return new TrackerArgs(textSnapshot, lines, containingRange, spanTrackingMode, ContainingCodeTrackerType.CoverageLines); - } + var mockLogger = new Mock(); - public IContainingCodeTracker CreateDirty(ITextSnapshot currentSnapshot, CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) - { - throw new NotImplementedException(); - } + _ = ChangedOutsideEditor(true, "filepath", autoMoqer => autoMoqer.SetInstance(mockLogger.Object)); + + mockLogger.Verify(logger => logger.Log("Not creating editor marks for filepath as text has changed")); + } + } + + internal class ContainingCodeTrackedLinesBuilder_ContentType_FileLineCoverageService_Tests + { + [Test] + public void Should_Create_CoverageLines_ContainingCodeTracker_For_Each_Line_When_Null_CodeSpanRanges() + { + var line1 = new Line(1); + var line2 = new Line(2); + + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName).Returns("contenttypename"); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(2); + var autoMoqer = new AutoMoqer(); + var mockCodeSpanRangeContainingCodeTrackerFactory = autoMoqer.GetMock(); + var containingCodeTracker1 = new Mock().Object; + var containingCodeTracker2 = new Mock().Object; + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + mockTextSnapshot.Object, new List { line1 }, TestHelper.CodeSpanRangeFromLine(line1), SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker1); + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + mockTextSnapshot.Object, new List { line2 }, TestHelper.CodeSpanRangeFromLine(line2), SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker2); + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var trackedLinesFromFactory = new Mock().Object; + + mockContainingCodeTrackedLinesFactory.Setup(containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + new List { containingCodeTracker1, containingCodeTracker2 }, + It.IsAny(), + It.IsAny() + )).Returns(trackedLinesFromFactory); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName).Returns("contenttypename"); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.FileCodeSpanRangeService).Returns(new Mock().Object); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - public IContainingCodeTracker CreateNotIncluded(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + var trackedLinesWithState = containingCodeTrackedLinesBuilder.Create(new List { line1, line2 }, mockTextSnapshot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + + Assert.False(trackedLinesWithState.UsedFileCodeSpanRangeService); + Assert.That(trackedLinesWithState.Wrapped, Is.SameAs(trackedLinesFromFactory)); + } + + private struct OtherLineText + { + public int LineNumber { get; set; } + public string Text { get; set; } + } + + private class DummyFileCodeSpanRangeService : IFileCodeSpanRangeService + { + private readonly ITextSnapshot expectedSnapshot; + private readonly List codeSpanRanges; + + public DummyFileCodeSpanRangeService(ITextSnapshot expectedSnapshot, List codeSpanRanges) { - return new TrackerArgs(textSnapshot, null, containingRange, spanTrackingMode, ContainingCodeTrackerType.NotIncluded); + this.expectedSnapshot = expectedSnapshot; + this.codeSpanRanges = codeSpanRanges; } - - public IContainingCodeTracker CreateOtherLines(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + public List GetFileCodeSpanRanges(ITextSnapshot snapshot) { - return new TrackerArgs(textSnapshot, null, containingRange, spanTrackingMode, ContainingCodeTrackerType.OtherLines); + Assert.That(snapshot, Is.SameAs(expectedSnapshot)); + return codeSpanRanges; } } - [TestCaseSource(typeof(RoslynDataClass), nameof(RoslynDataClass.TestCases))] - public void Should_Create_ContainingCodeTrackers_In_Order_Contained_Lines_And_Single_Line_When_Roslyn_Languages - ( - List codeSpanRanges, + private void TestCreatesContainingCodeTrackers( List lines, - List expected, - Action,bool> setUpExcluder, - int lineCount - ) + bool coverageOnlyFromFileCodeSpanRangeService, + List codeSpanRanges, + int textSnapshotLineCount, + Action textSnapshotCallback, + Action> setUpCodeSpanRangeContainingCodeTrackerFactory, + List otherLineTexts, + List expectedOrderedContainingCodeTrackers + ) { - new List { true, false }.ForEach(UseRoslynWhenTextChanges => - { - var autoMoqer = new AutoMoqer(); - var mockAppOptions = new Mock(); - mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode) - .Returns(UseRoslynWhenTextChanges ? EditorCoverageColouringMode.UseRoslynWhenTextChanges : EditorCoverageColouringMode.DoNotUseRoslynWhenTextChanges); - autoMoqer.Setup(appOptionsProvider => appOptionsProvider.Get()) - .Returns(mockAppOptions.Object); - autoMoqer.SetInstance(new DummyCodeSpanRangeContainingCodeTrackerFactory()); - autoMoqer.SetInstance(new TestThreadHelper()); - var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - setUpExcluder(autoMoqer.GetMock(), isCSharp); - - var mockTextSnapshot = new Mock(); - mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(lineCount); - var mockRoslynService = autoMoqer.GetMock(); - var textSpans = codeSpanRanges.Select(codeSpanRange => new TextSpan(codeSpanRange.StartLine, codeSpanRange.EndLine - codeSpanRange.StartLine)).ToList(); - mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)).ReturnsAsync(textSpans); - textSpans.ForEach(textSpan => - { - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(textSpan.Start)).Returns(textSpan.Start); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(textSpan.End)).Returns(textSpan.End); - }); + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(textSnapshotLineCount); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName).Returns("contenttypename"); + textSnapshotCallback(mockTextSnapshot.Object); - var newCodeTracker = autoMoqer.GetMock().Object; - autoMoqer.Setup(newCodeTrackerFactory => newCodeTrackerFactory.Create(isCSharp)) - .Returns(newCodeTracker); - - var expectedTrackedLines = new TrackedLines(null, null, null); - var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); - mockContainingCodeTrackedLinesFactory.Setup( - containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( - It.IsAny>(), - newCodeTracker, - UseRoslynWhenTextChanges ? containingCodeTrackedLinesBuilder : null) - ).Callback, INewCodeTracker, IFileCodeSpanRangeService>((containingCodeTrackers, _, __) => - { - var invocationArgs = containingCodeTrackers.Select(t => t as TrackerArgs).ToList(); - Assert.True(invocationArgs.Select(args => args.Snapshot).All(snapshot => snapshot == mockTextSnapshot.Object)); - Assert.That(invocationArgs, Is.EqualTo(expected)); - }).Returns(expectedTrackedLines); + var autoMoqer = new AutoMoqer(); + var mockTextSnapshotText = autoMoqer.GetMock(MockBehavior.Strict); + otherLineTexts.ForEach( + otherLineText => mockTextSnapshotText.Setup( + textSnapshotText => textSnapshotText.GetLineText(mockTextSnapshot.Object, otherLineText.LineNumber) + ).Returns(otherLineText.Text)); + var mockCodeSpanRangeContainingCodeTrackerFactory = autoMoqer.GetMock(); + setUpCodeSpanRangeContainingCodeTrackerFactory(mockCodeSpanRangeContainingCodeTrackerFactory); + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var trackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + expectedOrderedContainingCodeTrackers, + It.IsAny(), + It.IsAny() + )).Returns(trackedLinesFromFactory); - var trackedLines = containingCodeTrackedLinesBuilder.Create(lines, mockTextSnapshot.Object, isCSharp ? Language.CSharp : Language.VB); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName).Returns("contenttypename"); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.CoverageOnlyFromFileCodeSpanRangeService).Returns(coverageOnlyFromFileCodeSpanRangeService); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.FileCodeSpanRangeService).Returns( + new DummyFileCodeSpanRangeService(mockTextSnapshot.Object,codeSpanRanges)); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); - Assert.That(trackedLines, Is.SameAs(expectedTrackedLines)); - }); - + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + var trackedLinesWithState = containingCodeTrackedLinesBuilder.Create(lines, mockTextSnapshot.Object,"") as ContainingCodeTrackerTrackedLinesWithState; + Assert.That(trackedLinesWithState.Wrapped, Is.SameAs(trackedLinesFromFactory)); + Assert.True(trackedLinesWithState.UsedFileCodeSpanRangeService); } - public class RoslynDataClass + [Test] + public void Should_Create_CoverageLinesTracker_For_Adjusted_Lines_In_CodeSpanRange() { - public class RoslynTestCase : TestCaseData - { - public RoslynTestCase - ( - List codeSpanRanges, - List lines, - List expected, - Action,bool> setUpCodeExcluder, - int lineCount = 100, - string testName = null - - ) : base(codeSpanRanges, lines, expected, setUpCodeExcluder,lineCount) + var coverageLinesTracker = new Mock().Object; + + var line1 = new Line(1); + var line2 = new Line(2); + ITextSnapshot textSnapshotForSetup = null; + TestCreatesContainingCodeTrackers( + new List { line1, line2 }, + false, + new List { new CodeSpanRange(0,1) }, + 2, + textSnapshot => textSnapshotForSetup = textSnapshot, + mockCodeSpanRangeContainingCodeTrackerFactory => { - if (testName != null) - { - this.SetName(testName); - } - - } - } - private static ILine GetLine(int lineNumber) - { - var mockLine = new Mock(); - mockLine.Setup(line => line.Number).Returns(lineNumber); - return mockLine.Object; - } - - private static void ExcludeAllLines(Mock mockCodeLineExcluder,bool isCSharp) - { - mockCodeLineExcluder.Setup(excluder => excluder.ExcludeIfNotCode(It.IsAny(), It.IsAny(), isCSharp)).Returns(true); - } + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { line1, line2}, + new CodeSpanRange(0, 1), + SpanTrackingMode.EdgeExclusive) + ).Returns(coverageLinesTracker); + }, + new List { }, + new List { coverageLinesTracker} + ); + } - public static IEnumerable TestCases - { - get + [Test] + public void Should_Create_NotIncludedTracker_For_CodeSpanRange_With_No_Coverage() + { + var notIncludedTracker = new Mock().Object; + + ITextSnapshot textSnapshotForSetup = null; + TestCreatesContainingCodeTrackers( + new List { }, + false, + new List { new CodeSpanRange(0, 3) }, + 4, + textSnapshot => textSnapshotForSetup = textSnapshot, + mockCodeSpanRangeContainingCodeTrackerFactory => { - { - var test1CodeSpanRanges = new List - { - new CodeSpanRange(0,10), - new CodeSpanRange(20,30) - }; - var test1Lines = new List - { - GetLine(5), - GetLine(6), - - GetLine(25), - GetLine(26), - }; - - yield return new RoslynTestCase( - test1CodeSpanRanges, - test1Lines, - new List - { - TrackerArgs.ExpectedCoverageLines(new List{ test1Lines[0], test1Lines[1] }, test1CodeSpanRanges[0], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedCoverageLines(new List{ test1Lines[2], test1Lines[3] }, test1CodeSpanRanges[1], SpanTrackingMode.EdgeExclusive) - }, - ExcludeAllLines - ); - } + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - { - var test2CodeSpanRanges = new List - { - new CodeSpanRange(10,20), - new CodeSpanRange(25,40), - new CodeSpanRange(60,70), - }; + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateNotIncluded( + textSnapshotForSetup, + new CodeSpanRange(0, 3), + SpanTrackingMode.EdgeExclusive) + ).Returns(notIncludedTracker); + }, + new List { }, + new List { notIncludedTracker } + ); + } - var test2Lines = new List - { - GetLine(5),//single - GetLine(6),// single + [Test] + public void Should_Create_OtherLinesTracker_For_Lines_Between_CodeSpanRanges_That_Are_Not_Whitespace() + { + var coverageLinesRange1 = new CodeSpanRange(0, 1); + var coverageLinesTracker = new Mock().Object; + var otherLineRange = new CodeSpanRange(2, 2); + var otherLineTracker = new Mock().Object; + var coverageLinesRange2 = new CodeSpanRange(3, 4); + var coverageLinesTracker2 = new Mock().Object; + + var range1Line = new Line(1); + var range2Line = new Line(4); + ITextSnapshot textSnapshotForSetup = null; + TestCreatesContainingCodeTrackers( + new List { range1Line, range2Line }, + false, + new List { coverageLinesRange1, coverageLinesRange2 }, + 5, + textSnapshot => textSnapshotForSetup = textSnapshot, + mockCodeSpanRangeContainingCodeTrackerFactory => + { + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - GetLine(15),// range + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { range1Line }, + coverageLinesRange1, + SpanTrackingMode.EdgeExclusive) + ).Returns(coverageLinesTracker); - GetLine(45),//skip + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - GetLine(65),// range - }; - yield return new RoslynTestCase(test2CodeSpanRanges, test2Lines, new List - { - TrackerArgs.ExpectedSingleCoverageLines(test2Lines[0], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedSingleCoverageLines(test2Lines[1], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedCoverageLines(new List{ test2Lines[2] }, test2CodeSpanRanges[0], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedNotIncluded(test2CodeSpanRanges[1], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedSingleCoverageLines(test2Lines[3], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedCoverageLines(new List < ILine > { test2Lines[4] }, test2CodeSpanRanges[2], SpanTrackingMode.EdgeExclusive), - }, ExcludeAllLines); - } + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshotForSetup, + otherLineRange, + SpanTrackingMode.EdgeNegative) + ).Returns(otherLineTracker); - { - var test3CodeSpanRanges = new List - { - new CodeSpanRange(10,20), - }; - var test3Lines = new List { GetLine(21) }; // for line number adjustment - yield return new RoslynTestCase(test3CodeSpanRanges, test3Lines, new List - { - TrackerArgs.ExpectedCoverageLines(test3Lines, test3CodeSpanRanges[0], SpanTrackingMode.EdgeExclusive) - }, ExcludeAllLines); - } + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - { - var test4CodeSpanRanges = new List - { - new CodeSpanRange(10,20), - }; - var test4Lines = new List { GetLine(50) }; - yield return new RoslynTestCase(test4CodeSpanRanges, test4Lines, new List - { - TrackerArgs.ExpectedNotIncluded(test4CodeSpanRanges[0], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedSingleCoverageLines(test4Lines[0], SpanTrackingMode.EdgeExclusive) - }, ExcludeAllLines); - } + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { range2Line}, + coverageLinesRange2, + SpanTrackingMode.EdgeExclusive) + ).Returns(coverageLinesTracker2); + }, + new List { + new OtherLineText { LineNumber = 2, Text = "text" }, + }, + new List { coverageLinesTracker, otherLineTracker, coverageLinesTracker2 } + ); + } - { - void ExcludeOrIncludeCodeLines(Mock mockTextSnapshotLineExcluder, bool isCSharp, List codeLineNumbers, bool exclude) - { - mockTextSnapshotLineExcluder.Setup(excluder => excluder.ExcludeIfNotCode( - It.IsAny(), - It.Is(lineNumber => codeLineNumbers.Contains(lineNumber)), isCSharp)).Returns(exclude); - } - void SetupExcluder(Mock mockTextSnapshotLineExcluder, bool isCSharp) - { - ExcludeOrIncludeCodeLines(mockTextSnapshotLineExcluder, isCSharp, new List { 0, 2, 21, 23 }, false); - ExcludeOrIncludeCodeLines(mockTextSnapshotLineExcluder, isCSharp, new List { 1, 3, 4, 22 }, true); - } - var test5CodeSpanRanges = new List - { - new CodeSpanRange(5,20), - }; - var test5Lines = new List { GetLine(15) }; - yield return new RoslynTestCase(test5CodeSpanRanges, test5Lines, new List - { - TrackerArgs.ExpectedOtherLines(new CodeSpanRange(0,0), SpanTrackingMode.EdgeNegative), - TrackerArgs.ExpectedOtherLines(new CodeSpanRange(2,2), SpanTrackingMode.EdgeNegative), - TrackerArgs.ExpectedCoverageLines(test5Lines, test5CodeSpanRanges[0], SpanTrackingMode.EdgeExclusive), - TrackerArgs.ExpectedOtherLines(new CodeSpanRange(21,21), SpanTrackingMode.EdgeNegative), - TrackerArgs.ExpectedOtherLines( new CodeSpanRange(23,23), SpanTrackingMode.EdgeNegative), - }, SetupExcluder, 24, "Other lines"); - } + [Test] + public void Should_Create_OtherLinesTracker_For_Each_Line_After_Last_CodeSpanRange_That_Is_Not_Whitespace() + { + var coverageLinesTracker = new Mock().Object; + var otherLineTracker = new Mock().Object; + + var line1 = new Line(1); + var line2 = new Line(2); + ITextSnapshot textSnapshotForSetup = null; + TestCreatesContainingCodeTrackers( + new List { line1, line2 }, + false, + new List { new CodeSpanRange(0, 1) }, + 4, + textSnapshot => textSnapshotForSetup = textSnapshot, + mockCodeSpanRangeContainingCodeTrackerFactory => + { + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { line1, line2 }, + new CodeSpanRange(0, 1), + SpanTrackingMode.EdgeExclusive) + ).Returns(coverageLinesTracker); - } - } + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshotForSetup, + CodeSpanRange.SingleLine(2), + SpanTrackingMode.EdgeNegative) + ).Returns(otherLineTracker); + + }, + new List { + new OtherLineText { LineNumber = 2, Text = "text" }, + new OtherLineText { LineNumber = 3, Text = " " } + }, + new List { coverageLinesTracker, otherLineTracker } + ); } - [TestCase(ContainingCodeTrackerType.CoverageLines, 1, DynamicCoverageType.Covered)] - [TestCase(ContainingCodeTrackerType.NotIncluded, 1, DynamicCoverageType.NotIncluded)] - public void Should_Serialize_State_From_TrackedLines_ContainingCodeTrackers( - ContainingCodeTrackerType containingCodeTrackerType, int lineNumber, DynamicCoverageType coverageType + [TestCase(true)] + [TestCase(false)] + public void Should_Create_CoverageLinesTracker_For_Each_CoverageLine_Not_In_CodeSpanRange_If_CoverageOnlyFromFileCodeSpanRangeService_Is_False( + bool coverageOnlyFromFileCodeSpanRangeService ) { - var autoMoqer = new AutoMoqer(); + // before first CodeSpanRange + var coverageLineNotInRangeRange1 = new CodeSpanRange(0, 0); + var notInRangeCoverageLineTracker1 = new Mock().Object; + var notInRangeOtherLineTracker1 = new Mock().Object; + + var coverageLinesRange1 = new CodeSpanRange(1, 1); + var coverageLinesTracker = new Mock().Object; + + // in between CodeSpanRange2 + var coverageLineNotInRangeRange2 = new CodeSpanRange(2, 2); + var notInRangeCoverageLineTracker2 = new Mock().Object; + var notInRangeOtherLineTracker2 = new Mock().Object; + + var coverageLinesRange2 = new CodeSpanRange(3, 3); + var coverageLinesTracker2 = new Mock().Object; + + // after last CodeSpanRange + var coverageLineNotInRangeRange3 = new CodeSpanRange(4, 4); + var notInRangeCoverageLineTracker3 = new Mock().Object; + var notInRangeOtherLineTracker3 = new Mock().Object; + + var notInRangeLine1 = new Line(1); + var range1Line = new Line(2); + var notInRangeLine2 = new Line(3); + var range2Line = new Line(4); + var notInRangeLine3 = new Line(5); + ITextSnapshot textSnapshotForSetup = null; + TestCreatesContainingCodeTrackers( + new List { notInRangeLine1, range1Line, notInRangeLine2, range2Line, notInRangeLine3 }, + coverageOnlyFromFileCodeSpanRangeService, + new List { coverageLinesRange1, coverageLinesRange2 }, + 5, + textSnapshot => textSnapshotForSetup = textSnapshot, + mockCodeSpanRangeContainingCodeTrackerFactory => + { + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - var mockJsonConvertService = autoMoqer.GetMock(); - mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.SerializeObject(It.IsAny())).Returns("SerializedState"); - - var mockContainingCodeTracker = new Mock(); - var codeSpanRange = new CodeSpanRange(1, 2); - var containingCodeTrackerState = new ContainingCodeTrackerState(containingCodeTrackerType, codeSpanRange, new List { new DynamicLine(lineNumber,coverageType) }); - mockContainingCodeTracker.Setup(containingCodeTracker => containingCodeTracker.GetState()).Returns(containingCodeTrackerState); - var containingCodeTrackers = new List { - mockContainingCodeTracker.Object, - }; + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { notInRangeLine1}, + coverageLineNotInRangeRange1, + SpanTrackingMode.EdgeExclusive) + ).Returns(notInRangeCoverageLineTracker1); - var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - var serialized = containingCodeTrackedLinesBuilder.Serialize( - new TrackedLines(containingCodeTrackers, null, null)); - - Assert.That("SerializedState", Is.EqualTo(serialized)); + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshotForSetup, + coverageLineNotInRangeRange1, + SpanTrackingMode.EdgeNegative) + ).Returns(notInRangeOtherLineTracker1); - var serializedState = mockJsonConvertService.Invocations.GetMethodInvocationSingleArgument>( - nameof(IJsonConvertService.SerializeObject)).Single().Single(); - - Assert.That(serializedState.Type, Is.EqualTo(containingCodeTrackerType)); - Assert.That(serializedState.CodeSpanRange, Is.SameAs(codeSpanRange)); - var serializedLine = serializedState.Lines.Single(); - Assert.That(serializedLine.Number, Is.EqualTo(lineNumber)); - Assert.That(serializedLine.CoverageType, Is.EqualTo(coverageType)); - } + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( - private Mock EnsureAppOptions(AutoMoqer autoMoqer) - { - var mockAppOptions = new Mock(); - autoMoqer.Setup( - appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); - return mockAppOptions; + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { range1Line }, + coverageLinesRange1, + SpanTrackingMode.EdgeExclusive) + ).Returns(coverageLinesTracker); + + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { notInRangeLine2}, + coverageLineNotInRangeRange2, + SpanTrackingMode.EdgeExclusive) + ).Returns(notInRangeCoverageLineTracker2); + + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshotForSetup, + coverageLineNotInRangeRange2, + SpanTrackingMode.EdgeNegative) + ).Returns(notInRangeOtherLineTracker2); + + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { range2Line }, + coverageLinesRange2, + SpanTrackingMode.EdgeExclusive) + ).Returns(coverageLinesTracker2); + + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshotForSetup, + new List { notInRangeLine3 }, + coverageLineNotInRangeRange3, + SpanTrackingMode.EdgeExclusive) + ).Returns(notInRangeCoverageLineTracker3); + + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshotForSetup, + coverageLineNotInRangeRange3, + SpanTrackingMode.EdgeNegative) + ).Returns(notInRangeOtherLineTracker3); + }, + coverageOnlyFromFileCodeSpanRangeService ? new List { + new OtherLineText { LineNumber = 0, Text = "text" }, + new OtherLineText { LineNumber = 2, Text = "text" }, + new OtherLineText { LineNumber = 4, Text = "text" }, + } : new List { }, + coverageOnlyFromFileCodeSpanRangeService ? + new List { + notInRangeOtherLineTracker1, + coverageLinesTracker, + notInRangeOtherLineTracker2, + coverageLinesTracker2, + notInRangeOtherLineTracker3 + } : + new List { + notInRangeCoverageLineTracker1, + coverageLinesTracker, + notInRangeCoverageLineTracker2, + coverageLinesTracker2, + notInRangeCoverageLineTracker3 + } + ); } - private void Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( - ContainingCodeTrackerType containingCodeTrackerType, - List dynamicLines, - Action< - Mock, - IContainingCodeTracker, - CodeSpanRange, - ITextSnapshot> setupContainingCodeTrackerFactory - ) + private void DeserializesContainingCodeTrackerTest( + SerializedContainingCodeTracker serializedContainingCodeTracker, + Func, ITextSnapshot, IContainingCodeTracker> setUpContainingCodeTracker) { - var mockTextSnapshot = new Mock(); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(1)).Returns(10); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(3)).Returns(20); - var autoMoqer = new AutoMoqer(); - autoMoqer.SetInstance(new TestThreadHelper()); - EnsureAppOptions(autoMoqer); + var mockTextSnaphot = new Mock(); + var expectedContainingCodeTracker = setUpContainingCodeTracker(autoMoqer.GetMock(), mockTextSnaphot.Object); + + mockTextSnaphot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName) + .Returns("contenttypename"); + mockTextSnaphot.Setup(textSnapshot => textSnapshot.GetText()).Returns("text"); - var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName) + .Returns("contenttypename"); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.FileCodeSpanRangeService) + .Returns(new DummyFileCodeSpanRangeService(mockTextSnaphot.Object, new List { serializedContainingCodeTracker.CodeSpanRange })); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); + + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var containingCodeTrackerTrackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + new List { expectedContainingCodeTracker}, + It.IsAny(), + It.IsAny() + )).Returns(containingCodeTrackerTrackedLinesFromFactory); var mockJsonConvertService = autoMoqer.GetMock(); - var codeSpanRange = new CodeSpanRange(10, 20); - var serializedState = new SerializedState(codeSpanRange, containingCodeTrackerType, dynamicLines); - mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject>("serializedState")) - .Returns(new List { serializedState }); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serializedState")) + .Returns( + new SerializedEditorDynamicCoverage { + Text = "text", + SerializedContainingCodeTrackers = new List { serializedContainingCodeTracker }, + UsedFileCodeSpanRangeService = true + }); - var mockRoslynService = autoMoqer.GetMock(); - mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) - .ReturnsAsync(new List { new TextSpan(1, 2) }); + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - var newCodeTracker = new Mock().Object; - autoMoqer.Setup( - newCodeTrackerFactory => newCodeTrackerFactory.Create( - isCSharp, - new List { }, - mockTextSnapshot.Object)).Returns(newCodeTracker); - - var containingCodeTracker = new Mock().Object; - setupContainingCodeTrackerFactory( - autoMoqer.GetMock(), - containingCodeTracker, - codeSpanRange, - mockTextSnapshot.Object); - - var expectedTrackedLines = new TrackedLines(null, null, null); - autoMoqer.Setup( - containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( - new List { containingCodeTracker }, - newCodeTracker, - containingCodeTrackedLinesBuilder - )).Returns(expectedTrackedLines); - var trackedLines = containingCodeTrackedLinesBuilder.Create( - "serializedState", - mockTextSnapshot.Object, - isCSharp ? Language.CSharp : Language.VB - ); - Assert.That(expectedTrackedLines, Is.SameAs(trackedLines)); + var containingCodeTrackerTrackedLinesWithState = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnaphot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + + Assert.True(containingCodeTrackerTrackedLinesWithState.UsedFileCodeSpanRangeService); + Assert.That(containingCodeTrackerTrackedLinesWithState.Wrapped, Is.SameAs(containingCodeTrackerTrackedLinesFromFactory)); + } [Test] - public void Should_Use_Deserialized_OtherLinesTracker_If_CodeSpanRange_Has_Not_Changed() + public void Should_Deserialize_OtherLines() { - Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + var serializedOtherLines = new SerializedContainingCodeTracker( + new CodeSpanRange(1, 1), ContainingCodeTrackerType.OtherLines, - new List { }, - (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => - { - mockContainingCodeTrackerFactory.Setup( - codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( - textSnapshot, - codeSpanRange, - SpanTrackingMode.EdgeNegative - )).Returns(containingCodeTracker); - } - ); + new List { }); + + DeserializesContainingCodeTrackerTest(serializedOtherLines, (mockCodeSpanRangeContainingCodeTrackerFactory, textSnapshot) => + { + var containingCodeTracker = new Mock().Object; + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshot, + new CodeSpanRange(1, 1), + SpanTrackingMode.EdgeNegative) + ).Returns(containingCodeTracker); + return containingCodeTracker; + }); } [Test] - public void Should_Use_Deserialized_NotIncludedTracker_If_CodeSpanRange_Has_Not_Changed() + public void Should_Deserialize_NotIncluded() { - Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + var serializedOtherLines = new SerializedContainingCodeTracker( + new CodeSpanRange(1, 1), ContainingCodeTrackerType.NotIncluded, - new List { }, - (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => - { - mockContainingCodeTrackerFactory.Setup( - codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateNotIncluded( - textSnapshot, - codeSpanRange, - SpanTrackingMode.EdgeExclusive - )).Returns(containingCodeTracker); - } - ); + new List { }); + + DeserializesContainingCodeTrackerTest(serializedOtherLines, (mockCodeSpanRangeContainingCodeTrackerFactory, textSnapshot) => + { + var containingCodeTracker = new Mock().Object; + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateNotIncluded( + textSnapshot, + new CodeSpanRange(1, 1), + SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker); + return containingCodeTracker; + }); } - - [TestCase(DynamicCoverageType.Covered, CoverageType.Covered)] - [TestCase(DynamicCoverageType.NotCovered, CoverageType.NotCovered)] - [TestCase(DynamicCoverageType.Partial, CoverageType.Partial)] - public void Should_Use_Deserialized_CoverageLinesTracker_If_CodeSpanRange_Has_Not_Changed( - DynamicCoverageType dynamicCoverageType, - CoverageType expectedCoverageType) + [Test] + public void Should_Deserialize_Dirty() { - Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + var serializedOtherLines = new SerializedContainingCodeTracker( + new CodeSpanRange(1, 1), ContainingCodeTrackerType.CoverageLines, - new List { new DynamicLine(1, dynamicCoverageType), new DynamicLine(2, dynamicCoverageType)}, - (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => - { - mockContainingCodeTrackerFactory.Setup( - codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( - textSnapshot, - new List { new Line(2, expectedCoverageType), new Line(3, expectedCoverageType) }, - codeSpanRange, - SpanTrackingMode.EdgeExclusive - )).Returns(containingCodeTracker); - } - ); + new List { new DynamicLine(1, DynamicCoverageType.Dirty)}); + + DeserializesContainingCodeTrackerTest(serializedOtherLines, (mockCodeSpanRangeContainingCodeTrackerFactory, textSnapshot) => + { + var containingCodeTracker = new Mock().Object; + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateDirty( + textSnapshot, + new CodeSpanRange(1, 1), + SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker); + return containingCodeTracker; + }); } [Test] - public void Should_Use_Deserialized_CoverageLinesTracker_For_Dirty_When_DirtyIf_CodeSpanRange_Has_Not_Changed() + public void Should_Deserialize_CoverageLines() { - Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + var serializedOtherLines = new SerializedContainingCodeTracker( + new CodeSpanRange(1, 3), ContainingCodeTrackerType.CoverageLines, - new List { new DynamicLine(1, DynamicCoverageType.Dirty) }, - (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => - { - mockContainingCodeTrackerFactory.Setup( - codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateDirty( - textSnapshot, - codeSpanRange, - SpanTrackingMode.EdgeExclusive - )).Returns(containingCodeTracker); - } - ); + new List { + new DynamicLine(1, DynamicCoverageType.Covered), + new DynamicLine(2, DynamicCoverageType.NotCovered), + new DynamicLine(3, DynamicCoverageType.Partial) + }); + + DeserializesContainingCodeTrackerTest(serializedOtherLines, (mockCodeSpanRangeContainingCodeTrackerFactory, textSnapshot) => + { + var containingCodeTracker = new Mock().Object; + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + textSnapshot, + new List { + new Line(2, CoverageType.Covered), + new Line(3, CoverageType.NotCovered), + new Line(4, CoverageType.Partial), + }, + new CodeSpanRange(1, 3), + SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker); + return containingCodeTracker; + }); } [Test] - public void Should_Not_Use_Deserialized_If_CodeSpanRange_Has_Changed() + public void Should_Recreate_With_No_New_CodeTracker_If_No_Line_Excluder() { - var mockTextSnapshot = new Mock(); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(1)).Returns(10); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(3)).Returns(20); - var autoMoqer = new AutoMoqer(); - EnsureAppOptions(autoMoqer); - - autoMoqer.SetInstance(new TestThreadHelper()); - var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + autoMoqer.GetMock(MockBehavior.Strict); + var mockTextSnaphot = new Mock(); - var mockJsonConvertService = autoMoqer.GetMock(); - var codeSpanRange = new CodeSpanRange(100, 200); - var serializedState = new SerializedState(codeSpanRange, ContainingCodeTrackerType.OtherLines, new List()); - mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject>("serializedState")) - .Returns(new List { serializedState }); + mockTextSnaphot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName) + .Returns("contenttypename"); + mockTextSnaphot.Setup(textSnapshot => textSnapshot.GetText()).Returns("text"); - var mockRoslynService = autoMoqer.GetMock(); - mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) - .ReturnsAsync(new List { new TextSpan(1, 2) }); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName) + .Returns("contenttypename"); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.FileCodeSpanRangeService) + .Returns(new DummyFileCodeSpanRangeService(mockTextSnaphot.Object, new List { new CodeSpanRange(10,10) })); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); - var expectedTrackedLines = new TrackedLines(null, null, null); - autoMoqer.Setup( + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var containingCodeTrackerTrackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup( containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( - new List { }, - It.IsAny(), + new List {}, + null, It.IsAny() - )).Returns(expectedTrackedLines); + )).Returns(containingCodeTrackerTrackedLinesFromFactory); + + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serializedState")) + .Returns(new SerializedEditorDynamicCoverage { Text = "text", SerializedContainingCodeTrackers = new List {}, UsedFileCodeSpanRangeService = true }); + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + + var containingCodeTrackerTrackedLinesWithState = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnaphot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + + Assert.That(containingCodeTrackerTrackedLinesWithState.Wrapped, Is.SameAs(containingCodeTrackerTrackedLinesFromFactory)); - var trackedLines = containingCodeTrackedLinesBuilder.Create( - "serializedState", - mockTextSnapshot.Object, - isCSharp ? Language.CSharp : Language.VB - ); - - Assert.That(expectedTrackedLines, Is.SameAs(trackedLines)); } - [TestCase(true)] - [TestCase(false)] - public void Should_Deserialize_Dependent_Upon_AppOption_EditorCoverageColouringMode_UseRoslynWhenTextChanges(bool useRoslynWhenTextChanges) + [TestCase(true,new int[] { 30 })] + [TestCase(false, new int[] { 30, 31, 32, 33, 34, 35 })] + public void Should_Recreated_With_NewCodeTracker_With_Lines_From_CodeSpanRanges(bool useFileCodeSpanRangeServiceForChanges, int[] expectedNewLineNumbers) { - var mockTextSnapshot = new Mock(); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(1)).Returns(10); - mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(3)).Returns(20); - var autoMoqer = new AutoMoqer(); - var mockAppOptions = EnsureAppOptions(autoMoqer); - mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode) - .Returns(useRoslynWhenTextChanges ? EditorCoverageColouringMode.UseRoslynWhenTextChanges : EditorCoverageColouringMode.DoNotUseRoslynWhenTextChanges); - autoMoqer.SetInstance(new TestThreadHelper()); - var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + var serializedCodeSpanRange = new CodeSpanRange(10, 20); - var mockJsonConvertService = autoMoqer.GetMock(); - var codeSpanRange = new CodeSpanRange(100, 200); - var serializedState = new SerializedState(codeSpanRange, ContainingCodeTrackerType.OtherLines, new List()); - mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject>("serializedState")) - .Returns(new List { serializedState }); + var mockTextSnaphot = new Mock(); - var mockRoslynService = autoMoqer.GetMock(); - mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) - .ReturnsAsync(new List { new TextSpan(1, 2) }); + mockTextSnaphot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName) + .Returns("contenttypename"); + mockTextSnaphot.Setup(textSnapshot => textSnapshot.GetText()).Returns("text"); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName) + .Returns("contenttypename"); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.FileCodeSpanRangeService) + .Returns( + new DummyFileCodeSpanRangeService( + mockTextSnaphot.Object, + new List { + new CodeSpanRange(10, 20), + new CodeSpanRange(30, 35) + } + ) + ); + var lineExcluder = new Mock().Object; + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.LineExcluder).Returns(lineExcluder); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.UseFileCodeSpanRangeServiceForChanges) + .Returns(useFileCodeSpanRangeServiceForChanges); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); + + var mockNewCodeTrackerFactory = autoMoqer.GetMock(); + var newCodeTracker = new Mock().Object; - var expectedLines = useRoslynWhenTextChanges ? new List { 10 } : new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; - autoMoqer.Setup( - newCodeTrackerFactory => newCodeTrackerFactory.Create( - isCSharp, - expectedLines, - mockTextSnapshot.Object)).Returns(newCodeTracker); - - var expectedTrackedLines = new TrackedLines(null, null, null); - autoMoqer.Setup( + mockNewCodeTrackerFactory.Setup(newCodeTrackerFactory => newCodeTrackerFactory.Create(lineExcluder, expectedNewLineNumbers, mockTextSnaphot.Object)) + .Returns(newCodeTracker); + + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var containingCodeTrackerTrackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup( containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( - new List { }, + new List { null}, newCodeTracker, - useRoslynWhenTextChanges ? containingCodeTrackedLinesBuilder : null - )).Returns(expectedTrackedLines); + It.IsAny() + )).Returns(containingCodeTrackerTrackedLinesFromFactory); + + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serializedState")) + .Returns(new SerializedEditorDynamicCoverage { + Text = "text", + SerializedContainingCodeTrackers = new List { + new SerializedContainingCodeTracker(serializedCodeSpanRange, ContainingCodeTrackerType.OtherLines, new List{ }) + }, + UsedFileCodeSpanRangeService = true + }); + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - var trackedLines = containingCodeTrackedLinesBuilder.Create( - "serializedState", - mockTextSnapshot.Object, - isCSharp ? Language.CSharp : Language.VB - ); - Assert.That(expectedTrackedLines, Is.SameAs(trackedLines)); + var containingCodeTrackerTrackedLinesWithState = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnaphot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + + Assert.That(containingCodeTrackerTrackedLinesWithState.Wrapped, Is.SameAs(containingCodeTrackerTrackedLinesFromFactory)); } + } + internal class ContainingCodeTrackedLinesBuilder_ContentType_No_FileLineCoverageService_Tests + { [Test] - public void Should_IFileCodeSpanRangeService_Using_Roslyn_Distinct() + public void Should_Create_CoverageLines_ContainingCodeTracker_For_Each_Line() { + var line1 = new Line(1); + var line2 = new Line(2); + var mockTextSnapshot = new Mock(); - mockTextSnapshot.Setup(textSnaphot => textSnaphot.GetLineNumberFromPosition(1)).Returns(1); - mockTextSnapshot.Setup(textSnaphot => textSnaphot.GetLineNumberFromPosition(11)).Returns(1); - mockTextSnapshot.Setup(textSnaphot => textSnaphot.GetLineNumberFromPosition(15)).Returns(1); - mockTextSnapshot.Setup(textSnaphot => textSnaphot.GetLineNumberFromPosition(20)).Returns(1); - mockTextSnapshot.Setup(textSnaphot => textSnaphot.GetLineNumberFromPosition(30)).Returns(2); - mockTextSnapshot.Setup(textSnaphot => textSnaphot.GetLineNumberFromPosition(40)).Returns(3); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName).Returns("contenttypename"); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(5); + var autoMoqer = new AutoMoqer(); + var mockCodeSpanRangeContainingCodeTrackerFactory = autoMoqer.GetMock(); + var containingCodeTracker1 = new Mock().Object; + var containingCodeTracker2 = new Mock().Object; + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + mockTextSnapshot.Object, new List { line1 }, TestHelper.CodeSpanRangeFromLine(line1), SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker1); + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + mockTextSnapshot.Object, new List { line2 }, TestHelper.CodeSpanRangeFromLine(line2), SpanTrackingMode.EdgeExclusive) + ).Returns(containingCodeTracker2); + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var trackedLinesFromFactory = new Mock().Object; + + mockContainingCodeTrackedLinesFactory.Setup(containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + new List { containingCodeTracker1, containingCodeTracker2 }, + It.IsAny(), + It.IsAny() + )).Returns(trackedLinesFromFactory); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName).Returns("contenttypename"); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType .Object}); + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + var trackedLinesWithState = containingCodeTrackedLinesBuilder.Create(new List { line1, line2 }, mockTextSnapshot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + + Assert.False(trackedLinesWithState.UsedFileCodeSpanRangeService); + Assert.That(trackedLinesWithState.Wrapped, Is.SameAs(trackedLinesFromFactory)); + } + [TestCase(DynamicCoverageType.Covered, CoverageType.Covered)] + [TestCase(DynamicCoverageType.NotCovered, CoverageType.NotCovered)] + [TestCase(DynamicCoverageType.Partial, CoverageType.Partial)] + public void Should_Create_ContainingCodeTrackers_From_Serialized_State_If_Text_Has_Not_Changed_Outside_Editor( + DynamicCoverageType dynamicCoverageType, + CoverageType expectedAdjustedCoverageType + ) + { var autoMoqer = new AutoMoqer(); - var mockRoslynService = autoMoqer.GetMock(); - mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) - .ReturnsAsync(new List { new TextSpan(1, 10), new TextSpan(15,5),new TextSpan(30,10) }); - autoMoqer.SetInstance(new TestThreadHelper()); + var coverageCodeSpanRange = new CodeSpanRange(0, 0); + var dirtyCodeSpanRange = new CodeSpanRange(1, 1); + + var mockTextSnaphot = new Mock(); + mockTextSnaphot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName) + .Returns("contenttypename"); + + mockTextSnaphot.Setup(textSnapshot => textSnapshot.GetText()).Returns("text"); + + var coverageContainingCodeTracker = new Mock().Object; + var dirtyContainingCodeTracker = new Mock().Object; + var createdContainingCodeTrackers = new List { coverageContainingCodeTracker, dirtyContainingCodeTracker }; + var mockCodeSpanRangeContainingCodeTrackerFactory = autoMoqer.GetMock(); + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines( + mockTextSnaphot.Object, + // adjusted IDynamicLine + new List{ new Line(1, expectedAdjustedCoverageType) }, + coverageCodeSpanRange, + SpanTrackingMode.EdgeExclusive + )).Returns(coverageContainingCodeTracker); + + mockCodeSpanRangeContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateDirty( + mockTextSnaphot.Object, + dirtyCodeSpanRange, + SpanTrackingMode.EdgeExclusive + )).Returns(dirtyContainingCodeTracker); + + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var containingCodeTrackerTrackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + createdContainingCodeTrackers, + It.IsAny(), + null + )).Returns(containingCodeTrackerTrackedLinesFromFactory); + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName) + .Returns("contenttypename"); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object}); + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serializedState")) + .Returns( + new SerializedEditorDynamicCoverage { + UsedFileCodeSpanRangeService = false, + SerializedContainingCodeTrackers = new List + { + new SerializedContainingCodeTracker(coverageCodeSpanRange, ContainingCodeTrackerType.CoverageLines, new List + { + new DynamicLine(0, dynamicCoverageType) + }), + new SerializedContainingCodeTracker(dirtyCodeSpanRange, ContainingCodeTrackerType.CoverageLines, new List + { + new DynamicLine(1, DynamicCoverageType.Dirty) + }) + }, + Text = "text" + } + ); + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); - var fileCodeSpanRanges = containingCodeTrackedLinesBuilder.GetFileCodeSpanRanges(mockTextSnapshot.Object); + + var containingCodeTrackerTrackedLinesWithState = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnaphot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + Assert.False(containingCodeTrackerTrackedLinesWithState.UsedFileCodeSpanRangeService); + Assert.That(containingCodeTrackerTrackedLinesWithState.Wrapped, Is.SameAs(containingCodeTrackerTrackedLinesFromFactory)); + + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Use_NewCodeTracker_With_NewLines_And_Line_Excluder_If_CoverageContentTypeProvides(bool hasLineExcluder) + { + var autoMoqer = new AutoMoqer(); + + var mockTextSnaphot = new Mock(); + mockTextSnaphot.SetupGet(textSnapshot => textSnapshot.ContentType.TypeName) + .Returns("contenttypename"); + mockTextSnaphot.Setup(textSnapshot => textSnapshot.GetText()).Returns("text"); + + var newLineExcluder = new Mock().Object; + var mockCoverageContentType = new Mock(); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.ContentTypeName) + .Returns("contenttypename"); + mockCoverageContentType.SetupGet(coverageContentType => coverageContentType.LineExcluder).Returns(hasLineExcluder ? newLineExcluder : null); + autoMoqer.SetInstance(new ICoverageContentType[] { mockCoverageContentType.Object }); + + var newCodeTracker = new Mock().Object; + var mockNewCodeTrackerFactory = autoMoqer.GetMock(); + mockNewCodeTrackerFactory.Setup(newCodeTrackeFactory => newCodeTrackeFactory.Create( + newLineExcluder, + new List { 1,2,3}, + mockTextSnaphot.Object + )) + .Returns(newCodeTracker); + + var mockContainingCodeTrackedLinesFactory = autoMoqer.GetMock(); + var containingCodeTrackerTrackedLinesFromFactory = new Mock().Object; + mockContainingCodeTrackedLinesFactory.Setup( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + It.IsAny>(), + hasLineExcluder ? newCodeTracker : null, + null + )).Returns(containingCodeTrackerTrackedLinesFromFactory); - Assert.That(fileCodeSpanRanges, Is.EqualTo(new List { CodeSpanRange.SingleLine(1), new CodeSpanRange(2, 3) })); + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serializedState")) + .Returns( + new SerializedEditorDynamicCoverage { + Text = "text", + SerializedContainingCodeTrackers = new List(), + NewCodeLineNumbers = new List { 1,2,3} + }); + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + + var containingCodeTrackerTrackedLinesWithState = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnaphot.Object, "") as ContainingCodeTrackerTrackedLinesWithState; + + Assert.That(containingCodeTrackerTrackedLinesWithState.Wrapped, Is.SameAs(containingCodeTrackerTrackedLinesFromFactory)); + } } + } diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs index 4ea8abeb..c922d84f 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs @@ -1,8 +1,11 @@ -using AutoMoq; +using System; +using AutoMoq; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.Utilities; using FineCodeCoverage.Engine; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Impl; using FineCodeCoverageTests.Test_helpers; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -51,14 +54,20 @@ public void Manage_Should_Create_Singleton_IBufferLineCoverage() public void Manage_Should_Create_Singleton_IBufferLineCoverage_With_Last_Coverage_And_Dependencies(bool hasLastCoverage) { var autoMocker = new AutoMoqer(); + + var now = new DateTime(); + autoMocker.GetMock().Setup(dateTimeService => dateTimeService.Now).Returns(now); + var eventAggregator = autoMocker.GetMock().Object; var trackedLinesFactory = autoMocker.GetMock().Object; var dynamicCoverageManager = autoMocker.Create(); - IFileLineCoverage lastCoverage = null; + LastCoverage lastCoverage = null; if (hasLastCoverage) { - lastCoverage = new Mock().Object; - dynamicCoverageManager.Handle(new NewCoverageLinesMessage { CoverageLines = lastCoverage}); + var fileLineCoverage = new Mock().Object; + lastCoverage = new LastCoverage(fileLineCoverage, now); + (dynamicCoverageManager as IListener).Handle(new TestExecutionStartingMessage()); + dynamicCoverageManager.Handle(new NewCoverageLinesMessage { CoverageLines = fileLineCoverage}); } var mockTextInfo = new Mock(); diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs index 3b886b8d..7b4a28d0 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs @@ -1,6 +1,7 @@ using AutoMoq; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.Utilities; using FineCodeCoverage.Engine; using FineCodeCoverage.Options; using Microsoft.VisualStudio.Settings; @@ -43,6 +44,12 @@ public void Should_Delete_WritableUserSettingsStore_Collection_When_NewCoverageL public void Should_SaveSerializedCoverage_To_The_Store_Creating_Collection_If_Does_Not_Exist(bool collectionExists) { var autoMoqer = new AutoMoqer(); + var mockDateTimeService = autoMoqer.GetMock(); + var now = DateTime.Now; + mockDateTimeService.SetupGet(dateTimeService => dateTimeService.Now).Returns(now); + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.SerializeObject( + new SerializedCoverageWhen { Serialized = "serialized coverage", When = now })).Returns("serialized"); var mockWritableSettingsStore = new Mock(); mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.CollectionExists("FCC.DynamicCoverageStore")).Returns(collectionExists); autoMoqer.Setup( @@ -50,7 +57,7 @@ public void Should_SaveSerializedCoverage_To_The_Store_Creating_Collection_If_Do var dynamicCoverageStore = autoMoqer.Create(); - dynamicCoverageStore.SaveSerializedCoverage("filePath", "serialized"); + dynamicCoverageStore.SaveSerializedCoverage("filePath", "serialized coverage"); mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.CreateCollection("FCC.DynamicCoverageStore"), Times.Exactly(collectionExists ? 0 : 1)); mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.SetString("FCC.DynamicCoverageStore", "filePath", "serialized"), Times.Once); @@ -76,6 +83,7 @@ public void Should_Return_Null_For_GetSerializedCoverage_When_Collection_Does_No public void Should_Return_From_Collection_When_Property_Exists(bool propertyExists) { var autoMoqer = new AutoMoqer(); + var mockWritableSettingsStore = new Mock(); mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.CollectionExists("FCC.DynamicCoverageStore")).Returns(true); mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.PropertyExists("FCC.DynamicCoverageStore", "filePath")).Returns(propertyExists); @@ -83,11 +91,24 @@ public void Should_Return_From_Collection_When_Property_Exists(bool propertyExis autoMoqer.Setup( writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(mockWritableSettingsStore.Object); + var deserializedCoverageWhen = new SerializedCoverageWhen { When = DateTime.Now, Serialized = "serializedCoverage coverage" }; + var mockJsonConvertService = autoMoqer.GetMock(); + mockJsonConvertService.Setup(jsonConvertService => jsonConvertService.DeserializeObject("serialized")) + .Returns(deserializedCoverageWhen); + var dynamicCoverageStore = autoMoqer.Create(); - var serializedCoverage = dynamicCoverageStore.GetSerializedCoverage("filePath"); + var serializedCoverageWhen = dynamicCoverageStore.GetSerializedCoverage("filePath"); - Assert.AreEqual(propertyExists ? "serialized" : null, serializedCoverage); + if (propertyExists) + { + Assert.AreSame(deserializedCoverageWhen, serializedCoverageWhen); + } + else + { + Assert.Null(serializedCoverageWhen); + + } } private void FileRename( diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs index 5e80d3c5..497ec787 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs @@ -5,20 +5,13 @@ namespace FineCodeCoverageTests.Editor.DynamicCoverage { internal class LineExcluder_Tests { - [TestCase(true, " ", true)] - [TestCase(true, " //", true)] - [TestCase(true, " #pragma", true)] - [TestCase(true, " using", true)] - [TestCase(true, " not excluded", false)] - [TestCase(false, " ", true)] - [TestCase(false, " '", true)] - [TestCase(false, " REM", true)] - [TestCase(false, " #pragma", true)] - [TestCase(true, " not excluded", false)] - public void Should_Exclude_If_Not_Code(bool isCSharp, string text, bool expectedExclude) + [TestCase(new string[0]," ", true)] + [TestCase(new string[0], " x", false)] + [TestCase(new string[] { "y", "x"}, " x", true)] + public void Should_Exclude_If_Not_Code(string[] exclusions, string text, bool expectedExclude) { - var codeLineExcluder = new LineExcluder(); - var exclude = codeLineExcluder.ExcludeIfNotCode(text, isCSharp); + var codeLineExcluder = new LineExcluder(exclusions); + var exclude = codeLineExcluder.ExcludeIfNotCode(text); Assert.That(exclude, Is.EqualTo(expectedExclude)); } } diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs index 5ce8a247..9f83d68b 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs @@ -12,7 +12,7 @@ internal class NewCodeTracker_Tests [Test] public void Should_Have_No_Lines_Initially_When_Passed_No_Line_Numbers() { - var newCodeTracker = new NewCodeTracker(true, null,null); + var newCodeTracker = new NewCodeTracker(null,null); Assert.That(newCodeTracker.Lines, Is.Empty); } @@ -36,10 +36,10 @@ public void Should_Track_Not_Excluded_Line_Numbers_Passed_To_Ctor() trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 2) ).Returns(mockTrackedNewCodeLine2.Object); var mockLineExcluder = new Mock(); - mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("exclude", true)).Returns(true); - mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("notexclude", true)).Returns(false); + mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("exclude")).Returns(true); + mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("notexclude")).Returns(false); - var newCodeTracker = new NewCodeTracker(true, mockTrackedNewCodeLineFactory.Object, mockLineExcluder.Object, new List { 1, 2 }, textSnapshot); + var newCodeTracker = new NewCodeTracker(mockTrackedNewCodeLineFactory.Object, mockLineExcluder.Object, new List { 1, 2 }, textSnapshot); var line = newCodeTracker.Lines.Single(); Assert.That(line, Is.SameAs(expectedLine)); @@ -68,10 +68,9 @@ public void Should_Return_Lines_Ordered_By_Line_Number(bool reverseOrder) .Returns(mockSecondTrackedNewCodeLine.Object); var newCodeTracker = new NewCodeTracker( - true, - mockTrackedNewCodeLineFactory.Object, - new Mock().Object, - new List { 1, 2 }, + mockTrackedNewCodeLineFactory.Object, + new Mock().Object, + new List { 1, 2 }, textSnapshot ); @@ -90,7 +89,7 @@ public void Should_Return_Lines_Ordered_By_Line_Number(bool reverseOrder) public void Should_Add_New_TrackedNewCodeLines_For_Non_Excluded_New_Start_Lines(bool exclude) { var textSnapshot = new Mock().Object; - + var mockNewDynamicLine = new Mock(); mockNewDynamicLine.SetupGet(l => l.Number).Returns(10); var newDynamicLine = mockNewDynamicLine.Object; @@ -101,19 +100,18 @@ public void Should_Add_New_TrackedNewCodeLines_For_Non_Excluded_New_Start_Lines( mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) ).Returns(mockTrackedNewCodeLine.Object); - + var mockLineExcluder = new Mock(); - mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("text", true)).Returns(exclude); + mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("text")).Returns(exclude); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object, mockLineExcluder.Object ); var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( - textSnapshot, - new List { new SpanAndLineRange(new Span(),1,2), new SpanAndLineRange(new Span(), 1, 2) }, + textSnapshot, + new List { new SpanAndLineRange(new Span(), 1, 2), new SpanAndLineRange(new Span(), 1, 2) }, null); if (exclude) @@ -130,7 +128,6 @@ public void Should_Add_New_TrackedNewCodeLines_For_Non_Excluded_New_Start_Lines( Assert.That(changedLineNumbers.Single(), Is.EqualTo(1)); } - } [Test] @@ -138,12 +135,12 @@ public void Should_Not_Have_Changed_Lines_When_Line_Exists_And_Not_Updated_Or_Ex { var textSnapshot = new Mock().Object; var currentTextSnapshot = new Mock().Object; - + var mockTrackedNewCodeLine = new Mock(); var dynamicLine = new Mock().Object; mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(dynamicLine); mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.Update(currentTextSnapshot)) - .Returns(new TrackedNewCodeLineUpdate("",1,1)); + .Returns(new TrackedNewCodeLineUpdate("", 1, 1)); mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.GetText(textSnapshot)).Returns("exclude"); var mockTrackedNewCodeLineFactory = new Mock(); mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => @@ -151,11 +148,11 @@ public void Should_Not_Have_Changed_Lines_When_Line_Exists_And_Not_Updated_Or_Ex ).Returns(mockTrackedNewCodeLine.Object); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( - currentTextSnapshot, - new List { new SpanAndLineRange(new Span(),1,1) }, + currentTextSnapshot, + new List { new SpanAndLineRange(new Span(), 1, 1) }, null); Assert.That(changedLineNumbers, Is.Empty); @@ -180,9 +177,9 @@ public void Should_Have_Changed_Line_When_Line_Exists_And_Excluded() ).Returns(mockTrackedNewCodeLine.Object); var mockLineExcluder = new Mock(); - mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("updated", true)).Returns(true); + mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("updated")).Returns(true); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object,mockLineExcluder.Object, new List { 1 }, textSnapshot); + mockTrackedNewCodeLineFactory.Object, mockLineExcluder.Object, new List { 1 }, textSnapshot); var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( currentTextSnapshot, @@ -210,11 +207,11 @@ public void Should_Have_Old_And_New_Line_Numbers_When_Line_Number_Updated() ).Returns(mockTrackedNewCodeLine.Object); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( currentTextSnapshot, - new List { }, + new List { }, null); Assert.That(changedLineNumbers, Is.EqualTo(new List { 1, 2 })); @@ -246,7 +243,7 @@ public void Should_Use_The_New_Line_Number_To_Reduce_Possible_New_Lines(bool new ).Returns(newMockTrackedNewCodeLine.Object); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); var potentialNewLineNumber = newLineNumberReduces ? 2 : 3; var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( @@ -264,8 +261,8 @@ public void Should_Use_The_New_Line_Number_To_Reduce_Possible_New_Lines(bool new Assert.That(changedLineNumbers, Is.EqualTo(new List { 1, 2, 3 })); Assert.That(newCodeTracker.Lines.Count(), Is.EqualTo(2)); } - - + + } #endregion @@ -285,12 +282,12 @@ public void Should_Have_No_Changes_When_All_CodeSpanRange_Start_Line_Numbers_Are ).Returns(mockTrackedNewCodeLine.Object); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( new Mock().Object, null, - new List { new CodeSpanRange(1,3)}); + new List { new CodeSpanRange(1, 3) }); Assert.That(changedLineNumbers, Is.Empty); Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(mockDynamicLine.Object)); @@ -310,14 +307,14 @@ public void Should_Have_Single_Changed_Line_Number_When_CodeSpanRange_Start_Line ).Returns(mockTrackedNewCodeLine.Object); var newCodeTracker = new NewCodeTracker( - true, mockTrackedNewCodeLineFactory.Object, null); + mockTrackedNewCodeLineFactory.Object, null); var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( currentTextSnapshot, null, new List { new CodeSpanRange(1, 3) }); - Assert.That(changedLineNumbers.Single(),Is.EqualTo(1)); + Assert.That(changedLineNumbers.Single(), Is.EqualTo(1)); Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(dynamicLine)); } @@ -348,10 +345,9 @@ public void Should_Remove_Tracked_Lines_That_Are_Not_CodeSpanRange_Start() ).Returns(mockRemovedTrackedNewCodeLine2.Object); var newCodeTracker = new NewCodeTracker( - true, - mockTrackedNewCodeLineFactory.Object, - new Mock().Object, - new List { 1,2,}, + mockTrackedNewCodeLineFactory.Object, + new Mock().Object, + new List { 1, 2, }, textSnapshot); Assert.That(newCodeTracker.Lines.Count(), Is.EqualTo(2)); diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/RoslynFileCodeSpanRangeService_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/RoslynFileCodeSpanRangeService_Tests.cs new file mode 100644 index 00000000..28e1b680 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/RoslynFileCodeSpanRangeService_Tests.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using AutoMoq; +using FineCodeCoverage.Core.Utilities.VsThreading; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; +using FineCodeCoverage.Editor.Roslyn; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.TestHelpers; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class RoslynFileCodeSpanRangeService_Tests + { + [TestCase(EditorCoverageColouringMode.UseRoslynWhenTextChanges,true)] + [TestCase(EditorCoverageColouringMode.DoNotUseRoslynWhenTextChanges, false)] + public void Should_UseFileCodeSpanRangeServiceForChanges_When_Options_Not_DoNotUseRoslynWhenTextChanges( + EditorCoverageColouringMode editorCoverageColouringMode, + bool expectedUseFileCodeSpanRangeServiceForChanges + ) + { + var autoMoqer = new AutoMoqer(); + autoMoqer.SetInstance(new TestThreadHelper()); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode).Returns(editorCoverageColouringMode); + var mockAppOptionsProvider = autoMoqer.GetMock(); + mockAppOptionsProvider.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); + + var roslynFileCodeSpanRangeService = autoMoqer.Create(); + + Assert.That(roslynFileCodeSpanRangeService.UseFileCodeSpanRangeServiceForChanges, Is.EqualTo(expectedUseFileCodeSpanRangeServiceForChanges)); + } + + [Test] + public void Should_GetFileCodeSpanRanges_By_Converting_The_Coverage_TextSpans_Using_The_TextSnapshot() + { + var autoMoqer = new AutoMoqer(); + autoMoqer.SetInstance(new TestThreadHelper()); + + var mockTextSnapshot = new Mock(); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(0)).Returns(1); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(10)).Returns(2); + var mockRoslynService = autoMoqer.GetMock(); + mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) + .ReturnsAsync(new List { new TextSpan(0, 10) }); + + var roslynFileCodeSpanRangeService = autoMoqer.Create(); + var fileCodeSpanRanges = roslynFileCodeSpanRangeService.GetFileCodeSpanRanges(mockTextSnapshot.Object); + + Assert.That(fileCodeSpanRanges, Is.EqualTo(new List { new CodeSpanRange(1, 2) })); + } + + [Test] + public void Should_Return_Itself_As_FileCodeSpanRangeService() + { + var autoMoqer = new AutoMoqer(); + var roslynFileCodeSpanRangeService = autoMoqer.Create(); + + Assert.That(roslynFileCodeSpanRangeService.FileCodeSpanRangeService, Is.SameAs(roslynFileCodeSpanRangeService)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs index 2317b786..f2c560a2 100644 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs @@ -10,9 +10,9 @@ internal class SerializedState_Tests [Test] public void Is_Serializable() { - var states = new List + var states = new List { - new SerializedState(new CodeSpanRange(1,5), ContainingCodeTrackerType.OtherLines, new List + new SerializedContainingCodeTracker(new CodeSpanRange(1,5), ContainingCodeTrackerType.OtherLines, new List { new DynamicLine(1, DynamicCoverageType.Dirty) }) @@ -21,7 +21,7 @@ public void Is_Serializable() var jsonConvertService = new JsonConvertService(); var serialized = jsonConvertService.SerializeObject(states); - var deserialized = jsonConvertService.DeserializeObject>(serialized); + var deserialized = jsonConvertService.DeserializeObject>(serialized); var state = deserialized[0]; Assert.That(state.Lines.Count, Is.EqualTo(1)); Assert.That(state.Lines[0].Number, Is.EqualTo(1)); diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TextSnapshotLineExcluder_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TextSnapshotLineExcluder_Tests.cs deleted file mode 100644 index 6ee3ed70..00000000 --- a/FineCodeCoverageTests/Editor/DynamicCoverage/TextSnapshotLineExcluder_Tests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FineCodeCoverage.Editor.DynamicCoverage; -using Microsoft.VisualStudio.Text; -using Moq; -using NUnit.Framework; - -namespace FineCodeCoverageTests.Editor.DynamicCoverage -{ - internal class TextSnapshotLineExcluder_Tests - { - [TestCase(true,true)] - [TestCase(false,true)] - [TestCase(true, false)] - [TestCase(false, false)] - public void Should_Delegate(bool isCSharp, bool exclude) - { - var textSnapshot = new Mock().Object; - var mockTextSnapshotText = new Mock(); - mockTextSnapshotText.Setup(textSnapshotText => textSnapshotText.GetLineText(textSnapshot, 1)).Returns("line text"); - var mockLineExcluder = new Mock(); - mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("line text", isCSharp)).Returns(exclude); - var textSnapshotLineExcluder = new TextSnapshotLineExcluder(mockTextSnapshotText.Object, mockLineExcluder.Object); - - Assert.That(textSnapshotLineExcluder.ExcludeIfNotCode(textSnapshot, 1, isCSharp), Is.EqualTo(exclude)); - } - } -} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/VBCoverageContentType_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/VBCoverageContentType_Tests.cs new file mode 100644 index 00000000..b86952f8 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/VBCoverageContentType_Tests.cs @@ -0,0 +1,59 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class VBCoverageContentType_Tests + { + [Test] + public void Should_Have_Basic_Content_Type() + { + Assert.That(new VBCoverageContentType(null).ContentTypeName, Is.EqualTo("Basic")); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Delegate_To_IRoslynFileCodeSpanRangeService(bool roslynUseFileCodeSpanRangeServiceForChanges) + { + var fileCodeSpanRangeServiceFromRoslyn = new Mock().Object; + var mockRoslynFileCodeSpanRangeService = new Mock(); + mockRoslynFileCodeSpanRangeService.SetupGet(roslynFileCodeSpanRangeService => roslynFileCodeSpanRangeService.FileCodeSpanRangeService) + .Returns(fileCodeSpanRangeServiceFromRoslyn); + + mockRoslynFileCodeSpanRangeService.SetupGet(roslynFileCodeSpanRangeService => roslynFileCodeSpanRangeService.UseFileCodeSpanRangeServiceForChanges) + .Returns(roslynUseFileCodeSpanRangeServiceForChanges); + + var vbCoverageContentType = new VBCoverageContentType(mockRoslynFileCodeSpanRangeService.Object); + + Assert.That(vbCoverageContentType.FileCodeSpanRangeService, Is.SameAs(fileCodeSpanRangeServiceFromRoslyn)); + Assert.That(vbCoverageContentType.UseFileCodeSpanRangeServiceForChanges, Is.EqualTo(roslynUseFileCodeSpanRangeServiceForChanges)); + } + + [Test] + public void Should_Allow_For_ILine_Missed_By_Roslyn() + { + Assert.False(new VBCoverageContentType(null).CoverageOnlyFromFileCodeSpanRangeService); + } + + [Test] + public void Should_LineExclude_Comments() + { + Assert.True(new VBCoverageContentType(null).LineExcluder.ExcludeIfNotCode("'")); + } + + [Test] + public void Should_LineExclude_REM() + { + Assert.True(new VBCoverageContentType(null).LineExcluder.ExcludeIfNotCode("REM")); + } + + [Test] + public void Should_LineExclude_Compiler_Directives() + { + Assert.True(new VBCoverageContentType(null).LineExcluder.ExcludeIfNotCode("#")); + } + + } +} diff --git a/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs b/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs index fe6feedf..d30824bc 100644 --- a/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs +++ b/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs @@ -332,6 +332,25 @@ public interface IMyInterface "; AssertShouldNotVisit(text); } + + [Test] + public void Should_Delete_State_When_Visits() + { + var text = @" +public class MyClass +{ + public int MyProperty { get; set; } +} +"; + var cSharpContainingCodeVisitor = new CSharpContainingCodeVisitor(); + var compilation = ParseCompilation(text); + var textSpans1 = cSharpContainingCodeVisitor.GetSpans(compilation); + var textSpans2 = cSharpContainingCodeVisitor.GetSpans(compilation); + var nodes1 = cSharpContainingCodeVisitor.GetNodes(compilation); + var nodes2 = cSharpContainingCodeVisitor.GetNodes(compilation); + Assert.That(textSpans1.Count, Is.EqualTo(textSpans2.Count)); + Assert.That(nodes1.Count, Is.EqualTo(nodes2.Count)); + } } } diff --git a/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs b/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs index 47fc8e70..04a618f2 100644 --- a/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs +++ b/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs @@ -13,7 +13,7 @@ internal abstract class ContainingCodeVisitor_Tests_Base protected (List, SyntaxNode) Visit(string compilationText) { - var rootNode = ParseCompilation(compilationText); + var rootNode = ParseCompilation(compilationText); var textSpans = GetVisitor().GetSpans(rootNode); return (textSpans, rootNode); } diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs index 3d10a159..643afb77 100644 --- a/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs +++ b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs @@ -47,6 +47,7 @@ public void Should_Send_CoverageTypeFilterChangedMessage_With_The_New_Filter_Whe }; var autoMocker = new AutoMoqer(); + autoMocker.SetInstance(new IFileExcluder[0]); var mockAppOptionsProvider = autoMocker.GetMock(); mockAppOptionsProvider.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(firstOptions); @@ -79,6 +80,7 @@ public void Should_Use_The_Last_Filter_For_Comparisons() }; var autoMocker = new AutoMoqer(); + autoMocker.SetInstance(new IFileExcluder[0]); var mockAppOptionsProvider = autoMocker.GetMock(); var coverageTaggerProvider = autoMocker.Create>(); @@ -90,6 +92,7 @@ public void Should_Use_The_Last_Filter_For_Comparisons() public void Should_Not_Create_A_Coverage_Tagger_When_The_TextBuffer_Associated_Document_Has_No_FilePath() { var autoMocker = new AutoMoqer(); + autoMocker.SetInstance(new IFileExcluder[0]); var mockTextInfo = new Mock(); mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns((string)null); autoMocker.Setup(textInfoFactory => textInfoFactory.Create(It.IsAny(), It.IsAny())) @@ -101,11 +104,38 @@ public void Should_Not_Create_A_Coverage_Tagger_When_The_TextBuffer_Associated_D Assert.That(tagger, Is.Null); } + [TestCase(true)] + [TestCase(false)] + public void Should_Not_Create_A_Coverage_Tagger_When_FilePath_Is_Excluded(bool isExcluded) + { + var autoMocker = new AutoMoqer(); + var filePath = "filePath"; + var contentTypeTypeName = "contentType"; + var mockFirstFileExcluder = new Mock(); + mockFirstFileExcluder.Setup(fileExcluder => fileExcluder.Exclude(filePath)).Returns(true); + var mockSecondFileExcluder = new Mock(); + mockSecondFileExcluder.SetupGet(fileExcluder => fileExcluder.ContentTypeName).Returns(contentTypeTypeName); + mockSecondFileExcluder.Setup(fileExcluder => fileExcluder.Exclude(filePath)).Returns(isExcluded); + autoMocker.SetInstance(new IFileExcluder[] { mockFirstFileExcluder.Object, mockSecondFileExcluder.Object}); + var mockTextInfo = new Mock(); + mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns(filePath); + autoMocker.Setup(textInfoFactory => textInfoFactory.Create(It.IsAny(), It.IsAny())) + .Returns(mockTextInfo.Object); + var coverageTaggerProvider = autoMocker.Create>(); + var mockTextBuffer = new Mock(); + mockTextBuffer.SetupGet(textBuffer => textBuffer.ContentType.TypeName).Returns(contentTypeTypeName); + var tagger = coverageTaggerProvider.CreateTagger(new Mock().Object, mockTextBuffer.Object); + + Assert.That(tagger, isExcluded ? Is.Null : Is.Not.Null); + } + [TestCase] public void Should_Create_A_Coverage_Tagger_With_BufferLineCoverage_From_DynamicCoverageManager_And_Last_Coverage_Type_Filter_When_The_TextBuffer_Has_An_Associated_File_Document() { var textView = new Mock().Object; - var textBuffer = new Mock().Object; + var mockTextBuffer = new Mock(); + mockTextBuffer.SetupGet(tb => tb.ContentType.TypeName).Returns(""); + var textBuffer = mockTextBuffer.Object; DummyCoverageTypeFilter lastFilter = null; DummyCoverageTypeFilter.Initialized += (sender, args) => { @@ -117,6 +147,7 @@ public void Should_Create_A_Coverage_Tagger_With_BufferLineCoverage_From_Dynamic }; var autoMocker = new AutoMoqer(); + autoMocker.SetInstance(new IFileExcluder[0]); var mockTextInfo = new Mock(); mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("file"); autoMocker.Setup(textInfoFactory => textInfoFactory.Create(textView, textBuffer)).Returns(mockTextInfo.Object); diff --git a/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs index 6f4fa757..66ec48c9 100644 --- a/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs +++ b/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs @@ -1,4 +1,6 @@ -using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; using FineCodeCoverage.Editor.Tagging.Classification; using FineCodeCoverage.Editor.Tagging.GlyphMargin; using FineCodeCoverage.Editor.Tagging.OverviewMargin; @@ -20,7 +22,7 @@ public void Should_Only_Be_Interested_In_CSharp_VB_And_CPP() { var contentTypeAttributes = type.GetCustomAttributes(typeof(ContentTypeAttribute), false); var contentTypes = contentTypeAttributes.OfType().Select(ct => ct.ContentTypes); - Assert.That(contentTypes, Is.EqualTo(new[] { SupportedContentTypeLanguages.CSharp, SupportedContentTypeLanguages.VisualBasic, SupportedContentTypeLanguages.CPP })); + Assert.That(contentTypes, Is.EqualTo(new[] { CSharpCoverageContentType.ContentType, VBCoverageContentType.ContentType, CPPCoverageContentType.ContentType, BlazorCoverageContentType.ContentType })); }); } } diff --git a/FineCodeCoverageTests/FineCodeCoverageTests.csproj b/FineCodeCoverageTests/FineCodeCoverageTests.csproj index 20ad6b01..8a0ee2cb 100644 --- a/FineCodeCoverageTests/FineCodeCoverageTests.csproj +++ b/FineCodeCoverageTests/FineCodeCoverageTests.csproj @@ -70,7 +70,14 @@ + + + + + + + @@ -82,7 +89,6 @@ - diff --git a/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs index ea6ffcfc..9feac54e 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs @@ -731,5 +731,6 @@ internal class TestCoverageProjectOptions : IAppOptions public bool ShowNotIncludedInOverviewMargin { get; set; } public bool ShowNotIncludedInGlyphMargin { get; set; } public bool ShowLineNotIncludedHighlighting { get; set; } + public bool BlazorCoverageLinesFromGeneratedSource { get; set; } } } diff --git a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs index 033c0c8c..80cb96eb 100644 --- a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs +++ b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs @@ -392,8 +392,14 @@ public void Should_Not_Handle_OperationState_Changes_When_The_testOperationState RaiseTestExecutionCancelling(); Assert.That(invoked, Is.EqualTo(canInvoke)); - + } - } + [Test] + public void Should_Send_TestExecutionStartingMessage_When_TestExecutionStarting() + { + var operation = new Mock().Object; + RaiseTestExecutionStarting(operation); + mocker.Verify(eventAggregator => eventAggregator.SendMessage(It.IsAny(),null)); + } } } \ No newline at end of file diff --git a/README.md b/README.md index bbe04c07..d2e62576 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,13 @@ You can turn off editor colouring by setting the visual studio option EditorCove You can also set the option to DoNotUseRoslynWhenTextChanges if there is a performance issue. By doing so new lines colouring will not be as good. If you switch to one of the EditorCoverageColouringMode options then you will need to re-run coverage. +For Blazor components with @code blocks coverage lines can be generated outside these regions. +When the Roslyn syntax tree is available to FCC you can set the option BlazorCoverageLinesFromGeneratedSource to true to limit coverage lines in .razor file to those in generated source. + +FCC tracks the visual studio editor and saves this information when a file is closed. If upon re-opening a file the text has changed outside of a document window there will be no coverage marks for this file as the coverage lines are no longer expected to be correct.. + +There will also be no editor marks if you edit a file whilst FCC is collecting coverage. + ## Why use MS Code Coverage ? With the old coverage FCC needed to copy your test dll and dependencies and run OpenCover or Coverlet on those files. This is not necessary with ms code coverage. @@ -284,6 +291,7 @@ If you are using option 1) then project and global options will only be used whe |--|---| |**Common**|| |EditorCoverageColouringMode|Set to Off, or Set to DoNotUseRoslynWhenTextChanges if there is a performance issue| +|BlazorCoverageLinesFromGeneratedSource|Set to true to limit coverage lines in .razor file to those in generated source ( when available)| |ShowEditorCoverage|Set to false to disable all editor coverage indicators| |ShowCoverageInGlyphMargin|Set to false to prevent coverage marks in the glyph margin| |ShowCoveredInGlyphMargin|Set to false to prevent covered marks in the glyph margin| diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorCoverageContentType.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorCoverageContentType.cs new file mode 100644 index 00000000..5667a094 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorCoverageContentType.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; +using FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + [Export(typeof(ICoverageContentType))] + [Export(typeof(IFileExcluder))] + internal class BlazorCoverageContentType : ICoverageContentType, IFileExcluder + { + [ImportingConstructor] + public BlazorCoverageContentType( + IBlazorFileCodeSpanRangeService blazorFileCodeSpanRangeService, + IAppOptionsProvider appOptionsProvider + ) + { + this.blazorFileCodeSpanRangeService = blazorFileCodeSpanRangeService; + this.appOptionsProvider = appOptionsProvider; + } + + public const string ContentType = "Razor"; + private readonly IBlazorFileCodeSpanRangeService blazorFileCodeSpanRangeService; + private readonly IAppOptionsProvider appOptionsProvider; + + public string ContentTypeName => ContentType; + + public IFileCodeSpanRangeService FileCodeSpanRangeService => this.blazorFileCodeSpanRangeService; + + public bool CoverageOnlyFromFileCodeSpanRangeService => this.appOptionsProvider.Get().BlazorCoverageLinesFromGeneratedSource; + + // Unfortunately, the generated docuent from the workspace is not up to date + public bool UseFileCodeSpanRangeServiceForChanges => false; + + public ILineExcluder LineExcluder { get; } = new LineExcluder( + CSharpCoverageContentType.Exclusions.Concat(new string[] { "<", "@" }).ToArray() + ); + + public bool Exclude(string filePath) => Path.GetExtension(filePath) != ".razor"; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorFileCodeSpanRangeService.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorFileCodeSpanRangeService.cs new file mode 100644 index 00000000..d0c3581c --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorFileCodeSpanRangeService.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using FineCodeCoverage.Core.Utilities.VsThreading; +using FineCodeCoverage.Editor.DynamicCoverage.Utilities; +using FineCodeCoverage.Editor.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + [Export(typeof(IBlazorFileCodeSpanRangeService))] + internal class BlazorFileCodeSpanRangeService : IBlazorFileCodeSpanRangeService + { + private readonly IBlazorGeneratedDocumentRootFinder blazorGeneratedDocumentRootFinder; + private readonly ICSharpCodeCoverageNodeVisitor cSharpCodeCoverageNodeVisitor; + private readonly ISyntaxNodeLocationMapper syntaxNodeLocationMapper; + private readonly ITextInfoFactory textInfoFactory; + private readonly IBlazorGeneratedFilePathMatcher blazorGeneratedFilePathMatcher; + private readonly IThreadHelper threadHelper; + + [ImportingConstructor] + public BlazorFileCodeSpanRangeService( + IBlazorGeneratedDocumentRootFinder blazorGeneratedDocumentRootFinder, + ICSharpCodeCoverageNodeVisitor cSharpCodeCoverageNodeVisitor, + ISyntaxNodeLocationMapper syntaxNodeLocationMapper, + ITextInfoFactory textInfoFactory, + IBlazorGeneratedFilePathMatcher blazorGeneratedFilePathMatcher, + IThreadHelper threadHelper + ) + { + this.blazorGeneratedDocumentRootFinder = blazorGeneratedDocumentRootFinder; + this.cSharpCodeCoverageNodeVisitor = cSharpCodeCoverageNodeVisitor; + this.syntaxNodeLocationMapper = syntaxNodeLocationMapper; + this.textInfoFactory = textInfoFactory; + this.blazorGeneratedFilePathMatcher = blazorGeneratedFilePathMatcher; + this.threadHelper = threadHelper; + } + + public List GetFileCodeSpanRanges(ITextSnapshot snapshot) + { + string filePath = this.textInfoFactory.GetFilePath(snapshot.TextBuffer); + SyntaxNode generatedDocumentSyntaxRoot = this.threadHelper.JoinableTaskFactory.Run( + () => this.blazorGeneratedDocumentRootFinder.FindSyntaxRootAsync(snapshot.TextBuffer, filePath, this.blazorGeneratedFilePathMatcher) + ); + if (generatedDocumentSyntaxRoot != null) + { + + List nodes = this.cSharpCodeCoverageNodeVisitor.GetNodes(generatedDocumentSyntaxRoot); + if(nodes.Count == 0) + { + return null; // sometimes the generated document has not been generated + } + + return nodes.Select(node => new { Node = node, MappedLineSpan = this.syntaxNodeLocationMapper.Map(node) }) + .Where(a => a.MappedLineSpan.Path == filePath) + .Select(a => new CodeSpanRange( + a.MappedLineSpan.StartLinePosition.Line, + a.MappedLineSpan.EndLinePosition.Line) + ).ToList(); + } + + return null; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorGeneratedDocumentRootFinder.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorGeneratedDocumentRootFinder.cs new file mode 100644 index 00000000..a8d79357 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorGeneratedDocumentRootFinder.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IBlazorGeneratedDocumentRootFinder))] + internal class BlazorGeneratedDocumentRootFinder : IBlazorGeneratedDocumentRootFinder + { + public async Task FindSyntaxRootAsync(ITextBuffer textBuffer, string filePath, IBlazorGeneratedFilePathMatcher blazorGeneratedFilePathMatcher) + { + Workspace ws = textBuffer.GetWorkspace(); + if (ws != null) + { + IEnumerable projects = ws.CurrentSolution.Projects; + foreach (Project project in projects) + { + foreach (Document document in project.Documents) + { + string docFilePath = document.FilePath; + if (blazorGeneratedFilePathMatcher.IsBlazorGeneratedFilePath(filePath,docFilePath)) + { + return await document.GetSyntaxRootAsync(); + } + } + } + } + + return null; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorGeneratedFilePathMatcher.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorGeneratedFilePathMatcher.cs new file mode 100644 index 00000000..82563a1c --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/BlazorGeneratedFilePathMatcher.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + [Export(typeof(IBlazorGeneratedFilePathMatcher))] + internal class BlazorGeneratedFilePathMatcher : IBlazorGeneratedFilePathMatcher + { + public bool IsBlazorGeneratedFilePath(string razorFilePath, string generatedFilePath) + => generatedFilePath.StartsWith($"{razorFilePath}."); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorFileCodeSpanRangeService.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorFileCodeSpanRangeService.cs new file mode 100644 index 00000000..76a0c061 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorFileCodeSpanRangeService.cs @@ -0,0 +1,4 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + internal interface IBlazorFileCodeSpanRangeService : IFileCodeSpanRangeService { } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorGeneratedDocumentRootFinder.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorGeneratedDocumentRootFinder.cs new file mode 100644 index 00000000..7d4f0e61 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorGeneratedDocumentRootFinder.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + internal interface IBlazorGeneratedDocumentRootFinder + { + Task FindSyntaxRootAsync(ITextBuffer textBuffer, string filePath, IBlazorGeneratedFilePathMatcher razorGeneratedFilePathMatcher); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorGeneratedFilePathMatcher.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorGeneratedFilePathMatcher.cs new file mode 100644 index 00000000..d0fac2dc --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Blazor/IBlazorGeneratedFilePathMatcher.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor +{ + interface IBlazorGeneratedFilePathMatcher + { + bool IsBlazorGeneratedFilePath(string razorFilePath, string generatedfilePath); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/CPPCoverageContentType.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/CPPCoverageContentType.cs new file mode 100644 index 00000000..15fb846b --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/CPPCoverageContentType.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes +{ + [Export(typeof(ICoverageContentType))] + internal class CPPCoverageContentType : ICoverageContentType + { + public const string ContentType = "C/C++"; + public string ContentTypeName => ContentType; + + /* + todo - https://learn.microsoft.com/en-us/previous-versions/t41260xs(v=vs.140) + non C++ https://learn.microsoft.com/en-us/dotnet/api/envdte80.filecodemodel2?view=visualstudiosdk-2022 + */ + public IFileCodeSpanRangeService FileCodeSpanRangeService => null; + + // not relevant + [ExcludeFromCodeCoverage] + public bool CoverageOnlyFromFileCodeSpanRangeService => false; + [ExcludeFromCodeCoverage] + public bool UseFileCodeSpanRangeServiceForChanges => false; + + public ILineExcluder LineExcluder => null; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/CSharpCoverageContentType.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/CSharpCoverageContentType.cs new file mode 100644 index 00000000..8da56fc9 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/CSharpCoverageContentType.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.Composition; +using FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn +{ + [Export(typeof(ICoverageContentType))] + internal class CSharpCoverageContentType : ICoverageContentType + { + [ImportingConstructor] + public CSharpCoverageContentType(IRoslynFileCodeSpanRangeService roslynFileCodeSpanRangeService) + => this.roslynFileCodeSpanRangeService = roslynFileCodeSpanRangeService; + + public const string ContentType = "CSharp"; + private readonly IRoslynFileCodeSpanRangeService roslynFileCodeSpanRangeService; + + public string ContentTypeName => ContentType; + + public IFileCodeSpanRangeService FileCodeSpanRangeService + => this.roslynFileCodeSpanRangeService.FileCodeSpanRangeService; + + public bool UseFileCodeSpanRangeServiceForChanges + => this.roslynFileCodeSpanRangeService.UseFileCodeSpanRangeServiceForChanges; + + public bool CoverageOnlyFromFileCodeSpanRangeService => false; + + public static string[] Exclusions { get; } = new string[] { "//", "#", "using" }; + + public ILineExcluder LineExcluder { get; } = new LineExcluder(Exclusions); + + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/IRoslynFileCodeSpanRangeService.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/IRoslynFileCodeSpanRangeService.cs new file mode 100644 index 00000000..50ba8ca6 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/IRoslynFileCodeSpanRangeService.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn +{ + internal interface IRoslynFileCodeSpanRangeService + { + IFileCodeSpanRangeService FileCodeSpanRangeService { get; } + bool UseFileCodeSpanRangeServiceForChanges { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/RoslynFileCodeSpanRangeService.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/RoslynFileCodeSpanRangeService.cs new file mode 100644 index 00000000..53a1524a --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/RoslynFileCodeSpanRangeService.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using FineCodeCoverage.Core.Utilities.VsThreading; +using FineCodeCoverage.Editor.Roslyn; +using FineCodeCoverage.Options; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn +{ + [Export(typeof(IRoslynFileCodeSpanRangeService))] + internal class RoslynFileCodeSpanRangeService : IFileCodeSpanRangeService, IRoslynFileCodeSpanRangeService + { + private readonly IRoslynService roslynService; + private readonly IAppOptionsProvider appOptionsProvider; + private readonly IThreadHelper threadHelper; + + [ImportingConstructor] + public RoslynFileCodeSpanRangeService( + IRoslynService roslynService, + IAppOptionsProvider appOptionsProvider, + IThreadHelper threadHelper + ) + { + + this.roslynService = roslynService; + this.appOptionsProvider = appOptionsProvider; + this.threadHelper = threadHelper; + } + + private CodeSpanRange GetCodeSpanRange(TextSpan span, ITextSnapshot textSnapshot) + { + int startLine = textSnapshot.GetLineNumberFromPosition(span.Start); + int endLine = textSnapshot.GetLineNumberFromPosition(span.End); + return new CodeSpanRange(startLine, endLine); + } + + public List GetFileCodeSpanRanges(ITextSnapshot snapshot) + { + List textSpans = this.threadHelper.JoinableTaskFactory.Run( + () => this.roslynService.GetContainingCodeSpansAsync(snapshot) + ); + + return textSpans.Select(textSpan => this.GetCodeSpanRange(textSpan, snapshot)).ToList(); + } + + public IFileCodeSpanRangeService FileCodeSpanRangeService => this; + + public bool UseFileCodeSpanRangeServiceForChanges + => this.appOptionsProvider.Get().EditorCoverageColouringMode != EditorCoverageColouringMode.DoNotUseRoslynWhenTextChanges; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/VBCoverageContentType.cs b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/VBCoverageContentType.cs new file mode 100644 index 00000000..a1852dbc --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/ContentTypes/Roslyn/VBCoverageContentType.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.Composition; +using FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction; + +namespace FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn +{ + [Export(typeof(ICoverageContentType))] + internal class VBCoverageContentType : ICoverageContentType + { + [ImportingConstructor] + public VBCoverageContentType(IRoslynFileCodeSpanRangeService roslynFileCodeSpanRangeService) + => this.roslynFileCodeSpanRangeService = roslynFileCodeSpanRangeService; + + public const string ContentType = "Basic"; + private readonly IRoslynFileCodeSpanRangeService roslynFileCodeSpanRangeService; + + public string ContentTypeName => ContentType; + + public IFileCodeSpanRangeService FileCodeSpanRangeService + => this.roslynFileCodeSpanRangeService.FileCodeSpanRangeService; + + public bool CoverageOnlyFromFileCodeSpanRangeService => false; + + public bool UseFileCodeSpanRangeServiceForChanges + => this.roslynFileCodeSpanRangeService.UseFileCodeSpanRangeServiceForChanges; + + public ILineExcluder LineExcluder { get; } = new LineExcluder(new string[] { "REM", "'", "#" }); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs index 04df9b03..09f41c56 100644 --- a/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs +++ b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs @@ -2,57 +2,73 @@ using System.Collections.Generic; using System.Linq; using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Editor.Tagging.Base; using FineCodeCoverage.Engine; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Impl; using FineCodeCoverage.Options; using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; namespace FineCodeCoverage.Editor.DynamicCoverage { - internal class BufferLineCoverage : IBufferLineCoverage, IListener + internal class BufferLineCoverage : + IBufferLineCoverage, IListener, IListener { private readonly ITextInfo textInfo; private readonly IEventAggregator eventAggregator; private readonly ITrackedLinesFactory trackedLinesFactory; private readonly IDynamicCoverageStore dynamicCoverageStore; private readonly IAppOptionsProvider appOptionsProvider; - private readonly Language language; + private readonly ILogger logger; private readonly ITextBuffer2 textBuffer; - private ITrackedLines trackedLines; private bool? editorCoverageModeOff; private IFileLineCoverage fileLineCoverage; + private Nullable lastChanged; + private DateTime lastTestExecutionStarting; + + public ITrackedLines TrackedLines { get; set; } + + internal enum SerializedCoverageState + { + NotSerialized, OutOfDate, Ok + } + public BufferLineCoverage( - IFileLineCoverage fileLineCoverage, + ILastCoverage lastCoverage, ITextInfo textInfo, IEventAggregator eventAggregator, ITrackedLinesFactory trackedLinesFactory, IDynamicCoverageStore dynamicCoverageStore, - IAppOptionsProvider appOptionsProvider + IAppOptionsProvider appOptionsProvider, + ILogger logger ) { - this.fileLineCoverage = fileLineCoverage; - this.language = SupportedContentTypeLanguages.GetLanguage(textInfo.TextBuffer.ContentType.TypeName); + if (lastCoverage != null) + { + this.fileLineCoverage = lastCoverage.FileLineCoverage; + this.lastTestExecutionStarting = lastCoverage.TestExecutionStartingDate; + } + this.textBuffer = textInfo.TextBuffer; this.textInfo = textInfo; this.eventAggregator = eventAggregator; this.trackedLinesFactory = trackedLinesFactory; this.dynamicCoverageStore = dynamicCoverageStore; this.appOptionsProvider = appOptionsProvider; + this.logger = logger; void AppOptionsChanged(IAppOptions appOptions) { bool newEditorCoverageModeOff = appOptions.EditorCoverageColouringMode == EditorCoverageColouringMode.Off; - if (this.trackedLines != null && newEditorCoverageModeOff && this.editorCoverageModeOff != newEditorCoverageModeOff) + this.editorCoverageModeOff = newEditorCoverageModeOff; + if (this.TrackedLines != null && newEditorCoverageModeOff) { - this.trackedLines = null; + this.TrackedLines = null; this.SendCoverageChangedMessage(); } - - this.editorCoverageModeOff = newEditorCoverageModeOff; } appOptionsProvider.OptionsChanged += AppOptionsChanged; - if (fileLineCoverage != null) + if (this.fileLineCoverage != null) { this.CreateTrackedLinesIfRequired(true); } @@ -61,7 +77,7 @@ void AppOptionsChanged(IAppOptions appOptions) this.textBuffer.ChangedOnBackground += this.TextBuffer_ChangedOnBackground; void textViewClosedHandler(object s, EventArgs e) { - this.UpdateDynamicCoverageStore(); + this.UpdateDynamicCoverageStore((s as ITextView).TextSnapshot); this.textBuffer.Changed -= this.TextBuffer_ChangedOnBackground; textInfo.TextView.Closed -= textViewClosedHandler; appOptionsProvider.OptionsChanged -= AppOptionsChanged; @@ -71,11 +87,24 @@ void textViewClosedHandler(object s, EventArgs e) textInfo.TextView.Closed += textViewClosedHandler; } - private void UpdateDynamicCoverageStore() + private void UpdateDynamicCoverageStore(ITextSnapshot textSnapshot) { - if (this.trackedLines != null) + if (this.TrackedLines != null) { - this.dynamicCoverageStore.SaveSerializedCoverage(this.textInfo.FilePath, this.trackedLinesFactory.Serialize(this.trackedLines)); + string snapshotText = textSnapshot.GetText(); + if (this.FileSystemReflectsTrackedLines(snapshotText)) + { + // this only applies to the last coverage run. + // the DynamicCoverageStore ensures this is removed when next coverage is run + this.dynamicCoverageStore.SaveSerializedCoverage( + this.textInfo.FilePath, + this.trackedLinesFactory.Serialize(this.TrackedLines, snapshotText) + ); + } + else + { + this.dynamicCoverageStore.RemoveSerializedCoverage(this.textInfo.FilePath); + } } else { @@ -83,64 +112,141 @@ private void UpdateDynamicCoverageStore() } } + //todo - behaviour if exception reading text + private bool FileSystemReflectsTrackedLines(string snapshotText) + => this.textInfo.GetFileText() == snapshotText; + private void CreateTrackedLinesIfRequired(bool initial) { if (this.EditorCoverageColouringModeOff()) { - this.trackedLines = null; + this.TrackedLines = null; } else + { + this.TryCreateTrackedLines(initial); + } + } + + private void TryCreateTrackedLines(bool initial) + { + try { this.CreateTrackedLines(initial); } + catch (Exception e) + { + this.logger.Log($"Error creating tracked lines for {this.textInfo.FilePath}", e); + } } private void CreateTrackedLinesIfRequiredWithMessage() { - bool hadTrackedLines = this.trackedLines != null; - this.CreateTrackedLinesIfRequired(false); - bool hasTrackedLines = this.trackedLines != null; + bool hadTrackedLines = this.TrackedLines != null; + if (!this.lastChanged.HasValue || this.lastChanged < this.lastTestExecutionStarting) + { + this.CreateTrackedLinesIfRequired(false); + } + else + { + this.logger.Log($"Not creating editor marks for {this.textInfo.FilePath} as it was changed after test execution started"); + this.TrackedLines = null; + } + + bool hasTrackedLines = this.TrackedLines != null; if (hadTrackedLines || hasTrackedLines) { this.SendCoverageChangedMessage(); } } + private (SerializedCoverageState,string) GetSerializedCoverageInfo(SerializedCoverageWhen serializedCoverageWhen) + { + DateTime lastWriteTime = this.textInfo.GetLastWriteTime(); + + + if (serializedCoverageWhen == null) + { + SerializedCoverageState state = lastWriteTime > this.lastTestExecutionStarting ? + SerializedCoverageState.OutOfDate : + SerializedCoverageState.NotSerialized; + return (state, null); + } + + /* + If there is a When then it applies to the current coverage run ( as DynamicCoverageStore removes ) + as When is written when the text view is closed it is always - LastWriteTime < When + */ + return serializedCoverageWhen.When < lastWriteTime + ? ((SerializedCoverageState, string))(SerializedCoverageState.OutOfDate, null) + : (SerializedCoverageState.Ok, serializedCoverageWhen.Serialized); + } + private void CreateTrackedLines(bool initial) { + string filePath = this.textInfo.FilePath; ITextSnapshot currentSnapshot = this.textBuffer.CurrentSnapshot; if (initial) { - string serializedCoverage = this.dynamicCoverageStore.GetSerializedCoverage(this.textInfo.FilePath); - if (serializedCoverage != null) + SerializedCoverageWhen serializedCoverageWhen = this.dynamicCoverageStore.GetSerializedCoverage( + filePath + ); + (SerializedCoverageState state, string serializedCoverage) = this.GetSerializedCoverageInfo(serializedCoverageWhen); + switch (state) { - this.trackedLines = this.trackedLinesFactory.Create(serializedCoverage, currentSnapshot, this.language); - return; + case SerializedCoverageState.NotSerialized: + break; + case SerializedCoverageState.Ok: + this.TrackedLines = this.trackedLinesFactory.Create( + serializedCoverage, currentSnapshot, filePath); + return; + default: // Out of date + this.logger.Log($"Not creating editor marks for {this.textInfo.FilePath} as coverage is out of date"); + return; } } var lines = this.fileLineCoverage.GetLines(this.textInfo.FilePath).ToList(); - this.trackedLines = this.trackedLinesFactory.Create(lines, currentSnapshot, this.language); + this.TrackedLines = this.trackedLinesFactory.Create(lines, currentSnapshot, filePath); } private bool EditorCoverageColouringModeOff() { + // as handling the event do not need to check the value again + if (this.editorCoverageModeOff.HasValue) + { + return this.editorCoverageModeOff.Value; + } + this.editorCoverageModeOff = this.appOptionsProvider.Get().EditorCoverageColouringMode == EditorCoverageColouringMode.Off; return this.editorCoverageModeOff.Value; } - private void TextBuffer_ChangedOnBackground(object sender, TextContentChangedEventArgs e) + private void TextBuffer_ChangedOnBackground(object sender, TextContentChangedEventArgs textContentChangedEventArgs) { - if (this.trackedLines != null) + this.lastChanged = DateTime.Now; + if (this.TrackedLines != null) { - this.UpdateTrackedLines(e); + this.TryUpdateTrackedLines(textContentChangedEventArgs); } } - private void UpdateTrackedLines(TextContentChangedEventArgs e) + private void TryUpdateTrackedLines(TextContentChangedEventArgs textContentChangedEventArgs) { - IEnumerable changedLineNumbers = this.trackedLines.GetChangedLineNumbers(e.After, e.Changes.Select(change => change.NewSpan).ToList()) - .Where(changedLine => changedLine >= 0 && changedLine < e.After.LineCount); + try + { + this.UpdateTrackedLines(textContentChangedEventArgs); + } + catch (Exception e) + { + this.logger.Log($"Error updating tracked lines for {this.textInfo.FilePath}", e); + } + } + + private void UpdateTrackedLines(TextContentChangedEventArgs textContentChangedEventArgs) + { + IEnumerable changedLineNumbers = this.TrackedLines.GetChangedLineNumbers(textContentChangedEventArgs.After, textContentChangedEventArgs.Changes.Select(change => change.NewSpan).ToList()) + .Where(changedLine => changedLine >= 0 && changedLine < textContentChangedEventArgs.After.LineCount); this.SendCoverageChangedMessageIfChanged(changedLineNumbers); } @@ -156,16 +262,16 @@ private void SendCoverageChangedMessage(IEnumerable changedLineNumbers = nu => this.eventAggregator.SendMessage(new CoverageChangedMessage(this, this.textInfo.FilePath, changedLineNumbers)); public IEnumerable GetLines(int startLineNumber, int endLineNumber) - => this.trackedLines == null ? Enumerable.Empty() : this.trackedLines.GetLines(startLineNumber, endLineNumber); + => this.TrackedLines == null ? Enumerable.Empty() : this.TrackedLines.GetLines(startLineNumber, endLineNumber); public void Handle(NewCoverageLinesMessage message) { this.fileLineCoverage = message.CoverageLines; - bool hadTrackedLines = this.trackedLines != null; + bool hadTrackedLines = this.TrackedLines != null; if (this.fileLineCoverage == null) { - this.trackedLines = null; + this.TrackedLines = null; if (hadTrackedLines) { this.SendCoverageChangedMessage(); @@ -176,5 +282,7 @@ public void Handle(NewCoverageLinesMessage message) this.CreateTrackedLinesIfRequiredWithMessage(); } } + + public void Handle(TestExecutionStartingMessage message) => this.lastTestExecutionStarting = DateTime.Now; } } diff --git a/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs index 897c4750..1876ca2a 100644 --- a/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Impl; using FineCodeCoverage.Options; namespace FineCodeCoverage.Editor.DynamicCoverage @@ -12,28 +13,33 @@ internal class BufferLineCoverageFactory : IBufferLineCoverageFactory { private readonly IDynamicCoverageStore dynamicCoverageStore; private readonly IAppOptionsProvider appOptionsProvider; + private readonly ILogger logger; [ImportingConstructor] public BufferLineCoverageFactory( IDynamicCoverageStore dynamicCoverageStore, - IAppOptionsProvider appOptionsProvider + IAppOptionsProvider appOptionsProvider, + ILogger logger ) { this.appOptionsProvider = appOptionsProvider; + this.logger = logger; this.dynamicCoverageStore = dynamicCoverageStore; } public IBufferLineCoverage Create( - IFileLineCoverage fileLineCoverage, + LastCoverage lastCoverage, ITextInfo textInfo, IEventAggregator eventAggregator, ITrackedLinesFactory trackedLinesFactory ) => new BufferLineCoverage( - fileLineCoverage, + lastCoverage, textInfo, eventAggregator, trackedLinesFactory, this.dynamicCoverageStore, - this.appOptionsProvider); + this.appOptionsProvider, + this.logger + ); } } diff --git a/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs b/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs index 7794ad8a..c8e20961 100644 --- a/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs +++ b/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs @@ -3,7 +3,9 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() internal class CoverageChangedMessage +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() { public IBufferLineCoverage BufferLineCoverage { get; } public string AppliesTo { get; } @@ -15,5 +17,10 @@ public CoverageChangedMessage(IBufferLineCoverage bufferLineCoverage, string app this.AppliesTo = appliesTo; this.ChangedLineNumbers = changedLineNumbers; } + + public override bool Equals(object obj) => obj is CoverageChangedMessage message && + message.BufferLineCoverage == this.BufferLineCoverage && + message.AppliesTo == this.AppliesTo && + message.ChangedLineNumbers == this.ChangedLineNumbers; } } diff --git a/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs b/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs index f1a60292..086a38f5 100644 --- a/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs +++ b/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs @@ -1,36 +1,44 @@ -using System.ComponentModel.Composition; +using System; +using System.ComponentModel.Composition; using FineCodeCoverage.Core.Initialization; using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage.Utilities; using FineCodeCoverage.Engine; -using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Impl; namespace FineCodeCoverage.Editor.DynamicCoverage { [Export(typeof(IInitializable))] [Export(typeof(IDynamicCoverageManager))] - internal class DynamicCoverageManager : IDynamicCoverageManager, IListener, IInitializable + internal class DynamicCoverageManager : IDynamicCoverageManager, IListener, IListener, IInitializable { private readonly IEventAggregator eventAggregator; private readonly ITrackedLinesFactory trackedLinesFactory; private readonly IBufferLineCoverageFactory bufferLineCoverageFactory; - private IFileLineCoverage lastCoverageLines; + private readonly IDateTimeService dateTimeService; + private LastCoverage lastCoverage; + private DateTime lastTestExecutionStartingDate; [ImportingConstructor] public DynamicCoverageManager( IEventAggregator eventAggregator, ITrackedLinesFactory trackedLinesFactory, - IBufferLineCoverageFactory bufferLineCoverageFactory) + IBufferLineCoverageFactory bufferLineCoverageFactory, + IDateTimeService dateTimeService) { this.bufferLineCoverageFactory = bufferLineCoverageFactory; + this.dateTimeService = dateTimeService; _ = eventAggregator.AddListener(this); this.eventAggregator = eventAggregator; this.trackedLinesFactory = trackedLinesFactory; } - public void Handle(NewCoverageLinesMessage message) => this.lastCoverageLines = message.CoverageLines; - + public void Handle(NewCoverageLinesMessage message) => this.lastCoverage = new LastCoverage(message.CoverageLines, this.lastTestExecutionStartingDate); + + public void Handle(TestExecutionStartingMessage message) => this.lastTestExecutionStartingDate = this.dateTimeService.Now; + public IBufferLineCoverage Manage(ITextInfo textInfo) => textInfo.TextBuffer.Properties.GetOrCreateSingletonProperty( - () => this.bufferLineCoverageFactory.Create(this.lastCoverageLines, textInfo, this.eventAggregator, this.trackedLinesFactory) + () => this.bufferLineCoverageFactory.Create(this.lastCoverage, textInfo, this.eventAggregator, this.trackedLinesFactory) ); } } diff --git a/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs b/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs index 1e66e629..38b88c77 100644 --- a/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs @@ -6,7 +6,7 @@ namespace FineCodeCoverage.Editor.DynamicCoverage internal interface IBufferLineCoverageFactory { IBufferLineCoverage Create( - IFileLineCoverage fileLineCoverage, ITextInfo textInfo, IEventAggregator eventAggregator, ITrackedLinesFactory trackedLinesFactory + LastCoverage lastCoverage, ITextInfo textInfo, IEventAggregator eventAggregator, ITrackedLinesFactory trackedLinesFactory ); } } diff --git a/SharedProject/Editor/DynamicCoverage/Management/ILastCoverage.cs b/SharedProject/Editor/DynamicCoverage/Management/ILastCoverage.cs new file mode 100644 index 00000000..ff50810e --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/ILastCoverage.cs @@ -0,0 +1,11 @@ +using System; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ILastCoverage + { + IFileLineCoverage FileLineCoverage { get; } + DateTime TestExecutionStartingDate { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs index 6a52ce9e..8c52654a 100644 --- a/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs @@ -7,8 +7,8 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { internal interface ITrackedLinesFactory { - ITrackedLines Create(List lines, ITextSnapshot textSnapshot, Language language); - ITrackedLines Create(string serializedCoverage, ITextSnapshot currentSnapshot, Language language); - string Serialize(ITrackedLines trackedLines); + ITrackedLines Create(List lines, ITextSnapshot textSnapshot, string filePath); + ITrackedLines Create(string serializedCoverage, ITextSnapshot currentSnapshot, string filePath); + string Serialize(ITrackedLines trackedLines, string text); } } diff --git a/SharedProject/Editor/DynamicCoverage/Management/LastCoverage.cs b/SharedProject/Editor/DynamicCoverage/Management/LastCoverage.cs new file mode 100644 index 00000000..8f134f76 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/LastCoverage.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + internal class LastCoverage : ILastCoverage + +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + { + public LastCoverage(IFileLineCoverage fileLineCoverage, DateTime testExecutionStartingDate) + { + this.FileLineCoverage = fileLineCoverage; + this.TestExecutionStartingDate = testExecutionStartingDate; + } + public IFileLineCoverage FileLineCoverage { get; } + public DateTime TestExecutionStartingDate { get; } + + [ExcludeFromCodeCoverage] + public override bool Equals(object obj) => obj is LastCoverage coverage && this.FileLineCoverage == coverage.FileLineCoverage && this.TestExecutionStartingDate == coverage.TestExecutionStartingDate; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs index 698fe299..4ce3f42c 100644 --- a/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs @@ -5,7 +5,7 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { internal interface INewCodeTrackerFactory { - INewCodeTracker Create(bool isCSharp); - INewCodeTracker Create(bool isCSharp, IEnumerable lineNumbers, ITextSnapshot textSnapshot); + INewCodeTracker Create(ILineExcluder lineExcluder); + INewCodeTracker Create(ILineExcluder lineExcluder, IEnumerable lineNumbers, ITextSnapshot textSnapshot); } } diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs index 7d0bdcf8..29cc89cd 100644 --- a/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs +++ b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs @@ -7,26 +7,22 @@ namespace FineCodeCoverage.Editor.DynamicCoverage internal class NewCodeTracker : INewCodeTracker { private readonly List trackedNewCodeLines = new List(); - private readonly bool isCSharp; private readonly ITrackedNewCodeLineFactory trackedNewCodeLineFactory; private readonly ILineExcluder codeLineExcluder; - public NewCodeTracker(bool isCSharp, ITrackedNewCodeLineFactory trackedNewCodeLineFactory, ILineExcluder codeLineExcluder) + public NewCodeTracker(ITrackedNewCodeLineFactory trackedNewCodeLineFactory, ILineExcluder codeLineExcluder) { - this.isCSharp = isCSharp; this.trackedNewCodeLineFactory = trackedNewCodeLineFactory; this.codeLineExcluder = codeLineExcluder; } public NewCodeTracker( - bool isCSharp, ITrackedNewCodeLineFactory trackedNewCodeLineFactory, ILineExcluder codeLineExcluder, IEnumerable lineNumbers, ITextSnapshot currentSnapshot ) { - this.isCSharp = isCSharp; this.trackedNewCodeLineFactory = trackedNewCodeLineFactory; this.codeLineExcluder = codeLineExcluder; foreach (int lineNumber in lineNumbers) @@ -138,7 +134,7 @@ private bool RemoveTrackedNewCodeLineIfExcluded( string newText) { bool excluded = false; - if (this.codeLineExcluder.ExcludeIfNotCode(newText, this.isCSharp)) + if (this.codeLineExcluder.ExcludeIfNotCode(newText)) { excluded = true; removals.Add(trackedNewCodeLine); @@ -160,7 +156,7 @@ private bool AddTrackedNewCodeLineIfNotExcluded(ITextSnapshot currentSnapshot, i bool added = false; ITrackedNewCodeLine trackedNewCodeLine = this.CreateTrackedNewCodeLine(currentSnapshot, lineNumber); string text = trackedNewCodeLine.GetText(currentSnapshot); - if (!this.codeLineExcluder.ExcludeIfNotCode(text, this.isCSharp)) + if (!this.codeLineExcluder.ExcludeIfNotCode(text)) { this.trackedNewCodeLines.Add(trackedNewCodeLine); added = true; diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs index 2e1d1dc3..5e6b32d5 100644 --- a/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs @@ -10,20 +10,15 @@ namespace FineCodeCoverage.Editor.DynamicCoverage internal class NewCodeTrackerFactory : INewCodeTrackerFactory { private readonly ITrackedNewCodeLineFactory trackedNewCodeLineFactory; - private readonly ILineExcluder codeLineExcluder; [ImportingConstructor] public NewCodeTrackerFactory( - ITrackedNewCodeLineFactory trackedNewCodeLineFactory, - ILineExcluder codeLineExcluder - ) - { - this.trackedNewCodeLineFactory = trackedNewCodeLineFactory; - this.codeLineExcluder = codeLineExcluder; - } - public INewCodeTracker Create(bool isCSharp) => new NewCodeTracker(isCSharp, this.trackedNewCodeLineFactory, this.codeLineExcluder); + ITrackedNewCodeLineFactory trackedNewCodeLineFactory + ) => this.trackedNewCodeLineFactory = trackedNewCodeLineFactory; - public INewCodeTracker Create(bool isCSharp, IEnumerable lineNumbers, ITextSnapshot textSnapshot) - => new NewCodeTracker(isCSharp, this.trackedNewCodeLineFactory, this.codeLineExcluder, lineNumbers, textSnapshot); + public INewCodeTracker Create(ILineExcluder lineExcluder) => new NewCodeTracker(this.trackedNewCodeLineFactory, lineExcluder); + + public INewCodeTracker Create(ILineExcluder lineExcluder, IEnumerable lineNumbers, ITextSnapshot textSnapshot) + => new NewCodeTracker(this.trackedNewCodeLineFactory, lineExcluder, lineNumbers, textSnapshot); } } diff --git a/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs b/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs index ba9665dd..6e1c546a 100644 --- a/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs +++ b/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs @@ -1,5 +1,6 @@ using System.ComponentModel.Composition; using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage.Utilities; using FineCodeCoverage.Engine; using FineCodeCoverage.Options; using Microsoft.VisualStudio.Settings; @@ -10,6 +11,8 @@ namespace FineCodeCoverage.Editor.DynamicCoverage internal class DynamicCoverageStore : IDynamicCoverageStore, IListener { private readonly IWritableUserSettingsStoreProvider writableUserSettingsStoreProvider; + private readonly IJsonConvertService jsonConvertService; + private readonly IDateTimeService dateTimeService; private const string dynamicCoverageStoreCollectionName = "FCC.DynamicCoverageStore"; private WritableSettingsStore writableUserSettingsStore; private WritableSettingsStore WritableUserSettingsStore @@ -25,18 +28,21 @@ private WritableSettingsStore WritableUserSettingsStore } } - // todo needs to listen for solution change as well as vs shutdown to clear - // needs to listen to https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.ivstrackprojectdocuments2.onafterrenamefile?view=visualstudiosdk-2019 - // also the normal coverage needs to listen for file name changed + // when visual studio is closed SolutionEvents AfterClosing event is fired, the FCCEngine + // will NewCoverageLinesMessage and the store will be removed [ImportingConstructor] public DynamicCoverageStore( IWritableUserSettingsStoreProvider writableUserSettingsStoreProvider, IFileRenameListener fileRenameListener, - IEventAggregator eventAggregator + IEventAggregator eventAggregator, + IJsonConvertService jsonConvertService, + IDateTimeService dateTimeService ) { _ = eventAggregator.AddListener(this); this.writableUserSettingsStoreProvider = writableUserSettingsStoreProvider; + this.jsonConvertService = jsonConvertService; + this.dateTimeService = dateTimeService; fileRenameListener.ListenForFileRename((oldFileName, newFileName) => { bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); @@ -52,13 +58,14 @@ IEventAggregator eventAggregator }); } - public string GetSerializedCoverage(string filePath) + public SerializedCoverageWhen GetSerializedCoverage(string filePath) { bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); return !collectionExists ? null : this.WritableUserSettingsStore.PropertyExists(dynamicCoverageStoreCollectionName, filePath) - ? this.WritableUserSettingsStore.GetString(dynamicCoverageStoreCollectionName, filePath) + ? this.jsonConvertService.DeserializeObject( + this.WritableUserSettingsStore.GetString(dynamicCoverageStoreCollectionName, filePath)) : null; } @@ -70,10 +77,19 @@ public void SaveSerializedCoverage(string filePath, string serializedCoverage) this.WritableUserSettingsStore.CreateCollection(dynamicCoverageStoreCollectionName); } - this.WritableUserSettingsStore.SetString(dynamicCoverageStoreCollectionName, filePath, serializedCoverage); + var serializedCoverageWhen = new SerializedCoverageWhen + { + Serialized = serializedCoverage, + When = this.dateTimeService.Now + }; + string toSerialize = this.jsonConvertService.SerializeObject(serializedCoverageWhen); + this.WritableUserSettingsStore.SetString(dynamicCoverageStoreCollectionName, filePath, toSerialize); } - public void Handle(NewCoverageLinesMessage message) + // this is fundamental - the store is for restoring the coverage of the current coverage only + public void Handle(NewCoverageLinesMessage message) => this.RemoveStore(); + + private void RemoveStore() { bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); if (collectionExists) diff --git a/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs b/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs index 029059b0..1e159161 100644 --- a/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs +++ b/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs @@ -1,9 +1,23 @@ -namespace FineCodeCoverage.Editor.DynamicCoverage +using System; + +namespace FineCodeCoverage.Editor.DynamicCoverage { +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + internal class SerializedCoverageWhen +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + { + public string Serialized { get; set; } + public DateTime When { get; set; } + + public override bool Equals(object obj) + => obj is SerializedCoverageWhen when && + this.Serialized == when.Serialized && + this.When == when.When; + } internal interface IDynamicCoverageStore { - string GetSerializedCoverage(string filePath); + SerializedCoverageWhen GetSerializedCoverage(string filePath); void RemoveSerializedCoverage(string filePath); - void SaveSerializedCoverage(string filePath, string serialized); + void SaveSerializedCoverage(string filePath, string serializedCoverage); } } diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs index 87ac9a37..6dd2631d 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs @@ -8,7 +8,7 @@ namespace FineCodeCoverage.Editor.DynamicCoverage [Export(typeof(IContainingCodeTrackedLinesFactory))] internal class ContainingCodeTrackedLinesFactory : IContainingCodeTrackedLinesFactory { - public TrackedLines Create( + public IContainingCodeTrackerTrackedLines Create( List containingCodeTrackers, INewCodeTracker newCodeTracker, IFileCodeSpanRangeService fileCodeSpanRangeService diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTrackerTrackedLines.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTrackerTrackedLines.cs new file mode 100644 index 00000000..4b370159 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTrackerTrackedLines.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IContainingCodeTrackerTrackedLines : ITrackedLines + { + IReadOnlyList ContainingCodeTrackers { get; } + INewCodeTracker NewCodeTracker { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs index 9ba84175..45ccde1d 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs @@ -5,23 +5,24 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { - internal class TrackedLines : ITrackedLines + internal class TrackedLines : IContainingCodeTrackerTrackedLines { private readonly List containingCodeTrackers; - private readonly INewCodeTracker newCodeTracker; private readonly IFileCodeSpanRangeService fileCodeSpanRangeService; public IReadOnlyList ContainingCodeTrackers => this.containingCodeTrackers; private readonly bool useFileCodeSpanRangeService; + public INewCodeTracker NewCodeTracker { get; } + public TrackedLines( List containingCodeTrackers, INewCodeTracker newCodeTracker, - IFileCodeSpanRangeService roslynService) + IFileCodeSpanRangeService fileCodeSpanRangeService) { this.containingCodeTrackers = containingCodeTrackers; - this.newCodeTracker = newCodeTracker; - this.fileCodeSpanRangeService = roslynService; + this.NewCodeTracker = newCodeTracker; + this.fileCodeSpanRangeService = fileCodeSpanRangeService; this.useFileCodeSpanRangeService = this.fileCodeSpanRangeService != null && newCodeTracker != null; } @@ -80,12 +81,12 @@ public IEnumerable GetChangedLineNumbers(ITextSnapshot currentSnapshot, Lis } private IEnumerable GetNewCodeTrackerChangedLineNumbers(ITextSnapshot currentSnapshot, List spanAndLineRanges) - => this.newCodeTracker == null ? Enumerable.Empty() : this.GetNewCodeTrackerChangedLineNumbersActual(currentSnapshot, spanAndLineRanges); + => this.NewCodeTracker == null ? Enumerable.Empty() : this.GetNewCodeTrackerChangedLineNumbersActual(currentSnapshot, spanAndLineRanges); private IEnumerable GetNewCodeTrackerChangedLineNumbersActual(ITextSnapshot currentSnapshot, List spanAndLineRanges) { List newCodeCodeRanges = this.GetNewCodeCodeRanges(currentSnapshot); - return this.newCodeTracker.GetChangedLineNumbers(currentSnapshot, spanAndLineRanges, newCodeCodeRanges); + return this.NewCodeTracker.GetChangedLineNumbers(currentSnapshot, spanAndLineRanges, newCodeCodeRanges); } private List GetNewCodeCodeRanges(ITextSnapshot currentSnapshot) @@ -105,6 +106,7 @@ private List GetNewCodeCodeRanges( var newCodeCodeRanges = new List(); int i = 0, j = 0; + containingCodeTrackersCodeSpanRanges = containingCodeTrackersCodeSpanRanges.OrderBy(codeSpanRange => codeSpanRange.StartLine).ToList(); while (i < fileCodeSpanRanges.Count && j < containingCodeTrackersCodeSpanRanges.Count) { CodeSpanRange fileRange = fileCodeSpanRanges[i]; @@ -153,7 +155,7 @@ private IEnumerable GetLinesFromContainingCodeTrackers(int startLi => this.containingCodeTrackers.Select(containingCodeTracker => this.GetLines(containingCodeTracker.Lines, startLineNumber, endLineNumber)) .TakeUntil(a => a.done).SelectMany(a => a.lines); - private IEnumerable NewCodeTrackerLines() => this.newCodeTracker?.Lines ?? Enumerable.Empty(); + private IEnumerable NewCodeTrackerLines() => this.NewCodeTracker?.Lines ?? Enumerable.Empty(); private IEnumerable GetNewLines(int startLineNumber, int endLineNumber) => this.LinesApplicableToStartLineNumber(this.NewCodeTrackerLines(), startLineNumber) diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs index 456d398d..4534a02d 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs @@ -13,6 +13,7 @@ public CodeSpanRange(int startLine, int endLine) public int StartLine { get; set; } public int EndLine { get; set; } + [ExcludeFromCodeCoverage] public override bool Equals(object obj) => obj is CodeSpanRange codeSpanRange && codeSpanRange.StartLine == this.StartLine && codeSpanRange.EndLine == this.EndLine; diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs index f524a330..158662fe 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs @@ -1,89 +1,103 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Core.Utilities.VsThreading; -using FineCodeCoverage.Editor.Roslyn; -using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction; using FineCodeCoverage.Engine.Model; -using FineCodeCoverage.Options; -using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; namespace FineCodeCoverage.Editor.DynamicCoverage { [Export(typeof(ITrackedLinesFactory))] - internal class ContainingCodeTrackedLinesBuilder : ITrackedLinesFactory, IFileCodeSpanRangeService + internal class ContainingCodeTrackedLinesBuilder : ITrackedLinesFactory { - private readonly IRoslynService roslynService; + private readonly ICoverageContentType[] coverageContentTypes; private readonly ICodeSpanRangeContainingCodeTrackerFactory containingCodeTrackerFactory; private readonly IContainingCodeTrackedLinesFactory containingCodeTrackedLinesFactory; private readonly INewCodeTrackerFactory newCodeTrackerFactory; - private readonly IThreadHelper threadHelper; - private readonly ITextSnapshotLineExcluder textSnapshotLineExcluder; private readonly IJsonConvertService jsonConvertService; - private readonly IAppOptionsProvider appOptionsProvider; + private readonly ITextSnapshotText textSnapshotText; + private readonly ILogger logger; [ImportingConstructor] public ContainingCodeTrackedLinesBuilder( - IRoslynService roslynService, + [ImportMany] + ICoverageContentType[] coverageContentTypes, ICodeSpanRangeContainingCodeTrackerFactory containingCodeTrackerFactory, IContainingCodeTrackedLinesFactory containingCodeTrackedLinesFactory, INewCodeTrackerFactory newCodeTrackerFactory, - IThreadHelper threadHelper, - ITextSnapshotLineExcluder textSnapshotLineExcluder, IJsonConvertService jsonConvertService, - IAppOptionsProvider appOptionsProvider + ITextSnapshotText textSnapshotText, + ILogger logger ) { - this.roslynService = roslynService; + this.coverageContentTypes = coverageContentTypes; this.containingCodeTrackerFactory = containingCodeTrackerFactory; this.containingCodeTrackedLinesFactory = containingCodeTrackedLinesFactory; this.newCodeTrackerFactory = newCodeTrackerFactory; - this.threadHelper = threadHelper; - this.textSnapshotLineExcluder = textSnapshotLineExcluder; this.jsonConvertService = jsonConvertService; - this.appOptionsProvider = appOptionsProvider; + this.textSnapshotText = textSnapshotText; + this.logger = logger; } - private bool UseRoslynWhenTextChanges() - => this.appOptionsProvider.Get().EditorCoverageColouringMode == EditorCoverageColouringMode.UseRoslynWhenTextChanges; - - private CodeSpanRange GetCodeSpanRange(TextSpan span, ITextSnapshot textSnapshot) + private ICoverageContentType GetCoverageContentType(ITextSnapshot textSnapshot) { - int startLine = textSnapshot.GetLineNumberFromPosition(span.Start); - int endLine = textSnapshot.GetLineNumberFromPosition(span.End); - return new CodeSpanRange(startLine, endLine); + string contentTypeName = textSnapshot.ContentType.TypeName; + return this.coverageContentTypes.First( + coverageContentType => coverageContentType.ContentTypeName == contentTypeName); } - public ITrackedLines Create(List lines, ITextSnapshot textSnapshot, Language language) + private IFileCodeSpanRangeService GetFileCodeSpanRangeServiceForChanges(ICoverageContentType coverageContentType) + => coverageContentType.UseFileCodeSpanRangeServiceForChanges ? coverageContentType.FileCodeSpanRangeService : null; + + private INewCodeTracker GetNewCodeTrackerIfProvidesLineExcluder(ILineExcluder lineExcluder) + => lineExcluder == null ? null : this.newCodeTrackerFactory.Create(lineExcluder); + + public ITrackedLines Create(List lines, ITextSnapshot textSnapshot, string filePath) { - List containingCodeTrackers = this.CreateContainingCodeTrackers(lines, textSnapshot, language); - INewCodeTracker newCodeTracker = language == Language.CPP ? null : this.newCodeTrackerFactory.Create(language == Language.CSharp); - IFileCodeSpanRangeService fileCodeSpanRangeService = this.GetFileCodeSpanRangeService(language); - return this.containingCodeTrackedLinesFactory.Create(containingCodeTrackers, newCodeTracker, fileCodeSpanRangeService); - } + if (this.AnyLinesOutsideTextSnapshot(lines, textSnapshot)) + { + this.logger.Log($"Not creating editor marks for {filePath} as some coverage lines are outside the text snapshot"); + return null; + } - private IFileCodeSpanRangeService GetFileCodeSpanRangeService(Language language) - => language == Language.CPP ? null : this.GetRoslynFileCodeSpanRangeService(this.UseRoslynWhenTextChanges()); + ICoverageContentType coverageContentType = this.GetCoverageContentType(textSnapshot); + IFileCodeSpanRangeService fileCodeSpanRangeService = coverageContentType.FileCodeSpanRangeService; + (List containingCodeTrackers, bool usedFileCodeSpanRangeService) = this.CreateContainingCodeTrackers( + lines, textSnapshot, fileCodeSpanRangeService, coverageContentType.CoverageOnlyFromFileCodeSpanRangeService); - private IFileCodeSpanRangeService GetRoslynFileCodeSpanRangeService(bool useRoslynWhenTextChanges) - => useRoslynWhenTextChanges ? this : null; + IContainingCodeTrackerTrackedLines trackedLines = this.containingCodeTrackedLinesFactory.Create( + containingCodeTrackers, + this.GetNewCodeTrackerIfProvidesLineExcluder(coverageContentType.LineExcluder), + this.GetFileCodeSpanRangeServiceForChanges(coverageContentType)); - private List CreateContainingCodeTrackers(List lines, ITextSnapshot textSnapshot, Language language) + return new ContainingCodeTrackerTrackedLinesWithState(trackedLines, usedFileCodeSpanRangeService); + } + + private (List containingCodeTrackers, bool usedFileCodeSpanRangeService) CreateContainingCodeTrackers( + List lines, + ITextSnapshot textSnapshot, + IFileCodeSpanRangeService fileCodeSpanRangeService, + bool coverageOnlyFromFileCodeSpanRangeService + ) { - if (language == Language.CPP) + if (fileCodeSpanRangeService != null) { - /* - todo - https://learn.microsoft.com/en-us/previous-versions/t41260xs(v=vs.140) - non C++ https://learn.microsoft.com/en-us/dotnet/api/envdte80.filecodemodel2?view=visualstudiosdk-2022 - */ - return lines.Select(line => this.CreateSingleLineContainingCodeTracker(textSnapshot, line)).ToList(); + List codeSpanRanges = fileCodeSpanRangeService.GetFileCodeSpanRanges(textSnapshot); + if (codeSpanRanges != null) + { + return (this.CreateContainingCodeTrackersFromCodeSpanRanges( + lines, textSnapshot, codeSpanRanges, coverageOnlyFromFileCodeSpanRangeService), true); + } } - return this.CreateRoslynContainingCodeTrackers(lines, textSnapshot, language == Language.CSharp); + return (lines.Select(line => this.CreateSingleLineContainingCodeTracker(textSnapshot, line)).ToList(), false); } + private bool AnyLinesOutsideTextSnapshot(List lines, ITextSnapshot textSnapshot) + => lines.Any(line => line.Number - 1 >= textSnapshot.LineCount); + private IContainingCodeTracker CreateSingleLineContainingCodeTracker(ITextSnapshot textSnapshot, ILine line) => this.CreateCoverageLines(textSnapshot, new List { line }, CodeSpanRange.SingleLine(line.Number - 1)); @@ -100,42 +114,44 @@ private IContainingCodeTracker CreateCoverageLines(ITextSnapshot textSnapshot, L private IContainingCodeTracker CreateNotIncluded(ITextSnapshot textSnapshot, CodeSpanRange containingRange) => this.containingCodeTrackerFactory.CreateNotIncluded(textSnapshot, containingRange, SpanTrackingMode.EdgeExclusive); - private List CreateRoslynContainingCodeTrackers(List lines, ITextSnapshot textSnapshot, bool isCSharp) + private List CreateContainingCodeTrackersFromCodeSpanRanges( + List lines, + ITextSnapshot textSnapshot, + List codeSpanRanges, + bool coverageOnlyFromFileCodeSpanRangeService + ) { var containingCodeTrackers = new List(); - int currentLine = 0; - // this should not happen - just in case missed something with Roslyn - void CreateSingleLineContainingCodeTrackerInCase(ILine line) - => containingCodeTrackers.Add(this.CreateSingleLineContainingCodeTracker(textSnapshot, line)); - - List codeSpanRanges = this.GetRoslynCodeSpanRanges(textSnapshot); - int currentCodeSpanIndex = -1; - CodeSpanRange currentCodeSpanRange = null; - SetNextCodeSpanRange(); - var containedLines = new List(); - - void SetNextCodeSpanRange() + Func GetNextCreator(List list) { - currentCodeSpanIndex++; - CodeSpanRange previousCodeSpanRange = currentCodeSpanRange; - currentCodeSpanRange = currentCodeSpanIndex < codeSpanRanges.Count - ? codeSpanRanges[currentCodeSpanIndex] - : null; - } + T GetNext() + { + T next = list.FirstOrDefault(); + if (next != null) + { + list.RemoveAt(0); + } - void TrackOtherLines() - { - int to = currentCodeSpanRange.StartLine - 1; - TrackOtherLinesTo(to); - currentLine = currentCodeSpanRange.EndLine + 1; + return next; + } + + return GetNext; } - void TrackOtherLinesTo(int to) + Func GetNextLine = GetNextCreator(lines); + Func GetNextCodeSpanRange = GetNextCreator(codeSpanRanges); + + + ILine line = GetNextLine(); + CodeSpanRange codeSpanRange = GetNextCodeSpanRange(); + var containedLines = new List(); + bool InCodeSpanRange(int lineNumber) => codeSpanRange != null && codeSpanRange.StartLine <= lineNumber && codeSpanRange.EndLine >= lineNumber; + bool AtEndOfCodeSpanRange(int lineNumber) => codeSpanRange != null && codeSpanRange.EndLine == lineNumber; + bool LineAtLineNumber(int lineNumber) => line != null && line.Number - 1 == lineNumber; + void CreateOtherLine(int otherCodeLine) { - if (to < currentLine) return; - IEnumerable otherCodeLines = Enumerable.Range(currentLine, to - currentLine + 1) - .Where(lineNumber => !this.textSnapshotLineExcluder.ExcludeIfNotCode(textSnapshot, lineNumber, isCSharp)); - foreach (int otherCodeLine in otherCodeLines) + string lineText = this.textSnapshotText.GetLineText(textSnapshot, otherCodeLine); + if (!string.IsNullOrWhiteSpace(lineText)) { containingCodeTrackers.Add( this.CreateOtherLines( @@ -146,101 +162,99 @@ void TrackOtherLinesTo(int to) } } - void CreateRangeContainingCodeTracker() - { - TrackOtherLines(); - IContainingCodeTracker containingCodeTracker = containedLines.Count > 0 - ? this.CreateCoverageLines(textSnapshot, containedLines, currentCodeSpanRange) - : this.CreateNotIncluded(textSnapshot, currentCodeSpanRange); - containingCodeTrackers.Add(containingCodeTracker); - - containedLines = new List(); - SetNextCodeSpanRange(); - } - - void LineAction(ILine line) + for (int lineNumber = 0; lineNumber < textSnapshot.LineCount; lineNumber++) { - if (currentCodeSpanRange == null) + bool inCodeSpanRange = InCodeSpanRange(lineNumber); + if(LineAtLineNumber(lineNumber)) { - CreateSingleLineContainingCodeTrackerInCase(line); - } - else - { - int adjustedLine = line.Number - 1; - if (adjustedLine < currentCodeSpanRange.StartLine) - { - CreateSingleLineContainingCodeTrackerInCase(line); - } - else if (adjustedLine > currentCodeSpanRange.EndLine) + if(inCodeSpanRange) { - CreateRangeContainingCodeTracker(); - - LineAction(line); - + containedLines.Add(line); } else { - containedLines.Add(line); + if (!coverageOnlyFromFileCodeSpanRangeService) + { + containingCodeTrackers.Add(this.CreateSingleLineContainingCodeTracker(textSnapshot, line)); + } + else + { + CreateOtherLine(lineNumber); + } } + + line = GetNextLine(); + } + else if (!inCodeSpanRange) + { + CreateOtherLine(lineNumber); } - } - foreach (ILine line in lines) // these are in order` - { - LineAction(line); - } + if (AtEndOfCodeSpanRange(lineNumber)) + { + IContainingCodeTracker containingCodeTracker = containedLines.Count > 0 + ? this.CreateCoverageLines(textSnapshot, containedLines, codeSpanRange) + : this.CreateNotIncluded(textSnapshot, codeSpanRange); + containingCodeTrackers.Add(containingCodeTracker); - while (currentCodeSpanRange != null) - { - CreateRangeContainingCodeTracker(); + containedLines = new List(); + codeSpanRange = GetNextCodeSpanRange(); + } } - TrackOtherLinesTo(textSnapshot.LineCount - 1); return containingCodeTrackers; } - private ITrackedLines RecreateTrackedLinesFromCPPStates(List states, ITextSnapshot currentSnapshot) - { - var containingCodeTrackers = this.StatesWithinSnapshot(states, currentSnapshot) - .Select(state => this.RecreateCoverageLines(state, currentSnapshot)).ToList(); - return this.containingCodeTrackedLinesFactory.Create(containingCodeTrackers, null, null); - } + #region Serialization - private IEnumerable StatesWithinSnapshot(IEnumerable states, ITextSnapshot currentSnapshot) + private IContainingCodeTrackerTrackedLines RecreateTrackedLinesNoFileCodeSpanRangeService( + List serializedContainingCodeTrackers, + ITextSnapshot currentSnapshot, + ILineExcluder lineExcluder, + List newCodeLines + ) { - int numLines = currentSnapshot.LineCount; - return states.Where(state => state.CodeSpanRange.EndLine < numLines); + var containingCodeTrackers = serializedContainingCodeTrackers.Select( + serializedContainingCodeTracker => this.RecreateCoverageLines( + serializedContainingCodeTracker, currentSnapshot) + ).ToList(); + return this.containingCodeTrackedLinesFactory.Create( + containingCodeTrackers, + this.GetNewCodeTrackerIfProvidesLineExcluder(lineExcluder, newCodeLines, currentSnapshot), + null); } - private IContainingCodeTracker RecreateCoverageLines(SerializedState state, ITextSnapshot currentSnapshot) + private INewCodeTracker GetNewCodeTrackerIfProvidesLineExcluder(ILineExcluder lineExcluder, List newCodeLines, ITextSnapshot textSnapshot) + => lineExcluder == null ? null : this.newCodeTrackerFactory.Create(lineExcluder, newCodeLines, textSnapshot); + + private IContainingCodeTracker RecreateCoverageLines( + SerializedContainingCodeTracker serializedContainingCodeTracker, ITextSnapshot currentSnapshot) { - CodeSpanRange codeSpanRange = state.CodeSpanRange; - return state.Lines[0].CoverageType == DynamicCoverageType.Dirty + CodeSpanRange codeSpanRange = serializedContainingCodeTracker.CodeSpanRange; + return serializedContainingCodeTracker.Lines[0].CoverageType == DynamicCoverageType.Dirty ? this.containingCodeTrackerFactory.CreateDirty(currentSnapshot, codeSpanRange, SpanTrackingMode.EdgeExclusive) - : this.CreateCoverageLines(currentSnapshot, this.AdjustCoverageLines(state.Lines), codeSpanRange); + : this.CreateCoverageLines(currentSnapshot, this.AdjustCoverageLines(serializedContainingCodeTracker.Lines), codeSpanRange); } private List AdjustCoverageLines(List dynamicLines) => dynamicLines.Select(dynamicLine => new AdjustedLine(dynamicLine)).Cast().ToList(); - private List GetRoslynCodeSpanRanges(ITextSnapshot currentSnapshot) - { - List roslynContainingCodeSpans = this.threadHelper.JoinableTaskFactory.Run(() => this.roslynService.GetContainingCodeSpansAsync(currentSnapshot)); - return roslynContainingCodeSpans.Select(roslynCodeSpan => this.GetCodeSpanRange(roslynCodeSpan, currentSnapshot)).Distinct().ToList(); - } - - private List RecreateContainingCodeTrackersWithUnchangedCodeSpanRange( - List codeSpanRanges, - List states, + private List RecreateContainingCodeTrackers( + List serializedContainingCodeTrackers, ITextSnapshot currentSnapshot - ) => states.Where(state => codeSpanRanges.Remove(state.CodeSpanRange)) - .Select(state => this.RecreateContainingCodeTracker(state, currentSnapshot)).ToList(); + ) => serializedContainingCodeTrackers.Select( + serializedContainingCodeTracker => this.RecreateContainingCodeTracker( + serializedContainingCodeTracker, currentSnapshot) + ).ToList(); - private IContainingCodeTracker RecreateContainingCodeTracker(SerializedState state, ITextSnapshot currentSnapshot) + private IContainingCodeTracker RecreateContainingCodeTracker( + SerializedContainingCodeTracker serializedContainingCodeTracker, + ITextSnapshot currentSnapshot + ) { - CodeSpanRange codeSpanRange = state.CodeSpanRange; + CodeSpanRange codeSpanRange = serializedContainingCodeTracker.CodeSpanRange; IContainingCodeTracker containingCodeTracker = null; - switch (state.Type) + switch (serializedContainingCodeTracker.Type) { case ContainingCodeTrackerType.OtherLines: containingCodeTracker = this.CreateOtherLines(currentSnapshot, codeSpanRange); @@ -249,27 +263,61 @@ private IContainingCodeTracker RecreateContainingCodeTracker(SerializedState sta containingCodeTracker = this.CreateNotIncluded(currentSnapshot, codeSpanRange); break; case ContainingCodeTrackerType.CoverageLines: - containingCodeTracker = this.RecreateCoverageLines(state, currentSnapshot); + containingCodeTracker = this.RecreateCoverageLines(serializedContainingCodeTracker, currentSnapshot); break; } return containingCodeTracker; } - private ITrackedLines RecreateTrackedLinesFromRoslynState(List states, ITextSnapshot currentSnapshot, bool isCharp) + private IContainingCodeTrackerTrackedLines RecreateTrackedLinesFileCodeSpanRangeService( + List serializedContainingCodeTrackers, + ITextSnapshot currentSnapshot, + ICoverageContentType coverageContentType) + { + List containingCodeTrackers = this.RecreateContainingCodeTrackers( + serializedContainingCodeTrackers, currentSnapshot); + List codeSpanRanges = coverageContentType.FileCodeSpanRangeService.GetFileCodeSpanRanges(currentSnapshot); + INewCodeTracker newCodeTracker = this.RecreateNewCodeTracker( + serializedContainingCodeTrackers, + currentSnapshot, + coverageContentType, + codeSpanRanges); + return this.containingCodeTrackedLinesFactory.Create( + containingCodeTrackers, + newCodeTracker, + this.GetFileCodeSpanRangeServiceForChanges(coverageContentType) + ); + } + + private INewCodeTracker RecreateNewCodeTracker( + List serializedContainingCodeTrackers, + ITextSnapshot currentSnapshot, + ICoverageContentType coverageContentType, + List codeSpanRanges + ) + { + if (coverageContentType.LineExcluder == null) return null; + + List newCodeSpanRanges = this.GetNewCodeSpanRanges( + codeSpanRanges, + serializedContainingCodeTrackers.Select(serializedContainingCodeTracker => serializedContainingCodeTracker.CodeSpanRange)); + IEnumerable newCodeLineNumbers = this.GetRecreateNewCodeLineNumbers(newCodeSpanRanges, coverageContentType.UseFileCodeSpanRangeServiceForChanges); + return this.newCodeTrackerFactory.Create(coverageContentType.LineExcluder, newCodeLineNumbers, currentSnapshot); + } + + private List GetNewCodeSpanRanges(List currentCodeSpanRanges, IEnumerable serializedCodeSpanRanges) { - bool useRoslynWhenTextChanges = this.UseRoslynWhenTextChanges(); - IFileCodeSpanRangeService roslynFileCodeSpanRangeService = this.GetRoslynFileCodeSpanRangeService(useRoslynWhenTextChanges); - List codeSpanRanges = this.GetRoslynCodeSpanRanges(currentSnapshot); - List containingCodeTrackers = this.RecreateContainingCodeTrackersWithUnchangedCodeSpanRange(codeSpanRanges, states, currentSnapshot); - IEnumerable newCodeLineNumbers = this.GetRecreateNewCodeLineNumbers(codeSpanRanges, useRoslynWhenTextChanges); - INewCodeTracker newCodeTracker = this.newCodeTrackerFactory.Create(isCharp, newCodeLineNumbers, currentSnapshot); - - return this.containingCodeTrackedLinesFactory.Create(containingCodeTrackers, newCodeTracker, roslynFileCodeSpanRangeService); + foreach (CodeSpanRange serializedCodeSpanRange in serializedCodeSpanRanges) + { + _ = currentCodeSpanRanges.Remove(serializedCodeSpanRange); + } + + return currentCodeSpanRanges; } - private IEnumerable GetRecreateNewCodeLineNumbers(List newCodeCodeRanges, bool useRoslynWhenTextChanges) - => useRoslynWhenTextChanges + private IEnumerable GetRecreateNewCodeLineNumbers(List newCodeCodeRanges, bool hasFileCodeSpanRangeServiceForChanges) + => hasFileCodeSpanRangeServiceForChanges ? this.StartLines(newCodeCodeRanges) : this.EveryLineInCodeSpanRanges(newCodeCodeRanges); @@ -281,26 +329,70 @@ private IEnumerable EveryLineInCodeSpanRanges(List newCodeCo newCodeCodeRange.StartLine, newCodeCodeRange.EndLine - newCodeCodeRange.StartLine + 1) ); - public ITrackedLines Create(string serializedCoverage, ITextSnapshot currentSnapshot, Language language) + + public ITrackedLines Create(string serializedCoverage, ITextSnapshot currentSnapshot, string filePath) + { + SerializedEditorDynamicCoverage serializedEditorDynamicCoverage = this.jsonConvertService.DeserializeObject(serializedCoverage); + bool usedFileCodeSpanRangeService = serializedEditorDynamicCoverage.UsedFileCodeSpanRangeService; + bool textUnchanged = this.TextUnchanged(serializedEditorDynamicCoverage, currentSnapshot); + if (!textUnchanged) + { + this.logger.Log($"Not creating editor marks for {filePath} as text has changed"); + return null; + } + + IContainingCodeTrackerTrackedLines trackedLines = this.RecreateTrackedLines( + serializedEditorDynamicCoverage.SerializedContainingCodeTrackers, + serializedEditorDynamicCoverage.NewCodeLineNumbers, + currentSnapshot, + usedFileCodeSpanRangeService + ); + + return new ContainingCodeTrackerTrackedLinesWithState(trackedLines, usedFileCodeSpanRangeService); + } + + private IContainingCodeTrackerTrackedLines RecreateTrackedLines( + List serializedContainingCodeTrackers, + List newCodeLineNumbers, + ITextSnapshot currentSnapshot, + bool usedFileCodeSpanRangeService + ) { - List states = this.jsonConvertService.DeserializeObject>(serializedCoverage); - return language == Language.CPP - ? this.RecreateTrackedLinesFromCPPStates(states, currentSnapshot) - : this.RecreateTrackedLinesFromRoslynState(states, currentSnapshot, language == Language.CSharp); + ICoverageContentType coverageContentType = this.GetCoverageContentType(currentSnapshot); + return usedFileCodeSpanRangeService ? + this.RecreateTrackedLinesFileCodeSpanRangeService(serializedContainingCodeTrackers, currentSnapshot, coverageContentType) : + this.RecreateTrackedLinesNoFileCodeSpanRangeService(serializedContainingCodeTrackers, currentSnapshot, coverageContentType.LineExcluder, newCodeLineNumbers); } - public string Serialize(ITrackedLines trackedLines) + private bool TextUnchanged(SerializedEditorDynamicCoverage serializedEditorDyamicCoverage, ITextSnapshot textSnapshot) { - var trackedLinesImpl = trackedLines as TrackedLines; - List states = this.GetSerializedStates(trackedLinesImpl); - return this.jsonConvertService.SerializeObject(states); + string previousText = serializedEditorDyamicCoverage.Text; + string currentText = textSnapshot.GetText(); + return previousText == currentText; } - private List GetSerializedStates(TrackedLines trackedLines) - => trackedLines.ContainingCodeTrackers.Select( - containingCodeTracker => SerializedState.From(containingCodeTracker.GetState())).ToList(); + public string Serialize(ITrackedLines trackedLines, string text) + { + var containingCodeTrackerTrackedLinesWithState = trackedLines as ContainingCodeTrackerTrackedLinesWithState; + List serializedContainingCodeTrackers = this.GetSerializedContainingCodeTrackers(containingCodeTrackerTrackedLinesWithState); + var newCodeLineNumbers = new List(); + if (containingCodeTrackerTrackedLinesWithState.NewCodeTracker != null) + { + newCodeLineNumbers = containingCodeTrackerTrackedLinesWithState.NewCodeTracker.Lines.Select(l => l.Number).ToList(); + } - public List GetFileCodeSpanRanges(ITextSnapshot snapshot) => this.GetRoslynCodeSpanRanges(snapshot); + return this.jsonConvertService.SerializeObject( + new SerializedEditorDynamicCoverage { + SerializedContainingCodeTrackers = serializedContainingCodeTrackers, + Text = text, + NewCodeLineNumbers = newCodeLineNumbers, + UsedFileCodeSpanRangeService = containingCodeTrackerTrackedLinesWithState.UsedFileCodeSpanRangeService + }); + } + + private List GetSerializedContainingCodeTrackers(IContainingCodeTrackerTrackedLines trackedLines) + => trackedLines.ContainingCodeTrackers.Select( + containingCodeTracker => SerializedContainingCodeTracker.From(containingCodeTracker.GetState())).ToList(); private class AdjustedLine : ILine { @@ -314,5 +406,6 @@ public AdjustedLine(IDynamicLine dynamicLine) public CoverageType CoverageType { get; } } + #endregion } } diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackerTrackedLinesWithState.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackerTrackedLinesWithState.cs new file mode 100644 index 00000000..ad3832bf --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackerTrackedLinesWithState.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction +{ + [ExcludeFromCodeCoverage] + internal class ContainingCodeTrackerTrackedLinesWithState : IContainingCodeTrackerTrackedLines + { + public IContainingCodeTrackerTrackedLines Wrapped { get; } + public ContainingCodeTrackerTrackedLinesWithState(IContainingCodeTrackerTrackedLines trackedLines, bool usedFileCodeSpanRangeService) + { + this.Wrapped = trackedLines; + this.UsedFileCodeSpanRangeService = usedFileCodeSpanRangeService; + } + + public bool UsedFileCodeSpanRangeService { get; set; } + public IReadOnlyList ContainingCodeTrackers => this.Wrapped.ContainingCodeTrackers; + public INewCodeTracker NewCodeTracker => this.Wrapped.NewCodeTracker; + + public IEnumerable GetChangedLineNumbers(ITextSnapshot currentSnapshot, List newSpanChanges) + => this.Wrapped.GetChangedLineNumbers(currentSnapshot, newSpanChanges); + public IEnumerable GetLines(int startLineNumber, int endLineNumber) + => this.Wrapped.GetLines(startLineNumber, endLineNumber); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs index cc490418..a65af438 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs @@ -4,7 +4,7 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { internal interface IContainingCodeTrackedLinesFactory { - TrackedLines Create( + IContainingCodeTrackerTrackedLines Create( List containingCodeTrackers, INewCodeTracker newCodeTracker, IFileCodeSpanRangeService fileCodeSpanRangeService diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ICoverageContentType.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ICoverageContentType.cs new file mode 100644 index 00000000..3ece73fd --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ICoverageContentType.cs @@ -0,0 +1,11 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction +{ + internal interface ICoverageContentType + { + string ContentTypeName { get; } + IFileCodeSpanRangeService FileCodeSpanRangeService { get; } + bool CoverageOnlyFromFileCodeSpanRangeService { get; } + bool UseFileCodeSpanRangeServiceForChanges { get; } + ILineExcluder LineExcluder { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedState.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedContainingCodeTracker.cs similarity index 65% rename from SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedState.cs rename to SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedContainingCodeTracker.cs index eff5485c..2765ec4c 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedState.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedContainingCodeTracker.cs @@ -3,17 +3,17 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { - internal class SerializedState + internal class SerializedContainingCodeTracker { - public SerializedState(CodeSpanRange codeSpanRange, ContainingCodeTrackerType type, List dynamicLines) + public SerializedContainingCodeTracker(CodeSpanRange codeSpanRange, ContainingCodeTrackerType type, List dynamicLines) { this.CodeSpanRange = codeSpanRange; this.Type = type; this.Lines = dynamicLines; } - public static SerializedState From(ContainingCodeTrackerState containingCodeTrackerState) - => new SerializedState( + public static SerializedContainingCodeTracker From(ContainingCodeTrackerState containingCodeTrackerState) + => new SerializedContainingCodeTracker( containingCodeTrackerState.CodeSpanRange, containingCodeTrackerState.Type, containingCodeTrackerState.Lines.Select(line => new DynamicLine(line.Number, line.CoverageType)).ToList() diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedEditorDynamicCoverage.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedEditorDynamicCoverage.cs new file mode 100644 index 00000000..9fca2940 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedEditorDynamicCoverage.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage.TrackedLinesImpl.Construction +{ + internal class SerializedEditorDynamicCoverage + { + public List SerializedContainingCodeTrackers { get; set; } + public string Text { get; set; } + public List NewCodeLineNumbers { get; set; } + public bool UsedFileCodeSpanRangeService { get; set; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs index 3f4a0a4d..a0fce76c 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs @@ -44,11 +44,11 @@ List nonIntersecting public IEnumerable GetUpdatedLineNumbers( TrackingSpanRangeProcessResult trackingSpanRangeProcessResult, ITextSnapshot currentSnapshot, - List newSpanAndLIneRanges + List newSpanAndLineRanges ) { List changedLineNumbers = this.CreateDirtyLineIfRequired( - newSpanAndLIneRanges, + newSpanAndLineRanges, trackingSpanRangeProcessResult.NonIntersectingSpans, trackingSpanRangeProcessResult.TextChanged, currentSnapshot, diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs index cf121f22..dc61e705 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs @@ -5,7 +5,7 @@ namespace FineCodeCoverage.Editor.DynamicCoverage { internal interface ITrackingSpanRange { - TrackingSpanRangeProcessResult Process(ITextSnapshot currentSnapshot, List newSpanAndLIneRanges); + TrackingSpanRangeProcessResult Process(ITextSnapshot currentSnapshot, List newSpanAndLineRanges); ITrackingSpan GetFirstTrackingSpan(); CodeSpanRange ToCodeSpanRange(); } diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs index 576ae603..f9d85d0e 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs @@ -2,6 +2,6 @@ { internal interface ILineExcluder { - bool ExcludeIfNotCode(string text, bool isCSharp); + bool ExcludeIfNotCode(string text); } } diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ITextSnapshotLineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ITextSnapshotLineExcluder.cs deleted file mode 100644 index 593a2658..00000000 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ITextSnapshotLineExcluder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.VisualStudio.Text; - -namespace FineCodeCoverage.Editor.DynamicCoverage -{ - internal interface ITextSnapshotLineExcluder - { - bool ExcludeIfNotCode(ITextSnapshot textSnapshot, int lineNumber, bool isCSharp); - } -} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/LineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/LineExcluder.cs index 372b9492..0130021f 100644 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/LineExcluder.cs +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/LineExcluder.cs @@ -6,19 +6,17 @@ namespace FineCodeCoverage.Editor.DynamicCoverage [Export(typeof(ILineExcluder))] internal class LineExcluder : ILineExcluder { - private static readonly string[] cSharpExclusions = new string[] { "//", "#", "using" }; - private static readonly string[] vbExclusions = new string[] { "REM", "'", "#" }; + private readonly string[] startsWithExclusions; - public bool ExcludeIfNotCode(string text, bool isCSharp) + public LineExcluder(string[] startsWithExclusions) => this.startsWithExclusions = startsWithExclusions; + + public bool ExcludeIfNotCode(string text) { string trimmedLineText = text.Trim(); - return trimmedLineText.Length == 0 || this.StartsWithExclusion(trimmedLineText, isCSharp); + return trimmedLineText.Length == 0 || this.StartsWithExclusion(trimmedLineText); } - private bool StartsWithExclusion(string text, bool isCSharp) - { - string[] languageExclusions = isCSharp ? cSharpExclusions : vbExclusions; - return languageExclusions.Any(languageExclusion => text.StartsWith(languageExclusion)); - } + private bool StartsWithExclusion(string text) + => this.startsWithExclusions.Any(languageExclusion => text.StartsWith(languageExclusion)); } } diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/TextSnapshotLineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/TextSnapshotLineExcluder.cs deleted file mode 100644 index 2bcee75a..00000000 --- a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/TextSnapshotLineExcluder.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Text; - -namespace FineCodeCoverage.Editor.DynamicCoverage -{ - [Export(typeof(ITextSnapshotLineExcluder))] - internal class TextSnapshotLineExcluder : ITextSnapshotLineExcluder - { - private readonly ITextSnapshotText textSnapshotText; - private readonly ILineExcluder codeLineExcluder; - - [ImportingConstructor] - public TextSnapshotLineExcluder(ITextSnapshotText textSnapshotText, ILineExcluder codeLineExcluder) - { - this.textSnapshotText = textSnapshotText; - this.codeLineExcluder = codeLineExcluder; - } - public bool ExcludeIfNotCode(ITextSnapshot textSnapshot, int lineNumber, bool isCSharp) - => this.codeLineExcluder.ExcludeIfNotCode(this.textSnapshotText.GetLineText(textSnapshot, lineNumber), isCSharp); - } -} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/DateTimeService.cs b/SharedProject/Editor/DynamicCoverage/Utilities/DateTimeService.cs new file mode 100644 index 00000000..9637c921 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/DateTimeService.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Editor.DynamicCoverage.Utilities +{ + [Export(typeof(IDateTimeService))] + internal class DateTimeService : IDateTimeService + { + public DateTime Now => DateTime.Now; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/IDateTimeService.cs b/SharedProject/Editor/DynamicCoverage/Utilities/IDateTimeService.cs new file mode 100644 index 00000000..614a347d --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/IDateTimeService.cs @@ -0,0 +1,9 @@ +using System; + +namespace FineCodeCoverage.Editor.DynamicCoverage.Utilities +{ + internal interface IDateTimeService + { + DateTime Now { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ISyntaxNodeLocationMapper.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ISyntaxNodeLocationMapper.cs new file mode 100644 index 00000000..2b6bf077 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ISyntaxNodeLocationMapper.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; + +namespace FineCodeCoverage.Editor.DynamicCoverage.Utilities +{ + internal interface ISyntaxNodeLocationMapper + { + FileLinePositionSpan Map(SyntaxNode node); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs index caaf1f08..15f21a19 100644 --- a/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.Text; +using System; +using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; namespace FineCodeCoverage.Editor.DynamicCoverage @@ -8,5 +9,7 @@ internal interface ITextInfo string FilePath { get; } ITextBuffer2 TextBuffer { get; } ITextView TextView { get; } + string GetFileText(); + DateTime GetLastWriteTime(); } } diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs index 09f4a800..df4f38c9 100644 --- a/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs @@ -6,5 +6,6 @@ namespace FineCodeCoverage.Editor.DynamicCoverage internal interface ITextInfoFactory { ITextInfo Create(ITextView textView, ITextBuffer textBuffer); + string GetFilePath(ITextBuffer textBuffer); } } diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/SyntaxNodeLocationMapper.cs b/SharedProject/Editor/DynamicCoverage/Utilities/SyntaxNodeLocationMapper.cs new file mode 100644 index 00000000..0350ed32 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/SyntaxNodeLocationMapper.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace FineCodeCoverage.Editor.DynamicCoverage.Utilities +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ISyntaxNodeLocationMapper))] + internal class SyntaxNodeLocationMapper : ISyntaxNodeLocationMapper + { + public FileLinePositionSpan Map(SyntaxNode node) => node.GetLocation().GetMappedLineSpan(); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs index 76a47d93..6b1e4203 100644 --- a/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs +++ b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs @@ -1,8 +1,12 @@ -using Microsoft.VisualStudio.Text; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; namespace FineCodeCoverage.Editor.DynamicCoverage { + [ExcludeFromCodeCoverage] internal class TextInfo : ITextInfo { private bool triedGetProperty; @@ -32,5 +36,7 @@ public TextInfo(ITextView textView, ITextBuffer textBuffer) public ITextView TextView { get; } public ITextBuffer2 TextBuffer { get; } public string FilePath => this.TextDocument?.FilePath; + public string GetFileText() => File.ReadAllText(this.FilePath); + public DateTime GetLastWriteTime() => new FileInfo(this.FilePath).LastWriteTime; } } diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs index 507b6ea0..a2f8a7cd 100644 --- a/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs +++ b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs @@ -10,5 +10,6 @@ namespace FineCodeCoverage.Editor.DynamicCoverage internal class TextInfoFactory : ITextInfoFactory { public ITextInfo Create(ITextView textView, ITextBuffer textBuffer) => new TextInfo(textView, textBuffer); + public string GetFilePath(ITextBuffer textBuffer) => new TextInfo(null, textBuffer).FilePath; } } diff --git a/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs b/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs index 35837965..1d73b2df 100644 --- a/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs +++ b/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.Composition; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -7,13 +8,18 @@ namespace FineCodeCoverage.Editor.Roslyn { - internal class CSharpContainingCodeVisitor : CSharpSyntaxVisitor, ILanguageContainingCodeVisitor + [Export(typeof(ICSharpCodeCoverageNodeVisitor))] + internal class CSharpContainingCodeVisitor : CSharpSyntaxVisitor, ILanguageContainingCodeVisitor, ICSharpCodeCoverageNodeVisitor { - private readonly List spans = new List(); - public List GetSpans(SyntaxNode rootNode) + private readonly List nodes = new List(); + public List GetSpans(SyntaxNode rootNode) + => this.GetNodes(rootNode).Select(node => node.Span).ToList(); + + public List GetNodes(SyntaxNode rootNode) { + this.nodes.Clear(); this.Visit(rootNode); - return this.spans; + return this.nodes; } #if VS2022 @@ -100,6 +106,6 @@ private void VisitMembers(SyntaxList members) private bool IsAbstract(SyntaxTokenList modifiers) => modifiers.Any(modifier => modifier.IsKind(SyntaxKind.AbstractKeyword)); - private void AddNode(SyntaxNode node) => this.spans.Add(node.Span); + private void AddNode(SyntaxNode node) => this.nodes.Add(node); } } diff --git a/SharedProject/Editor/Roslyn/ICSharpCodeCoverageNodeVisitor.cs b/SharedProject/Editor/Roslyn/ICSharpCodeCoverageNodeVisitor.cs new file mode 100644 index 00000000..dd3691c5 --- /dev/null +++ b/SharedProject/Editor/Roslyn/ICSharpCodeCoverageNodeVisitor.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal interface ICSharpCodeCoverageNodeVisitor + { + List GetNodes(SyntaxNode rootNode); + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs b/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs index d980b138..4e5ae3cb 100644 --- a/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs +++ b/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs @@ -1,4 +1,5 @@ -using FineCodeCoverage.Core.Utilities; +using System.Linq; +using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Editor.DynamicCoverage; using FineCodeCoverage.Options; using Microsoft.VisualStudio.Text; @@ -15,6 +16,7 @@ internal class CoverageTaggerProvider : ICoverageTagg private readonly ILineSpanTagger coverageTagger; private readonly IDynamicCoverageManager dynamicCoverageManager; private readonly ITextInfoFactory textInfoFactory; + private readonly IFileExcluder[] fileExcluders; private TCoverageTypeFilter coverageTypeFilter; public CoverageTaggerProvider( @@ -23,10 +25,12 @@ public CoverageTaggerProvider( ILineSpanLogic lineSpanLogic, ILineSpanTagger coverageTagger, IDynamicCoverageManager dynamicCoverageManager, - ITextInfoFactory textInfoFactory) + ITextInfoFactory textInfoFactory, + IFileExcluder[] fileExcluders) { this.dynamicCoverageManager = dynamicCoverageManager; this.textInfoFactory = textInfoFactory; + this.fileExcluders = fileExcluders; IAppOptions appOptions = appOptionsProvider.Get(); this.coverageTypeFilter = this.CreateFilter(appOptions); appOptionsProvider.OptionsChanged += this.AppOptionsProvider_OptionsChanged; @@ -53,11 +57,17 @@ private void AppOptionsProvider_OptionsChanged(IAppOptions appOptions) } } + private bool ExcludeContentTypeFile(string contentType,string filePath) + { + IFileExcluder contentTypeExcluder = this.fileExcluders.FirstOrDefault(fileExcluder => fileExcluder.ContentTypeName == contentType); + return contentTypeExcluder != null && contentTypeExcluder.Exclude(filePath); + } + public ICoverageTagger CreateTagger(ITextView textView, ITextBuffer textBuffer) { ITextInfo textInfo = this.textInfoFactory.Create(textView, textBuffer); string filePath = textInfo.FilePath; - if (filePath == null) + if (filePath == null || this.ExcludeContentTypeFile(textBuffer.ContentType.TypeName, filePath)) { return null; } diff --git a/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs b/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs index 2764ace7..77183d62 100644 --- a/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs +++ b/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs @@ -16,6 +16,7 @@ internal class CoverageTaggerProviderFactory : ICoverageTaggerProviderFactory private readonly ILineSpanLogic lineSpanLogic; private readonly IDynamicCoverageManager dynamicCoverageManager; private readonly ITextInfoFactory textInfoFactory; + private readonly IFileExcluder[] fileExcluders; [ImportingConstructor] public CoverageTaggerProviderFactory( @@ -23,7 +24,9 @@ public CoverageTaggerProviderFactory( IAppOptionsProvider appOptionsProvider, ILineSpanLogic lineSpanLogic, IDynamicCoverageManager dynamicCoverageManager, - ITextInfoFactory textInfoFactory + ITextInfoFactory textInfoFactory, + [ImportMany] + IFileExcluder[] fileExcluders ) { this.eventAggregator = eventAggregator; @@ -31,6 +34,7 @@ ITextInfoFactory textInfoFactory this.lineSpanLogic = lineSpanLogic; this.dynamicCoverageManager = dynamicCoverageManager; this.textInfoFactory = textInfoFactory; + this.fileExcluders = fileExcluders; } public ICoverageTaggerProvider Create(ILineSpanTagger tagger) where TTag : ITag @@ -41,7 +45,8 @@ public ICoverageTaggerProvider Create(ILineSpan this.lineSpanLogic, tagger, this.dynamicCoverageManager, - this.textInfoFactory + this.textInfoFactory, + this.fileExcluders ); } } diff --git a/SharedProject/Editor/Tagging/Base/IFileExcluder.cs b/SharedProject/Editor/Tagging/Base/IFileExcluder.cs new file mode 100644 index 00000000..a12b4633 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/IFileExcluder.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface IFileExcluder + { + string ContentTypeName { get; } + bool Exclude(string filePath); + } +} diff --git a/SharedProject/Editor/Tagging/Base/SupportedContentTypeLanguages.cs b/SharedProject/Editor/Tagging/Base/SupportedContentTypeLanguages.cs deleted file mode 100644 index b7702967..00000000 --- a/SharedProject/Editor/Tagging/Base/SupportedContentTypeLanguages.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FineCodeCoverage.Editor.Tagging.Base -{ - internal enum Language { CSharp, VB, CPP } - internal class SupportedContentTypeLanguages - { - public const string CSharp = "CSharp"; - public const string VisualBasic = "Basic"; - public const string CPP = "C/C++"; - public static Language GetLanguage(string contentType) - => contentType == CSharp ? Language.CSharp : contentType == VisualBasic ? Language.VB : Language.CPP; - } -} diff --git a/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs b/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs index c2785f59..787fa7c5 100644 --- a/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs +++ b/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs @@ -1,4 +1,7 @@ using System.ComponentModel.Composition; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; using FineCodeCoverage.Editor.Management; using FineCodeCoverage.Editor.Tagging.Base; using Microsoft.VisualStudio.Text; @@ -9,9 +12,10 @@ namespace FineCodeCoverage.Editor.Tagging.Classification { - [ContentType(SupportedContentTypeLanguages.CSharp)] - [ContentType(SupportedContentTypeLanguages.VisualBasic)] - [ContentType(SupportedContentTypeLanguages.CPP)] + [ContentType(CSharpCoverageContentType.ContentType)] + [ContentType(VBCoverageContentType.ContentType)] + [ContentType(CPPCoverageContentType.ContentType)] + [ContentType(BlazorCoverageContentType.ContentType)] [TagType(typeof(IClassificationTag))] [Name("FCC.CoverageLineClassificationTaggerProvider")] [Export(typeof(IViewTaggerProvider))] diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs index 9b424fb5..dc635d53 100644 --- a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs @@ -1,5 +1,8 @@ using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; using FineCodeCoverage.Editor.Tagging.Base; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Tagging; @@ -9,9 +12,10 @@ namespace FineCodeCoverage.Editor.Tagging.GlyphMargin { [ExcludeFromCodeCoverage] - [ContentType(SupportedContentTypeLanguages.CSharp)] - [ContentType(SupportedContentTypeLanguages.VisualBasic)] - [ContentType(SupportedContentTypeLanguages.CPP)] + [ContentType(CSharpCoverageContentType.ContentType)] + [ContentType(VBCoverageContentType.ContentType)] + [ContentType(CPPCoverageContentType.ContentType)] + [ContentType(BlazorCoverageContentType.ContentType)] [TagType(typeof(CoverageLineGlyphTag))] [Order(Before = "VsTextMarker")] [Name(Vsix.GlyphFactoryProviderName)] diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs index e06c252e..7901bb99 100644 --- a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs @@ -2,6 +2,9 @@ using System.Windows.Media; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; using FineCodeCoverage.Editor.Management; using FineCodeCoverage.Editor.Tagging.Base; using Microsoft.VisualStudio.Text; @@ -11,9 +14,10 @@ namespace FineCodeCoverage.Editor.Tagging.GlyphMargin { - [ContentType(SupportedContentTypeLanguages.CSharp)] - [ContentType(SupportedContentTypeLanguages.VisualBasic)] - [ContentType(SupportedContentTypeLanguages.CPP)] + [ContentType(CSharpCoverageContentType.ContentType)] + [ContentType(VBCoverageContentType.ContentType)] + [ContentType(CPPCoverageContentType.ContentType)] + [ContentType(BlazorCoverageContentType.ContentType)] [TagType(typeof(CoverageLineGlyphTag))] [Name(Vsix.TaggerProviderName)] [Export(typeof(IViewTaggerProvider))] diff --git a/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs b/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs index b700096b..d48e1a56 100644 --- a/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs +++ b/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs @@ -1,4 +1,7 @@ using System.ComponentModel.Composition; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Blazor; +using FineCodeCoverage.Editor.DynamicCoverage.ContentTypes.Roslyn; using FineCodeCoverage.Editor.Management; using FineCodeCoverage.Editor.Tagging.Base; using Microsoft.VisualStudio.Text; @@ -8,9 +11,10 @@ namespace FineCodeCoverage.Editor.Tagging.OverviewMargin { - [ContentType(SupportedContentTypeLanguages.CSharp)] - [ContentType(SupportedContentTypeLanguages.VisualBasic)] - [ContentType(SupportedContentTypeLanguages.CPP)] + [ContentType(CSharpCoverageContentType.ContentType)] + [ContentType(VBCoverageContentType.ContentType)] + [ContentType(CPPCoverageContentType.ContentType)] + [ContentType(BlazorCoverageContentType.ContentType)] [TagType(typeof(OverviewMarkTag))] [Name("FCC.CoverageLineOverviewMarkTaggerProvider")] [Export(typeof(IViewTaggerProvider))] diff --git a/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs b/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs index dd5c584c..91f6535d 100644 --- a/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs +++ b/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs @@ -13,6 +13,7 @@ using FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage; using System.Threading; using FineCodeCoverage.Core.Initialization; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Impl { @@ -32,6 +33,7 @@ internal class TestContainerDiscoverer : ITestContainerDiscoverer private readonly IAppOptionsProvider appOptionsProvider; private readonly IReportGeneratorUtil reportGeneratorUtil; private readonly IMsCodeCoverageRunSettingsService msCodeCoverageRunSettingsService; + private readonly IEventAggregator eventAggregator; internal Dictionary> testOperationStateChangeHandlers; private bool cancelling; private MsCodeCoverageCollectionStatus msCodeCoverageCollectionStatus; @@ -59,12 +61,14 @@ public TestContainerDiscoverer ILogger logger, IAppOptionsProvider appOptionsProvider, IReportGeneratorUtil reportGeneratorUtil, - IMsCodeCoverageRunSettingsService msCodeCoverageRunSettingsService + IMsCodeCoverageRunSettingsService msCodeCoverageRunSettingsService, + IEventAggregator eventAggregator ) { this.appOptionsProvider = appOptionsProvider; this.reportGeneratorUtil = reportGeneratorUtil; this.msCodeCoverageRunSettingsService = msCodeCoverageRunSettingsService; + this.eventAggregator = eventAggregator; this.fccEngine = fccEngine; this.testOperationStateInvocationManager = testOperationStateInvocationManager; this.testOperationFactory = testOperationFactory; @@ -92,6 +96,7 @@ private bool CoverageDisabled(IAppOptions settings) private async Task TestExecutionStartingAsync(IOperation operation) { + this.eventAggregator.SendMessage(new TestExecutionStartingMessage()); cancelling = false; runningInParallel = false; StopCoverage(); diff --git a/SharedProject/Impl/TestContainerDiscovery/TestExecutionStartingMessage.cs b/SharedProject/Impl/TestContainerDiscovery/TestExecutionStartingMessage.cs new file mode 100644 index 00000000..6f73ebe1 --- /dev/null +++ b/SharedProject/Impl/TestContainerDiscovery/TestExecutionStartingMessage.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FineCodeCoverage.Impl +{ + internal class TestExecutionStartingMessage + { + } +} diff --git a/SharedProject/Options/AppOptionsPage.cs b/SharedProject/Options/AppOptionsPage.cs index 38525309..dfa31db9 100644 --- a/SharedProject/Options/AppOptionsPage.cs +++ b/SharedProject/Options/AppOptionsPage.cs @@ -280,6 +280,10 @@ You can also ignore additional attributes by adding to this list (short name or #region editorColouringControlCategory + [Category(editorColouringControlCategory)] + [Description("Set to true to limit coverage lines in .razor file to those in generated source ( when available)")] + public bool BlazorCoverageLinesFromGeneratedSource { get; set; } + [Category(editorColouringControlCategory)] [Description("Set to false to disable all editor coverage indicators")] //[DisplayName("Show Editor Coverage")] diff --git a/SharedProject/Options/AppOptionsProvider.cs b/SharedProject/Options/AppOptionsProvider.cs index ae668b63..005fa566 100644 --- a/SharedProject/Options/AppOptionsProvider.cs +++ b/SharedProject/Options/AppOptionsProvider.cs @@ -247,7 +247,8 @@ internal class AppOptions : IAppOptions public bool ShowEditorCoverage { get; set; } public bool UseEnterpriseFontsAndColors { get; set; } public EditorCoverageColouringMode EditorCoverageColouringMode { get; set; } - - + + public bool BlazorCoverageLinesFromGeneratedSource { get; set; } + } } diff --git a/SharedProject/Options/IAppOptions.cs b/SharedProject/Options/IAppOptions.cs index b0cd1d8d..b2eba47c 100644 --- a/SharedProject/Options/IAppOptions.cs +++ b/SharedProject/Options/IAppOptions.cs @@ -122,6 +122,7 @@ internal interface IAppOptions : IMsCodeCoverageOptions, IOpenCoverCoverletExclu bool ShowToolWindowToolbar { get; set; } NamespaceQualification NamespaceQualification { get; set; } + bool BlazorCoverageLinesFromGeneratedSource { get; set; } } internal enum NamespaceQualification diff --git a/SharedProject/Output/OutputToolWindowPackage.cs b/SharedProject/Output/OutputToolWindowPackage.cs index abb144aa..43c7587f 100644 --- a/SharedProject/Output/OutputToolWindowPackage.cs +++ b/SharedProject/Output/OutputToolWindowPackage.cs @@ -12,8 +12,6 @@ using EnvDTE80; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Core.Initialization; -using FineCodeCoverage.Impl; -using FineCodeCoverage.Editor.Management; namespace FineCodeCoverage.Output { @@ -100,7 +98,6 @@ await OutputToolWindowCommand.InitializeAsync( componentModel.GetService(), componentModel.GetService() ); - await componentModel.GetService().InitializeAsync(cancellationToken); } diff --git a/SharedProject/SharedProject.projitems b/SharedProject/SharedProject.projitems index 4f32be23..bde0104e 100644 --- a/SharedProject/SharedProject.projitems +++ b/SharedProject/SharedProject.projitems @@ -186,19 +186,40 @@ + + + + + + + + + + + + + + - + + + + + + + + @@ -231,8 +252,6 @@ - - @@ -240,6 +259,7 @@ + @@ -257,6 +277,7 @@ + @@ -277,6 +298,7 @@ + @@ -285,7 +307,6 @@ - @@ -348,6 +369,7 @@ + @@ -415,9 +437,7 @@ - -