diff --git a/.gitignore b/.gitignore index bd4d918a..de9a4f43 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ x64/ *.ncrunchproject _NCrunch_WebCompiler /MSBuild_Logs +/MigrationBackup/f1627d49/FineCodeCoverageTests diff --git a/Art/Options-Global.png b/Art/Options-Global.png index 2f030f15..d1bd3286 100644 Binary files a/Art/Options-Global.png and b/Art/Options-Global.png differ diff --git a/Art/Output-Coverage.png b/Art/Output-Coverage.png index 16a68653..f82c794e 100644 Binary files a/Art/Output-Coverage.png and b/Art/Output-Coverage.png differ diff --git a/Art/Output-RiskHotspots.png b/Art/Output-RiskHotspots.png index b4d83785..531cfa8b 100644 Binary files a/Art/Output-RiskHotspots.png and b/Art/Output-RiskHotspots.png differ diff --git a/Art/Output-Summary.png b/Art/Output-Summary.png index 8b44de21..3e5817b7 100644 Binary files a/Art/Output-Summary.png and b/Art/Output-Summary.png differ diff --git a/Art/preview-coverage.png b/Art/preview-coverage.png index 02705ac5..aa88146d 100644 Binary files a/Art/preview-coverage.png and b/Art/preview-coverage.png differ diff --git a/FineCodeCoverage.sln b/FineCodeCoverage.sln index cadbfd26..9826e5fe 100644 --- a/FineCodeCoverage.sln +++ b/FineCodeCoverage.sln @@ -7,7 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FineCodeCoverage", "FineCod EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{208EE360-4076-4680-A9B7-2BA9C17EA9FB}" ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore .github\workflows\addVsixLinkToIssues.yaml = .github\workflows\addVsixLinkToIssues.yaml diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index 2b17f798..e0a731f1 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -150,8 +150,26 @@ 1.4.1 + + 3.11.0 + + + 3.11.0 + + + 3.11.0 + + + 3.11.0 + + + 3.11.0 + - 3.8.0 + 3.11.0 + + + 16.9.20 compile; build; native; contentfiles; analyzers; buildtransitive @@ -159,6 +177,11 @@ 11.0.61030 + + 17.9.28 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/FineCodeCoverage/source.extension.vsixmanifest b/FineCodeCoverage/source.extension.vsixmanifest index a4809dbb..f554f1a2 100644 --- a/FineCodeCoverage/source.extension.vsixmanifest +++ b/FineCodeCoverage/source.extension.vsixmanifest @@ -7,7 +7,6 @@ https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage Resources\LICENSE Resources\logo.png - Resources\preview-coverage.png visual studio; code coverage; c#; vb; .net core; coverlet; unit test; free; community edition diff --git a/FineCodeCoverage2022/FineCodeCoverage2022.csproj b/FineCodeCoverage2022/FineCodeCoverage2022.csproj index c5d38f63..2f44f446 100644 --- a/FineCodeCoverage2022/FineCodeCoverage2022.csproj +++ b/FineCodeCoverage2022/FineCodeCoverage2022.csproj @@ -32,7 +32,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;VS2022 prompt 4 @@ -145,8 +145,26 @@ 1.4.1 + + 4.8.0 + + + 4.8.0 + + + 4.8.0 + + + 4.8.0 + + + 4.8.0 + - 4.0.1 + 4.8.0 + + + 17.7.40 compile; build; native; contentfiles; analyzers; buildtransitive @@ -154,6 +172,11 @@ 11.0.61030 + + 17.9.28 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -167,6 +190,9 @@ 1.0.0 + + 2.16.36 + 3.3.0 diff --git a/FineCodeCoverage2022/Properties/AssemblyInfo.cs b/FineCodeCoverage2022/Properties/AssemblyInfo.cs index 1ea8818b..338f561d 100644 --- a/FineCodeCoverage2022/Properties/AssemblyInfo.cs +++ b/FineCodeCoverage2022/Properties/AssemblyInfo.cs @@ -31,3 +31,6 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: InternalsVisibleTo("FineCodeCoverageTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/FineCodeCoverage2022/source.extension.vsixmanifest b/FineCodeCoverage2022/source.extension.vsixmanifest index 757c6250..67cfccb0 100644 --- a/FineCodeCoverage2022/source.extension.vsixmanifest +++ b/FineCodeCoverage2022/source.extension.vsixmanifest @@ -7,7 +7,6 @@ https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage Resources\LICENSE Resources\logo.png - Resources\preview-coverage.png visual studio; code coverage; c#; vb; .net core; coverlet; unit test; free; community edition diff --git a/FineCodeCoverageTests/AppOptionsProvider_Tests.cs b/FineCodeCoverageTests/AppOptionsProvider_Tests.cs index 05d6be95..dce37834 100644 --- a/FineCodeCoverageTests/AppOptionsProvider_Tests.cs +++ b/FineCodeCoverageTests/AppOptionsProvider_Tests.cs @@ -4,6 +4,7 @@ using AutoMoq; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Options; +using Microsoft.VisualStudio.Settings; using Moq; using NUnit.Framework; @@ -13,16 +14,16 @@ public class AppOptionsProvider_Tests { private AutoMoqer autoMocker; private AppOptionsProvider appOptionsProvider; - private Mock mockWritableSettingsStore; + private Mock mockWritableSettingsStore; [SetUp] public void Setup() { autoMocker = new AutoMoqer(); appOptionsProvider = autoMocker.Create(); - mockWritableSettingsStore = new Mock(); - var mockWritableSettingsStoreProvider = autoMocker.GetMock(); - mockWritableSettingsStoreProvider.Setup( + mockWritableSettingsStore = new Mock(); + var mockWritableUserSettingsStoreProvider = autoMocker.GetMock(); + mockWritableUserSettingsStoreProvider.Setup( writableSettingsStoreProvider => writableSettingsStoreProvider.Provide() ).Returns(mockWritableSettingsStore.Object); } @@ -205,7 +206,16 @@ public void Should_Not_Default_Any_Other_AppOptions_Properties() nameof(IAppOptions.ShowPartiallyCoveredInOverviewMargin), nameof(IAppOptions.ShowToolWindowToolbar), nameof(IAppOptions.Hide0Coverable), - nameof(IAppOptions.DisabledNoCoverage) + nameof(IAppOptions.DisabledNoCoverage), + nameof(IAppOptions.ShowEditorCoverage), + nameof(IAppOptions.ShowCoverageInGlyphMargin), + nameof(IAppOptions.ShowCoveredInGlyphMargin), + nameof(IAppOptions.ShowUncoveredInGlyphMargin), + nameof(IAppOptions.ShowPartiallyCoveredInGlyphMargin), + nameof(IAppOptions.ShowLineCoveredHighlighting), + nameof(IAppOptions.ShowLinePartiallyCoveredHighlighting), + nameof(IAppOptions.ShowLineUncoveredHighlighting), + nameof(IAppOptions.UseEnterpriseFontsAndColors) }; CollectionAssert.AreEquivalent(expectedSetters.Select(s => $"set_{s}"), invocationNames); } @@ -273,7 +283,6 @@ internal void Should_Use_Deseralized_String_From_Store_For_AppOption_Property(Fu { nameof(IAppOptions.AttributesInclude), new string[]{ "ainclude"}}, { nameof(IAppOptions.CompanyNamesExclude), new string[]{ "cexclude"}}, { nameof(IAppOptions.CompanyNamesInclude), new string[]{ "cinclude"}}, - { nameof(IAppOptions.CoverageColoursFromFontsAndColours), true}, { nameof(IAppOptions.CoverletCollectorDirectoryPath), "p"}, { nameof(IAppOptions.CoverletConsoleCustomPath), "cp"}, { nameof(IAppOptions.CoverletConsoleGlobal), true}, @@ -313,14 +322,34 @@ internal void Should_Use_Deseralized_String_From_Store_For_AppOption_Property(Fu { nameof(IAppOptions.ShowCoverageInOverviewMargin),true}, { nameof(IAppOptions.ShowCoveredInOverviewMargin),true}, { nameof(IAppOptions.ShowPartiallyCoveredInOverviewMargin),true}, + { nameof(IAppOptions.ShowDirtyInOverviewMargin), true }, + { nameof(IAppOptions.ShowNewInOverviewMargin), true }, { nameof(IAppOptions.ShowUncoveredInOverviewMargin),true}, + { nameof(IAppOptions.ShowNotIncludedInOverviewMargin),true}, { nameof(IAppOptions.ShowToolWindowToolbar),true}, {nameof(IAppOptions.ExcludeAssemblies),new string[]{ "Exclude"} }, {nameof(IAppOptions.IncludeAssemblies),new string[]{ "Include"} }, {nameof(IAppOptions.NamespaceQualification),NamespaceQualification.AlwaysUnqualified }, {nameof(IAppOptions.OpenCoverRegister),OpenCoverRegister.Default }, {nameof(IAppOptions.OpenCoverTarget),"" }, - {nameof(IAppOptions.OpenCoverTargetArgs),"" } + {nameof(IAppOptions.OpenCoverTargetArgs),"" }, + {nameof(IAppOptions.ShowEditorCoverage),true }, + {nameof(IAppOptions.ShowCoverageInGlyphMargin),true }, + {nameof(IAppOptions.ShowCoveredInGlyphMargin),true }, + {nameof(IAppOptions.ShowPartiallyCoveredInGlyphMargin),true }, + {nameof(IAppOptions.ShowUncoveredInGlyphMargin),true }, + {nameof(IAppOptions.ShowDirtyInGlyphMargin),true }, + {nameof(IAppOptions.ShowNewInGlyphMargin),true }, + {nameof(IAppOptions.ShowNotIncludedInGlyphMargin),true }, + {nameof(IAppOptions.ShowLineCoverageHighlighting),true }, + {nameof(IAppOptions.ShowLineCoveredHighlighting),true }, + {nameof(IAppOptions.ShowLinePartiallyCoveredHighlighting),true }, + {nameof(IAppOptions.ShowLineUncoveredHighlighting),true }, + {nameof(IAppOptions.ShowLineDirtyHighlighting),true }, + {nameof(IAppOptions.ShowLineNewHighlighting),true }, + {nameof(IAppOptions.ShowLineNotIncludedHighlighting),true }, + {nameof(IAppOptions.UseEnterpriseFontsAndColors),true }, + {nameof(IAppOptions.EditorCoverageColouringMode), EditorCoverageColouringMode.UseRoslynWhenTextChanges } }; var mockJsonConvertService = autoMocker.GetMock(); mockJsonConvertService.Setup( diff --git a/FineCodeCoverageTests/CoberturaDeserializer_Tests.cs b/FineCodeCoverageTests/CoberturaDeserializer_Tests.cs new file mode 100644 index 00000000..d88e3c52 --- /dev/null +++ b/FineCodeCoverageTests/CoberturaDeserializer_Tests.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using System.IO; +using FineCodeCoverage.Engine.Cobertura; +using System; +using System.Linq; + +namespace FineCodeCoverageTests +{ + public class CoberturaDeserializer_Tests + { + [Test] + public void Should_Deserialize_What_Is_Required() + { + var cobertura = @" + + + + + + + + + + + + + + + + + + + + + + +"; + + string fileName = System.IO.Path.GetTempPath() + Guid.NewGuid().ToString() + ".xml"; + File.WriteAllText(fileName, cobertura); + var report = new CoberturaDerializer().Deserialize(fileName); + var package = report.Packages.Single(); + Assert.AreEqual("DemoOpenCover", package.Name); + var packageClass = package.Classes.Single(); + Assert.AreEqual(".LargeClass", packageClass.Name); + Assert.AreEqual(@"C:\Users\tonyh\source\repos\DemoOpenCover\DemoOpenCover\LargeClass.cs", packageClass.Filename); + Assert.AreEqual(1, packageClass.LineRate); + Assert.AreEqual(1, packageClass.BranchRate); + Assert.AreEqual(501, packageClass.Complexity); + var method = packageClass.Methods.Single(); + Assert.AreEqual("Method0", method.Name); + Assert.AreEqual("()", method.Signature); + Assert.AreEqual(1, method.LineRate); + Assert.AreEqual(1, method.BranchRate); + var line = method.Lines.Single(); + Assert.AreEqual(1, line.Number); + Assert.AreEqual(1, line.Hits); + line = packageClass.Lines.Single(); + Assert.AreEqual(1, line.Number); + Assert.AreEqual(1, line.Hits); + } + } +} diff --git a/FineCodeCoverageTests/CoberturaUtil_Tests.cs b/FineCodeCoverageTests/CoberturaUtil_Tests.cs new file mode 100644 index 00000000..78dc901f --- /dev/null +++ b/FineCodeCoverageTests/CoberturaUtil_Tests.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine.Cobertura; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverageTests.TestHelpers; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests +{ + public class CoberturaUtil_Tests + { + [Test] + public void Should_Populate_And_Sort_FileLineCoverage_From_Deserialized_Report() + { + var autoMoqer = new AutoMoqer(); + var reportPath = "reportpath"; + + var noHitsLine = new Line + { + Hits = 0, + Number = 1 + }; + var partialHitsLine = new Line + { + Hits = 1, + ConditionCoverage = "99% .....", + Number = 2 + }; + var coveredLine = new Line + { + Hits = 1, + ConditionCoverage = "100% .....", + Number = 3 + }; + var noConditionCoverageLine = new Line + { + Hits = 1, + Number = 4 + }; + var coverageReport = new CoverageReport + { + Packages = new List + { + new Package + { + Classes = new List + { + new Class + { + Filename = "filename", + + Lines = new List + { + noHitsLine, + partialHitsLine, + coveredLine, + noConditionCoverageLine + } + + } + } + } + } + }; + autoMoqer.Setup(x => x.Deserialize(reportPath)).Returns(coverageReport); + var mockFileLineCoverage = new Mock(); + var expectedLines = new List + { + CreateExpectedLine(1, CoverageType.NotCovered), + CreateExpectedLine(2, CoverageType.Partial), + CreateExpectedLine(3, CoverageType.Covered), + CreateExpectedLine(4, CoverageType.Covered) + }; + var sorted = false; + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.Add("filename", MoqMatchers.EnumerableExpected( + expectedLines, + (a, b) => a.Number == b.Number && a.CoverageType == b.CoverageType) + )).Callback(() => Assert.That(sorted, Is.False)); + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.Sort()).Callback(() => sorted = true); + + autoMoqer.Setup(fileLineCoverageFactory => fileLineCoverageFactory.Create()) + .Returns(mockFileLineCoverage.Object); + var coberturaUtil = autoMoqer.Create(); + + var processed = coberturaUtil.ProcessCoberturaXml(reportPath); + + + Assert.That(processed, Is.SameAs(mockFileLineCoverage.Object)); + mockFileLineCoverage.VerifyAll(); + } + + [Test] + public void Should_Update_FileLineCoverage_When_File_Renamed() + { + var autoMoqer = new AutoMoqer(); + var mockFileRenameListener = autoMoqer.GetMock(); + Action fileRenamedCallback = null; + mockFileRenameListener.Setup(fileRenameListener => fileRenameListener.ListenForFileRename(It.IsAny>())) + .Callback>(action => fileRenamedCallback = action); + + var coverageReport = new CoverageReport + { + Packages = new List() + }; + autoMoqer.Setup(x => x.Deserialize(It.IsAny())).Returns(coverageReport); + var mockFileLineCoverage = new Mock(); + autoMoqer.Setup(fileLineCoverageFactory => fileLineCoverageFactory.Create()) + .Returns(mockFileLineCoverage.Object); + var coberturaUtil = autoMoqer.Create(); + + coberturaUtil.ProcessCoberturaXml(""); + fileRenamedCallback("oldfile", "newfile"); + + mockFileLineCoverage.Verify(fileLineCoverage => fileLineCoverage.UpdateRenamed("oldfile", "newfile"), Times.Once); + + } + + [Test] + public void Should_Not_Throw_When_File_Renamed_And_No_Coverage() + { + var autoMoqer = new AutoMoqer(); + var mockFileRenameListener = autoMoqer.GetMock(); + mockFileRenameListener.Setup(fileRenameListener => fileRenameListener.ListenForFileRename(It.IsAny>())) + .Callback>(action => action("","")); + + var coberturaUtil = autoMoqer.Create(); + + + } + + private void SourceFilesTest( + Action sourceFilesAssertion, + List packages, + string assemblyName, + string qualifiedClassName, + int file = -1) + { + var autoMoqer = new AutoMoqer(); + autoMoqer.Setup(fileLineCoverageFactory => fileLineCoverageFactory.Create()) + .Returns(new Mock().Object); + + var coverageReport = new CoverageReport + { + Packages = packages + }; + autoMoqer.Setup(x => x.Deserialize(It.IsAny())).Returns(coverageReport); + + var coberturaUtil = autoMoqer.Create(); + coberturaUtil.ProcessCoberturaXml(""); + + var sourceFiles = coberturaUtil.GetSourceFiles(assemblyName, qualifiedClassName, file); + sourceFilesAssertion(sourceFiles); + } + + [Test] + public void Should_Return_Empty_Source_Files_If_Assembly_Not_In_The_CoverageReport() + { + var packages = new List + { + new Package + { + Name = "otherassembly", + Classes = new List() + } + }; + + SourceFilesTest(sourceFiles => Assert.That(sourceFiles, Is.Empty), packages, "missingassembly", "qcn"); + } + + private void AssemblyInReportTest(Action sourceFilesAssertion, int fileIndex = -1) + { + var packages = new List + { + new Package + { + Name = "assembly", + Classes = new List + { + new Class + { + Name = "qcn", + Filename = "file1", + Lines = new List{ } + }, + new Class + { + Name = "qcn", + Filename = "file2", + Lines = new List{ } + }, + new Class + { + Name = "other", + Filename = "file3", + Lines = new List{ } + } + } + } + }; + + SourceFilesTest(sourceFilesAssertion, packages, "assembly", "qcn",fileIndex); + } + + [Test] + public void Should_Return_Source_Files_Of_All_Matching_Classes_When_FileIndex_Is_Minus1() + { + AssemblyInReportTest(sourceFiles => Assert.That(sourceFiles, Is.EqualTo(new List { "file1", "file2"}))); + } + + [TestCase(0)] + [TestCase(1)] + public void Should_Return_Single_SourceFile_When_FileIndex_Is_Not_Minus1(int fileIndex) + { + var expectedFile = fileIndex == 0 ? "file1" : "file2"; + AssemblyInReportTest(sourceFiles => Assert.That(sourceFiles, Is.EqualTo(new List { expectedFile })),fileIndex); + } + + private static ILine CreateExpectedLine(int number, CoverageType coverageType) + { + var mockLine = new Mock(); + mockLine.SetupGet(x => x.Number).Returns(number); + mockLine.SetupGet(x => x.CoverageType).Returns(coverageType); + return mockLine.Object; + } + } +} diff --git a/FineCodeCoverageTests/CoverageProject_Settings_Tests.cs b/FineCodeCoverageTests/CoverageProject_Settings_Tests.cs index 5335b8ac..05eac5a9 100644 --- a/FineCodeCoverageTests/CoverageProject_Settings_Tests.cs +++ b/FineCodeCoverageTests/CoverageProject_Settings_Tests.cs @@ -2,7 +2,7 @@ using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Model; using FineCodeCoverage.Options; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using Moq; using NUnit.Framework; using StructureMap.AutoMocking; @@ -85,7 +85,7 @@ private List Provide(string projectDirectoryFCCOptions, string solutio public class CoverageProjectSettingsProvider_Tests { [Test] - public async Task Should_Return_The_FineCodeCoverage_Labelled_PropertyGroup() + public async Task Should_Return_The_FineCodeCoverage_Labelled_PropertyGroup_Async() { var coverageProjectSettingsProvider = new CoverageProjectSettingsProvider(null); var mockCoverageProject = new Mock(); @@ -108,7 +108,7 @@ public async Task Should_Return_The_FineCodeCoverage_Labelled_PropertyGroup() [TestCase(true)] [TestCase(false)] - public async Task Should_Return_Using_VsBuild_When_No_Labelled_PropertyGroup(bool returnNull) + public async Task Should_Return_Using_VsBuild_When_No_Labelled_PropertyGroup_Async(bool returnNull) { var mockCoverageProject = new Mock(); var coverageProjectGuid = Guid.NewGuid(); @@ -635,7 +635,7 @@ private XElement CreateIncludeReferencedProjectsElement(bool include) public class CoverageProjectSettingsManager_Tests { [Test] - public async Task Should_Provide_The_Merged_Result_Using_Global_Options() + public async Task Should_Provide_The_Merged_Result_Using_Global_Options_Async() { var mockAppOptionsProvider = new Mock(); var mockAppOptions = new Mock(); @@ -662,7 +662,7 @@ public async Task Should_Provide_The_Merged_Result_Using_Global_Options() } [Test] - public async Task Should_Provide_The_Merged_Result_Using_FCC_Settings_Files() + public async Task Should_Provide_The_Merged_Result_Using_FCC_Settings_Files_Async() { var mockCoverageProject = new Mock(); mockCoverageProject.Setup(cp => cp.ProjectFile).Returns("SomeProject/SomeProject.csproj"); @@ -693,7 +693,7 @@ public async Task Should_Provide_The_Merged_Result_Using_FCC_Settings_Files() } [Test] - public async Task Should_Provide_The_Merged_Result_Using_Project_Settings() + public async Task Should_Provide_The_Merged_Result_Using_Project_Settings_Async() { var coverageProject = new Mock().Object; @@ -723,7 +723,7 @@ public async Task Should_Provide_The_Merged_Result_Using_Project_Settings() } [Test] - public async Task Should_Add_Common_Assembly_Excludes_Includes() + public async Task Should_Add_Common_Assembly_Excludes_Includes_Async() { var mockAppOptions = new Mock(); mockAppOptions.SetupAllProperties(); diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs index ead21d0e..32dc7b9d 100644 --- a/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs @@ -7,7 +7,7 @@ using AutoMoq; using FineCodeCoverage.Engine; using FineCodeCoverage.Options; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using Moq; using NUnit.Framework; diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs index 1918f5f3..8d6d032a 100644 --- a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs @@ -7,7 +7,7 @@ using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine; using FineCodeCoverage.Engine.Model; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using Moq; using NUnit.Framework; diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs index 7e81b05e..e6ae0d00 100644 --- a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; using FineCodeCoverage.Engine; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using NUnit.Framework; namespace FineCodeCoverageTests diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs index e2009e3e..cf6276fe 100644 --- a/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs @@ -7,7 +7,7 @@ using AutoMoq; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using NUnit.Framework; namespace FineCodeCoverageTests.CoverageToolOutput_Tests diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs index 0a1cd1d6..69675a62 100644 --- a/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine; using NUnit.Framework; @@ -13,7 +8,7 @@ namespace FineCodeCoverageTests.CoverageToolOutput_Tests class SolutionFolderProvider_Tests { private string tempDirectory; - private FileUtil fileUtil = new FileUtil(); + private readonly FileUtil fileUtil = new FileUtil(); [SetUp] public void Create_Temp_Directory() diff --git a/FineCodeCoverageTests/CoverageUtilManager_Tests.cs b/FineCodeCoverageTests/CoverageUtilManager_Tests.cs index d3ffdfbc..655b07b9 100644 --- a/FineCodeCoverageTests/CoverageUtilManager_Tests.cs +++ b/FineCodeCoverageTests/CoverageUtilManager_Tests.cs @@ -35,7 +35,7 @@ public void Initialize_Should_Initialize_The_Coverage_Utils() [TestCase(true)] [TestCase(false)] - public async Task Should_Run_The_Appropriate_Cover_Tool_Based_On_IsDotNetSdkStyle(bool isDotNetSdkStyle) + public async Task Should_Run_The_Appropriate_Cover_Tool_Based_On_IsDotNetSdkStyle_Async(bool isDotNetSdkStyle) { var mockProject = new Mock(); mockProject.Setup(cp => cp.IsDotNetSdkStyle()).Returns(isDotNetSdkStyle); diff --git a/FineCodeCoverageTests/CoverletConsole_Tests.cs b/FineCodeCoverageTests/CoverletConsole_Tests.cs index 3bda5f47..2cbe7342 100644 --- a/FineCodeCoverageTests/CoverletConsole_Tests.cs +++ b/FineCodeCoverageTests/CoverletConsole_Tests.cs @@ -57,10 +57,6 @@ private void AssertHasSetting(List coverletSettings, string setting) Assert.IsTrue(coverletSettings.Any(coverletSetting => coverletSetting == setting)); } - private void AssertHasEscapedSetting(List coverletSettings, string setting) - { - AssertHasSetting(coverletSettings, CommandLineArguments.AddQuotes(setting)); - } } public class CoverletConsoleExecuteRequestProvider_Tests @@ -154,7 +150,7 @@ public void Should_Initilize_IFCCCoverletConsoleExeProvider() } [Test] - public async Task Should_Execute_The_Request_From_The_Execute_Request_Provider_With_Space_Delimited_Settings() + public async Task Should_Execute_The_Request_From_The_Execute_Request_Provider_With_Space_Delimited_Settings_Async() { await RunSuccessfullyAsync(); @@ -162,7 +158,7 @@ public async Task Should_Execute_The_Request_From_The_Execute_Request_Provider_W } [Test] - public async Task Should_Log_Settings_Before_Executing() + public async Task Should_Log_Settings_Before_Executing_Async() { var mockLogger = mocker.GetMock(); mockLogger.Setup(logger => logger.Log(It.IsAny>())).Callback>(messages => @@ -202,7 +198,7 @@ public void Should_Log_With_ExecuteResponse_ExitCode_And_Output_When_ExitCode_Is } [Test] - public async Task Should_Log_The_ExecuteResponse_Output_On_Success() + public async Task Should_Log_The_ExecuteResponse_Output_On_Success_Async() { var mockLogger = mocker.GetMock(); mockLogger.Setup(logger => logger.Log(It.IsAny())).Callback(messages => diff --git a/FineCodeCoverageTests/CoverletDataCollectorUtil_CanUseDataCollector_Tests.cs b/FineCodeCoverageTests/CoverletDataCollectorUtil_CanUseDataCollector_Tests.cs index 740e54a7..8af564e7 100644 --- a/FineCodeCoverageTests/CoverletDataCollectorUtil_CanUseDataCollector_Tests.cs +++ b/FineCodeCoverageTests/CoverletDataCollectorUtil_CanUseDataCollector_Tests.cs @@ -5,7 +5,7 @@ using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Coverlet; using FineCodeCoverage.Engine.Model; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using Moq; using NUnit.Framework; diff --git a/FineCodeCoverageTests/CoverletDataCollectorUtil_RunAsync_Tests.cs b/FineCodeCoverageTests/CoverletDataCollectorUtil_RunAsync_Tests.cs index 8b60562d..0aaa407e 100644 --- a/FineCodeCoverageTests/CoverletDataCollectorUtil_RunAsync_Tests.cs +++ b/FineCodeCoverageTests/CoverletDataCollectorUtil_RunAsync_Tests.cs @@ -60,7 +60,7 @@ private DirectoryInfo CreateTemporaryDirectory() [Test] - public async Task Should_Get_Settings_With_TestDllFile() + public async Task Should_Get_Settings_With_TestDllFile_Async() { mockCoverageProject.Setup(cp => cp.TestDllFile).Returns("test.dll"); mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns(""); @@ -71,7 +71,7 @@ public async Task Should_Get_Settings_With_TestDllFile() } [Test] - public async Task Should_Get_Settings_With_Exclude_From_CoverageProject_And_RunSettings() + public async Task Should_Get_Settings_With_Exclude_From_CoverageProject_And_RunSettings_Async() { var projectExclude = new string[] { "excluded" }; mockCoverageProject.Setup(cp => cp.Settings.Exclude).Returns(projectExclude); @@ -84,7 +84,7 @@ public async Task Should_Get_Settings_With_Exclude_From_CoverageProject_And_RunS } [Test] - public async Task Should_Not_Throw_When_Project_Setttings_Exclude_Is_Null() + public async Task Should_Not_Throw_When_Project_Setttings_Exclude_Is_Null_Async() { var referencedExcluded = new List { "referencedExcluded" }; mockCoverageProject.Setup(cp => cp.ExcludedReferencedProjects).Returns(referencedExcluded); @@ -95,7 +95,7 @@ public async Task Should_Not_Throw_When_Project_Setttings_Exclude_Is_Null() } [Test] - public async Task Should_Get_Settings_With_ExcludeByFile_From_CoverageProject_And_RunSettings() + public async Task Should_Get_Settings_With_ExcludeByFile_From_CoverageProject_And_RunSettings_Async() { var projectExcludeByFile = new string[] { "excludedByFile" }; mockCoverageProject.Setup(cp => cp.Settings.ExcludeByFile).Returns(projectExcludeByFile); @@ -107,7 +107,7 @@ public async Task Should_Get_Settings_With_ExcludeByFile_From_CoverageProject_An } [Test] - public async Task Should_Get_Settings_With_ExcludeByAttribute_From_CoverageProject_And_RunSettings() + public async Task Should_Get_Settings_With_ExcludeByAttribute_From_CoverageProject_And_RunSettings_Async() { var projectExcludeByAttribute = new string[] { "excludedByAttribute" }; mockCoverageProject.Setup(cp => cp.Settings.ExcludeByAttribute).Returns(projectExcludeByAttribute); @@ -117,7 +117,7 @@ public async Task Should_Get_Settings_With_ExcludeByAttribute_From_CoverageProje } [Test] - public async Task Should_Get_Settings_With_Include_From_CoverageProject_And_RunSettings() + public async Task Should_Get_Settings_With_Include_From_CoverageProject_And_RunSettings_Async() { var projectInclude= new string[] { "included" }; mockCoverageProject.Setup(cp => cp.Settings.Include).Returns(projectInclude); @@ -130,7 +130,7 @@ public async Task Should_Get_Settings_With_Include_From_CoverageProject_And_RunS [TestCase(true,"true")] [TestCase(false, "false")] - public async Task Should_Get_Settings_With_IncludeTestAssembly_From_CoverageProject_And_RunSettings(bool projectIncludeTestAssembly, string runSettingsIncludeTestAssembly) + public async Task Should_Get_Settings_With_IncludeTestAssembly_From_CoverageProject_And_RunSettings_Async(bool projectIncludeTestAssembly, string runSettingsIncludeTestAssembly) { mockCoverageProject.Setup(cp => cp.Settings.IncludeTestAssembly).Returns(projectIncludeTestAssembly); mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns(""); @@ -141,7 +141,7 @@ public async Task Should_Get_Settings_With_IncludeTestAssembly_From_CoverageProj [TestCase(true)] [TestCase(false)] - public async Task Should_Initialize_With_Options_And_Run_Settings_First(bool runSettingsOnly) + public async Task Should_Initialize_With_Options_And_Run_Settings_First_Async(bool runSettingsOnly) { mockCoverageProject.Setup(cp => cp.RunSettingsFile).Returns(".runsettings"); mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns("output"); @@ -157,7 +157,7 @@ public async Task Should_Initialize_With_Options_And_Run_Settings_First(bool run } [Test] - public async Task Should_Get_Settings_With_ResultsDirectory() + public async Task Should_Get_Settings_With_ResultsDirectory_Async() { mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns("outputfolder"); await coverletDataCollectorUtil.RunAsync(CancellationToken.None); @@ -165,21 +165,21 @@ public async Task Should_Get_Settings_With_ResultsDirectory() } [Test] - public async Task Should_Get_Settings_With_Blame() + public async Task Should_Get_Settings_With_Blame_Async() { await coverletDataCollectorUtil.RunAsync(CancellationToken.None); mockDataCollectorSettingsBuilder.Verify(b => b.WithBlame()); } [Test] - public async Task Should_Get_Settings_With_NoLogo() + public async Task Should_Get_Settings_With_NoLogo_Async() { await coverletDataCollectorUtil.RunAsync(CancellationToken.None); mockDataCollectorSettingsBuilder.Verify(b => b.WithNoLogo()); } [Test] - public async Task Should_Get_Settings_With_Diagnostics() + public async Task Should_Get_Settings_With_Diagnostics_Async() { mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns("outputfolder"); await coverletDataCollectorUtil.RunAsync(CancellationToken.None); @@ -187,7 +187,7 @@ public async Task Should_Get_Settings_With_Diagnostics() } [Test] - public async Task Should_Get_Settings_With_IncludeDirectory_From_RunSettings() + public async Task Should_Get_Settings_With_IncludeDirectory_From_RunSettings_Async() { mockRunSettingsCoverletConfiguration.Setup(rsc => rsc.IncludeDirectory).Returns("includeDirectory"); await coverletDataCollectorUtil.RunAsync(CancellationToken.None); @@ -195,7 +195,7 @@ public async Task Should_Get_Settings_With_IncludeDirectory_From_RunSettings() } [Test] - public async Task Should_Get_Settings_With_SingleHit_From_RunSettings() + public async Task Should_Get_Settings_With_SingleHit_From_RunSettings_Async() { mockRunSettingsCoverletConfiguration.Setup(rsc => rsc.SingleHit).Returns("true..."); await coverletDataCollectorUtil.RunAsync(CancellationToken.None); @@ -203,7 +203,7 @@ public async Task Should_Get_Settings_With_SingleHit_From_RunSettings() } [Test] - public async Task Should_Get_Settings_With_UseSourceLink_From_RunSettings() + public async Task Should_Get_Settings_With_UseSourceLink_From_RunSettings_Async() { mockRunSettingsCoverletConfiguration.Setup(rsc => rsc.UseSourceLink).Returns("true..."); await coverletDataCollectorUtil.RunAsync(CancellationToken.None); @@ -211,7 +211,7 @@ public async Task Should_Get_Settings_With_UseSourceLink_From_RunSettings() } [Test] - public async Task Should_Get_Settings_With_SkipAutoProps_From_RunSettings() + public async Task Should_Get_Settings_With_SkipAutoProps_From_RunSettings_Async() { mockRunSettingsCoverletConfiguration.Setup(rsc => rsc.SkipAutoProps).Returns("true..."); await coverletDataCollectorUtil.RunAsync(CancellationToken.None); @@ -219,7 +219,7 @@ public async Task Should_Get_Settings_With_SkipAutoProps_From_RunSettings() } [Test] - public async Task Should_Log_VSTest_Run_With_Settings() + public async Task Should_Log_VSTest_Run_With_Settings_Async() { mockCoverageProject.Setup(cp => cp.ProjectName).Returns("TestProject"); mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns(""); @@ -229,7 +229,7 @@ public async Task Should_Log_VSTest_Run_With_Settings() } [Test] - public async Task Should_Execute_DotNet_Test_Collect_XPlat_With_Settings_Using_The_ProcessUtil() + public async Task Should_Execute_DotNet_Test_Collect_XPlat_With_Settings_Using_The_ProcessUtil_Async() { mockCoverageProject.Setup(cp => cp.ProjectOutputFolder).Returns("projectOutputFolder"); mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns(""); @@ -240,7 +240,7 @@ public async Task Should_Execute_DotNet_Test_Collect_XPlat_With_Settings_Using_T mocker.Verify(p => p.ExecuteAsync(It.Is(er => er.Arguments == @"test --collect:""XPlat Code Coverage"" settings --test-adapter-path testadapterpath" && er.FilePath == "dotnet" && er.WorkingDirectory == "projectOutputFolder"),ct)); } - private async Task Use_Custom_TestAdapterPath() + private async Task Use_Custom_TestAdapterPath_Async() { CreateTemporaryDirectory(); mockCoverageProject.Setup(cp => cp.ProjectOutputFolder).Returns("projectOutputFolder"); @@ -254,21 +254,21 @@ private async Task Use_Custom_TestAdapterPath() } [Test] - public async Task Should_Use_Custom_TestAdapterPath_Quoted_If_Specified_In_Settings_And_Exists() + public async Task Should_Use_Custom_TestAdapterPath_Quoted_If_Specified_In_Settings_And_Exists_Async() { - var ct = await Use_Custom_TestAdapterPath(); + var ct = await Use_Custom_TestAdapterPath_Async(); mocker.Verify(p => p.ExecuteAsync(It.Is(er => er.Arguments == $@"test --collect:""XPlat Code Coverage"" settings --test-adapter-path ""{tempDirectory}""" && er.FilePath == "dotnet" && er.WorkingDirectory == "projectOutputFolder"),ct)); } [Test] - public async Task Should_Log_When_Using_Custom_TestAdapterPath() + public async Task Should_Log_When_Using_Custom_TestAdapterPath_Async() { - await Use_Custom_TestAdapterPath(); + await Use_Custom_TestAdapterPath_Async(); mocker.Verify(l => l.Log($"Using custom coverlet data collector : {tempDirectory}")); } [Test] - public async Task Should_Use_The_ProcessResponseProcessor() + public async Task Should_Use_The_ProcessResponseProcessor_Async() { mockCoverageProject.Setup(cp => cp.ProjectName).Returns("TestProject"); mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns(""); @@ -289,7 +289,7 @@ public async Task Should_Use_The_ProcessResponseProcessor() [TestCase(2, false)] [TestCase(1,true)] [TestCase(0, true)] - public async Task Should_Only_Be_Successful_With_ExitCode_0_Or_1(int exitCode, bool expectedSuccess) + public async Task Should_Only_Be_Successful_With_ExitCode_0_Or_1_Async(int exitCode, bool expectedSuccess) { var mockProcessResponseProcessor = mocker.GetMock(); Func _exitCodePredicate = null; @@ -303,7 +303,7 @@ public async Task Should_Only_Be_Successful_With_ExitCode_0_Or_1(int exitCode, b } [Test] - public async Task Should_Correct_The_CoberturaPath_Given_Successful_Execution() + public async Task Should_Correct_The_CoberturaPath_Given_Successful_Execution_Async() { mockCoverageProject.Setup(cp => cp.CoverageOutputFolder).Returns("outputFolder"); mockCoverageProject.Setup(cp => cp.CoverageOutputFile).Returns("outputFile"); diff --git a/FineCodeCoverageTests/CoverletUtil_Tests.cs b/FineCodeCoverageTests/CoverletUtil_Tests.cs index e5610620..2450d3ab 100644 --- a/FineCodeCoverageTests/CoverletUtil_Tests.cs +++ b/FineCodeCoverageTests/CoverletUtil_Tests.cs @@ -29,7 +29,7 @@ public void Should_Initialize_The_GlobalTool_And_DataCollector() } [Test] - public async Task Should_Use_The_DataCollector_If_Possible() + public async Task Should_Use_The_DataCollector_If_Possible_Async() { var ct = CancellationToken.None; var project = new Mock().Object; @@ -44,7 +44,7 @@ public async Task Should_Use_The_DataCollector_If_Possible() } [Test] - public async Task Should_Use_The_Global_Tool_If_Not_Possible() + public async Task Should_Use_The_Global_Tool_If_Not_Possible_Async() { var ct = CancellationToken.None; var project = new Mock().Object; diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs new file mode 100644 index 00000000..e2b1b50e --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/BufferLineCoverage_Tests.cs @@ -0,0 +1,456 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Options; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class NormalizedTextChangeCollection : List, INormalizedTextChangeCollection + { + public bool IncludesLineChanges => throw new NotImplementedException(); + } + + internal class BufferLineCoverage_Tests + { + private AutoMoqer autoMoqer; + private Mock mockTextSnapshot; + private Mock mockTextBuffer; + private Mock mockTextView; + private ITextSnapshot textSnapshot; + private ITextInfo textInfo; + private Mock mockAppOptions; + + private ILine CreateLine() + { + var mockLine = new Mock(); + mockLine.SetupGet(line => line.Number).Returns(1); + mockLine.SetupGet(line => line.CoverageType).Returns(CoverageType.Partial); + return mockLine.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); + } + private void SimpleTextInfoSetUp(bool editorCoverageOff = false,string contentTypeName = "CSharp") + { + autoMoqer = new AutoMoqer(); + SetupEditorCoverageColouringMode(autoMoqer, editorCoverageOff); + 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); + } + + [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) + { + SimpleTextInfoSetUp(false,contentTypeName); + + var lines = new List { CreateLine()}; + var mockFileLineCoverage = autoMoqer.GetMock(); + mockFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines("filepath")).Returns(lines); + + var bufferLineCoverage = autoMoqer.Create(); + + autoMoqer.Verify(trackedLinesFactory => trackedLinesFactory.Create(lines, textSnapshot, expectedLanguage)); + } + + [Test] + public void Should_Not_Create_TrackedLines_If_EditorCoverageColouringMode_Is_Off() + { + SimpleTextInfoSetUp(true); + + var bufferLineCoverage = autoMoqer.Create(); + + autoMoqer.Verify(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); + } + + [Test] + public void Should_Not_Create_TrackedLines_From_NewCoverageLinesMessage_If_EditorCoverageColouringMode_Is_Off() + { + SimpleTextInfoSetUp(true); + + var bufferLineCoverage = autoMoqer.Create(); + + var newCoverageLinesMessage = new NewCoverageLinesMessage { CoverageLines = new Mock().Object }; + bufferLineCoverage.Handle(newCoverageLinesMessage); + + autoMoqer.Verify(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); + } + + [Test] + public void Should_Not_Throw_If_No_Initial_Coverage() + { + SimpleTextInfoSetUp(); + + new BufferLineCoverage(null, textInfo, new Mock().Object, null, null,new Mock().Object); + } + + [Test] + public void GetLines_Should_Delegate_To_TrackedLines() + { + SimpleTextInfoSetUp(); + + var mockTrackedLines = new Mock(); + var lines = new List(); + mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(2, 5)).Returns(lines); + + autoMoqer.Setup( + trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(mockTrackedLines.Object); + + var bufferLineCoverage = autoMoqer.Create(); + + Assert.That(bufferLineCoverage.GetLines(2, 5), Is.SameAs(lines)); + } + + [Test] + public void Should_Listen_For_Coverage_Changed() + { + SimpleTextInfoSetUp(); + + var bufferLineCoverage = autoMoqer.Create(); + + autoMoqer.Verify(eventAggregator => eventAggregator.AddListener(bufferLineCoverage, null)); + } + + [Test] + public void Should_Have_Empty_Lines_When_Coverage_Cleared() + { + SimpleTextInfoSetUp(); + + autoMoqer.Setup( + trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(new Mock().Object); + + var bufferLineCoverage = autoMoqer.Create(); + + bufferLineCoverage.Handle(new NewCoverageLinesMessage()); + + var lines = bufferLineCoverage.GetLines(2, 5); + Assert.That(lines, Is.Empty); + } + + [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); + + var lines = new List { CreateLine()}; + var mockNewFileLineCoverage = autoMoqer.GetMock(); + mockNewFileLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines("filepath")).Returns(lines); + + var mockTrackedLines = new Mock(); + var dynamicLines = new List(); + mockTrackedLines.Setup(trackedLines => trackedLines.GetLines(2, 5)).Returns(dynamicLines); + autoMoqer.Setup( + trackedLinesFactory => trackedLinesFactory.Create(lines, mockCurrentSnapshot.Object, Language.CSharp) + ).Returns(mockTrackedLines.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); + } + else + { + Assert.That(bufferLines, Is.SameAs(dynamicLines)); + } + } + + [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) + { + SimpleTextInfoSetUp(); + var trackedLines = new Mock().Object; + 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(); + + var newCoverageLinesMessage = new NewCoverageLinesMessage(); + if(hasCoverageLines) + { + newCoverageLinesMessage.CoverageLines = new Mock().Object; + } + bufferLineCoverage.Handle(newCoverageLinesMessage); + + autoMoqer.Verify( + eventAggregator => eventAggregator.SendMessage( + It.Is(message => message.AppliesTo == "filepath" && message.BufferLineCoverage == bufferLineCoverage) + , null + ), expectedSends ? Times.Once() : Times.Never()); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Update_TrackedLines_When_Text_Buffer_ChangedOnBackground(bool textLinesChanged) + { + SimpleTextInfoSetUp(); + + var afterSnapshot = new Mock().Object; + + var newSpan = new Span(1, 2); + var mockTrackedLines = new Mock(); + var changedLineNumbers = textLinesChanged ? new List { 1, 2 } : new List(); + mockTrackedLines.Setup(trackedLines => trackedLines.GetChangedLineNumbers(afterSnapshot, new List { newSpan })) + .Returns(changedLineNumbers); + autoMoqer.Setup(trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(mockTrackedLines.Object); + + + var bufferLineCoverage = autoMoqer.Create(); + + mockTextBuffer.Raise(textBuffer => textBuffer.ChangedOnBackground += null, CreateTextContentChangedEventArgs(afterSnapshot, newSpan)); + + autoMoqer.Verify( + eventAggregator => eventAggregator.SendMessage( + It.Is(message => message.AppliesTo == "filepath" && message.BufferLineCoverage == bufferLineCoverage && message.ChangedLineNumbers == changedLineNumbers) + , null + ), Times.Exactly(textLinesChanged ? 1 : 0)); + + } + + [Test] + public void Should_Not_Throw_When_Text_Buffer_Changed_And_No_Coverage() + { + SimpleTextInfoSetUp(); + + autoMoqer.Setup( + trackedLinesFactory => trackedLinesFactory.Create(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(new Mock().Object); + + var bufferLineCoverage = autoMoqer.Create(); + + // clear coverage + bufferLineCoverage.Handle(new NewCoverageLinesMessage()); + + mockTextBuffer.Raise(textBuffer => textBuffer.Changed += null, CreateTextContentChangedEventArgs(new Mock().Object, new Span(0, 0))); + } + + private TextContentChangedEventArgs CreateTextContentChangedEventArgs(ITextSnapshot afterSnapshot, params Span[] newSpans) + { + var normalizedTextChangeCollection = new NormalizedTextChangeCollection(); + foreach (var newSpan in newSpans) + { + var mockTextChange = new Mock(); + mockTextChange.SetupGet(textChange => textChange.NewSpan).Returns(newSpan); + normalizedTextChangeCollection.Add(mockTextChange.Object); + }; + + var mockBeforeSnapshot = new Mock(); + mockBeforeSnapshot.Setup(snapshot => snapshot.Version.Changes).Returns(normalizedTextChangeCollection); + return new TextContentChangedEventArgs(mockBeforeSnapshot.Object, afterSnapshot, EditOptions.None, null); + } + + [Test] + public void Should_Stop_Listening_When_TextView_Closed() + { + SimpleTextInfoSetUp(); + + var bufferLineCoverage = autoMoqer.Create(); + + 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); + autoMoqer.Setup( + trackedLinesFactory => trackedLinesFactory.Serialize(trackedLines) + ).Returns("serialized"); + + autoMoqer.Create(); + + mockTextView.Raise(textView => textView.Closed += null, EventArgs.Empty); + + autoMoqer.Verify(dynamicCoverageStore => dynamicCoverageStore.SaveSerializedCoverage("filepath", "serialized")); + } + + [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"); + + var bufferLineCoverage = autoMoqer.Create(); + // clear coverage + bufferLineCoverage.Handle(new NewCoverageLinesMessage()); + + mockTextView.Raise(textView => textView.Closed += null, EventArgs.Empty); + + autoMoqer.Verify(dynamicCoverageStore => dynamicCoverageStore.RemoveSerializedCoverage("filepath")); + } + + [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); + } + + [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); + + var bufferLineCoverage = autoMoqer.Create(); + Assert.That(bufferLineCoverage.GetLines(2, 5).Count(), Is.EqualTo(1)); + + 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); + } + + [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); + + var bufferLineCoverage = autoMoqer.Create(); + Assert.That(bufferLineCoverage.GetLines(2, 5).Count(), Is.EqualTo(1)); + + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(appOptions => appOptions.EditorCoverageColouringMode).Returns(EditorCoverageColouringMode.DoNotUseRoslynWhenTextChanges); + autoMoqer.GetMock().Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, mockAppOptions.Object); + + autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage(It.IsAny(), null), Times.Never()); + + Assert.That(bufferLineCoverage.GetLines(2, 5).Count(), Is.EqualTo(1)); + } + + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/CodeSpanRangeContainingCodeTrackerFactory_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/CodeSpanRangeContainingCodeTrackerFactory_Tests.cs new file mode 100644 index 00000000..b64360d4 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/CodeSpanRangeContainingCodeTrackerFactory_Tests.cs @@ -0,0 +1,166 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class CodeSpanRangeContainingCodeTrackerFactory_Tests + { + [TestCase(SpanTrackingMode.EdgePositive)] + [TestCase(SpanTrackingMode.EdgeInclusive)] + public void Should_CreateCoverageLines_From_TrackedCoverageLines_And_TrackingSpanRange(SpanTrackingMode spanTrackingMode) + { + var textSnapshot = new Mock().Object; + var mockLine = new Mock(); + mockLine.SetupGet(line => line.Number).Returns(5); + var adjustedLine = 4; + var mockLine2 = new Mock(); + mockLine2.SetupGet(line => line.Number).Returns(6); + var adjustedLine2 = 5; + var codeSpanRange = new CodeSpanRange(1, 10); + + var autoMoqer = new AutoMoqer(); + var trackingSpanStart = new Mock().Object; + var trackingSpanEnd = new Mock().Object; + var trackingSpanLine = new Mock().Object; + var trackingSpanLine2 = new Mock().Object; + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.StartLine, spanTrackingMode)) + .Returns(trackingSpanStart); + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.EndLine, spanTrackingMode)) + .Returns(trackingSpanEnd); + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, adjustedLine, spanTrackingMode)) + .Returns(trackingSpanLine); + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, adjustedLine2, spanTrackingMode)) + .Returns(trackingSpanLine2); + + + var coverageLine = new Mock().Object; + var coverageLine2 = new Mock().Object; + autoMoqer.Setup(coverageLineFactory => coverageLineFactory.Create(trackingSpanLine, mockLine.Object)) + .Returns(coverageLine); + autoMoqer.Setup(coverageLineFactory => coverageLineFactory.Create(trackingSpanLine2, mockLine2.Object)) + .Returns(coverageLine2); + var trackedCoverageLines = new Mock().Object; + autoMoqer.Setup( + trackedCoverageLinesFactory => trackedCoverageLinesFactory.Create(new List { coverageLine, coverageLine2 })) + .Returns(trackedCoverageLines); + + var trackingSpanRange = new Mock().Object; + autoMoqer.Setup( + trackingSpanRangeFactory => trackingSpanRangeFactory.Create(trackingSpanStart, trackingSpanEnd, textSnapshot)) + .Returns(trackingSpanRange); + + var containingCodeTracker = new Mock().Object; + autoMoqer.Setup( + trackedContainingCodeTrackerFactory => trackedContainingCodeTrackerFactory.CreateCoverageLines(trackingSpanRange,trackedCoverageLines)) + .Returns(containingCodeTracker); + + var codeSpanRangeContainingCodeTrackerFactory = autoMoqer.Create(); + + Assert.That(codeSpanRangeContainingCodeTrackerFactory.CreateCoverageLines(textSnapshot, new List { mockLine.Object, mockLine2.Object },codeSpanRange, spanTrackingMode), Is.SameAs(containingCodeTracker)); + } + + [TestCase(SpanTrackingMode.EdgePositive)] + [TestCase(SpanTrackingMode.EdgeInclusive)] + public void Should_CreateOtherLines_From_TrackingSpanRange(SpanTrackingMode spanTrackingMode) + { + var textSnapshot = new Mock().Object; + var codeSpanRange = new CodeSpanRange(1, 10); + + var autoMoqer = new AutoMoqer(); + + var trackingSpanStart = new Mock().Object; + var trackingSpanEnd = new Mock().Object; + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.StartLine, spanTrackingMode)) + .Returns(trackingSpanStart); + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.EndLine, spanTrackingMode)) + .Returns(trackingSpanEnd); + + var trackingSpanRange = new Mock().Object; + autoMoqer.Setup( + trackingSpanRangeFactory => trackingSpanRangeFactory.Create(trackingSpanStart, trackingSpanEnd, textSnapshot)) + .Returns(trackingSpanRange); + + var containingCodeTracker = new Mock().Object; + autoMoqer.Setup( + trackedContainingCodeTrackerFactory => trackedContainingCodeTrackerFactory.CreateOtherLines(trackingSpanRange)) + .Returns(containingCodeTracker); + + var codeSpanRangeContainingCodeTrackerFactory = autoMoqer.Create(); + + Assert.That(codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines(textSnapshot, codeSpanRange, spanTrackingMode), Is.SameAs(containingCodeTracker)); + } + + [TestCase(SpanTrackingMode.EdgePositive)] + [TestCase(SpanTrackingMode.EdgeInclusive)] + public void Should_Create_NotIncluded_From_TrackingSpanRange_And_NotIncluded_TrackingLine(SpanTrackingMode spanTrackingMode) + { + var textSnapshot = new Mock().Object; + var codeSpanRange = new CodeSpanRange(1, 10); + + var autoMoqer = new AutoMoqer(); + + var trackingSpanStart = new Mock().Object; + var trackingSpanEnd = new Mock().Object; + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.StartLine, spanTrackingMode)) + .Returns(trackingSpanStart); + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.EndLine, spanTrackingMode)) + .Returns(trackingSpanEnd); + + var firstTrackingSpan = new Mock().Object; + var mockTrackingSpanRange = new Mock(); + mockTrackingSpanRange.Setup(trackingSpanRange => trackingSpanRange.GetFirstTrackingSpan()).Returns(firstTrackingSpan); + autoMoqer.Setup( + trackingSpanRangeFactory => trackingSpanRangeFactory.Create(trackingSpanStart, trackingSpanEnd, textSnapshot)) + .Returns(mockTrackingSpanRange.Object); + + var trackingLine = new Mock().Object; + autoMoqer.Setup(notIncludedLineFactory => notIncludedLineFactory.Create(firstTrackingSpan,textSnapshot)) + .Returns(trackingLine); + + var containingCodeTracker = new Mock().Object; + autoMoqer.Setup( + trackedContainingCodeTrackerFactory => trackedContainingCodeTrackerFactory.CreateNotIncluded(trackingLine,mockTrackingSpanRange.Object)) + .Returns(containingCodeTracker); + + var codeSpanRangeContainingCodeTrackerFactory = autoMoqer.Create(); + + Assert.That(codeSpanRangeContainingCodeTrackerFactory.CreateNotIncluded(textSnapshot, codeSpanRange, spanTrackingMode), Is.SameAs(containingCodeTracker)); + } + + [TestCase(SpanTrackingMode.EdgePositive)] + [TestCase(SpanTrackingMode.EdgeInclusive)] + public void Should_CreateDirty_From_TrackingSpanRange_And_TextSnapshot(SpanTrackingMode spanTrackingMode) + { + var textSnapshot = new Mock().Object; + var codeSpanRange = new CodeSpanRange(1, 10); + + var autoMoqer = new AutoMoqer(); + + var trackingSpanStart = new Mock().Object; + var trackingSpanEnd = new Mock().Object; + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.StartLine, spanTrackingMode)) + .Returns(trackingSpanStart); + autoMoqer.Setup(trackingLineFactory => trackingLineFactory.CreateTrackingSpan(textSnapshot, codeSpanRange.EndLine, spanTrackingMode)) + .Returns(trackingSpanEnd); + + var trackingSpanRange = new Mock().Object; + autoMoqer.Setup( + trackingSpanRangeFactory => trackingSpanRangeFactory.Create(trackingSpanStart, trackingSpanEnd, textSnapshot)) + .Returns(trackingSpanRange); + + var containingCodeTracker = new Mock().Object; + autoMoqer.Setup( + trackedContainingCodeTrackerFactory => trackedContainingCodeTrackerFactory.CreateDirty(trackingSpanRange,textSnapshot)) + .Returns(containingCodeTracker); + + var codeSpanRangeContainingCodeTrackerFactory = autoMoqer.Create(); + + Assert.That(codeSpanRangeContainingCodeTrackerFactory.CreateDirty(textSnapshot, codeSpanRange, spanTrackingMode), Is.SameAs(containingCodeTracker)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs new file mode 100644 index 00000000..6d8a7b29 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/ContainingCodeTrackedLinesBuilder_Tests.cs @@ -0,0 +1,780 @@ +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.Engine.Model; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.TestHelpers; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + class Line : ILine + { + public Line(int number, CoverageType coverageType) + { + Number = number; + CoverageType = coverageType; + } + public override bool Equals(object obj) + { + var other = obj as ILine; + return other.Number == Number && other.CoverageType == CoverageType; + } + + [ExcludeFromCodeCoverage] + public override int GetHashCode() + { + int hashCode = 1698846147; + hashCode = hashCode * -1521134295 + Number.GetHashCode(); + hashCode = hashCode * -1521134295 + CoverageType.GetHashCode(); + return hashCode; + } + + public int Number { get; } + + public CoverageType CoverageType { get; } + } + + internal static class TestHelper + { + public static CodeSpanRange CodeSpanRangeFromLine(ILine line) + { + return CodeSpanRange.SingleLine(line.Number - 1); + } + } + internal class ContainingCodeTrackedLinesBuilder_CPP_Tests + { + [Test] + public void Should_Create_ContainingCodeTracker_For_Each_Line_When_CPP() + { + + var autoMoqer = new AutoMoqer(); + + 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 containingCodeTrackedLinesBuilder = autoMoqer.Create(); + var trackedLines = containingCodeTrackedLinesBuilder.Create(lines, textSnapshot, Language.CPP); + + Assert.That(trackedLines, Is.EqualTo(expectedTrackedLines)); + + } + + [Test] + public void Should_Use_CPP_Deserialized_When_CodeSpanRange_Within_Total_Lines() + { + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.LineCount).Returns(40); + 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 expectedTrackedLines = new TrackedLines(null, null, null); + autoMoqer.Setup( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + new List { coverageLineTracker, dirtyLineTracker }, + null, + null + )).Returns(expectedTrackedLines); + + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + var trackedLines = containingCodeTrackedLinesBuilder.Create("serializedState", mockTextSnapshot.Object, Language.CPP); + + Assert.That(expectedTrackedLines, Is.SameAs(trackedLines)); + } + } + + [TestFixture(true)] + [TestFixture(false)] + internal class ContainingCodeTrackedLinesBuilder_Tests + { + private readonly bool isCSharp; + + public ContainingCodeTrackedLinesBuilder_Tests(bool isCSharp) + { + this.isCSharp = isCSharp; + } + + +#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() + { + public List LinesInRange { get; } + public CodeSpanRange CodeSpanRange { get; } + public ITextSnapshot Snapshot { get; } + public SpanTrackingMode SpanTrackingMode { get; } + public ContainingCodeTrackerType TrackerType { get; } + + public static TrackerArgs ExpectedSingleCoverageLines(ILine line, SpanTrackingMode spanTrackingMode) + { + return ExpectedCoverageLines(new List { line }, TestHelper.CodeSpanRangeFromLine(line), spanTrackingMode); + } + + public static TrackerArgs ExpectedCoverageLines(List lines, CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) + { + return new TrackerArgs(null, lines, codeSpanRange, spanTrackingMode, ContainingCodeTrackerType.CoverageLines); + } + + public static TrackerArgs ExpectedOtherLines(CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) + { + return new TrackerArgs(null, null, codeSpanRange, spanTrackingMode, ContainingCodeTrackerType.OtherLines); + } + + public static TrackerArgs ExpectedNotIncluded(CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) + { + return new TrackerArgs(null, null, codeSpanRange, spanTrackingMode, ContainingCodeTrackerType.NotIncluded); + } + public TrackerArgs( + ITextSnapshot textSnapsot, + List lines, + CodeSpanRange codeSpanRange, + SpanTrackingMode spanTrackingMode, + ContainingCodeTrackerType trackerType) + { + Snapshot = textSnapsot; + LinesInRange = lines; + CodeSpanRange = codeSpanRange; + SpanTrackingMode = spanTrackingMode; + TrackerType = trackerType; + } + + private static bool LinesEqual(List firstLines, List secondLines) + { + if (firstLines == null && secondLines == null) return true; + var linesEqual = firstLines.Count == secondLines.Count; + if (linesEqual) + { + for (var i = 0; i < firstLines.Count; i++) + { + if (firstLines[i] != secondLines[i]) + { + linesEqual = false; + break; + } + } + } + return linesEqual; + } + + 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); + } + + public IEnumerable Lines => throw new System.NotImplementedException(); + + public IContainingCodeTrackerProcessResult ProcessChanges(ITextSnapshot currentSnapshot, List newSpanChanges) + { + throw new System.NotImplementedException(); + } + + public ContainingCodeTrackerState GetState() + { + throw new NotImplementedException(); + } + } + + class DummyCodeSpanRangeContainingCodeTrackerFactory : ICodeSpanRangeContainingCodeTrackerFactory + { + public IContainingCodeTracker CreateCoverageLines(ITextSnapshot textSnapshot, List lines, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + { + return new TrackerArgs(textSnapshot, lines, containingRange, spanTrackingMode, ContainingCodeTrackerType.CoverageLines); + } + + public IContainingCodeTracker CreateDirty(ITextSnapshot currentSnapshot, CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode) + { + throw new NotImplementedException(); + } + + public IContainingCodeTracker CreateNotIncluded(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + { + return new TrackerArgs(textSnapshot, null, containingRange, spanTrackingMode, ContainingCodeTrackerType.NotIncluded); + } + + public IContainingCodeTracker CreateOtherLines(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + { + return new TrackerArgs(textSnapshot, null, containingRange, spanTrackingMode, ContainingCodeTrackerType.OtherLines); + } + } + + [TestCaseSource(typeof(RoslynDataClass), nameof(RoslynDataClass.TestCases))] + public void Should_Create_ContainingCodeTrackers_In_Order_Contained_Lines_And_Single_Line_When_Roslyn_Languages + ( + List codeSpanRanges, + List lines, + List expected, + Action,bool> setUpExcluder, + int lineCount + ) + { + 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 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 trackedLines = containingCodeTrackedLinesBuilder.Create(lines, mockTextSnapshot.Object, isCSharp ? Language.CSharp : Language.VB); + + Assert.That(trackedLines, Is.SameAs(expectedTrackedLines)); + }); + + + } + + public class RoslynDataClass + { + 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) + { + 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); + } + + public static IEnumerable TestCases + { + get + { + { + 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 + ); + } + + { + var test2CodeSpanRanges = new List + { + new CodeSpanRange(10,20), + new CodeSpanRange(25,40), + new CodeSpanRange(60,70), + }; + + var test2Lines = new List + { + GetLine(5),//single + GetLine(6),// single + + GetLine(15),// range + + GetLine(45),//skip + + 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); + } + + { + 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); + } + + { + 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); + } + + { + 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"); + } + + } + } + } + + [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 + ) + { + var autoMoqer = new AutoMoqer(); + + 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, + }; + + var containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + var serialized = containingCodeTrackedLinesBuilder.Serialize( + new TrackedLines(containingCodeTrackers, null, null)); + + Assert.That("SerializedState", Is.EqualTo(serialized)); + + 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)); + } + + private Mock EnsureAppOptions(AutoMoqer autoMoqer) + { + var mockAppOptions = new Mock(); + autoMoqer.Setup( + appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); + return mockAppOptions; + } + + private void Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + ContainingCodeTrackerType containingCodeTrackerType, + List dynamicLines, + Action< + Mock, + IContainingCodeTracker, + CodeSpanRange, + ITextSnapshot> setupContainingCodeTrackerFactory + ) + { + 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 containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + 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 }); + + var mockRoslynService = autoMoqer.GetMock(); + mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) + .ReturnsAsync(new List { new TextSpan(1, 2) }); + + 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)); + } + + [Test] + public void Should_Use_Deserialized_OtherLinesTracker_If_CodeSpanRange_Has_Not_Changed() + { + Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + ContainingCodeTrackerType.OtherLines, + new List { }, + (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => + { + mockContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateOtherLines( + textSnapshot, + codeSpanRange, + SpanTrackingMode.EdgeNegative + )).Returns(containingCodeTracker); + } + ); + } + + [Test] + public void Should_Use_Deserialized_NotIncludedTracker_If_CodeSpanRange_Has_Not_Changed() + { + Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + ContainingCodeTrackerType.NotIncluded, + new List { }, + (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => + { + mockContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateNotIncluded( + textSnapshot, + codeSpanRange, + SpanTrackingMode.EdgeExclusive + )).Returns(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) + { + Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + 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); + } + ); + } + + [Test] + public void Should_Use_Deserialized_CoverageLinesTracker_For_Dirty_When_DirtyIf_CodeSpanRange_Has_Not_Changed() + { + Should_Use_Deserialized_IContainingCodeTracker_If_CodeSpanRange_Has_Not_Changed( + ContainingCodeTrackerType.CoverageLines, + new List { new DynamicLine(1, DynamicCoverageType.Dirty) }, + (mockContainingCodeTrackerFactory, containingCodeTracker, codeSpanRange, textSnapshot) => + { + mockContainingCodeTrackerFactory.Setup( + codeSpanRangeContainingCodeTrackerFactory => codeSpanRangeContainingCodeTrackerFactory.CreateDirty( + textSnapshot, + codeSpanRange, + SpanTrackingMode.EdgeExclusive + )).Returns(containingCodeTracker); + } + ); + } + + [Test] + public void Should_Not_Use_Deserialized_If_CodeSpanRange_Has_Changed() + { + 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(); + + 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 mockRoslynService = autoMoqer.GetMock(); + mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) + .ReturnsAsync(new List { new TextSpan(1, 2) }); + + var expectedTrackedLines = new TrackedLines(null, null, null); + autoMoqer.Setup( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + new List { }, + It.IsAny(), + It.IsAny() + )).Returns(expectedTrackedLines); + + 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) + { + 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 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 mockRoslynService = autoMoqer.GetMock(); + mockRoslynService.Setup(roslynService => roslynService.GetContainingCodeSpansAsync(mockTextSnapshot.Object)) + .ReturnsAsync(new List { new TextSpan(1, 2) }); + + 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( + containingCodeTrackedLinesFactory => containingCodeTrackedLinesFactory.Create( + new List { }, + newCodeTracker, + useRoslynWhenTextChanges ? containingCodeTrackedLinesBuilder : null + )).Returns(expectedTrackedLines); + + var trackedLines = containingCodeTrackedLinesBuilder.Create( + "serializedState", + mockTextSnapshot.Object, + isCSharp ? Language.CSharp : Language.VB + ); + + Assert.That(expectedTrackedLines, Is.SameAs(trackedLines)); + } + + [Test] + public void Should_IFileCodeSpanRangeService_Using_Roslyn_Distinct() + { + 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); + + 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 containingCodeTrackedLinesBuilder = autoMoqer.Create(); + + var fileCodeSpanRanges = containingCodeTrackedLinesBuilder.GetFileCodeSpanRanges(mockTextSnapshot.Object); + + Assert.That(fileCodeSpanRanges, Is.EqualTo(new List { CodeSpanRange.SingleLine(1), new CodeSpanRange(2, 3) })); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/CoverageCodeTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/CoverageCodeTracker_Tests.cs new file mode 100644 index 00000000..8985075d --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/CoverageCodeTracker_Tests.cs @@ -0,0 +1,188 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverageTests.TestHelpers; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class CoverageCodeTracker_Tests + { + [Test] + public void Should_Have_Correct_ContainingCodeTrackerType() + { + var autoMoqer = new AutoMoqer(); + var containingCodeTracker = autoMoqer.Create(); + Assert.That(containingCodeTracker.Type, Is.EqualTo(ContainingCodeTrackerType.CoverageLines)); + } + + [Test] + public void Should_Return_Lines_From_TrackedCoverageLines_When_No_DirtyLine() + { + var autoMoqer = new AutoMoqer(); + var trackedLines = new List { new Mock().Object }; + autoMoqer.Setup>(trackedCoverageLines => trackedCoverageLines.Lines) + .Returns(trackedLines); + var containingCodeTracker = autoMoqer.Create(); + + Assert.That(trackedLines, Is.SameAs(containingCodeTracker.Lines)); + + } + + private static IDynamicLine CreateDynamicLine(int lineNumber) + { + var mockDynamicLine = new Mock(); + mockDynamicLine.SetupGet(dynamicLine => dynamicLine.Number).Returns(lineNumber); + return mockDynamicLine.Object; + } + + [TestCase(true,true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false,false)] + public void Should_Create_The_DirtyLine_When_Text_Changed_And_Intersected(bool textChanged, bool intersected) + { + var expectedCreatedDirtyLine = textChanged && intersected; + var textSnapshot = new Mock().Object; + var newSpanAndLineRanges = new List { new SpanAndLineRange(new Span(1, 2), 0, 1) }; + var autoMoqer = new AutoMoqer(); + + var mockTrackingSpanRange = new Mock(); + var firstTrackingSpan = new Mock().Object; + mockTrackingSpanRange.Setup(trackingSpanRange => trackingSpanRange.GetFirstTrackingSpan()).Returns(firstTrackingSpan); + + var mockDirtyLineFactory = autoMoqer.GetMock(); + var mockDirtyLine = new Mock(); + var dirtyDynamicLine = new Mock().Object; + mockDirtyLine.SetupGet(dirtyLine => dirtyLine.Line).Returns(dirtyDynamicLine); + mockDirtyLineFactory.Setup( + dirtyLineFactory => dirtyLineFactory.Create(firstTrackingSpan, textSnapshot) + ).Returns(mockDirtyLine.Object); + + var coverageCodeTracker = autoMoqer.Create(); + + var trackingSpanRangeProcessResult = new TrackingSpanRangeProcessResult( + mockTrackingSpanRange.Object, + intersected ? new List() : newSpanAndLineRanges, + false, + textChanged + ); + coverageCodeTracker.GetUpdatedLineNumbers(trackingSpanRangeProcessResult, textSnapshot, newSpanAndLineRanges); + + mockDirtyLineFactory.Verify( + dirtyLineFactory => dirtyLineFactory.Create(firstTrackingSpan, textSnapshot), + MoqAssertionsHelper.ExpectedTimes(expectedCreatedDirtyLine)); + + var lines = coverageCodeTracker.Lines; + if (expectedCreatedDirtyLine) + { + Assert.That(lines.Single(), Is.SameAs(dirtyDynamicLine)); + } + else + { + Assert.That(lines, Is.Empty); + } + } + + [Test] + public void Should_Return_The_Dirty_Line_Number_And_All_Tracked_Line_Numbers_When_Create_Dirty_Line() + { + var textSnapshot = new Mock().Object; + + var autoMoqer = new AutoMoqer(); + var coverageLines = new List { CreateDynamicLine(1), CreateDynamicLine(2) }; + autoMoqer.Setup>( + trackedCoverageLines => trackedCoverageLines.Lines + ).Returns(coverageLines); + var mockDirtyLineFactory = autoMoqer.GetMock(); + var mockDirtyLine = new Mock(); + mockDirtyLine.SetupGet(dirtyLine => dirtyLine.Line.Number).Returns(10); + mockDirtyLineFactory.Setup( + dirtyLineFactory => dirtyLineFactory.Create(It.IsAny(), textSnapshot) + ).Returns(mockDirtyLine.Object); + + var coverageCodeTracker = autoMoqer.Create(); + + var trackingSpanRangeProcessResult = new TrackingSpanRangeProcessResult( + new Mock().Object, + new List(), + false, + true + ); + + var updatedLineNumbers = coverageCodeTracker.GetUpdatedLineNumbers( + trackingSpanRangeProcessResult, + textSnapshot, + new List { new SpanAndLineRange(new Span(1, 2), 0, 1) }); + + Assert.That(updatedLineNumbers, Is.EqualTo(new List { 10, 1, 2 })); + } + + [Test] + public void Should_Update_TrackedCoverageLines_When_Do_Not_Create_DirtyLine() + { + var textSnapshot = new Mock().Object; + var autoMoqer = new AutoMoqer(); + var mockTrackedCoverageLines = autoMoqer.GetMock(); + var trackedCoverageLinesUpdatedLineNumbers = new List { 1, 2 }; + mockTrackedCoverageLines.Setup(trackedCoverageLines => trackedCoverageLines.GetUpdatedLineNumbers(textSnapshot)) + .Returns(trackedCoverageLinesUpdatedLineNumbers); + var newSpanAndLineRanges = new List { new SpanAndLineRange(new Span(1, 2), 0, 1) }; + var trackingSpanRangeProcessResult = new TrackingSpanRangeProcessResult( + new Mock().Object, + new List(), + false, + false + ); + + var coverageCodeTracker = autoMoqer.Create(); + + var updatedLineNumbers = coverageCodeTracker.GetUpdatedLineNumbers(trackingSpanRangeProcessResult, textSnapshot, newSpanAndLineRanges); + + Assert.That(updatedLineNumbers, Is.SameAs(trackedCoverageLinesUpdatedLineNumbers)); + } + + [Test] + public void Should_Update_DirtyLine_When_DirtyLine() + { + var textSnapshot2 = new Mock().Object; + var spanAndLineRange2 = new List() { new SpanAndLineRange(new Span(1, 3), 0, 0) }; + var autoMoqer = new AutoMoqer(); + var mockDirtyLineFactory = autoMoqer.GetMock(); + var mockDirtyLine = new Mock(); + mockDirtyLine.SetupGet(dirtyLine => dirtyLine.Line.Number).Returns(10); + var dirtyLineUpdatedLineNumbers =new List { 10, 20 }; + mockDirtyLine.Setup(dirtyLine => dirtyLine.Update(textSnapshot2)).Returns(dirtyLineUpdatedLineNumbers); + mockDirtyLineFactory.Setup(dirtyLineFactory => dirtyLineFactory.Create(It.IsAny(), It.IsAny())).Returns(mockDirtyLine.Object); + + var coverageCodeTracker = autoMoqer.Create(); + + var trackingSpanRangeProcessResult1 = new TrackingSpanRangeProcessResult( + new Mock().Object, + new List(), + false, + true + ); + coverageCodeTracker.GetUpdatedLineNumbers(trackingSpanRangeProcessResult1, new Mock().Object, new List() { new SpanAndLineRange(new Span(1, 2), 0, 0) }); + + var trackingSpanRangeProcessResult2 = new TrackingSpanRangeProcessResult( + new Mock().Object, + new List(), + false, + true + ); + + var updatedLineNumbers = coverageCodeTracker.GetUpdatedLineNumbers( + trackingSpanRangeProcessResult2, textSnapshot2, spanAndLineRange2); + + Assert.That(updatedLineNumbers, Is.EqualTo(dirtyLineUpdatedLineNumbers)); + + } + + } + + +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/CoverageLine_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/CoverageLine_Tests.cs new file mode 100644 index 00000000..630be1ac --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/CoverageLine_Tests.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class CoverageLine_Tests + { + [TestCase(CoverageType.Covered, DynamicCoverageType.Covered)] + [TestCase(CoverageType.NotCovered, DynamicCoverageType.NotCovered)] + [TestCase(CoverageType.Partial, DynamicCoverageType.Partial)] + public void Should_Have_A_DynamicLine_From_ILine_When_Constructed(CoverageType lineCoverageType, DynamicCoverageType expectedDynamicCoverageType) + { + var mockLine = new Mock(); + mockLine.SetupGet(l => l.CoverageType).Returns(lineCoverageType); + mockLine.SetupGet(l => l.Number).Returns(1); + + var coverageLine = new CoverageLine(null, mockLine.Object, null); + + Assert.That(coverageLine.Line.CoverageType, Is.EqualTo(expectedDynamicCoverageType)); + Assert.That(coverageLine.Line.Number, Is.EqualTo(0)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Be_Updated_If_The_Line_Number_Changes(bool updateLineNumber) + { + var mockLine = new Mock(); + mockLine.SetupGet(l => l.Number).Returns(1); + + var currentTextSnapshot = new Mock().Object; + var trackingSpan = new Mock().Object; + var mockLineTracker = new Mock(); + + var updatedLineNumber = updateLineNumber ? 10 : 0; + mockLineTracker.Setup(lineTracker => lineTracker.GetLineNumber(trackingSpan, currentTextSnapshot, true)) + .Returns(updatedLineNumber); + var coverageLine = new CoverageLine(trackingSpan, mockLine.Object, mockLineTracker.Object); + + var updatedLineNumbers = coverageLine.Update(currentTextSnapshot); + + Assert.That(updatedLineNumbers, Is.EqualTo(updateLineNumber ? new List { 0, 10 } : Enumerable.Empty())); + + Assert.That(coverageLine.Line.Number, Is.EqualTo(updatedLineNumber)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs new file mode 100644 index 00000000..4ea8abeb --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageManager_Tests.cs @@ -0,0 +1,84 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverageTests.Test_helpers; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class DynamicCoverageManager_Tests + { + [Test] + public void Should_Export_IInitializable() + { + ExportsInitializable.Should_Export_IInitializable(typeof(DynamicCoverageManager)); + } + + [Test] + public void Should_Listen_To_NewCoverageLinesMessage() + { + var autoMocker = new AutoMoqer(); + var dynamicCoverageManager = autoMocker.Create(); + + autoMocker.Verify(e => e.AddListener(dynamicCoverageManager, null), Times.Once()); + } + + [Test] + public void Manage_Should_Create_Singleton_IBufferLineCoverage() + { + var autoMocker = new AutoMoqer(); + var dynamicCoverageManager = autoMocker.Create(); + + var mockTextInfo = new Mock(); + var previousBufferLineCoverage = new Mock().Object; + var propertyCollection = new PropertyCollection(); + propertyCollection.GetOrCreateSingletonProperty(() => previousBufferLineCoverage); + mockTextInfo.Setup(textInfo => textInfo.TextBuffer.Properties).Returns(propertyCollection); + + var bufferLineCoverage = dynamicCoverageManager.Manage(mockTextInfo.Object); + + Assert.That(bufferLineCoverage, Is.SameAs(previousBufferLineCoverage)); + } + + [TestCase(true)] + [TestCase(false)] + public void Manage_Should_Create_Singleton_IBufferLineCoverage_With_Last_Coverage_And_Dependencies(bool hasLastCoverage) + { + var autoMocker = new AutoMoqer(); + var eventAggregator = autoMocker.GetMock().Object; + var trackedLinesFactory = autoMocker.GetMock().Object; + var dynamicCoverageManager = autoMocker.Create(); + IFileLineCoverage lastCoverage = null; + if (hasLastCoverage) + { + lastCoverage = new Mock().Object; + dynamicCoverageManager.Handle(new NewCoverageLinesMessage { CoverageLines = lastCoverage}); + } + + var mockTextInfo = new Mock(); + var textView = new Mock().Object; + var propertyCollection = new PropertyCollection(); + mockTextInfo.Setup(textInfo => textInfo.TextBuffer.Properties).Returns(propertyCollection); + + var newBufferLineCoverage = new Mock().Object; + var mockBufferLineCoverageFactory = autoMocker.GetMock(); + var mockTextDocument = new Mock(); + mockTextDocument.Setup(textDocument => textDocument.FilePath).Returns("filepath"); + mockBufferLineCoverageFactory.Setup( + bufferLineCoverageFactory => bufferLineCoverageFactory.Create(lastCoverage, mockTextInfo.Object, eventAggregator,trackedLinesFactory)) + .Returns(newBufferLineCoverage); + + + + var bufferLineCoverage = dynamicCoverageManager.Manage(mockTextInfo.Object); + + Assert.That(bufferLineCoverage, Is.SameAs(newBufferLineCoverage)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs new file mode 100644 index 00000000..3b886b8d --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/DynamicCoverageStore_Tests.cs @@ -0,0 +1,161 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Options; +using Microsoft.VisualStudio.Settings; +using Moq; +using NUnit.Framework; +using System; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class DynamicCoverageStore_Tests + { + [Test] + public void Should_Add_Itself_As_EventAggregator_Listener() + { + var autoMoqer = new AutoMoqer(); + var dynamicCoverageStore = autoMoqer.Create(); + + autoMoqer.Verify(eventAggregator => eventAggregator.AddListener(dynamicCoverageStore,null)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Delete_WritableUserSettingsStore_Collection_When_NewCoverageLinesMessage(bool collectionExists) + { + var autoMoqer = new AutoMoqer(); + var mockWritableSettingsStore = new Mock(); + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.CollectionExists("FCC.DynamicCoverageStore")).Returns(collectionExists); + autoMoqer.Setup( + writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(mockWritableSettingsStore.Object); + + var dynamicCoverageStore = autoMoqer.Create(); + + dynamicCoverageStore.Handle(new NewCoverageLinesMessage()); + + mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.DeleteCollection("FCC.DynamicCoverageStore"), Times.Exactly(collectionExists ? 1 : 0)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_SaveSerializedCoverage_To_The_Store_Creating_Collection_If_Does_Not_Exist(bool collectionExists) + { + var autoMoqer = new AutoMoqer(); + var mockWritableSettingsStore = new Mock(); + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.CollectionExists("FCC.DynamicCoverageStore")).Returns(collectionExists); + autoMoqer.Setup( + writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(mockWritableSettingsStore.Object); + + var dynamicCoverageStore = autoMoqer.Create(); + + dynamicCoverageStore.SaveSerializedCoverage("filePath", "serialized"); + + mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.CreateCollection("FCC.DynamicCoverageStore"), Times.Exactly(collectionExists ? 0 : 1)); + mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.SetString("FCC.DynamicCoverageStore", "filePath", "serialized"), Times.Once); + } + + [Test] + public void Should_Return_Null_For_GetSerializedCoverage_When_Collection_Does_Not_Exist() + { + var autoMoqer = new AutoMoqer(); + + autoMoqer.Setup( + writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(new Mock().Object); + + var dynamicCoverageStore = autoMoqer.Create(); + + var serializedCoverage = dynamicCoverageStore.GetSerializedCoverage("filePath"); + + Assert.IsNull(serializedCoverage); + } + + [TestCase(true)] + [TestCase(false)] + 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); + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.GetString("FCC.DynamicCoverageStore", "filePath")).Returns("serialized"); + autoMoqer.Setup( + writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(mockWritableSettingsStore.Object); + + var dynamicCoverageStore = autoMoqer.Create(); + + var serializedCoverage = dynamicCoverageStore.GetSerializedCoverage("filePath"); + + Assert.AreEqual(propertyExists ? "serialized" : null, serializedCoverage); + } + + private void FileRename( + Action> setupWritableSettingsStore = null, + Action> verifyWritableSettingsStore = null + ) + { + var autoMoqer = new AutoMoqer(); + var mockWritableSettingsStore = new Mock(); + setupWritableSettingsStore?.Invoke(mockWritableSettingsStore); + autoMoqer.Setup( + writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(mockWritableSettingsStore.Object); + var mockFileRenameListener = autoMoqer.GetMock(); + mockFileRenameListener.Setup(fileRenameListener => fileRenameListener.ListenForFileRename(It.IsAny>())) + .Callback>(action => action("oldFileName", "newFileName")); + + var dynamicCoverageStore = autoMoqer.Create(); + + mockFileRenameListener.VerifyAll(); + mockWritableSettingsStore.VerifyAll(); + verifyWritableSettingsStore?.Invoke(mockWritableSettingsStore); + } + + [Test] + public void Should_Listen_For_File_Rename_And_Not_Throw_If_Collection_Does_Not_Exist() + { + FileRename(); + + } + + [Test] + public void Should_Not_Throw_When_Rename_File_Not_In_The_Store() + { + FileRename(mockWritableSettingsStore => + { + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.CollectionExists("FCC.DynamicCoverageStore")).Returns(true); + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.PropertyExists("FCC.DynamicCoverageStore", "oldFileName")).Returns(false); + }); + } + + [Test] + public void Should_Update_The_FileName_In_The_Store() + { + FileRename(mockWritableSettingsStore => + { + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.CollectionExists("FCC.DynamicCoverageStore")).Returns(true); + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.PropertyExists("FCC.DynamicCoverageStore", "oldFileName")).Returns(true); + mockWritableSettingsStore.Setup(writableSettingsStore => writableSettingsStore.GetString("FCC.DynamicCoverageStore", "oldFileName")).Returns("serialized"); + }, mockWritableSettingsStore => + { + mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.SetString("FCC.DynamicCoverageStore", "newFileName", "serialized")); + mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.DeleteProperty("FCC.DynamicCoverageStore", "oldFileName")); + }); + } + + [Test] + public void Should_Remove_SerializedCoverage_From_Store() + { + var autoMoqer = new AutoMoqer(); + var mockWritableSettingsStore = new Mock(); + autoMoqer.Setup( + writableUserSettingsStoreProvider => writableUserSettingsStoreProvider.Provide()).Returns(mockWritableSettingsStore.Object); + + var dynamicCoverageStore = autoMoqer.Create(); + + dynamicCoverageStore.RemoveSerializedCoverage("filePath"); + + mockWritableSettingsStore.Verify(writableSettingsStore => writableSettingsStore.DeleteProperty("FCC.DynamicCoverageStore", "filePath")); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs new file mode 100644 index 00000000..5e80d3c5 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/LineExcluder_Tests.cs @@ -0,0 +1,25 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using NUnit.Framework; + +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) + { + var codeLineExcluder = new LineExcluder(); + var exclude = codeLineExcluder.ExcludeIfNotCode(text, isCSharp); + Assert.That(exclude, Is.EqualTo(expectedExclude)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/LineTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/LineTracker_Tests.cs new file mode 100644 index 00000000..31a8b83f --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/LineTracker_Tests.cs @@ -0,0 +1,88 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class LineTracker_Tests + { + private Mock GetMockTextSnapshot() + { + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(t => t.Length).Returns(100); + return mockTextSnapshot; + } + + [TestCase(SpanTrackingMode.EdgePositive)] + [TestCase(SpanTrackingMode.EdgeInclusive)] + public void Should_Create_EdgeExclusive_Tracking_Span_With_The_Extent_Of_The_Line(SpanTrackingMode spanTrackingMode) + { + var mockTextSnapshot = GetMockTextSnapshot(); + var mockTextSnapshotLine = new Mock(); + var lineExtent = new SnapshotSpan(mockTextSnapshot.Object, 10, 20); + mockTextSnapshotLine.SetupGet(l => l.Extent).Returns(lineExtent); + mockTextSnapshot.Setup(t => t.GetLineFromLineNumber(1)).Returns(mockTextSnapshotLine.Object); + + var trackingLineFactory = new LineTracker(); + var trackingSpan = trackingLineFactory.CreateTrackingSpan(mockTextSnapshot.Object, 1,spanTrackingMode); + + mockTextSnapshot.Verify(t => t.CreateTrackingSpan(new SnapshotSpan(mockTextSnapshot.Object, 10, 20), spanTrackingMode)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_GetLineNumber_From_Start_Or_End(bool fromEnd) + { + var mockTrackingSpan = new Mock(); + var mockTextSnapshot = GetMockTextSnapshot(); + var endPoint = new SnapshotPoint(mockTextSnapshot.Object, 50); + mockTrackingSpan.Setup(trackingSpan => trackingSpan.GetEndPoint(mockTextSnapshot.Object)) + .Returns(endPoint); + var startPoint = new SnapshotPoint(mockTextSnapshot.Object, 10); + mockTrackingSpan.Setup(trackingSpan => trackingSpan.GetStartPoint(mockTextSnapshot.Object)) + .Returns(startPoint); + + mockTextSnapshot.Setup(snapshot => snapshot.GetLineNumberFromPosition(endPoint)).Returns(5); + mockTextSnapshot.Setup(snapshot => snapshot.GetLineNumberFromPosition(startPoint)).Returns(1); + var lineNumber = new LineTracker().GetLineNumber(mockTrackingSpan.Object, mockTextSnapshot.Object, fromEnd); + + Assert.That(lineNumber, Is.EqualTo(fromEnd ? 5 : 1)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Get_Line_Number_And_Text_From_Start_Or_End(bool fromEnd) + { + (ITextSnapshotLine, SnapshotSpan) CreateTextSnapshotLineAndExtent(int lineNumber, ITextSnapshot textSnapshot) + { + var mockTextSnapshotLine = new Mock(); + mockTextSnapshotLine.SetupGet(l => l.LineNumber).Returns(lineNumber); + var extent = new SnapshotSpan(textSnapshot, 10, 20 + lineNumber); + mockTextSnapshotLine.SetupGet(l => l.Extent).Returns(extent); + return (mockTextSnapshotLine.Object, extent); + } + + var mockTrackingSpan = new Mock(); + var mockTextSnapshot = GetMockTextSnapshot(); + var endPoint = new SnapshotPoint(mockTextSnapshot.Object, 50); + mockTrackingSpan.Setup(trackingSpan => trackingSpan.GetEndPoint(mockTextSnapshot.Object)) + .Returns(endPoint); + var startPoint = new SnapshotPoint(mockTextSnapshot.Object, 10); + mockTrackingSpan.Setup(trackingSpan => trackingSpan.GetStartPoint(mockTextSnapshot.Object)) + .Returns(startPoint); + + var (endLine, endExtent) = CreateTextSnapshotLineAndExtent(5, mockTextSnapshot.Object); + var (startLine, startExtent) = CreateTextSnapshotLineAndExtent(1, mockTextSnapshot.Object); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromPosition(endPoint)).Returns(endLine); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetText(endExtent)).Returns("EndLineText"); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromPosition(startPoint)).Returns(startLine); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetText(startExtent)).Returns("StartLineText"); + + var info = new LineTracker().GetTrackedLineInfo(mockTrackingSpan.Object, mockTextSnapshot.Object, fromEnd); + + Assert.That(info.LineNumber, Is.EqualTo(fromEnd ? 5 : 1)); + Assert.That(info.LineText, Is.EqualTo(fromEnd ? "EndLineText" : "StartLineText")); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs new file mode 100644 index 00000000..5ce8a247 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/NewCodeTracker_Tests.cs @@ -0,0 +1,370 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class NewCodeTracker_Tests + { + [Test] + public void Should_Have_No_Lines_Initially_When_Passed_No_Line_Numbers() + { + var newCodeTracker = new NewCodeTracker(true, null,null); + + Assert.That(newCodeTracker.Lines, Is.Empty); + } + + + [Test] + public void Should_Track_Not_Excluded_Line_Numbers_Passed_To_Ctor() + { + var textSnapshot = new Mock().Object; + var mockTrackedNewCodeLineFactory = new Mock(); + var mockTrackedNewCodeLine1 = new Mock(); + mockTrackedNewCodeLine1.Setup(trackedNewCodeLine => trackedNewCodeLine.GetText(textSnapshot)).Returns("exclude"); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine1.Object); + var mockTrackedNewCodeLine2 = new Mock(); + mockTrackedNewCodeLine2.Setup(trackedNewCodeLine => trackedNewCodeLine.GetText(textSnapshot)).Returns("notexclude"); + var expectedLine = new Mock().Object; + mockTrackedNewCodeLine2.SetupGet(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(expectedLine); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + 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); + + var newCodeTracker = new NewCodeTracker(true, mockTrackedNewCodeLineFactory.Object, mockLineExcluder.Object, new List { 1, 2 }, textSnapshot); + + var line = newCodeTracker.Lines.Single(); + Assert.That(line, Is.SameAs(expectedLine)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Return_Lines_Ordered_By_Line_Number(bool reverseOrder) + { + var textSnapshot = new Mock().Object; + + var mockTrackedNewCodeLineFactory = new Mock(); + var mockFirstDynamicLine = new Mock(); + mockFirstDynamicLine.SetupGet(l => l.Number).Returns(reverseOrder ? 2 : 1); + var firstDynamicLine = mockFirstDynamicLine.Object; + var mockFirstTrackedNewCodeLine = new Mock(); + mockFirstTrackedNewCodeLine.SetupGet(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(firstDynamicLine); + var mockSecondTrackedNewCodeLine = new Mock(); + var mockSecondDynamicLine = new Mock(); + mockSecondDynamicLine.SetupGet(l => l.Number).Returns(reverseOrder ? 1 : 2); + var secondDynamicLine = mockSecondDynamicLine.Object; + mockSecondTrackedNewCodeLine.SetupGet(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(secondDynamicLine); + mockTrackedNewCodeLineFactory.SetupSequence(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, It.IsAny()) + ).Returns(mockFirstTrackedNewCodeLine.Object) + .Returns(mockSecondTrackedNewCodeLine.Object); + + var newCodeTracker = new NewCodeTracker( + true, + mockTrackedNewCodeLineFactory.Object, + new Mock().Object, + new List { 1, 2 }, + textSnapshot + ); + + Assert.That( + newCodeTracker.Lines, + Is.EqualTo( + reverseOrder ? new List { secondDynamicLine, firstDynamicLine } : + new List { firstDynamicLine, secondDynamicLine } + ) + ); + } + + #region SpanAndLineRanges updates + [TestCase(true)] + [TestCase(false)] + 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; + var mockTrackedNewCodeLine = new Mock(); + mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.GetText(textSnapshot)).Returns("text"); + mockTrackedNewCodeLine.SetupGet(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(newDynamicLine); + var mockTrackedNewCodeLineFactory = new Mock(); + 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); + + 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) }, + null); + + if (exclude) + { + Assert.That(newCodeTracker.Lines, Is.Empty); + Assert.That(changedLineNumbers, Is.Empty); + } + else + { + Assert.That( + newCodeTracker.Lines.Single(), + Is.SameAs(newDynamicLine) + ); + + Assert.That(changedLineNumbers.Single(), Is.EqualTo(1)); + } + + } + + [Test] + public void Should_Not_Have_Changed_Lines_When_Line_Exists_And_Not_Updated_Or_Excluded() + { + 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)); + mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.GetText(textSnapshot)).Returns("exclude"); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + + var newCodeTracker = new NewCodeTracker( + true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + currentTextSnapshot, + new List { new SpanAndLineRange(new Span(),1,1) }, + null); + + Assert.That(changedLineNumbers, Is.Empty); + Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(dynamicLine)); + } + + [Test] + public void Should_Have_Changed_Line_When_Line_Exists_And_Excluded() + { + 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("updated", 1, 1)); + mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.GetText(textSnapshot)).Returns("exclude"); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + + var mockLineExcluder = new Mock(); + mockLineExcluder.Setup(lineExcluder => lineExcluder.ExcludeIfNotCode("updated", true)).Returns(true); + var newCodeTracker = new NewCodeTracker( + true, mockTrackedNewCodeLineFactory.Object,mockLineExcluder.Object, new List { 1 }, textSnapshot); + + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + currentTextSnapshot, + new List { new SpanAndLineRange(new Span(), 1, 1) }, + null); + + Assert.That(changedLineNumbers, Is.EqualTo(new List { 1 })); + Assert.That(newCodeTracker.Lines, Is.Empty); + } + + [Test] + public void Should_Have_Old_And_New_Line_Numbers_When_Line_Number_Updated() + { + 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("updated", 2, 1)); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + + var newCodeTracker = new NewCodeTracker( + true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + currentTextSnapshot, + new List { }, + null); + + Assert.That(changedLineNumbers, Is.EqualTo(new List { 1, 2 })); + Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(dynamicLine)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Use_The_New_Line_Number_To_Reduce_Possible_New_Lines(bool newLineNumberReduces) + { + 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("updated", 2, 1)); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + + var newDynamicLine = new Mock().Object; + var newMockTrackedNewCodeLine = new Mock(); + newMockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(newDynamicLine); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(currentTextSnapshot, SpanTrackingMode.EdgeExclusive, 3) + ).Returns(newMockTrackedNewCodeLine.Object); + + var newCodeTracker = new NewCodeTracker( + true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + + var potentialNewLineNumber = newLineNumberReduces ? 2 : 3; + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + currentTextSnapshot, + new List { new SpanAndLineRange(new Span(), potentialNewLineNumber, potentialNewLineNumber) }, + null); + + if (newLineNumberReduces) + { + Assert.That(changedLineNumbers, Is.EqualTo(new List { 1, 2 })); + Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(dynamicLine)); + } + else + { + Assert.That(changedLineNumbers, Is.EqualTo(new List { 1, 2, 3 })); + Assert.That(newCodeTracker.Lines.Count(), Is.EqualTo(2)); + } + + + } + #endregion + + #region NewCodeCodeRanges updates + [Test] + public void Should_Have_No_Changes_When_All_CodeSpanRange_Start_Line_Numbers_Are_Already_Tracked() + { + var textSnapshot = new Mock().Object; + + var mockTrackedNewCodeLine = new Mock(); + var mockDynamicLine = new Mock(); + mockDynamicLine.SetupGet(dynamicLine => dynamicLine.Number).Returns(1); + mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(mockDynamicLine.Object); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + + var newCodeTracker = new NewCodeTracker( + true, mockTrackedNewCodeLineFactory.Object, new Mock().Object, new List { 1 }, textSnapshot); + + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + new Mock().Object, + null, + new List { new CodeSpanRange(1,3)}); + + Assert.That(changedLineNumbers, Is.Empty); + Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(mockDynamicLine.Object)); + } + + [Test] + public void Should_Have_Single_Changed_Line_Number_When_CodeSpanRange_Start_Line_Not_Tracked_And_No_Existing() + { + var currentTextSnapshot = new Mock().Object; + + var mockTrackedNewCodeLine = new Mock(); + var dynamicLine = new Mock().Object; + mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(dynamicLine); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(currentTextSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + + var newCodeTracker = new NewCodeTracker( + true, mockTrackedNewCodeLineFactory.Object, null); + + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + currentTextSnapshot, + null, + new List { new CodeSpanRange(1, 3) }); + + Assert.That(changedLineNumbers.Single(),Is.EqualTo(1)); + Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(dynamicLine)); + } + + private IDynamicLine CreateDynamicLine(int lineNumber) + { + var mockDynamicLine = new Mock(); + mockDynamicLine.SetupGet(dynamicLine => dynamicLine.Number).Returns(lineNumber); + return mockDynamicLine.Object; + } + + [Test] + public void Should_Remove_Tracked_Lines_That_Are_Not_CodeSpanRange_Start() + { + var textSnapshot = new Mock().Object; + + var mockTrackedNewCodeLine = new Mock(); + var dynamicLine = CreateDynamicLine(1); + mockTrackedNewCodeLine.Setup(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(dynamicLine); + var mockTrackedNewCodeLineFactory = new Mock(); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 1) + ).Returns(mockTrackedNewCodeLine.Object); + var mockRemovedTrackedNewCodeLine2 = new Mock(); + var removedDynamicLine = CreateDynamicLine(2); + mockRemovedTrackedNewCodeLine2.Setup(trackedNewCodeLine => trackedNewCodeLine.Line).Returns(removedDynamicLine); + mockTrackedNewCodeLineFactory.Setup(trackedNewCodeLineFactory => + trackedNewCodeLineFactory.Create(textSnapshot, SpanTrackingMode.EdgeExclusive, 2) + ).Returns(mockRemovedTrackedNewCodeLine2.Object); + + var newCodeTracker = new NewCodeTracker( + true, + mockTrackedNewCodeLineFactory.Object, + new Mock().Object, + new List { 1,2,}, + textSnapshot); + + Assert.That(newCodeTracker.Lines.Count(), Is.EqualTo(2)); + + var changedLineNumbers = newCodeTracker.GetChangedLineNumbers( + new Mock().Object, + null, + new List { new CodeSpanRange(1, 3) }); + + Assert.That(changedLineNumbers, Is.EqualTo(new List { 2 })); + Assert.That(newCodeTracker.Lines.Single(), Is.SameAs(dynamicLine)); + + } + #endregion + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/OtherLinesTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/OtherLinesTracker_Tests.cs new file mode 100644 index 00000000..c9d2baab --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/OtherLinesTracker_Tests.cs @@ -0,0 +1,34 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class OtherLinesTracker_Tests + { + [Test] + public void Should_Not_Have_Lines() + { + var otherLinesTracker = new OtherLinesTracker(); + + Assert.That(otherLinesTracker.Lines, Is.Empty); + } + + [Test] + public void Should_Never_Have_Updated_Line_Numbers() + { + var otherLinesTracker = new OtherLinesTracker(); + + Assert.That(otherLinesTracker.GetUpdatedLineNumbers(null, null, null), Is.Empty); + } + + [Test] + public void Should_Have_Correct_ContainingCodeTrackerType() + { + var otherLinesTracker = new OtherLinesTracker(); + + Assert.That(otherLinesTracker.Type, Is.EqualTo(ContainingCodeTrackerType.OtherLines)); + } + } + + +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs new file mode 100644 index 00000000..2317b786 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/SerializedState_Tests.cs @@ -0,0 +1,34 @@ +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using NUnit.Framework; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class SerializedState_Tests + { + [Test] + public void Is_Serializable() + { + var states = new List + { + new SerializedState(new CodeSpanRange(1,5), ContainingCodeTrackerType.OtherLines, new List + { + new DynamicLine(1, DynamicCoverageType.Dirty) + }) + }; + + + var jsonConvertService = new JsonConvertService(); + var serialized = jsonConvertService.SerializeObject(states); + 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)); + Assert.That(state.Lines[0].CoverageType, Is.EqualTo(DynamicCoverageType.Dirty)); + Assert.That(state.CodeSpanRange.Equals(new CodeSpanRange(1, 5)), Is.True); + Assert.That(state.Type, Is.EqualTo(ContainingCodeTrackerType.OtherLines)); + + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TextInfo_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TextInfo_Tests.cs new file mode 100644 index 00000000..a7a7e280 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TextInfo_Tests.cs @@ -0,0 +1,53 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TextInfo_Tests + { + [Test] + public void Should_Return_The_Current_FilePath_Each_Time() + { + var mockTextDocument = new Mock(); + mockTextDocument.SetupSequence(textDocument => textDocument.FilePath).Returns("file1").Returns("file2"); + var mockTextBuffer = new Mock(); + var propertyCollection = new PropertyCollection(); + propertyCollection.AddProperty(typeof(ITextDocument), mockTextDocument.Object); + mockTextBuffer.SetupGet(textBuffer => textBuffer.Properties).Returns(propertyCollection); + + var textInfo = new TextInfo(new Mock().Object, mockTextBuffer.Object); + + Assert.That(textInfo.FilePath, Is.EqualTo("file1")); + Assert.That(textInfo.FilePath, Is.EqualTo("file2")); + + } + + [Test] + public void Should_Have_Null_File_Path_When_No_TextDocument_In_Properties() + { + var mockTextBuffer = new Mock(); + var propertyCollection = new PropertyCollection(); + mockTextBuffer.SetupGet(textBuffer => textBuffer.Properties).Returns(propertyCollection); + + var textInfo = new TextInfo(new Mock().Object, mockTextBuffer.Object); + + Assert.That(textInfo.FilePath, Is.Null); + } + + [Test] + public void Should_Have_TextView_TextBuffer_Properties() + { + var textBuffer = new Mock().Object; + var textView = new Mock().Object; + var textInfo = new TextInfo(textView, textBuffer); + + Assert.That(textInfo.TextView, Is.SameAs(textView)); + Assert.That(textInfo.TextBuffer, Is.SameAs(textBuffer)); + } + + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TextSnapshotLineExcluder_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TextSnapshotLineExcluder_Tests.cs new file mode 100644 index 00000000..6ee3ed70 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TextSnapshotLineExcluder_Tests.cs @@ -0,0 +1,26 @@ +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/TrackedCoverageLines_Test.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedCoverageLines_Test.cs new file mode 100644 index 00000000..38309ae0 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedCoverageLines_Test.cs @@ -0,0 +1,64 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackedCoverageLines_Tests + { + [Test] + public void Should_Update_All_CoverageLine() + { + var textSnapshot = new Mock().Object; + Mock CreateMockCoverageLine(List updatedCoverageLines) + { + var mockCoverageLine = new Mock(); + mockCoverageLine.Setup(coverageLine => coverageLine.Update(textSnapshot)).Returns(updatedCoverageLines); + return mockCoverageLine; + } + + var mockCoverageLines = new List> + { + CreateMockCoverageLine(new List{ 1,2}), + CreateMockCoverageLine(new List{3,4}) + }; + + var trackedCoverageLines = new TrackedCoverageLines(mockCoverageLines.Select(mock => mock.Object).ToList()); + + + var updatedLineNumbers = trackedCoverageLines.GetUpdatedLineNumbers(textSnapshot).ToList(); + + mockCoverageLines.ForEach(mock => mock.Verify()); + + Assert.That(updatedLineNumbers, Is.EqualTo(new List { 1,2,3,4})); + } + + [Test] + public void Should_Return_Lines_From_CoverageLines() + { + var textSnapshot = new Mock().Object; + (ICoverageLine, IDynamicLine) SetUpCoverageLine() + { + var mockCoverageLine = new Mock(); + var dynamicLine = new Mock().Object; + mockCoverageLine.SetupGet(coverageLine => coverageLine.Line).Returns(dynamicLine); + return (mockCoverageLine.Object, dynamicLine); + } + + var (firstCoverageLine, firstDynamicLine) = SetUpCoverageLine(); + var (secondCoverageLine, secondDynamicLine) = SetUpCoverageLine(); + var trackedCoverageLines = new TrackedCoverageLines(new List { firstCoverageLine, secondCoverageLine}); + + var lines = trackedCoverageLines.Lines.ToList(); + + Assert.That(lines.Count(), Is.EqualTo(2)); + Assert.That(lines[0], Is.SameAs(firstDynamicLine)); + Assert.That(lines[1], Is.SameAs(secondDynamicLine)); + + + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedLines_Test.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedLines_Test.cs new file mode 100644 index 00000000..bdb8ddf2 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedLines_Test.cs @@ -0,0 +1,337 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackedLines_Test + { + private IContainingCodeTrackerProcessResult GetProcessResult(List unprocessedSpans,List changedLines = default,bool isEmpty = false) + { + changedLines = changedLines ?? new List(); + var mockContainingCodeTrackerProcessResult = new Mock(); + mockContainingCodeTrackerProcessResult.SetupGet(containingCodeTrackerProcessResult => containingCodeTrackerProcessResult.UnprocessedSpans).Returns(unprocessedSpans); + mockContainingCodeTrackerProcessResult.SetupGet(containingCodeTrackerProcessResult => containingCodeTrackerProcessResult.ChangedLines).Returns(changedLines); + mockContainingCodeTrackerProcessResult.SetupGet(containingCodeTrackerProcessResult => containingCodeTrackerProcessResult.IsEmpty).Returns(isEmpty); + return mockContainingCodeTrackerProcessResult.Object; + } + + [Test] + public void Should_Process_Changes_With_Unprocessed_Spans() + { + var mockTextSnapshot = new Mock(); + var newSpanChanges = new List { new Span(10, 10) }; + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(10)).Returns(1); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(20)).Returns(2); + + var changes = new List { new SpanAndLineRange(new Span(15, 5), 1, 1) }; + var unprocessedSpans = new List { new SpanAndLineRange(new Span(10, 5), 0, 1) }; + var mockContainingCodeTracker1 = new Mock(); + mockContainingCodeTracker1.Setup( + containingCodeTracker => containingCodeTracker.ProcessChanges( + mockTextSnapshot.Object, + new List { new SpanAndLineRange(newSpanChanges[0], 1, 2) })) + .Returns(GetProcessResult(unprocessedSpans)); + + var mockContainingCodeTracker2 = new Mock(); + mockContainingCodeTracker2.Setup(containingCodeTracker => containingCodeTracker.ProcessChanges(mockTextSnapshot.Object, unprocessedSpans)) + .Returns(GetProcessResult(unprocessedSpans)); + + var trackedLines = new TrackedLines( + new List { mockContainingCodeTracker1.Object, mockContainingCodeTracker2.Object }, null, null); + trackedLines.GetChangedLineNumbers(mockTextSnapshot.Object, newSpanChanges); + + mockContainingCodeTracker1.VerifyAll(); + mockContainingCodeTracker2.VerifyAll(); + + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Remove_ContainingCodeTracker_When_Empty(bool isEmpty) + { + var mockTextSnapshot = new Mock(); + + var mockContainingCodeTracker1 = new Mock(); + mockContainingCodeTracker1.Setup( + containingCodeTracker => containingCodeTracker.ProcessChanges( + mockTextSnapshot.Object, + It.IsAny>())) + .Returns(GetProcessResult(new List(), new List { }, isEmpty)); + + var trackedLines = new TrackedLines(new List { mockContainingCodeTracker1.Object }, null, null); + Assert.That(trackedLines.ContainingCodeTrackers, Is.EquivalentTo(new List { mockContainingCodeTracker1.Object })); + trackedLines.GetChangedLineNumbers(mockTextSnapshot.Object, new List()); + trackedLines.GetChangedLineNumbers(mockTextSnapshot.Object, new List()); + + var times = isEmpty ? Times.Once() : Times.Exactly(2); + mockContainingCodeTracker1.Verify( + containingCodeTracker => containingCodeTracker.ProcessChanges(mockTextSnapshot.Object, It.IsAny>()), times); + Assert.That(trackedLines.ContainingCodeTrackers, Has.Count.EqualTo(isEmpty ? 0 : 1)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_GetState_Of_Not_Empty_ContainingCodeTrackers_When_NewCodeTracker_And_FileCodeSpanRangeService(bool isEmpty) + { + var mockTextSnapshot = new Mock(); + + var mockContainingCodeTracker = new Mock(MockBehavior.Strict); + mockContainingCodeTracker.Setup( + containingCodeTracker => containingCodeTracker.ProcessChanges( + mockTextSnapshot.Object, + It.IsAny>())) + .Returns(GetProcessResult(new List(), new List { },isEmpty)); + + if (!isEmpty) + { + mockContainingCodeTracker.Setup( + containingCodeTracker => containingCodeTracker.GetState() + ).Returns( + new ContainingCodeTrackerState( + ContainingCodeTrackerType.OtherLines, + CodeSpanRange.SingleLine(1), + Enumerable.Empty())); + } + + + var mockFileCodeSpanRangeService = new Mock(); + mockFileCodeSpanRangeService.Setup(fileCodeSpanRangeService => fileCodeSpanRangeService.GetFileCodeSpanRanges(mockTextSnapshot.Object)) + .Returns(new List()); + var trackedLines = new TrackedLines( + new List { mockContainingCodeTracker.Object }, + new Mock().Object, + mockFileCodeSpanRangeService.Object); + + trackedLines.GetChangedLineNumbers(mockTextSnapshot.Object, new List()); + + mockContainingCodeTracker.VerifyAll(); + } + + [Test] + public void Should_GetChangedLineNumbers_From_ContainingCodeTrackers_And_NewCodeTracker_Distinct() + { + var mockTextSnapshot = new Mock(); + var mockNewCodeTracker = new Mock(); + var newCodeTrackerChangedLines = new List { 1, 2, 3 }; + mockNewCodeTracker.Setup(newCodeTracker => newCodeTracker.GetChangedLineNumbers( + mockTextSnapshot.Object, It.IsAny>(), It.IsAny>()) + ).Returns(newCodeTrackerChangedLines); + + var mockContainingCodeTracker = new Mock(); + mockContainingCodeTracker.Setup(containingCodeTracker => containingCodeTracker.ProcessChanges(mockTextSnapshot.Object, It.IsAny>())) + .Returns(GetProcessResult(new List(), new List { 3, 4, 5 })); + var containingCodeTrackers = new List { + mockContainingCodeTracker.Object + }; + + var trackedLines = new TrackedLines(containingCodeTrackers, mockNewCodeTracker.Object, null); + var changedLineNumbers = trackedLines.GetChangedLineNumbers(mockTextSnapshot.Object, new List()); + + Assert.That(changedLineNumbers, Is.EqualTo(new List { 3, 4, 5, 1, 2 })); + } + + [TestCaseSource(typeof(ApplyNewCodeCodeRangesTestData),nameof(ApplyNewCodeCodeRangesTestData.TestCases))] + public void Should_ApplyNewCodeCodeRanges( + List containingCodeTrackersCodeSpanRanges, + List fileCodeSpanRanges, + List expectedApplyNewCodeCodeRanges + ) + { + var mockTextSnapshot = new Mock(); + var containingCodeTrackers = containingCodeTrackersCodeSpanRanges.Select(codeSpanRange => + { + var mockContainingCodeTracker = new Mock(); + mockContainingCodeTracker.Setup(containingCodeTracker => containingCodeTracker.GetState()) + .Returns(new ContainingCodeTrackerState(ContainingCodeTrackerType.OtherLines, codeSpanRange, Enumerable.Empty())); + mockContainingCodeTracker.Setup(containingCodeTracker => containingCodeTracker.ProcessChanges( + It.IsAny(), + It.IsAny>()) + ).Returns(GetProcessResult(new List())); + return mockContainingCodeTracker.Object; + }).ToList(); + + var mockFileCodeSpanRangeService = new Mock(); + mockFileCodeSpanRangeService.Setup(fileCodeSpanRangeService => fileCodeSpanRangeService.GetFileCodeSpanRanges(mockTextSnapshot.Object)) + .Returns(fileCodeSpanRanges); + + var mockNewCodeTracker = new Mock(); + var changedLines = new List { 1, 2, 3 }; + mockNewCodeTracker.Setup(newCodeTracker => newCodeTracker.GetChangedLineNumbers( + mockTextSnapshot.Object, It.IsAny>(), expectedApplyNewCodeCodeRanges) + ).Returns(changedLines); + + var trackedLines = new TrackedLines(containingCodeTrackers, mockNewCodeTracker.Object, mockFileCodeSpanRangeService.Object); + + var changedLineNumbers = trackedLines.GetChangedLineNumbers(mockTextSnapshot.Object, new List()); + + Assert.That(changedLineNumbers, Is.EqualTo(changedLines)); + mockNewCodeTracker.VerifyAll(); + } + + + public class ApplyNewCodeCodeRangesTestData + { + public class ApplyNewCodeCodeRangesTestCase : TestCaseData + { + public ApplyNewCodeCodeRangesTestCase( + List containingCodeTrackersCodeSpanRanges, + List fileCodeSpanRanges, + List expectedApplyNewCodeCodeRanges, + string testName = null + ) : base( + containingCodeTrackersCodeSpanRanges, + fileCodeSpanRanges, + expectedApplyNewCodeCodeRanges + ) + { + if (testName != null) + { + this.SetName(testName); + } + } + } + + public static IEnumerable TestCases + { + get + { // removes exact match + yield return new ApplyNewCodeCodeRangesTestCase( + new List { CodeSpanRange.SingleLine(1) }, + new List { CodeSpanRange.SingleLine(1), CodeSpanRange.SingleLine(2) }, + new List { CodeSpanRange.SingleLine(2) } + ); + + // new at the beginning + yield return new ApplyNewCodeCodeRangesTestCase( + new List { CodeSpanRange.SingleLine(2) }, + new List { CodeSpanRange.SingleLine(1) }, + new List { CodeSpanRange.SingleLine(1) } + ); + + //new at the end + yield return new ApplyNewCodeCodeRangesTestCase( + new List { CodeSpanRange.SingleLine(1) }, + new List { CodeSpanRange.SingleLine(2) }, + new List { CodeSpanRange.SingleLine(2) } + ); + + // removes intersecting + yield return new ApplyNewCodeCodeRangesTestCase( + new List { CodeSpanRange.SingleLine(1) }, + new List { new CodeSpanRange(0,2)}, + new List { } + ); + } + } + } + + + private static IDynamicLine CreateDynamicLine(int lineNumber) + { + var mockDynamicLine = new Mock(); + mockDynamicLine.SetupGet(x => x.Number).Returns(lineNumber); + return mockDynamicLine.Object; + } + + [Test] + public void Should_Return_Lines_From_ContainingCodeTrackers() + { + var mockContainingCodeTracker1 = new Mock(); + var expectedLines = new List + { + CreateDynamicLine(10), + CreateDynamicLine(11), + CreateDynamicLine(18), + CreateDynamicLine(19), + CreateDynamicLine(20), + }; + mockContainingCodeTracker1.Setup(x => x.Lines).Returns(new List + { + CreateDynamicLine(9), + expectedLines[0], + expectedLines[1] + }); + var mockContainingCodeTracker2 = new Mock(); + mockContainingCodeTracker2.Setup(x => x.Lines).Returns(new List + { + expectedLines[2], + expectedLines[3], + expectedLines[4], + }); + + var trackedLines = new TrackedLines(new List + { + mockContainingCodeTracker1.Object, + mockContainingCodeTracker2.Object + }, null, null); + + var lines = trackedLines.GetLines(10, 20); + Assert.That(lines, Is.EqualTo(expectedLines)); + } + + [Test] + public void Should_Return_Lines_From_ContainingCodeTrackers_Exiting_Early() + { + var mockContainingCodeTracker1 = new Mock(); + mockContainingCodeTracker1.Setup(x => x.Lines).Returns(new List + { + CreateDynamicLine(10), + }); + var mockContainingCodeTracker2 = new Mock(); + mockContainingCodeTracker2.Setup(x => x.Lines).Returns(new List + { + CreateDynamicLine(21), + }); + + var notCalledMockContainingCodeTracker = new Mock(MockBehavior.Strict); + + var trackedLines = new TrackedLines(new List + { + mockContainingCodeTracker1.Object, + mockContainingCodeTracker2.Object, + notCalledMockContainingCodeTracker.Object + }, null, null); + + var lines = trackedLines.GetLines(10, 20).ToList(); + + mockContainingCodeTracker1.VerifyAll(); + mockContainingCodeTracker2.VerifyAll(); + } + + [Test] + public void Should_Return_Lines_From_NewCodeTracker_But_Not_If_Already_From_ContainingCodeTrackers() + { + var expectedLines = new List + { + CreateDynamicLine(10), + CreateDynamicLine(15), + }; + var mockContainingCodeTracker = new Mock(); + + mockContainingCodeTracker.Setup(x => x.Lines).Returns(new List + { + expectedLines[0] + }); + + var mockNewCodeTracker = new Mock(); + mockNewCodeTracker.SetupGet(newCodeTracker => newCodeTracker.Lines).Returns(new List + { + CreateDynamicLine(2), + CreateDynamicLine(10), + expectedLines[1], + CreateDynamicLine(50), + }); + var trackedLines = new TrackedLines(new List + { + mockContainingCodeTracker.Object, + }, mockNewCodeTracker.Object, null); + + var lines = trackedLines.GetLines(10, 20).ToList(); + Assert.That(lines, Is.EqualTo(expectedLines)); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedNewCodeLine_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedNewCodeLine_Tests.cs new file mode 100644 index 00000000..241f3a78 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackedNewCodeLine_Tests.cs @@ -0,0 +1,71 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackedNewCodeLine_Tests + { + [Test] + public void Should_Have_Line_With_Coverage_Type_NewLine_And_Line_Number() + { + var line = new TrackedNewCodeLine(new Mock().Object, 10, new Mock().Object).Line; + + Assert.That(line.CoverageType, Is.EqualTo(DynamicCoverageType.NewLine)); + Assert.That(line.Number, Is.EqualTo(10)); + } + + [Test] + public void Should_Delegate_GetText_To_LineTracker() + { + var textSnapshot = new Mock().Object; + var trackingSpan = new Mock().Object; + var mockLineTracker = new Mock(); + mockLineTracker.Setup(lineTracker => lineTracker.GetTrackedLineInfo(trackingSpan, textSnapshot, true)) + .Returns(new TrackedLineInfo(10, "line text")); + + var trackedNewCodeLine = new TrackedNewCodeLine(trackingSpan, 10, mockLineTracker.Object); + + Assert.That(trackedNewCodeLine.GetText(textSnapshot), Is.EqualTo("line text")); + } + + private (TrackedNewCodeLineUpdate, IDynamicLine) Update(int startLineNumber,int newLineNumber) + { + var textSnapshot = new Mock().Object; + var trackingSpan = new Mock().Object; + var mockLineTracker = new Mock(); + mockLineTracker.Setup(lineTracker => lineTracker.GetTrackedLineInfo(trackingSpan, textSnapshot, true)) + .Returns(new TrackedLineInfo(newLineNumber, "line text")); + + var trackedNewCodeLine = new TrackedNewCodeLine(trackingSpan, startLineNumber, mockLineTracker.Object); + + return (trackedNewCodeLine.Update(textSnapshot), trackedNewCodeLine.Line); + } + + [Test] + public void Should_Update_Line_Number_If_Changed() + { + var (_, line) = Update(10, 20); + + Assert.That(line.Number, Is.EqualTo(20)); + } + + [Test] + public void Should_Have_Old_And_New_Line_Numbers() + { + var (update, _) = Update(10, 20); + + Assert.That(update.OldLineNumber, Is.EqualTo(10)); + Assert.That(update.NewLineNumber, Is.EqualTo(20)); + } + + [Test] + public void Should_Return_Delegated_Line_Text() + { + var (update, _) = Update(10, 10); + + Assert.That(update.Text, Is.EqualTo("line text")); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingLineTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingLineTracker_Tests.cs new file mode 100644 index 00000000..8c93ca5e --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingLineTracker_Tests.cs @@ -0,0 +1,51 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackingLineTracker_Tests + { + [Test] + public void Should_Have_Single_TrackingLine_Line() + { + var mockTrackingLine = new Mock(); + var line = new Mock().Object; + mockTrackingLine.Setup(t => t.Line).Returns(line); + var trackingLineTracker = new TrackingLineTracker(mockTrackingLine.Object,ContainingCodeTrackerType.OtherLines); + + Assert.That(trackingLineTracker.Lines.Single(), Is.SameAs(line)); + } + + [Test] + public void Should_Update_The_TrackingLine_When_No_Empty() + { + var autoMoqer = new AutoMoqer(); + var textSnapshot = new Mock().Object; + var mockTrackingLine = autoMoqer.GetMock(); + var updatedLines = new List { 10, 11 }; + mockTrackingLine.Setup(trackingLine => trackingLine.Update(textSnapshot)).Returns(updatedLines); + + var trackingLineTracker = new TrackingLineTracker(mockTrackingLine.Object, ContainingCodeTrackerType.OtherLines); + + var updatedLineNumbers = trackingLineTracker.GetUpdatedLineNumbers(null, textSnapshot, null); + + Assert.That(updatedLineNumbers, Is.SameAs(updatedLines)); + } + + [TestCase(ContainingCodeTrackerType.NotIncluded)] + [TestCase(ContainingCodeTrackerType.CoverageLines)] + public void Should_Have_Correct_ContainingCodeTrackerType(ContainingCodeTrackerType containingCodeTrackerType) + { + var autoMoqer = new AutoMoqer(); + var notIncludedCodeTracker = new TrackingLineTracker(null, containingCodeTrackerType); + Assert.That(notIncludedCodeTracker.Type, Is.EqualTo(containingCodeTrackerType)); + } + } + + +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingLine_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingLine_Tests.cs new file mode 100644 index 00000000..2cdc4103 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingLine_Tests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackingLine_Tests + { + [TestCase(DynamicCoverageType.Dirty)] + [TestCase(DynamicCoverageType.NotIncluded)] + public void Should_Have_A_Line_From_The_Start_Point_When_Constructed(DynamicCoverageType coverageType) + { + var currentSnapshot = new Mock().Object; + var trackingSpan = new Mock().Object; + + var mockLineTracker = new Mock(); + mockLineTracker.Setup(lineTracker => lineTracker.GetLineNumber(trackingSpan, currentSnapshot, false)).Returns(10); + + var trackingLine = new TrackingLine(trackingSpan, currentSnapshot, mockLineTracker.Object, coverageType); + + AssertTrackingLine(trackingLine, 10, coverageType); + } + + private void AssertTrackingLine(TrackingLine trackingLine, int lineNumber, DynamicCoverageType coverageType) + { + var dynamicLine = trackingLine.Line; + + Assert.That(coverageType, Is.EqualTo(dynamicLine.CoverageType)); + Assert.That(lineNumber, Is.EqualTo(dynamicLine.Number)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Have_An_Updated_Line_When_Update(bool changeLineNumber) + { + var initialSnapshot = new Mock().Object; + var trackingSpan = new Mock().Object; + + var mockLineTracker = new Mock(); + mockLineTracker.Setup(lineTracker => lineTracker.GetLineNumber(trackingSpan, initialSnapshot, false)).Returns(10); + + var trackingLine = new TrackingLine(trackingSpan, initialSnapshot, mockLineTracker.Object, DynamicCoverageType.Dirty); + + var currentSnapshot = new Mock().Object; + var newLineNumber = changeLineNumber ? 11 : 10; + mockLineTracker.Setup(lineTracker => lineTracker.GetLineNumber(trackingSpan, currentSnapshot, false)) + .Returns(newLineNumber); + + var updatedLineNumbers = trackingLine.Update(currentSnapshot); + Assert.That(updatedLineNumbers, Is.EqualTo(changeLineNumber ? new List { 10, 11} : Enumerable.Empty())); + AssertTrackingLine(trackingLine, newLineNumber, DynamicCoverageType.Dirty); + } + } +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingSpanRangeUpdatingTracker_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingSpanRangeUpdatingTracker_Tests.cs new file mode 100644 index 00000000..977b1dcb --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingSpanRangeUpdatingTracker_Tests.cs @@ -0,0 +1,103 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackingSpanRangeUpdatingTracker_Tests + { + [Test] + public void Should_Get_Lines_From_IUpdatableDynamicLines() + { + var mockUpdatableDynamicLines = new Mock(); + var dynamicLines = Enumerable.Empty(); + mockUpdatableDynamicLines.SetupGet(updatableDynamicLines => updatableDynamicLines.Lines).Returns(dynamicLines); + + var trackingSpanRangeUpdatingTracker = new TrackingSpanRangeUpdatingTracker(null, mockUpdatableDynamicLines.Object); + + Assert.That(trackingSpanRangeUpdatingTracker.Lines, Is.SameAs(dynamicLines)); + } + + private static IDynamicLine CreateDynamicLine(int lineNumber) + { + var mockDynamicLine = new Mock(); + mockDynamicLine.SetupGet(dynamicLine => dynamicLine.Number).Returns(lineNumber); + return mockDynamicLine.Object; + } + + [Test] + public void Should_Not_Update_IUpdatableDynamicLines_When_Empty_Returning_All_UpdatableDynamicLines_Line_Numbers() + { + var textSnapshot = new Mock().Object; + var newSpanAndLineRanges = new List { new SpanAndLineRange(new Span(1, 2), 0, 0) }; + var mockTrackingSpanRange = new Mock(); + var nonIntersectingSpans = new List(); + mockTrackingSpanRange.Setup(trackingSpanRange => trackingSpanRange.Process(textSnapshot, newSpanAndLineRanges)) + .Returns(new TrackingSpanRangeProcessResult(mockTrackingSpanRange.Object, nonIntersectingSpans, true, false)); + var mockUpdatableDynamicLines = new Mock(MockBehavior.Strict); + + var dynamicLines = new List { CreateDynamicLine(1), CreateDynamicLine(2) }; + mockUpdatableDynamicLines.SetupGet(updatableDynamicLines => updatableDynamicLines.Lines).Returns(dynamicLines); + + var trackingSpanRangeUpdatingTracker = new TrackingSpanRangeUpdatingTracker(mockTrackingSpanRange.Object, mockUpdatableDynamicLines.Object); + + var result = trackingSpanRangeUpdatingTracker.ProcessChanges(textSnapshot, newSpanAndLineRanges); + + Assert.That(result.UnprocessedSpans, Is.SameAs(nonIntersectingSpans)); + Assert.That(result.ChangedLines, Is.EqualTo(new List { 1,2})); + Assert.That(result.IsEmpty, Is.True); + } + + [Test] + public void Should_Update_IUpdatableDynamicLines_When_Non_Empty() + { + var textSnapshot = new Mock().Object; + var newSpanAndLineRanges = new List { new SpanAndLineRange(new Span(1, 2), 0, 0) }; + var mockTrackingSpanRange = new Mock(); + var nonIntersectingSpans = new List(); + var trackingSpanRangeProcessResult = new TrackingSpanRangeProcessResult(mockTrackingSpanRange.Object, nonIntersectingSpans, false, false); + mockTrackingSpanRange.Setup(trackingSpanRange => trackingSpanRange.Process(textSnapshot, newSpanAndLineRanges)) + .Returns(trackingSpanRangeProcessResult); + var mockUpdatableDynamicLines = new Mock(); + var updatedLineNumbers = new List { 1, 2 }; + mockUpdatableDynamicLines.Setup( + updatableDynamicLines => updatableDynamicLines.GetUpdatedLineNumbers(trackingSpanRangeProcessResult, textSnapshot, newSpanAndLineRanges) + ).Returns(updatedLineNumbers); + + var trackingSpanRangeUpdatingTracker = new TrackingSpanRangeUpdatingTracker(mockTrackingSpanRange.Object, mockUpdatableDynamicLines.Object); + + var result = trackingSpanRangeUpdatingTracker.ProcessChanges(textSnapshot, newSpanAndLineRanges); + + Assert.That(result.UnprocessedSpans, Is.SameAs(nonIntersectingSpans)); + Assert.That(result.ChangedLines, Is.SameAs(updatedLineNumbers)); + Assert.That(result.IsEmpty, Is.False); + } + + [TestCase(ContainingCodeTrackerType.NotIncluded)] + [TestCase(ContainingCodeTrackerType.OtherLines)] + public void Should_GetState(ContainingCodeTrackerType containingCodeTrackerType) + { + var autoMoqer = new AutoMoqer(); + var mockUpdatableDynamicLines = autoMoqer.GetMock(); + mockUpdatableDynamicLines.SetupGet(updatableDynamicLines => updatableDynamicLines.Type).Returns(containingCodeTrackerType); + var lines = Enumerable.Empty(); + mockUpdatableDynamicLines.SetupGet(updatableDynamicLines => updatableDynamicLines.Lines).Returns(lines); + var codeSpanRange = new CodeSpanRange(1, 2); + autoMoqer.Setup(trackingSpanRange => trackingSpanRange.ToCodeSpanRange()).Returns(codeSpanRange); + var trackingSpanRangeUpdatingTracker = autoMoqer.Create(); + + var state = trackingSpanRangeUpdatingTracker.GetState(); + + Assert.That(containingCodeTrackerType, Is.EqualTo(state.Type)); + Assert.That(lines, Is.SameAs(state.Lines)); + Assert.That(codeSpanRange, Is.SameAs(state.CodeSpanRange)); + + } + } + + +} diff --git a/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingSpanRange_Tests.cs b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingSpanRange_Tests.cs new file mode 100644 index 00000000..f9a20627 --- /dev/null +++ b/FineCodeCoverageTests/Editor/DynamicCoverage/TrackingSpanRange_Tests.cs @@ -0,0 +1,148 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.DynamicCoverage +{ + internal class TrackingSpanRange_Tests + { + private (Mock, Mock, Mock) SetupTrackingSpans() + { + var mockFirstSnapshot = new Mock(); + + var mockStartTrackingSpan = new Mock(); + mockStartTrackingSpan.Setup(startTrackingspan => startTrackingspan.GetSpan(It.IsAny())) + .Returns(new SnapshotSpan(mockFirstSnapshot.Object, new Span())); + var mockEndTrackingSpan = new Mock(); + mockEndTrackingSpan.Setup(startTrackingspan => startTrackingspan.GetSpan(It.IsAny())) + .Returns(new SnapshotSpan(mockFirstSnapshot.Object, new Span())); + + return (mockFirstSnapshot, mockStartTrackingSpan, mockEndTrackingSpan); + } + private (TrackingSpanRange,Mock, Mock) CreateTrackingSpanRange(string firstText = "") + { + var (mockFirstSnapshot, mockStartTrackingSpan, mockEndTrackingSpan) = SetupTrackingSpans(); + mockFirstSnapshot.Setup(firstSnapshot => firstSnapshot.GetText(It.IsAny())).Returns(firstText); + + return ( + new TrackingSpanRange( + mockStartTrackingSpan.Object, + mockEndTrackingSpan.Object, + mockFirstSnapshot.Object, + new Mock().Object + ), + mockStartTrackingSpan, + mockEndTrackingSpan + ); + } + + [Test] + public void Should_Return_NonIntersecting_By_Range_Line_Numbers() + { + var (trackingSpanRange,mockFirstTrackingSpan, mockEndTrackingSpan) = CreateTrackingSpanRange(); + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(ts => ts.Length).Returns(1000); + void SetupSpan(Mock mockTrackingSpan,int end,int lineNumber) + { + mockTrackingSpan.Setup(trackingSpan => trackingSpan.GetSpan(mockTextSnapshot.Object)) + .Returns(new SnapshotSpan(mockTextSnapshot.Object, new Span(0, end))); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineNumberFromPosition(end)).Returns(lineNumber); + } + + // setting the range to lines between 5 and 10 + SetupSpan(mockFirstTrackingSpan,10, 5); + SetupSpan(mockEndTrackingSpan,20, 10); + + var expectedNewSpanAndLineRanges = new List + { + new SpanAndLineRange(new Span(0,1),0,0), + new SpanAndLineRange(new Span(160,10),11,11), + }; + + var newSpanAndLineRanges = new List + { + expectedNewSpanAndLineRanges[0], + + new SpanAndLineRange(new Span(50,10),4,5), + new SpanAndLineRange(new Span(100,10),5,6), + new SpanAndLineRange(new Span(110,10),7,7), + new SpanAndLineRange(new Span(120,10),8,8), + new SpanAndLineRange(new Span(130,10),9,9), + new SpanAndLineRange(new Span(140,10),10,10), + new SpanAndLineRange(new Span(150,10),10,11), + + expectedNewSpanAndLineRanges[1] + + }; + + var result = trackingSpanRange.Process(mockTextSnapshot.Object, newSpanAndLineRanges); + + Assert.That(expectedNewSpanAndLineRanges, Is.EqualTo(result.NonIntersectingSpans)); + } + + private TrackingSpanRangeProcessResult TextTest(string firstText, string changeText) + { + var (trackingSpanRange, mockFirstTrackingSpan, mockEndTrackingSpan) = CreateTrackingSpanRange(firstText); + var mockTextSnapshot = new Mock(); + var firstStart = 10; + var endEnd = 50; + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetText(new Span(firstStart, endEnd - firstStart))).Returns(changeText); + mockTextSnapshot.SetupGet(ts => ts.Length).Returns(1000); + void SetupSpan(Mock mockTrackingSpan, int start, int end) + { + mockTrackingSpan.Setup(trackingSpan => trackingSpan.GetSpan(mockTextSnapshot.Object)) + .Returns(new SnapshotSpan(mockTextSnapshot.Object, new Span(start, end - start))); + } + SetupSpan(mockFirstTrackingSpan, firstStart, 15); + SetupSpan(mockEndTrackingSpan, 20, endEnd); + + return trackingSpanRange.Process(mockTextSnapshot.Object, new List()); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Return_TextChanged_When_TextChanged_For_The_Range(bool changeText) + { + var result = TextTest("range text", changeText ? "new" : "range text"); + + Assert.That(result.TextChanged, Is.EqualTo(changeText)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Return_Empty_When_Range_Text_IsNullOrWhitespace(bool isEmpty) + { + var result = TextTest("", isEmpty ? " " : " range text "); + Assert.That(result.IsEmpty, Is.EqualTo(isEmpty)); + } + + [Test] + public void Should_GetFirstTrackingSpan() + { + var (trackingSpanRange,mockFirstTrackingSpan, _) = CreateTrackingSpanRange(); + + Assert.That(mockFirstTrackingSpan.Object, Is.SameAs(trackingSpanRange.GetFirstTrackingSpan())); + } + + [Test] + public void Should_Have_Correct_CodeSpanRange_When_Initialized() + { + var (mockTextSnapshot, mockStartTrackingSpan, mockEndTrackingSpan) = SetupTrackingSpans(); + var mockLineTracker = new Mock(); + mockLineTracker.Setup(lineTracker => lineTracker.GetLineNumber(mockStartTrackingSpan.Object, mockTextSnapshot.Object, false)) + .Returns(0); + mockLineTracker.Setup(lineTracker => lineTracker.GetLineNumber(mockEndTrackingSpan.Object, mockTextSnapshot.Object, true)) + .Returns(5); + var trackingSpanRange = new TrackingSpanRange( + mockStartTrackingSpan.Object, mockEndTrackingSpan.Object, mockTextSnapshot.Object, mockLineTracker.Object); + + var codeSpanRange = trackingSpanRange.ToCodeSpanRange(); + + Assert.That(codeSpanRange.StartLine, Is.EqualTo(0)); + Assert.That(codeSpanRange.EndLine, Is.EqualTo(5)); + + } + } +} diff --git a/FineCodeCoverageTests/Editor/Management/CoverageClassificationTypeService_Tests.cs b/FineCodeCoverageTests/Editor/Management/CoverageClassificationTypeService_Tests.cs new file mode 100644 index 00000000..84c7d8e7 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/CoverageClassificationTypeService_Tests.cs @@ -0,0 +1,163 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Utilities; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reflection; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class CapturingClassificationTypeRegistryService : IClassificationTypeRegistryService + { + public Dictionary ClassificationTypes { get; set; } = new Dictionary(); + public IClassificationType CreateClassificationType(string type, IEnumerable baseTypes) + { + throw new System.NotImplementedException(); + } + + public ILayeredClassificationType CreateClassificationType(ClassificationLayer layer, string type, IEnumerable baseTypes) + { + throw new System.NotImplementedException(); + } + + public IClassificationType CreateTransientClassificationType(IEnumerable baseTypes) + { + throw new System.NotImplementedException(); + } + + public IClassificationType CreateTransientClassificationType(params IClassificationType[] baseTypes) + { + throw new System.NotImplementedException(); + } + + public IClassificationType GetClassificationType(string type) + { + var classificationType = new Mock().Object; + ClassificationTypes.Add(type, classificationType); + return classificationType; + } + + public ILayeredClassificationType GetClassificationType(ClassificationLayer layer, string type) + { + throw new System.NotImplementedException(); + } + } + internal class CoverageClassificationTypeService_Tests + { + [Test] + public void Should_Export_ClassificationTypeDefinitions_For_The_Types_Requested_From_The_ClassificationTypeRegistryService() + { + var autoMoqer = new AutoMoqer(); + var mockClassificationTypeRegistryService = autoMoqer.GetMock(); + var mockClassificationFormatMapService = autoMoqer.GetMock(); + mockClassificationFormatMapService.Setup( + classificationFormatMapService => classificationFormatMapService.GetClassificationFormatMap("text").CurrentPriorityOrder + ).Returns(new ReadOnlyCollection(new List { new Mock().Object })); + + autoMoqer.Create(); + + var classificationTypeDefinitionProperties = typeof(CoverageClassificationTypeService).GetProperties().Where(p => p.PropertyType == typeof(ClassificationTypeDefinition)); + var names = new List(); + foreach (var classificationTypeDefinitionProperty in classificationTypeDefinitionProperties) + { + var exportAttribute = classificationTypeDefinitionProperty.GetCustomAttribute(); + Assert.That(exportAttribute, Is.Not.Null); + var name = classificationTypeDefinitionProperty.GetCustomAttribute().Name; + mockClassificationTypeRegistryService.Verify(classificationTypeRegistryService => classificationTypeRegistryService.GetClassificationType(name)); + names.Add(name); + } + Assert.That(names.Distinct(), Is.EquivalentTo(new List { + CoverageClassificationTypeService.FCCNotCoveredClassificationTypeName, + CoverageClassificationTypeService.FCCCoveredClassificationTypeName, + CoverageClassificationTypeService.FCCPartiallyCoveredClassificationTypeName, + CoverageClassificationTypeService.FCCDirtyClassificationTypeName, + CoverageClassificationTypeService.FCCNewLineClassificationTypeName, + CoverageClassificationTypeService.FCCNotIncludedClassificationTypeName + })); + } + + [Test] + public void Should_Correspond() + { + var autoMoqer = new AutoMoqer(); + var classificationTypeRegistryService = new CapturingClassificationTypeRegistryService(); + autoMoqer.SetInstance(classificationTypeRegistryService); + + var mockClassificationFormatMapService = autoMoqer.GetMock(); + var mockClassificationFormatMap = new Mock(); + mockClassificationFormatMap.SetupGet(classificationFormatMap => classificationFormatMap.CurrentPriorityOrder) + .Returns(new ReadOnlyCollection(new List { new Mock().Object })); + mockClassificationFormatMapService.Setup( + classificationFormatMapService => classificationFormatMapService.GetClassificationFormatMap("text") + ).Returns(mockClassificationFormatMap.Object); + + var coverageClassificationTypeService = autoMoqer.Create(); + foreach (var coverageType in Enum.GetValues(typeof(DynamicCoverageType)).Cast()) + { + var editorFormatDefinition = coverageClassificationTypeService.GetEditorFormatDefinitionName(coverageType); + var classificationType = classificationTypeRegistryService.ClassificationTypes[editorFormatDefinition]; + Assert.That(classificationType, Is.SameAs(coverageClassificationTypeService.GetClassificationType(coverageType))); + var mockCoverageTypeColour = new Mock(); + mockCoverageTypeColour.SetupGet(coverageTypeColour => coverageTypeColour.CoverageType).Returns(coverageType); + coverageClassificationTypeService.SetCoverageColours(new List() { mockCoverageTypeColour.Object }); + mockClassificationFormatMap.Verify( + classificationFormatMap => classificationFormatMap.AddExplicitTextProperties( + classificationType, + It.IsAny(), + It.IsAny())); + } + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Prioriitize_Clasifications_In_The_ClassificationFormatMap_Batch_Updating_If_Not_In_Batch_Update(bool isInBatchUpdate) + { + var autoMoqer = new AutoMoqer(); + var mockClassificationTypeRegistryService = autoMoqer.GetMock(); + var mockClassificationFormatMapService = autoMoqer.GetMock(); + var currentPriorityOrder = new List { new Mock().Object, null, new Mock().Object }; + var mockClassificationFormatMap = new Mock(); + mockClassificationFormatMap.SetupGet(classificationFormatMap => classificationFormatMap.IsInBatchUpdate).Returns(isInBatchUpdate); + mockClassificationFormatMapService.Setup( + classificationFormatMapService => classificationFormatMapService.GetClassificationFormatMap("text") + ).Returns(mockClassificationFormatMap.Object); + mockClassificationFormatMap.SetupGet( + classificationFormatMap => classificationFormatMap.CurrentPriorityOrder + ).Returns(new ReadOnlyCollection(currentPriorityOrder)); + + + var coverageClassificationTypeService = autoMoqer.Create(); + var textFormattingRunProperties = TextFormattingRunProperties.CreateTextFormattingRunProperties().SetBold(true); + List coverageTypeColours = new List + { + CreateCoverageTypeColour(DynamicCoverageType.Covered, textFormattingRunProperties) + }; + coverageClassificationTypeService.SetCoverageColours(coverageTypeColours); + + mockClassificationFormatMap.Verify( + classificationFormatMap => classificationFormatMap.AddExplicitTextProperties( + It.IsAny(), + textFormattingRunProperties, + currentPriorityOrder[2] + )); + + mockClassificationFormatMap.Verify(classificationFormatMap => classificationFormatMap.BeginBatchUpdate(), Times.Exactly(isInBatchUpdate ? 0 : 1)); + mockClassificationFormatMap.Verify(classificationFormatMap => classificationFormatMap.EndBatchUpdate(), Times.Exactly(isInBatchUpdate ? 0 : 1)); + } + private static ICoverageTypeColour CreateCoverageTypeColour(DynamicCoverageType coverageType, TextFormattingRunProperties textFormattingRunProperties) + { + var mockCoverageTypeColour = new Mock(); + mockCoverageTypeColour.SetupGet(coverageTypeColour => coverageTypeColour.CoverageType).Returns(coverageType); + mockCoverageTypeColour.SetupGet(coverageTypeColour => coverageTypeColour.TextFormattingRunProperties).Returns(textFormattingRunProperties); + return mockCoverageTypeColour.Object; + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/CoverageColoursManager_Tests.cs b/FineCodeCoverageTests/Editor/Management/CoverageColoursManager_Tests.cs new file mode 100644 index 00000000..796c6dd6 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/CoverageColoursManager_Tests.cs @@ -0,0 +1,243 @@ +using AutoMoq; +using Castle.Core.Internal; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverageTests.Test_helpers; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Utilities; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reflection; +using FineCodeCoverageTests.TestHelpers; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class CoverageColoursManager_Tests + { + private IEnumerable GetEditorFormatDefinitionProperties() + { + return typeof(CoverageColoursManager).GetProperties().Where(p => p.PropertyType == typeof(EditorFormatDefinition)); + } + + private IEnumerable GetEditorFormatDefinitionNames() + { + return GetEditorFormatDefinitionProperties().Select(p => p.GetAttribute().Name); + } + + [Test] + public void Should_Export_IInitializable() + { + ExportsInitializable.Should_Export_IInitializable(typeof(CoverageColoursManager)); + } + + [Test] + public void Should_Export_6_UserVisible_ClassificationFormatDefinitions() + { + var autoMoqer = new AutoMoqer(); + + var coverageColoursManager = autoMoqer.Create(); + + var editorFormatDefinitionProperties = GetEditorFormatDefinitionProperties().ToList(); + + Assert.That(editorFormatDefinitionProperties.Count, Is.EqualTo(6)); + editorFormatDefinitionProperties.ForEach(p => + { + Assert.That(p.GetAttribute(), Is.Not.Null); + Assert.That(p.GetAttribute(), Is.Not.Null); + Assert.That(p.GetAttribute().UserVisible, Is.True); + Assert.That(p.GetValue(coverageColoursManager), Is.InstanceOf()); + }); + } + + [Test] + public void Should_Listen_For_EditorFormatMap_Text_Changes_To_Markers_And_EditorFormatDefinitions() + { + var autoMoqer = new AutoMoqer(); + + var coverageColoursManager = autoMoqer.Create(); + + var expectedListenFor = new List + { + "Coverage Touched Area", + "Coverage Not Touched Area", + "Coverage Partially Touched Area", + }.Concat(GetEditorFormatDefinitionNames()).OrderByDescending(v => v); + + autoMoqer.Verify( + editorFormatMapTextSpecificListener => editorFormatMapTextSpecificListener.ListenFor( + It.Is>(listenedFor => expectedListenFor.SequenceEqual(listenedFor.OrderByDescending(v => v))), It.IsAny() + )); + } + + [Test] + public void Should_Initialize_ICoverageFontAndColorsCategoryItemNamesManager_With_FCCEditorFormatDefinitionNames() + { + var autoMoqer = new AutoMoqer(); + + autoMoqer.Create(); + + var fccEditorFormatDefinitionNames = (FCCEditorFormatDefinitionNames)autoMoqer.GetMock() + .Invocations.Where(i => i.Method.Name == nameof(ICoverageFontAndColorsCategoryItemNamesManager.Initialize)).Single().Arguments[0]; + var names = new List { + fccEditorFormatDefinitionNames.NewLines, + fccEditorFormatDefinitionNames.Dirty, + fccEditorFormatDefinitionNames.PartiallyCovered, + fccEditorFormatDefinitionNames.Covered, + fccEditorFormatDefinitionNames.NotCovered, + fccEditorFormatDefinitionNames.NotIncluded + }.OrderByDescending(n => n).ToList(); + + Assert.That(names.SequenceEqual(GetEditorFormatDefinitionNames().Distinct().OrderByDescending(n => n)), Is.True); + } + + [Test] + public void Should_Set_FontAndColorsInfosProvider_CoverageFontAndColorsCategoryItemNames_From_The_Manager() + { + var autoMoqer = new AutoMoqer(); + var coverageFontAndColorsCategoryItemNames = new Mock().Object; + autoMoqer.Setup( + coverageFontAndColorsCategoryItemNamesManager => coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames) + .Returns(coverageFontAndColorsCategoryItemNames); + + autoMoqer.Create(); + + var mockFontAndColorsInfosProvider = autoMoqer.GetMock(); + mockFontAndColorsInfosProvider.VerifySet(fontAndColorsInfosProvider => fontAndColorsInfosProvider.CoverageFontAndColorsCategoryItemNames = coverageFontAndColorsCategoryItemNames); + + } + + [Test] + public void Should_Delayed_Set_Classification_Type_Colors() + { + var autoMoqer = new AutoMoqer(); + var partialFontAndColorsInfo = new Mock().Object; + var partialTextFormattingRunProperties = TextFormattingRunProperties.CreateTextFormattingRunProperties().SetBold(true); + var mockTextFormattingRunPropertiesFactory = autoMoqer.GetMock(); + mockTextFormattingRunPropertiesFactory.Setup(textFormattingRunPropertiesFactory => textFormattingRunPropertiesFactory.Create( + partialFontAndColorsInfo + )).Returns(partialTextFormattingRunProperties); + + var coverageColoursManager = autoMoqer.Create(); + autoMoqer.Setup>( + fontAndColorsInfosProvider => fontAndColorsInfosProvider.GetFontAndColorsInfos() + ).Returns(new Dictionary { { DynamicCoverageType.Partial, partialFontAndColorsInfo } }); + + var mockEditorFormatMapTextSpecificListener = autoMoqer.GetMock(); + mockEditorFormatMapTextSpecificListener.Setup(efmtsl => efmtsl.PauseListeningWhenExecuting(It.IsAny())).Callback(action => action()); + var mockCoverageTextMarkerInitializeTiming = autoMoqer.GetMock(); + (mockCoverageTextMarkerInitializeTiming.Invocations[0].Arguments[0] as Action)(); + + autoMoqer.Verify( + coverageClassificationColourService => coverageClassificationColourService.SetCoverageColours( + It.IsAny>() + ), + Times.Once() + ); + + var coverageTypeColours = autoMoqer.GetMock() + .Invocations.GetMethodInvocationSingleArgument>(nameof(ICoverageClassificationColourService.SetCoverageColours)).First().ToList(); + Assert.That(coverageTypeColours.Count, Is.EqualTo(1)); + var coverageTypeColour = coverageTypeColours[0]; + Assert.That(coverageTypeColour.CoverageType, Is.EqualTo(DynamicCoverageType.Partial)); + Assert.That(coverageTypeColour.TextFormattingRunProperties, Is.SameAs(partialTextFormattingRunProperties)); + } + + private void Should_Set_Classification_Type_Colors_When_Changes_Pausing_Listening_For_Changes(Action changer) + { + var autoMoqer = new AutoMoqer(); + + var coverageColoursManager = autoMoqer.Create(); + var changedFontAndColorsInfos = new Dictionary + { + { DynamicCoverageType.Covered, new Mock().Object }, + { DynamicCoverageType.NotCovered, new Mock().Object }, + { DynamicCoverageType.Partial, new Mock().Object } + }; + var coverageTypes = changedFontAndColorsInfos.Keys.ToList(); + autoMoqer.Setup>( + fontAndColorsInfosProvider => fontAndColorsInfosProvider.GetChangedFontAndColorsInfos() + ).Returns(changedFontAndColorsInfos); + var mockTextFormattingRunPropertiesFactory = autoMoqer.GetMock(); + var changedTextFormattingRunProperties = new List + { + TextFormattingRunProperties.CreateTextFormattingRunProperties(), + TextFormattingRunProperties.CreateTextFormattingRunProperties().SetBold(true), + TextFormattingRunProperties.CreateTextFormattingRunProperties().SetItalic(true) + }; + var count = 0; + foreach (var change in changedFontAndColorsInfos) + { + mockTextFormattingRunPropertiesFactory.Setup( + textFormattingRunPropertiesFactory => textFormattingRunPropertiesFactory.Create(change.Value) + ) + .Returns(changedTextFormattingRunProperties[count]); + count++; + } + + var mockEditorFormatMapTextSpecificListener = autoMoqer.GetMock(); + mockEditorFormatMapTextSpecificListener.Setup(efmtsl => efmtsl.PauseListeningWhenExecuting(It.IsAny())).Callback(action => action()); + + changer(autoMoqer); + + + var coverageTypeColours = (autoMoqer.GetMock().Invocations[0].Arguments[0] as IEnumerable).ToList(); + Assert.That(coverageTypeColours.Count, Is.EqualTo(3)); + count = 0; + foreach (var coverageTypeColour in coverageTypeColours) + { + Assert.That(coverageTypeColour.CoverageType, Is.EqualTo(coverageTypes[count])); + Assert.That(coverageTypeColour.TextFormattingRunProperties, Is.SameAs(changedTextFormattingRunProperties[count])); + count++; + } + + mockEditorFormatMapTextSpecificListener.VerifyAll(); + } + + [Test] + public void Should_Set_Classification_Type_Colors_When_EditorFormatMap_Changes_Pausing_Listening_For_Changes() + { + Should_Set_Classification_Type_Colors_When_Changes_Pausing_Listening_For_Changes(autoMoqer => + { + var mockEditorFormatMapTextSpecificListener = autoMoqer.GetMock(); + var listener = mockEditorFormatMapTextSpecificListener.Invocations[0].Arguments[1] as Action; + listener(); + }); + } + + [Test] + public void Should_Not_Set_Classification_Type_Colors_When_No_Changes() + { + var autoMoqer = new AutoMoqer(); + + var coverageColoursManager = autoMoqer.Create(); + autoMoqer.Setup>( + fontAndColorsInfosProvider => fontAndColorsInfosProvider.GetChangedFontAndColorsInfos() + ).Returns(new Dictionary()); + var mockEditorFormatMapTextSpecificListener = autoMoqer.GetMock(); + var listener = mockEditorFormatMapTextSpecificListener.Invocations[0].Arguments[1] as Action; + listener(); + + autoMoqer.Verify( + coverageClassificationColourService => coverageClassificationColourService.SetCoverageColours( + It.IsAny>() + ), + Times.Never() + ); + } + + [Test] + public void Should_Set_Classification_Type_Colours_When_CoverageFontAndColorsCategoryItemNamesManager_Changed() + { + Should_Set_Classification_Type_Colors_When_Changes_Pausing_Listening_For_Changes(autoMoqer => + { + var mockCoverageFontAndColorsCategoryItemNamesManager = autoMoqer.GetMock(); + mockCoverageFontAndColorsCategoryItemNamesManager.Raise(mgr => mgr.Changed += null, EventArgs.Empty); + }); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/CoverageFontAndColorsCategoryItemNames_Tests.cs b/FineCodeCoverageTests/Editor/Management/CoverageFontAndColorsCategoryItemNames_Tests.cs new file mode 100644 index 00000000..dc873583 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/CoverageFontAndColorsCategoryItemNames_Tests.cs @@ -0,0 +1,188 @@ +using AutoMoq; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Options; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class CoverageFontAndColorsCategoryItemNames_Tests + { + [Test] + public void Should_Use_MEF_Category_And_FCCEditorFormatDefinitionNames_When_Vs_Does_Not_Have_Coverage_Markers() + { + var autoMoqer = new AutoMoqer(); + autoMoqer.Setup(x => x.HasCoverageMarkers()).Returns(false); + + Verify_Use_MEF_Category_And_FCCEditorFormatDefinitionNames(autoMoqer); + } + + [Test] + public void Should_Use_MEF_Category_And_FCCEditorFormatDefinitionNames_When_Vs_Does_Have_Coverage_Markers_But_Not_UseEnterpriseFontsAndColors() + { + var autoMoqer = new AutoMoqer(); + autoMoqer.Setup(x => x.HasCoverageMarkers()).Returns(true); + autoMoqer.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(new Mock().Object); + + Verify_Use_MEF_Category_And_FCCEditorFormatDefinitionNames(autoMoqer); + } + + [Test] + public void Should_Use_MEF_Category_For_Non_Markers_When_UseEnterpriseFontsAndColors() + { + var autoMoqer = new AutoMoqer(); + autoMoqer.Setup(x => x.HasCoverageMarkers()).Returns(true); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(appOptions => appOptions.UseEnterpriseFontsAndColors).Returns(true); + autoMoqer.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); + + var coverageFontAndColorsCategoryItemNamesManager = CreateAndInitialize(autoMoqer); + + var categoryItemNames = coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames; + + AssertNonMarkers(categoryItemNames); + } + + [Test] + public void Should_Use_VS_For_Markers_When_UseEnterpriseFontsAndColors() + { + var autoMoqer = new AutoMoqer(); + autoMoqer.Setup(x => x.HasCoverageMarkers()).Returns(true); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(appOptions => appOptions.UseEnterpriseFontsAndColors).Returns(true); + autoMoqer.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); + + var coverageFontAndColorsCategoryItemNamesManager = CreateAndInitialize(autoMoqer); + + var categoryItemNames = coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames; + + AssertVSMarkers(categoryItemNames); + } + + [TestCase(false,true, false, true)] + [TestCase(false, false, false, true)] + [TestCase(true,false, true, false)] + [TestCase(true, true, true, true)] + public void Change_Test(bool hasCoverageMarkers,bool useEnterpriseFontsAndColors, bool expectedChangedRaised, bool expectedMEF) + { + var autoMoqer = new AutoMoqer(); + autoMoqer.Setup(x => x.HasCoverageMarkers()).Returns(hasCoverageMarkers); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(appOptions => appOptions.UseEnterpriseFontsAndColors).Returns(useEnterpriseFontsAndColors); + var mockChangedAppOptions = new Mock(); + mockChangedAppOptions.SetupGet(appOptions => appOptions.UseEnterpriseFontsAndColors).Returns(!useEnterpriseFontsAndColors); + var mockAppOptionsProvider = autoMoqer.GetMock(); + mockAppOptionsProvider.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(mockAppOptions.Object); + + var coverageFontAndColorsCategoryItemNamesManager = CreateAndInitialize(autoMoqer); + + var changedRaised = false; + coverageFontAndColorsCategoryItemNamesManager.Changed += (sender, args) => + { + changedRaised = true; + }; + + var _ = coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames; + + mockAppOptionsProvider.Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, mockChangedAppOptions.Object); + + var changed = coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames; + var changedCovered = changed.Covered; + var changedNotCovered = changed.NotCovered; + var changedPartiallyCovered = changed.PartiallyCovered; + + + AssertNonMarkers(changed); + Assert.That(changedRaised, Is.EqualTo(expectedChangedRaised)); + if (expectedMEF) + { + AssertMEFMarkers(changed); + } + else + { + AssertVSMarkers(changed); + } + + } + + private CoverageFontAndColorsCategoryItemNamesManager CreateAndInitialize(AutoMoqer autoMoqer) + { + var coverageFontAndColorsCategoryItemNamesManager = autoMoqer.Create(); + coverageFontAndColorsCategoryItemNamesManager.Initialize(new FCCEditorFormatDefinitionNames( + "Covered", + "NotCovered", + "PartiallyCovered", + "NewLines", + "Dirty", + "NotIncluded" + )); + return coverageFontAndColorsCategoryItemNamesManager; + } + + private void Verify_Use_MEF_Category_And_FCCEditorFormatDefinitionNames(AutoMoqer autoMoqer) + { + var coverageFontAndColorsCategoryItemNamesManager = CreateAndInitialize(autoMoqer); + + var categoryItemNames = coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames; + AssertMEFMarkers(categoryItemNames); + + AssertNonMarkers(categoryItemNames); + } + + private void AssertMEFMarkers(ICoverageFontAndColorsCategoryItemNames categoryItemNames) + { + AssertMarkers(categoryItemNames, true, "Covered", "NotCovered", "PartiallyCovered"); + } + + private void AssertVSMarkers(ICoverageFontAndColorsCategoryItemNames categoryItemNames) + { + AssertMarkers(categoryItemNames, false, MarkerTypeNames.Covered, MarkerTypeNames.NotCovered, MarkerTypeNames.PartiallyCovered); + } + + private void AssertMarkers( + ICoverageFontAndColorsCategoryItemNames categoryItemNames, + bool expectedMef, + string expectedCoveredName, + string expectedNotCoveredName, + string expectedPartiallyCoveredName + ) + { + AssertCategory(new List { + categoryItemNames.Covered.Category, + categoryItemNames.NotCovered.Category, + categoryItemNames.PartiallyCovered.Category + }, expectedMef); + + Assert.That(categoryItemNames.Covered.ItemName, Is.EqualTo(expectedCoveredName)); + Assert.That(categoryItemNames.NotCovered.ItemName, Is.EqualTo(expectedNotCoveredName)); + Assert.That(categoryItemNames.PartiallyCovered.ItemName, Is.EqualTo(expectedPartiallyCoveredName)); + + } + + private void AssertNonMarkers( + ICoverageFontAndColorsCategoryItemNames categoryItemNames + ) + { + AssertCategory(new List { + categoryItemNames.NewLines.Category, + categoryItemNames.Dirty.Category, + categoryItemNames.NotIncluded.Category, + }, true); + + Assert.That(categoryItemNames.NewLines.ItemName, Is.EqualTo("NewLines")); + Assert.That(categoryItemNames.Dirty.ItemName, Is.EqualTo("Dirty")); + Assert.That(categoryItemNames.NotIncluded.ItemName, Is.EqualTo("NotIncluded")); + } + + private void AssertCategory(List categories, bool expectedMef) + { + var editorMEFCategory = new Guid("75A05685-00A8-4DED-BAE5-E7A50BFA929A"); + var editorTextMarkerFontAndColorCategory = new Guid("FF349800-EA43-46C1-8C98-878E78F46501"); + var expectedCategory = expectedMef ? editorMEFCategory : editorTextMarkerFontAndColorCategory; + categories.ForEach(category => Assert.That(category, Is.EqualTo(expectedCategory))); + } + + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/EditorFormatMapTextSpecificListener_Tests.cs b/FineCodeCoverageTests/Editor/Management/EditorFormatMapTextSpecificListener_Tests.cs new file mode 100644 index 00000000..866fba4a --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/EditorFormatMapTextSpecificListener_Tests.cs @@ -0,0 +1,55 @@ +using AutoMoq; +using FineCodeCoverage.Editor.Management; +using Microsoft.VisualStudio.Text.Classification; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class EditorFormatMapTextSpecificListener_Tests + { + [TestCase(new string[] { "This" }, new string[] { "That" }, false)] + [TestCase(new string[] { "Other", "Match" }, new string[] { "NoMatch", "Match" }, true)] + public void Should_Listen_For_Specific_FormatMappingChanged_Items(string[] listenFor, string[] changedItems, bool expectedInvocation) + { + var autoMoqer = new AutoMoqer(); + var mockEditorFormatMap = new Mock(); + autoMoqer.Setup(editorFormatMapService => editorFormatMapService.GetEditorFormatMap("text")) + .Returns(mockEditorFormatMap.Object); + var editorFormatTextSpecificListener = autoMoqer.Create(); + var invoked = false; + editorFormatTextSpecificListener.ListenFor(listenFor.ToList(), () => + { + invoked = true; + }); + mockEditorFormatMap.Raise(editorFormatMap => editorFormatMap.FormatMappingChanged += null, new FormatItemsEventArgs(new ReadOnlyCollection(changedItems))); + + Assert.That(invoked, Is.EqualTo(expectedInvocation)); + } + + [Test] + public void Should_Pause_Listening_When_Executing() + { + var autoMoqer = new AutoMoqer(); + var mockEditorFormatMap = new Mock(); + autoMoqer.Setup(editorFormatMapService => editorFormatMapService.GetEditorFormatMap("text")) + .Returns(mockEditorFormatMap.Object); + var editorFormatTextSpecificListener = autoMoqer.Create(); + var invoked = false; + editorFormatTextSpecificListener.ListenFor(new List { "Match" }, () => + { + invoked = true; + }); + editorFormatTextSpecificListener.PauseListeningWhenExecuting(() => + { + mockEditorFormatMap.Raise(editorFormatMap => editorFormatMap.FormatMappingChanged += null, new FormatItemsEventArgs(new ReadOnlyCollection(new string[] { "Match" }))); + }); + + Assert.That(invoked, Is.False); + } + + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/FontAndColorsInfo_Equatable_Tests.cs b/FineCodeCoverageTests/Editor/Management/FontAndColorsInfo_Equatable_Tests.cs new file mode 100644 index 00000000..9d66d3a7 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/FontAndColorsInfo_Equatable_Tests.cs @@ -0,0 +1,45 @@ +using FineCodeCoverage.Editor.Management; +using Moq; +using NUnit.Framework; +using System; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class FontAndColorsInfo_Equatable_Tests + { + [Test] + public void Should_Be_Equal_When_Bold_Same_And_IItemCoverageColours_Equals() + { + var fontAndColors = new FontAndColorsInfo(GetItemCoverageColours(true), true); + var fontAndColorsEqual = new FontAndColorsInfo(null, true); + + Assert.IsTrue(fontAndColors.Equals(fontAndColorsEqual)); + } + + [Test] + public void Should_Not_Be_Equal_If_Bold_Not_Equal() + { + var fontAndColors = new FontAndColorsInfo(GetItemCoverageColours(true), true); + var fontAndColorsNotEqual = new FontAndColorsInfo(null, false); + + Assert.IsFalse(fontAndColors.Equals(fontAndColorsNotEqual)); + } + + [Test] + public void Should_Not_Be_Equal_If_IItemCoverageColours_Not_Equal() + { + var fontAndColors = new FontAndColorsInfo(GetItemCoverageColours(false), true); + var fontAndColorsNotEqual = new FontAndColorsInfo(null, true); + + Assert.IsFalse(fontAndColors.Equals(fontAndColorsNotEqual)); + } + + private IItemCoverageColours GetItemCoverageColours(bool equals) + { + var mockItemCoverageColoursEquals = new Mock(); + var equatable = mockItemCoverageColoursEquals.As>(); + equatable.Setup(e => e.Equals(It.IsAny())).Returns(equals); + return mockItemCoverageColoursEquals.Object; + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/FontAndColorsInfosProvider_Tests.cs b/FineCodeCoverageTests/Editor/Management/FontAndColorsInfosProvider_Tests.cs new file mode 100644 index 00000000..940f1494 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/FontAndColorsInfosProvider_Tests.cs @@ -0,0 +1,258 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Core.Utilities.VsThreading; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverageTests.TestHelpers; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Windows.Media; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class FontAndColorsInfosProvider_Tests + { + + class CoverageFontAndColorsCategoryItemNames : ICoverageFontAndColorsCategoryItemNames + { + public Guid Guid1 { get; } + public Guid Guid2 { get; } + public FontAndColorsCategoryItemName Covered => new FontAndColorsCategoryItemName("Covered", Guid1); + + public FontAndColorsCategoryItemName Dirty => new FontAndColorsCategoryItemName("Dirty", Guid2); + + public FontAndColorsCategoryItemName NewLines => new FontAndColorsCategoryItemName("NewLines", Guid2); + + public FontAndColorsCategoryItemName NotCovered => new FontAndColorsCategoryItemName("NotCovered", Guid1); + + public FontAndColorsCategoryItemName PartiallyCovered => new FontAndColorsCategoryItemName("PartiallyCovered", Guid1); + + public FontAndColorsCategoryItemName NotIncluded => new FontAndColorsCategoryItemName("NotIncluded", Guid2); + + public CoverageFontAndColorsCategoryItemNames(bool singleCategory) + { + if(singleCategory) + { + Guid1 = Guid2 = Guid.NewGuid(); + } + else + { + Guid1 = Guid.NewGuid(); + Guid2 = Guid.NewGuid(); + } + } + } + + [Test] + public void Should_GetCoverageColours_Using_CoverageFontAndColorsCategoryItemNames_Only_If_Required() + { + var coverageFontAndColorsCategoryItemNames = new CoverageFontAndColorsCategoryItemNames(false); + var autoMoqer = new AutoMoqer(); + var mockFontsAndColorsHelper = autoMoqer.GetMock(); + mockFontsAndColorsHelper.Setup( + fontsAndColorsHelper => fontsAndColorsHelper.GetInfosAsync( + coverageFontAndColorsCategoryItemNames.Guid1, + new List { "Covered", "NotCovered", "PartiallyCovered" }) + ).ReturnsAsync(new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Green, Colors.Red), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.Orange), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black), + }); + mockFontsAndColorsHelper.Setup( + fontsAndColorsHelper => fontsAndColorsHelper.GetInfosAsync( + coverageFontAndColorsCategoryItemNames.Guid2, + new List { "Dirty", "NewLines", "NotIncluded"}) + ).ReturnsAsync(new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.Brown), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Pink, Colors.AliceBlue), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.AntiqueWhite, Colors.IndianRed), + }); + autoMoqer.SetInstance(new TestThreadHelper()); + var fontAndColorsInfosProvider = autoMoqer.Create(); + fontAndColorsInfosProvider.CoverageFontAndColorsCategoryItemNames = coverageFontAndColorsCategoryItemNames; + + var colours = fontAndColorsInfosProvider.GetCoverageColours(); + var coveredColours = colours.GetColour(DynamicCoverageType.Covered); + Assert.That(coveredColours.Foreground, Is.EqualTo(Colors.Green)); + Assert.That(coveredColours.Background, Is.EqualTo(Colors.Red)); + var unCoveredColours = colours.GetColour(DynamicCoverageType.NotCovered); + Assert.That(unCoveredColours.Foreground, Is.EqualTo(Colors.Blue)); + Assert.That(unCoveredColours.Background, Is.EqualTo(Colors.Orange)); + var partiallyCoveredColours = colours.GetColour(DynamicCoverageType.Partial); + Assert.That(partiallyCoveredColours.Foreground, Is.EqualTo(Colors.White)); + Assert.That(partiallyCoveredColours.Background, Is.EqualTo(Colors.Black)); + var dirtyColours = colours.GetColour(DynamicCoverageType.Dirty); + Assert.That(dirtyColours.Foreground, Is.EqualTo(Colors.Blue)); + Assert.That(dirtyColours.Background, Is.EqualTo(Colors.Brown)); + var newLinesColours = colours.GetColour(DynamicCoverageType.NewLine); + Assert.That(newLinesColours.Foreground, Is.EqualTo(Colors.Pink)); + Assert.That(newLinesColours.Background, Is.EqualTo(Colors.AliceBlue)); + var notIncludedColours = colours.GetColour(DynamicCoverageType.NotIncluded); + Assert.That(notIncludedColours.Foreground, Is.EqualTo(Colors.AntiqueWhite)); + Assert.That(notIncludedColours.Background, Is.EqualTo(Colors.IndianRed)); + + var previousColors = fontAndColorsInfosProvider.GetCoverageColours(); + + Assert.That(previousColors, Is.SameAs(colours)); + Assert.That(mockFontsAndColorsHelper.Invocations.Count, Is.EqualTo(2)); + } + + [Test] + public void GetChangedFontAndColorsInfos_Should_Return_Just_Changes() + { + var coverageFontAndColorsCategoryItemNames = new CoverageFontAndColorsCategoryItemNames(true); + var autoMoqer = new AutoMoqer(); + var mockFontsAndColorsHelper = autoMoqer.GetMock(); + var first = new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Green, Colors.Red), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Blue, Colors.Orange), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.AliceBlue), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Goldenrod, Colors.GreenYellow), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.AntiqueWhite, Colors.IndianRed), + }; + var second = new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Green, Colors.Red), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.Orange), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Pink, Colors.Gray,false), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.AliceBlue), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Goldenrod, Colors.GreenYellow), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.AntiqueWhite, Colors.IndianRed), + }; + mockFontsAndColorsHelper.SetupSequence( + fontsAndColorsHelper => fontsAndColorsHelper.GetInfosAsync( + coverageFontAndColorsCategoryItemNames.Guid1, + new List { "Covered", "NotCovered", "PartiallyCovered", "Dirty", "NewLines","NotIncluded" }) + ).ReturnsAsync(first).ReturnsAsync(second); + + autoMoqer.SetInstance(new TestThreadHelper()); + var fontAndColorsInfosProvider = autoMoqer.Create(); + fontAndColorsInfosProvider.CoverageFontAndColorsCategoryItemNames = coverageFontAndColorsCategoryItemNames; + var changed = fontAndColorsInfosProvider.GetChangedFontAndColorsInfos(); + Assert.That(changed.Count, Is.EqualTo(6)); + Assert.That(changed[DynamicCoverageType.Covered].IsBold, Is.True); + Assert.That(changed[DynamicCoverageType.Covered].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Green)); + Assert.That(changed[DynamicCoverageType.Covered].ItemCoverageColours.Background, Is.EqualTo(Colors.Red)); + Assert.That(changed[DynamicCoverageType.NotCovered].IsBold, Is.False); + Assert.That(changed[DynamicCoverageType.NotCovered].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Blue)); + Assert.That(changed[DynamicCoverageType.NotCovered].ItemCoverageColours.Background, Is.EqualTo(Colors.Orange)); + Assert.That(changed[DynamicCoverageType.Partial].IsBold, Is.True); + Assert.That(changed[DynamicCoverageType.Partial].ItemCoverageColours.Foreground, Is.EqualTo(Colors.White)); + Assert.That(changed[DynamicCoverageType.Partial].ItemCoverageColours.Background, Is.EqualTo(Colors.Black)); + Assert.That(changed[DynamicCoverageType.Dirty].IsBold, Is.True); + Assert.That(changed[DynamicCoverageType.Dirty].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Blue)); + Assert.That(changed[DynamicCoverageType.Dirty].ItemCoverageColours.Background, Is.EqualTo(Colors.AliceBlue)); + Assert.That(changed[DynamicCoverageType.NewLine].IsBold, Is.True); + Assert.That(changed[DynamicCoverageType.NewLine].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Goldenrod)); + Assert.That(changed[DynamicCoverageType.NewLine].ItemCoverageColours.Background, Is.EqualTo(Colors.GreenYellow)); + Assert.That(changed[DynamicCoverageType.NotIncluded].IsBold, Is.False); + Assert.That(changed[DynamicCoverageType.NotIncluded].ItemCoverageColours.Foreground, Is.EqualTo(Colors.AntiqueWhite)); + Assert.That(changed[DynamicCoverageType.NotIncluded].ItemCoverageColours.Background, Is.EqualTo(Colors.IndianRed)); + + changed = fontAndColorsInfosProvider.GetChangedFontAndColorsInfos(); + Assert.That(changed.Count, Is.EqualTo(1)); + var partialChange = changed[DynamicCoverageType.Partial]; + Assert.That(partialChange.IsBold, Is.False); + Assert.That(partialChange.ItemCoverageColours.Foreground, Is.EqualTo(Colors.Pink)); + Assert.That(partialChange.ItemCoverageColours.Background, Is.EqualTo(Colors.Gray)); + } + + [TestCase(true)] + [TestCase(false)] + public void GetChangedFontAndColorsInfos_Should_Raise_Event_When_Just_Changes(bool equal) + { + var coverageFontAndColorsCategoryItemNames = new CoverageFontAndColorsCategoryItemNames(true); + var autoMoqer = new AutoMoqer(); + var mockFontsAndColorsHelper = autoMoqer.GetMock(); + var first = new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Green, Colors.Red), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Blue, Colors.Orange), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.AntiqueWhite, Colors.IndianRed), + }; + var second = new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Green, Colors.Red,equal), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.Orange,equal), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Pink, Colors.Gray,equal), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black,equal), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black,equal), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.AntiqueWhite, Colors.IndianRed, equal), + }; + + mockFontsAndColorsHelper.SetupSequence( + fontsAndColorsHelper => fontsAndColorsHelper.GetInfosAsync( + coverageFontAndColorsCategoryItemNames.Guid1, + new List { "Covered", "NotCovered", "PartiallyCovered", "Dirty", "NewLines", "NotIncluded" }) + ).ReturnsAsync(first).ReturnsAsync(second); + + autoMoqer.SetInstance(new TestThreadHelper()); + var fontAndColorsInfosProvider = autoMoqer.Create(); + fontAndColorsInfosProvider.CoverageFontAndColorsCategoryItemNames = coverageFontAndColorsCategoryItemNames; + + fontAndColorsInfosProvider.GetChangedFontAndColorsInfos(); + fontAndColorsInfosProvider.GetChangedFontAndColorsInfos(); + + autoMoqer.Verify(eventAggregator => eventAggregator.SendMessage(It.IsAny(), null), Times.Exactly(equal ? 1 : 2)); + } + + [Test] + public void GetFontAndColorsInfos_Should_Return_All() + { + var coverageFontAndColorsCategoryItemNames = new CoverageFontAndColorsCategoryItemNames(true); + var autoMoqer = new AutoMoqer(); + var mockFontsAndColorsHelper = autoMoqer.GetMock(); + var first = new List + { + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Green, Colors.Red), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Blue, Colors.Orange), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.White, Colors.Black), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Blue, Colors.AliceBlue), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(true, Colors.Goldenrod, Colors.GreenYellow), + FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.AntiqueWhite, Colors.IndianRed), + }; + + mockFontsAndColorsHelper.Setup( + fontsAndColorsHelper => fontsAndColorsHelper.GetInfosAsync( + coverageFontAndColorsCategoryItemNames.Guid1, + new List { "Covered", "NotCovered", "PartiallyCovered", "Dirty", "NewLines","NotIncluded" }) + ).ReturnsAsync(first); + + autoMoqer.SetInstance(new TestThreadHelper()); + var fontAndColorsInfosProvider = autoMoqer.Create(); + fontAndColorsInfosProvider.CoverageFontAndColorsCategoryItemNames = coverageFontAndColorsCategoryItemNames; + fontAndColorsInfosProvider.GetChangedFontAndColorsInfos(); + + var fontAndColorsInfos = fontAndColorsInfosProvider.GetFontAndColorsInfos(); + Assert.That(fontAndColorsInfos.Count, Is.EqualTo(6)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Covered].IsBold, Is.True); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Covered].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Green)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Covered].ItemCoverageColours.Background, Is.EqualTo(Colors.Red)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NotCovered].IsBold, Is.False); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NotCovered].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Blue)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NotCovered].ItemCoverageColours.Background, Is.EqualTo(Colors.Orange)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Partial].IsBold, Is.True); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Partial].ItemCoverageColours.Foreground, Is.EqualTo(Colors.White)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Partial].ItemCoverageColours.Background, Is.EqualTo(Colors.Black)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Dirty].IsBold, Is.True); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Dirty].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Blue)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.Dirty].ItemCoverageColours.Background, Is.EqualTo(Colors.AliceBlue)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NewLine].IsBold, Is.True); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NewLine].ItemCoverageColours.Foreground, Is.EqualTo(Colors.Goldenrod)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NewLine].ItemCoverageColours.Background, Is.EqualTo(Colors.GreenYellow)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NotIncluded].IsBold, Is.False); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NotIncluded].ItemCoverageColours.Foreground, Is.EqualTo(Colors.AntiqueWhite)); + Assert.That(fontAndColorsInfos[DynamicCoverageType.NotIncluded].ItemCoverageColours.Background, Is.EqualTo(Colors.IndianRed)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/FontsAndColorsHelper_Tests.cs b/FineCodeCoverageTests/Editor/Management/FontsAndColorsHelper_Tests.cs new file mode 100644 index 00000000..84e83e4a --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/FontsAndColorsHelper_Tests.cs @@ -0,0 +1,83 @@ +using FineCodeCoverage.Editor.Management; +using FineCodeCoverageTests.TestHelpers; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.TextManager.Interop; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class FontsAndColorsHelper_Tests + { + [TestCase(true)] + [TestCase(false)] + public async Task Should_Use_IVsFontAndColorStorage_Async(bool isBold) + { + var serviceProvider = new Mock(); + var mockVsFontAndColorStorage = new Mock(); +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + mockVsFontAndColorStorage.Setup(vsFontsAndColorStorage => vsFontsAndColorStorage.GetItem("name", It.IsAny())) + .Callback((_, colorableItemsInfos) => + { + var colorableItemInfo = new ColorableItemInfo + { + crBackground = (uint)System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.AliceBlue), + crForeground = (uint)System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Aqua), + dwFontFlags = isBold ? (uint)FONTFLAGS.FF_BOLD : (uint)FONTFLAGS.FF_DEFAULT + }; + colorableItemsInfos[0] = colorableItemInfo; + }); + serviceProvider.Setup(x => x.GetService(typeof(IVsFontAndColorStorage))).Returns(mockVsFontAndColorStorage.Object); + var fontsAndColorsHelper = new FontsAndColorsHelper(serviceProvider.Object, new TestThreadHelper()); + var categoryGuid = Guid.NewGuid(); + var infos = await fontsAndColorsHelper.GetInfosAsync(categoryGuid, new List { "name" }); + var info = infos[0]; + Assert.That(isBold, Is.EqualTo(isBold)); + Assert.That(info.ItemCoverageColours.Background, Is.EqualTo(System.Windows.Media.Colors.AliceBlue)); + Assert.That(info.ItemCoverageColours.Foreground, Is.EqualTo(System.Windows.Media.Colors.Aqua)); + var flags = (uint)(__FCSTORAGEFLAGS.FCSF_READONLY | __FCSTORAGEFLAGS.FCSF_LOADDEFAULTS | __FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS | __FCSTORAGEFLAGS.FCSF_PROPAGATECHANGES); + mockVsFontAndColorStorage.Verify(x => x.OpenCategory(ref categoryGuid, flags), Times.Once); + mockVsFontAndColorStorage.Verify(x => x.CloseCategory(), Times.Once); +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread + } + + [Test] + public async Task Should_Return_Empty_When_OpenCategory_Fails_Async() + { + var serviceProvider = new Mock(); + var mockVsFontAndColorStorage = new Mock(); +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + mockVsFontAndColorStorage.Setup(vsFontAndColorStorage => vsFontAndColorStorage.OpenCategory(ref It.Ref.IsAny, It.IsAny())) + .Returns(VSConstants.E_FAIL); +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread + serviceProvider.Setup(x => x.GetService(typeof(IVsFontAndColorStorage))).Returns(mockVsFontAndColorStorage.Object); + + var fontsAndColorsHelper = new FontsAndColorsHelper(serviceProvider.Object, new TestThreadHelper()); + var infos = await fontsAndColorsHelper.GetInfosAsync(Guid.Empty, new List { "name" }); + + Assert.That(infos, Is.Empty); + + } + + [Test] + public async Task Should_Not_Throw_Exception_When_GetItem_Fails_Async() + { + var serviceProvider = new Mock(); + var mockVsFontAndColorStorage = new Mock(); +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + mockVsFontAndColorStorage.Setup(vsFontAndColorStorage => vsFontAndColorStorage.GetItem("name", It.IsAny())) + .Returns(VSConstants.E_FAIL); +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread + serviceProvider.Setup(x => x.GetService(typeof(IVsFontAndColorStorage))).Returns(mockVsFontAndColorStorage.Object); + + var fontsAndColorsHelper = new FontsAndColorsHelper(serviceProvider.Object, new TestThreadHelper()); + var infos = await fontsAndColorsHelper.GetInfosAsync(Guid.Empty, new List { "name" }); + + Assert.That(infos, Is.Empty); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/Helpers/FontAndColorsInfoFactory.cs b/FineCodeCoverageTests/Editor/Management/Helpers/FontAndColorsInfoFactory.cs new file mode 100644 index 00000000..399602ef --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/Helpers/FontAndColorsInfoFactory.cs @@ -0,0 +1,23 @@ +using FineCodeCoverage.Editor.Management; +using Moq; +using System; +using System.Windows.Media; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal static class FontAndColorsInfoFactory + { + public static IFontAndColorsInfo CreateFontAndColorsInfo(bool bold, Color foreground = default, Color background = default, bool equals = true) + { + var mockItemCoverageColours = new Mock(); + mockItemCoverageColours.SetupGet(itemCoverageColours => itemCoverageColours.Foreground).Returns(foreground); + mockItemCoverageColours.SetupGet(itemCoverageColours => itemCoverageColours.Background).Returns(background); + var mockFontAndColorsInfo = new Mock(); + var mockEquatable = mockFontAndColorsInfo.As>(); + mockEquatable.Setup(equatable => equatable.Equals(It.IsAny())).Returns(equals); + mockFontAndColorsInfo.SetupGet(fontAndColorsInfo => fontAndColorsInfo.IsBold).Returns(bold); + mockFontAndColorsInfo.SetupGet(fontAndColorsInfo => fontAndColorsInfo.ItemCoverageColours).Returns(mockItemCoverageColours.Object); + return mockFontAndColorsInfo.Object; + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/ItemCoverageColours_Equatable_Tests.cs b/FineCodeCoverageTests/Editor/Management/ItemCoverageColours_Equatable_Tests.cs new file mode 100644 index 00000000..81c442eb --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/ItemCoverageColours_Equatable_Tests.cs @@ -0,0 +1,36 @@ +using FineCodeCoverage.Editor.Management; +using NUnit.Framework; +using System.Windows.Media; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class ItemCoverageColours_Equatable_Tests + { + [Test] + public void Should_Be_Equal_When_Foreground_And_Background_Equals() + { + var itemCoverageColours = new ItemCoverageColours(Colors.Green, Colors.Red); + var itemCoverageColoursEqual = new ItemCoverageColours(Colors.Green, Colors.Red); + + Assert.IsTrue(itemCoverageColours.Equals(itemCoverageColoursEqual)); + } + + [Test] + public void Should_Not_Be_Equal_When_Foreground_Not_Equal() + { + var itemCoverageColours = new ItemCoverageColours(Colors.Green, Colors.Red); + var itemCoverageColoursNotEqual = new ItemCoverageColours(Colors.Blue, Colors.Red); + + Assert.IsFalse(itemCoverageColours.Equals(itemCoverageColoursNotEqual)); + } + + [Test] + public void Should_Not_Be_Equal_When_Background_Not_Equal() + { + var itemCoverageColours = new ItemCoverageColours(Colors.Green, Colors.Red); + var itemCoverageColoursNotEqual = new ItemCoverageColours(Colors.Green, Colors.Blue); + + Assert.IsFalse(itemCoverageColours.Equals(itemCoverageColoursNotEqual)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/TextFormattingRunPropertiesFactory_Tests.cs b/FineCodeCoverageTests/Editor/Management/TextFormattingRunPropertiesFactory_Tests.cs new file mode 100644 index 00000000..677f252a --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/TextFormattingRunPropertiesFactory_Tests.cs @@ -0,0 +1,39 @@ +using AutoMoq; +using FineCodeCoverage.Editor.Management; +using NUnit.Framework; +using System.Windows.Media; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class TextFormattingRunPropertiesFactory_Tests + { + [TestCase(true)] + [TestCase(false)] + public void Should_Be_Bold_If_Bold(bool bold) + { + var autoMoqer = new AutoMoqer(); + var textFormattingRunPropertiesFactory = autoMoqer.Create(); + Assert.That(textFormattingRunPropertiesFactory.Create(FontAndColorsInfoFactory.CreateFontAndColorsInfo(bold)).Bold, Is.EqualTo(bold)); + } + + [Test] + public void Should_Set_The_Foreground() + { + var autoMoqer = new AutoMoqer(); + var textFormattingRunPropertiesFactory = autoMoqer.Create(); + var foegroundBrush = textFormattingRunPropertiesFactory.Create(FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, Colors.Green)).ForegroundBrush as SolidColorBrush; + Assert.That(foegroundBrush.Color, Is.EqualTo(Colors.Green)); + } + + [Test] + public void Should_Set_The_Background() + { + var autoMoqer = new AutoMoqer(); + var textFormattingRunPropertiesFactory = autoMoqer.Create(); + var foegroundBrush = textFormattingRunPropertiesFactory.Create(FontAndColorsInfoFactory.CreateFontAndColorsInfo(false, default, Colors.Green)).BackgroundBrush as SolidColorBrush; + Assert.That(foegroundBrush.Color, Is.EqualTo(Colors.Green)); + } + + + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Management/VsHasCoverageMarkersLogic_Tests.cs b/FineCodeCoverageTests/Editor/Management/VsHasCoverageMarkersLogic_Tests.cs new file mode 100644 index 00000000..2d8eb6cc --- /dev/null +++ b/FineCodeCoverageTests/Editor/Management/VsHasCoverageMarkersLogic_Tests.cs @@ -0,0 +1,25 @@ +using AutoMoq; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Options; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.Management +{ + internal class VsHasCoverageMarkersLogic_Tests + { + [TestCase(true)] + [TestCase(false)] + public void Should_HasCoverageMarkers_When_External_Markers_Are_Not_In_The_Store(bool inTheStore) + { + var autoMoqer = new AutoMoqer(); + var mockReadOnlyConfigSettingsStoreProvider = autoMoqer.GetMock(); + var vsMarkerCollectionPath = @"Text Editor\External Markers\{b4ee9ead-e105-11d7-8a44-00065bbd20a4}"; + mockReadOnlyConfigSettingsStoreProvider.Setup(readOnlyConfigSettingsStoreProvider => + readOnlyConfigSettingsStoreProvider.Provide().CollectionExists(vsMarkerCollectionPath)).Returns(inTheStore); + + var vsHasCoverageMarkersLogic = autoMoqer.Create(); + + Assert.That(vsHasCoverageMarkersLogic.HasCoverageMarkers(), Is.EqualTo(inTheStore)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs b/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs new file mode 100644 index 00000000..fe6feedf --- /dev/null +++ b/FineCodeCoverageTests/Editor/Roslyn/CSharpContainingCodeVisitor_Tests.cs @@ -0,0 +1,337 @@ +using FineCodeCoverage.Editor.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.Roslyn +{ + internal class CSharpContainingCodeVisitor_Tests : ContainingCodeVisitor_Tests_Base + { + protected override SyntaxNode ParseCompilation(string compilationText) => SyntaxFactory.ParseCompilationUnit(compilationText); + + protected override ILanguageContainingCodeVisitor GetVisitor() => new CSharpContainingCodeVisitor(); + + [Test] + public void Should_Visit_Methods() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public void MyMethod() + { + var x = 1; + } + } +} +"; + AssertShouldVisit(text); + } + + [Ignore("failing in github actions only")] + public void Should_Work_With_File_Scope_Namespaces() + { + var text = @" +namespace MyNamespace.X.Y; + +public class MyClass +{ + public void MyMethod() + { + var x = 1; + } +} + +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Method_With_Expression_Bodies() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public int MyMethod() => 5 + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Constructors() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public MyClass(){ + + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Finalizers() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + ~MyClass(){ + + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Operators() + { + var text = @" + public class Conv1 + { + public static Conv1 operator +(Conv1 a, Conv1 b) + { + return new Conv1(); + } + } +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Conversions() + { + var text = @" + public class Conv1 + { + public static implicit operator Conv2(Conv1 d) + { + return new Conv2(); + } + } + + public class Conv2 + { + + } +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Property_Getters() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public int Property { + get { + return 5; + } + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Property_Setters() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + private int v; + public int Property { + set { + v = value; + } + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Auto_Implemented_Properties() + { + var text = @" +public class MyClass +{ + public int MyProperty { get; set; } +} +"; + var (textSpans, rootNode) = Visit(text); + Assert.That(textSpans.Count, Is.EqualTo(2)); + textSpans.ForEach(textSpan => AssertTextSpan(rootNode, textSpan)); + } + + [Test] + public void Should_Visit_Expression_Bodied_Read_Only_Property() + { + var text = @" +namespace MyNamespace +{ + public abstract class Abstract + { + public abstract int Property { get; } + } + + public class Concrete : Abstract + { + public override int Property => 1; + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Not_Visit_Abstract_Properties() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public abstract int AbstractProperty {get;set;} + } +} +"; + AssertShouldNotVisit(text); + } + + [Test] + public void Should_Visit_Indexers() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public string this[int i] + { + set { + int.Parse(value); + } + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Event_Accessors() + { + var text = @" +namespace MyNamespace +{ + public class MyClass + { + public event EventHandler AnEvent + { + add + { + + } + + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Structs() + { + var text = @" +namespace MyNamespace +{ + public struct MyStruct + { + public void MyMethod() + { + var x = 1; + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Records() + { + var text = @" +namespace MyNamespace +{ + public record Person(string FirstName, string LastName, string[] PhoneNumbers) + { + public virtual bool PrintMembers(StringBuilder stringBuilder) + { + stringBuilder.Append($""FirstName = {FirstName}, LastName = {LastName}, ""); + stringBuilder.Append($""PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}""); + return true; + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Interface_Default_Methods() + { + var text = @" +namespace MyNamespace +{ + public interface IMyInterface + { + void MyMethod() + { + var x = 1; + } + } +} +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Not_Visit_Interface_Properties_Without_Body() + { + var text = @" +namespace MyNamespace +{ + public interface IMyInterface + { + int Property {get;} + } +} +"; + AssertShouldNotVisit(text); + } + } + +} diff --git a/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs b/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs new file mode 100644 index 00000000..47fc8e70 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Roslyn/ContainingCodeVisitor_Tests_Base.cs @@ -0,0 +1,45 @@ +using FineCodeCoverage.Editor.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using NUnit.Framework; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.Roslyn +{ + internal abstract class ContainingCodeVisitor_Tests_Base + { + protected abstract ILanguageContainingCodeVisitor GetVisitor(); + protected abstract SyntaxNode ParseCompilation(string compilationText); + + protected (List, SyntaxNode) Visit(string compilationText) + { + var rootNode = ParseCompilation(compilationText); + var textSpans = GetVisitor().GetSpans(rootNode); + return (textSpans, rootNode); + } + + protected void AssertShouldNotVisit(string compilationText) + { + var (textSpans, _) = Visit(compilationText); + + Assert.That(textSpans, Is.Empty); + } + + protected void AssertTextSpan(SyntaxNode rootNode,TextSpan textSpan) where T : SyntaxNode + { + var textSpanNode = rootNode.FindNode(textSpan); + Assert.That(textSpanNode, Is.TypeOf()); + Assert.That(textSpanNode.Span, Is.EqualTo(textSpan)); + } + + protected void AssertShouldVisit(string compilationText) where T : SyntaxNode + { + var (textSpans, rootNode) = Visit(compilationText); + Assert.That(textSpans, Has.Count.EqualTo(1)); + var textSpan = textSpans[0]; + AssertTextSpan(rootNode, textSpan); + } + + } + +} diff --git a/FineCodeCoverageTests/Editor/Roslyn/LanguageContainingCodeVisitorFactory_Tests.cs b/FineCodeCoverageTests/Editor/Roslyn/LanguageContainingCodeVisitorFactory_Tests.cs new file mode 100644 index 00000000..2f6eda8c --- /dev/null +++ b/FineCodeCoverageTests/Editor/Roslyn/LanguageContainingCodeVisitorFactory_Tests.cs @@ -0,0 +1,32 @@ +using AutoMoq; +using FineCodeCoverage.Editor.Roslyn; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.Roslyn +{ + internal class LanguageContainingCodeVisitorFactory_Tests + { + [Test] + public void Should_Return_CSharp_ContainingCodeVisitor_When_IsCSharp_Is_True() + { + var autoMoqer = new AutoMoqer(); + var languageContainingCodeVisitorFactory = autoMoqer.Create(); + + var languageContainingCodeVisitor = languageContainingCodeVisitorFactory.Create(true); + + Assert.That(languageContainingCodeVisitor, Is.InstanceOf()); + } + + [Test] + public void Should_Return_VisualBasic_ContainingCodeVisitor_When_IsCSharp_Is_False() + { + var autoMoqer = new AutoMoqer(); + var languageContainingCodeVisitorFactory = autoMoqer.Create(); + + var languageContainingCodeVisitor = languageContainingCodeVisitorFactory.Create(false); + + Assert.That(languageContainingCodeVisitor, Is.InstanceOf()); + } + } + +} diff --git a/FineCodeCoverageTests/Editor/Roslyn/RoslynService_Tests.cs b/FineCodeCoverageTests/Editor/Roslyn/RoslynService_Tests.cs new file mode 100644 index 00000000..66c4027d --- /dev/null +++ b/FineCodeCoverageTests/Editor/Roslyn/RoslynService_Tests.cs @@ -0,0 +1,51 @@ +using AutoMoq; +using FineCodeCoverage.Editor.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FineCodeCoverageTests.Editor.Roslyn +{ + internal class RoslynService_Tests + { + [Test] + public async Task Should_Return_Empty_TextSpans_When_RootNodeAndLanguage_Is_Null_Async() + { + var autoMoqer = new AutoMoqer(); + var roslynService = autoMoqer.Create(); + + var containingCodeSpans = await roslynService.GetContainingCodeSpansAsync(new Mock().Object); + + Assert.That(containingCodeSpans, Is.Empty); + } + + [TestCase(LanguageNames.CSharp,true)] + [TestCase(LanguageNames.VisualBasic, false)] + public async Task Should_Return_TextSpans_For_The_Language_Async(string language,bool languageIsCSharp) + { + var textSnapshot = new Mock().Object; + var rootNode = SyntaxFactory.ParseCompilationUnit(""); + + var autoMoqer = new AutoMoqer(); + var roslynService = autoMoqer.Create(); + var mockLanguageContainingCodeVisitorFactory = autoMoqer.GetMock(); + var mockLanguageContainingCodeVisitor = new Mock(); + var textSpans = new List { new TextSpan(0, 1) }; + mockLanguageContainingCodeVisitor.Setup(languageContainingCodeVisitor => languageContainingCodeVisitor.GetSpans(rootNode)).Returns(textSpans); + mockLanguageContainingCodeVisitorFactory.Setup(x => x.Create(languageIsCSharp)).Returns(mockLanguageContainingCodeVisitor.Object); + + var mockTextSnapshotToSyntaxService = autoMoqer.GetMock(); + + mockTextSnapshotToSyntaxService.Setup(x => x.GetRootAndLanguageAsync(textSnapshot)).ReturnsAsync(new RootNodeAndLanguage(rootNode, language)); + var containingCodeSpans = await roslynService.GetContainingCodeSpansAsync(textSnapshot); + + Assert.That(containingCodeSpans, Is.SameAs(textSpans)); + } + } + +} diff --git a/FineCodeCoverageTests/Editor/Roslyn/VBContainingCodeVisitor_Tests.cs b/FineCodeCoverageTests/Editor/Roslyn/VBContainingCodeVisitor_Tests.cs new file mode 100644 index 00000000..601c8e48 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Roslyn/VBContainingCodeVisitor_Tests.cs @@ -0,0 +1,197 @@ +using FineCodeCoverage.Editor.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.Roslyn +{ + internal class VBContainingCodeVisitor_Tests : ContainingCodeVisitor_Tests_Base + { + protected override ILanguageContainingCodeVisitor GetVisitor() => new VBContainingCodeVisitor(); + + protected override SyntaxNode ParseCompilation(string compilationText) => SyntaxFactory.ParseCompilationUnit(compilationText); + + + [Test] + public void Should_Visit_Module_Methods() + { + var text = @" +Namespace NS + Public Module Module1 + Sub Sub1() + Debug.WriteLine(""Sub1"") + End Sub + End Module +End Namespace +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Class_Methods() + { + var text = @" +Public Class Class1 + Function Method() As Int32 + Return 1 + End Function +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Not_Visit_Partial_Methods() + { + var text = @" +Partial Public Class PartialClass + Partial Private Sub Method() + End Sub +End Class +"; + AssertShouldNotVisit(text); + } + + [Test] + public void Should_Visit_Constructors() + { + var text = @" +Public Class Class1 + Public Sub New() + Console.WriteLine(""Constructor"") + End Sub +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Operators() + { + var text = @" +Public Class Class1 + Public Shared Operator +(class1 As Class1) As Class1 + Return class1 + End Operator +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Property_Getters() + { + var text = @" +Public Class Class1 + Private getSetField As String + Property GetSet As String + Get + Return getSetField + End Get + End Property +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Property_Setters() + { + var text = @" +Public Class Class1 + Private getSetField As String + Property GetSet As String + Set(value As String) + getSetField = value + End Set + End Property +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Auto_Implemented_Properties() + { + var text = @" +Public Class Class1 + Public Property AutoImplemented() As Boolean +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Not_Visit_Abstract_Properties() + { + var text = @" +Public MustInherit Class Abstract + Public MustOverride Property P() As Integer +End Class +"; + AssertShouldNotVisit(text); + } + + [Test] + public void Should_Visit_Event_AddHandler() + { + var text = @" +Public Class Class1 + Public Custom Event TestEvent As EventHandler + AddHandler(value As EventHandler) + StaticMethod() + End AddHandler + End Event +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Event_RemoveHandler() + { + var text = @" +Public Class Class1 + Public Custom Event TestEvent As EventHandler + RemoveHandler(value As EventHandler) + StaticMethod() + End RemoveHandler + End Event +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Event_RaiseEvent() + { + var text = @" +Public Class Class1 + Public Custom Event TestEvent As EventHandler + RaiseEvent(sender As Object, e As EventArgs) + StaticMethod() + End RaiseEvent + End Event +End Class +"; + AssertShouldVisit(text); + } + + [Test] + public void Should_Visit_Structs() + { + var text = @" +Public Structure Struct1 + Public Sub Sub1() + Console.WriteLine(""Sub1"") + End Sub +End Structure + +End Class +"; + AssertShouldVisit(text); + } + } + +} diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs new file mode 100644 index 00000000..3d10a159 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTaggerProvider_Tests.cs @@ -0,0 +1,149 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.Editor.Tagging.Base.Types; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.Tagging.Base +{ + internal class CoverageTaggerProvider_Tests + { + [TestCase(false)] + [TestCase(true)] + public void Should_Send_CoverageTypeFilterChangedMessage_With_The_New_Filter_When_AppOptions_Change_For_the_Filter(bool filterChanged) + { + var firstOptions = new AppOptions(); + var changedOptions = new AppOptions(); + var isFirst = true; + DummyCoverageTypeFilter firstFilter = null; + DummyCoverageTypeFilter secondFilter = null; + DummyCoverageTypeFilter.Initialized += (sender, args) => + { + var filter = args.DummyCoverageTypeFilter; + if (isFirst) + { + firstFilter = filter; + Assert.That(filter.AppOptions, Is.SameAs(firstOptions)); + } + else + { + secondFilter = filter; + secondFilter.ChangedFunc = other => + { + Assert.That(firstFilter, Is.SameAs(other)); + Assert.That(secondFilter.AppOptions, Is.SameAs(changedOptions)); + return filterChanged; + }; + } + isFirst = false; + }; + + var autoMocker = new AutoMoqer(); + var mockAppOptionsProvider = autoMocker.GetMock(); + mockAppOptionsProvider.Setup(appOptionsProvider => appOptionsProvider.Get()).Returns(firstOptions); + + var coverageLineTaggerProviderBase = autoMocker.Create>(); + + mockAppOptionsProvider.Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, changedOptions); + + autoMocker.Verify(eventAggregator => eventAggregator.SendMessage( + It.Is(coverageTypeFilterChangedMessage => coverageTypeFilterChangedMessage.Filter == secondFilter), + null + ), filterChanged ? Times.Once() : Times.Never() + ); + } + + [Test] + public void Should_Use_The_Last_Filter_For_Comparisons() + { + var filters = new List(); + DummyCoverageTypeFilter.Initialized += (sender, args) => + { + var filter = args.DummyCoverageTypeFilter; + filter.ChangedFunc = other => + { + var index = filters.IndexOf(filter); + var lastFilter = filters.IndexOf(other); + Assert.That(index - lastFilter, Is.EqualTo(1)); + return true; + }; + filters.Add(filter); + }; + + var autoMocker = new AutoMoqer(); + var mockAppOptionsProvider = autoMocker.GetMock(); + var coverageTaggerProvider = autoMocker.Create>(); + + mockAppOptionsProvider.Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, new AppOptions()); + mockAppOptionsProvider.Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, new AppOptions()); + } + + [Test] + public void Should_Not_Create_A_Coverage_Tagger_When_The_TextBuffer_Associated_Document_Has_No_FilePath() + { + var autoMocker = new AutoMoqer(); + var mockTextInfo = new Mock(); + mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns((string)null); + autoMocker.Setup(textInfoFactory => textInfoFactory.Create(It.IsAny(), It.IsAny())) + .Returns(mockTextInfo.Object); + var coverageTaggerProvider = autoMocker.Create>(); + + var tagger = coverageTaggerProvider.CreateTagger(new Mock().Object, new Mock().Object); + + Assert.That(tagger, Is.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; + DummyCoverageTypeFilter lastFilter = null; + DummyCoverageTypeFilter.Initialized += (sender, args) => + { + lastFilter = args.DummyCoverageTypeFilter; + lastFilter.ChangedFunc = other => + { + return true; + }; + + }; + var autoMocker = new AutoMoqer(); + var mockTextInfo = new Mock(); + mockTextInfo.SetupGet(textInfo => textInfo.FilePath).Returns("file"); + autoMocker.Setup(textInfoFactory => textInfoFactory.Create(textView, textBuffer)).Returns(mockTextInfo.Object); + var bufferLineCoverage = new Mock().Object; + autoMocker.GetMock() + .Setup(dynamicCoverageManager => dynamicCoverageManager.Manage(mockTextInfo.Object)).Returns(bufferLineCoverage); + var coverageTaggerProvider = autoMocker.Create>(); + + var mockAppOptionsProvider = autoMocker.GetMock(); + mockAppOptionsProvider.Raise(appOptionsProvider => appOptionsProvider.OptionsChanged += null, new AppOptions()); + + var tagger = coverageTaggerProvider.CreateTagger(textView, textBuffer); + + Assert.That(tagger, Is.InstanceOf>()); + + var coverageTaggerType = typeof(CoverageTagger); + + var fileLineCoverageArg = coverageTaggerType.GetField("bufferLineCoverage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(tagger) as IBufferLineCoverage; + var coverageTypeFilterArg = coverageTaggerType.GetField("coverageTypeFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(tagger) as ICoverageTypeFilter; + var textInfoArg = coverageTaggerType.GetField("textInfo", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(tagger) as ITextInfo; + + Assert.Multiple(() => + { + Assert.That(textInfoArg, Is.SameAs(mockTextInfo.Object)); + Assert.That(fileLineCoverageArg, Is.SameAs(bufferLineCoverage)); + Assert.That(coverageTypeFilterArg, Is.SameAs(lastFilter)); + }); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTagger_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTagger_Tests.cs new file mode 100644 index 00000000..1c515e63 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTagger_Tests.cs @@ -0,0 +1,289 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverageTests.Editor.Tagging.Base.Types; +using FineCodeCoverageTests.Editor.Tagging.Types; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Tagging; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using LineSpan = FineCodeCoverageTests.Editor.Tagging.Types.LineSpan; + +namespace FineCodeCoverageTests.Editor.Tagging.Base +{ + internal class CoverageTagger_Tests + { + [Test] + public void Should_Listen_For_Changes() + { + var autoMoqer = new AutoMoqer(); + var coverageTagger = autoMoqer.Create>(); + + autoMoqer.Verify(eventAggregator => eventAggregator.AddListener(coverageTagger, null)); + } + + [Test] + public void Should_Unlisten_For_Changes_On_Dispose() + { + var autoMoqer = new AutoMoqer(); + var coverageTagger = autoMoqer.Create>(); + + coverageTagger.Dispose(); + autoMoqer.Verify(eventAggregator => eventAggregator.RemoveListener(coverageTagger)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Raise_Tags_Changed_For_CurrentSnapshot_Range_When_CoverageChangedMessage_Applies_No_Line_Numbers(bool appliesTo) + { + var autoMoqer = new AutoMoqer(); + var mockTextInfo = autoMoqer.GetMock(); + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(currentSnapshot => currentSnapshot.Length).Returns(10); + mockTextInfo.SetupGet(textBufferAndFile => textBufferAndFile.FilePath).Returns("filepath"); + mockTextInfo.SetupGet(textBufferAndFile => textBufferAndFile.TextBuffer.CurrentSnapshot).Returns(mockTextSnapshot.Object); + + var coverageTagger = autoMoqer.Create>(); + SnapshotSpan? snapshotSpan = null; + coverageTagger.TagsChanged += (sender, args) => + { + snapshotSpan = args.Span; + }; + coverageTagger.Handle(new CoverageChangedMessage(null, appliesTo ? "filepath" : "otherfile", null)); + + if (appliesTo) + { + Assert.Multiple(() => + { + Assert.That(snapshotSpan.Value.Snapshot, Is.SameAs(mockTextSnapshot.Object)); + Assert.That(snapshotSpan.Value.Start.Position, Is.EqualTo(0)); + Assert.That(snapshotSpan.Value.End.Position, Is.EqualTo(10)); + }); + } + else + { + Assert.That(snapshotSpan, Is.Null); + } + } + + [Test] + public void Should_Raise_Tags_Changed_For_Containing_Range_When_CoverageChangedMessage_With_Line_Numbers() + { + var autoMoqer = new AutoMoqer(); + var mockTextInfo = autoMoqer.GetMock(); + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(currentSnapshot => currentSnapshot.Length).Returns(1000); + ITextSnapshotLine CreateTextSnapshotLine(int start, int length) + { + var mockTextSnapshotLine = new Mock(); + mockTextSnapshotLine.SetupGet(textSnapshotLine => textSnapshotLine.Extent) + .Returns(new SnapshotSpan(mockTextSnapshot.Object, start, length)); + return mockTextSnapshotLine.Object; + }; + mockTextSnapshot.Setup(currentSnapshot => currentSnapshot.GetLineFromLineNumber(1)) + .Returns(CreateTextSnapshotLine(10,10)); + mockTextSnapshot.Setup(currentSnapshot => currentSnapshot.GetLineFromLineNumber(2)) + .Returns(CreateTextSnapshotLine(5,2)); + mockTextSnapshot.Setup(currentSnapshot => currentSnapshot.GetLineFromLineNumber(3)) + .Returns(CreateTextSnapshotLine(15, 10)); + mockTextInfo.SetupGet(textBufferAndFile => textBufferAndFile.FilePath).Returns("filepath"); + mockTextInfo.SetupGet(textBufferAndFile => textBufferAndFile.TextBuffer.CurrentSnapshot).Returns(mockTextSnapshot.Object); + + + var coverageTagger = autoMoqer.Create>(); + SnapshotSpan? snapshotSpan = null; + coverageTagger.TagsChanged += (sender, args) => + { + snapshotSpan = args.Span; + }; + coverageTagger.Handle(new CoverageChangedMessage(null, "filepath", new List { 1, 2, 3})); + + + Assert.Multiple(() => + { + Assert.That(snapshotSpan.Value.Snapshot, Is.SameAs(mockTextSnapshot.Object)); + Assert.That(snapshotSpan.Value.Start.Position, Is.EqualTo(5)); + Assert.That(snapshotSpan.Value.End.Position, Is.EqualTo(25)); + }); + + } + + [TestCase(true)] + [TestCase(false)] + public void Should_HasCoverage_When_Has(bool hasCoverage) + { + var coverageTagger = new CoverageTagger( + new Mock().Object, + hasCoverage ? new Mock().Object : null, + new Mock().Object, + new Mock().Object, + new Mock(MockBehavior.Strict).Object, + new Mock>().Object + ); + + Assert.That(coverageTagger.HasCoverage, Is.EqualTo(hasCoverage)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Raise_TagsChanged_For_CoverageTypeFilterChangedMessage_With_The_Same_TypeIdentifier_If_Has_Coverage(bool same) + { + var autoMoqer = new AutoMoqer(); + autoMoqer.SetInstance(new DummyCoverageTypeFilter()); + var mockTextInfo = autoMoqer.GetMock(); + + mockTextInfo.SetupGet(textBufferAndFile => textBufferAndFile.TextBuffer.CurrentSnapshot.Length).Returns(10); + + var coverageTagger = autoMoqer.Create>(); + var tagsChanged = false; + coverageTagger.TagsChanged += (sender, args) => + { + tagsChanged = true; + }; + + var filter = same ? new DummyCoverageTypeFilter() as ICoverageTypeFilter : new OtherCoverageTypeFilter(); + var coverageTypeFilterChangedMessage = new CoverageTypeFilterChangedMessage(filter); + coverageTagger.Handle(coverageTypeFilterChangedMessage); + + Assert.That(tagsChanged, Is.EqualTo(same)); + } + + [Test] + public void Should_Not_Raise_TagsChanged_For_CoverageTypeFilterChangedMessage_If_No_Coverage() + { + var coverageTagger = new CoverageTagger( + new Mock().Object, + null, + new DummyCoverageTypeFilter(), + new Mock().Object, + new Mock(MockBehavior.Strict).Object, + new Mock>().Object + ); + + var tagsChanged = false; + coverageTagger.TagsChanged += (sender, args) => + { + tagsChanged = true; + }; + + var coverageTypeFilterChangedMessage = new CoverageTypeFilterChangedMessage(new DummyCoverageTypeFilter()); + coverageTagger.Handle(coverageTypeFilterChangedMessage); + + Assert.That(tagsChanged, Is.False); + } + + [Test] + public void Should_Return_No_Tags_If_No_Coverage_Lines() + { + var coverageTagger = new CoverageTagger( + new Mock().Object, + null, + new Mock().Object, + new Mock().Object, + new Mock(MockBehavior.Strict).Object, + new Mock>().Object + ); + + var tags = coverageTagger.GetTags(new NormalizedSnapshotSpanCollection()); + + Assert.That(tags, Is.Empty); + } + + [Test] + public void Should_Return_No_Tags_If_ICoverageTypeFilter_Is_Disabled() + { + var autoMoqer = new AutoMoqer(); + autoMoqer.SetInstance(new DummyCoverageTypeFilter { Disabled = true }); + autoMoqer.SetInstance(new Mock(MockBehavior.Strict).Object); + + var coverageTagger = autoMoqer.Create>(); + + var tags = coverageTagger.GetTags(new NormalizedSnapshotSpanCollection()); + + Assert.That(tags, Is.Empty); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_GetLineSpans_From_LineSpanLogic_For_The_Spans_When_Coverage_And_Coverage_Filter_Enabled(bool newCoverage) + { + var autoMoqer = new AutoMoqer(); + var bufferLineCoverage = autoMoqer.GetMock().Object; + + var mockTextInfo = autoMoqer.GetMock(); + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(currentSnapshot => currentSnapshot.Length).Returns(10); + mockTextInfo.SetupGet(textBufferAndFile => textBufferAndFile.TextBuffer.CurrentSnapshot).Returns(mockTextSnapshot.Object); + mockTextInfo.SetupGet(textBufferWithFilePath => textBufferWithFilePath.FilePath).Returns("filepath"); + + var coverageTagger = autoMoqer.Create>(); + var spans = new NormalizedSnapshotSpanCollection(); + + var expectedBufferLineCoverageForLogic = bufferLineCoverage; + if (newCoverage) + { + expectedBufferLineCoverageForLogic = new Mock().Object; + coverageTagger.Handle(new CoverageChangedMessage(expectedBufferLineCoverageForLogic, "filepath",null)); + } + + coverageTagger.GetTags(spans); + + autoMoqer.Verify(lineSpanLogic => lineSpanLogic.GetLineSpans( + expectedBufferLineCoverageForLogic, spans)); + } + + [Test] + public void Should_GetTagsSpans_For_Filtered_LineSpans() + { + var autoMoqer = new AutoMoqer(); + var mockCoverageTypeFilter = autoMoqer.GetMock(); + + mockCoverageTypeFilter.Setup(coverageTypeFilter => coverageTypeFilter.Show(DynamicCoverageType.Covered)).Returns(false); + mockCoverageTypeFilter.Setup(coverageTypeFilter => coverageTypeFilter.Show(DynamicCoverageType.Partial)).Returns(false); + mockCoverageTypeFilter.Setup(coverageTypeFilter => coverageTypeFilter.Show(DynamicCoverageType.NotCovered)).Returns(false); + mockCoverageTypeFilter.Setup(coverageTypeFilter => coverageTypeFilter.Show(DynamicCoverageType.Dirty)).Returns(false); + mockCoverageTypeFilter.Setup(coverageTypeFilter => coverageTypeFilter.Show(DynamicCoverageType.NewLine)).Returns(true); + + var lineSpans = new List + { + new LineSpan{ Line = CreateLine(DynamicCoverageType.Covered),Span = SnapshotSpanFactory.Create(1)}, + new LineSpan{ Line = CreateLine(DynamicCoverageType.NotCovered), Span = SnapshotSpanFactory.Create(2)}, + new LineSpan{ Line = CreateLine(DynamicCoverageType.Partial), Span = SnapshotSpanFactory.Create(3)}, + new LineSpan{ Line = CreateLine(DynamicCoverageType.Dirty), Span = SnapshotSpanFactory.Create(4)}, + new LineSpan{ Line = CreateLine(DynamicCoverageType.NewLine), Span = SnapshotSpanFactory.Create(5)}, + }; + var expectedLineSpan = lineSpans[4]; + + var mockLineSpanTagger = autoMoqer.GetMock>(); + var tagSpan = new TagSpan(expectedLineSpan.Span, new DummyTag()); + mockLineSpanTagger.Setup(lineSpanTagger => lineSpanTagger.GetTagSpan(expectedLineSpan)).Returns(tagSpan); + + autoMoqer.Setup>( + lineSpanLogic => lineSpanLogic.GetLineSpans( + It.IsAny(), + It.IsAny() + ) + ) + .Returns(lineSpans); + + var coverageTagger = autoMoqer.Create>(); + + var tags = coverageTagger.GetTags(new NormalizedSnapshotSpanCollection()); + + Assert.That(tags, Is.EqualTo(new[] { tagSpan })); + mockCoverageTypeFilter.VerifyAll(); + + IDynamicLine CreateLine(DynamicCoverageType coverageType) + { + var mockLine = new Mock(); + mockLine.SetupGet(line => line.CoverageType).Returns(coverageType); + return mockLine.Object; + } + + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTypeFilterBase_Exception_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTypeFilterBase_Exception_Tests.cs new file mode 100644 index 00000000..9270f9f8 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/CoverageTypeFilterBase_Exception_Tests.cs @@ -0,0 +1,76 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.Tagging.Classification; +using FineCodeCoverage.Options; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace FineCodeCoverageTests.Editor.Tagging.Base +{ + internal class CoverageTypeFilterExceptions : CoverageTypeFilterBase + { + public override string TypeIdentifier => ""; + + protected override bool Enabled(IAppOptions appOptions) + { + return true; + } + public Func> ShowLookup; + protected override Dictionary GetShowLookup(IAppOptions appOptions) + { + return ShowLookup?.Invoke(); + } + } + + internal class CoverageTypeFilterBase_Exception_Tests + { + [Test] + public void Should_Throw_If_ShowLookup_Null() + { + var coverageTypeFilterExceptions = new CoverageTypeFilterExceptions(); + var appOptions = new Mock().SetupAllProperties().Object; + appOptions.ShowEditorCoverage = true; + + Assert.Throws(() => coverageTypeFilterExceptions.Initialize(appOptions)); + + } + + [Test] + public void Should_Throw_If_Incomplete_ShowLookup() + { + var coverageTypeFilterExceptions = new CoverageTypeFilterExceptions(); + coverageTypeFilterExceptions.ShowLookup = () => new Dictionary + { + { DynamicCoverageType.Covered, true }, + { DynamicCoverageType.NotCovered, true } + }; + var appOptions = new Mock().SetupAllProperties().Object; + appOptions.ShowEditorCoverage = true; + + Assert.Throws(() => coverageTypeFilterExceptions.Initialize(appOptions)); + + } + + [Test] + public void Should_Throw_When_Comparing_Different_ICoverageTypeFilter_For_Changes() + { + var coverageTypeFilterExceptions = new CoverageTypeFilterExceptions(); + coverageTypeFilterExceptions.ShowLookup = () => new Dictionary + { + { DynamicCoverageType.Covered, true }, + { DynamicCoverageType.NotCovered, true }, + { DynamicCoverageType.Partial,true }, + { DynamicCoverageType.Dirty, true }, + { DynamicCoverageType.NewLine,true }, + { DynamicCoverageType.NotIncluded,true }, + }; + var appOptions = new Mock().SetupAllProperties().Object; + appOptions.ShowEditorCoverage = true; + + var other = new CoverageClassificationFilter(); + Assert.Throws(() => coverageTypeFilterExceptions.Changed(other)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/LineSpanLogic_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Base/LineSpanLogic_Tests.cs new file mode 100644 index 00000000..d367f6bb --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/LineSpanLogic_Tests.cs @@ -0,0 +1,88 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.Tagging.Base +{ + internal class LineSpanLogic_Tests + { + class Line : ILine + { + public int Number { get; set; } + + public CoverageType CoverageType + { + get; set; + } + ITextSnapshotLine GetMockedLine(ITextSnapshot textSnapshot, int lineNumber, int start = 0, int end = 0) + { + var mockTextSnapshotLine = new Mock(); + mockTextSnapshotLine.SetupGet(textSnapshotLine => textSnapshotLine.LineNumber).Returns(lineNumber); + mockTextSnapshotLine.SetupGet(textSnapshotLine => textSnapshotLine.Start).Returns(new SnapshotPoint(textSnapshot, start)); + mockTextSnapshotLine.SetupGet(textSnapshotLine => textSnapshotLine.End).Returns(new SnapshotPoint(textSnapshot, end)); + return mockTextSnapshotLine.Object; + } + + [Test] + public void Should_ForEach_Normalized_Span_Should_Have_A_Full_LineSpan_For_Each_Coverage_Line_In_The_Range() + { + var mockBufferLineCoverage = new Mock(); + var firstLine = CreateDynamicLine(5, DynamicCoverageType.Covered); + var secondLine = CreateDynamicLine(17, DynamicCoverageType.NotCovered); + IDynamicLine CreateDynamicLine(int lineNumber, DynamicCoverageType coverageType) + { + var mockDynamicLine = new Mock(); + mockDynamicLine.SetupGet(dynamicLine => dynamicLine.Number).Returns(lineNumber); + mockDynamicLine.SetupGet(dynamicLine => dynamicLine.CoverageType).Returns(coverageType); + return mockDynamicLine.Object; + } + mockBufferLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines(0, 9)).Returns(new List + { + firstLine + }); + mockBufferLineCoverage.Setup(fileLineCoverage => fileLineCoverage.GetLines(14, 19)).Returns(new List + { + secondLine + }); + + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.Length).Returns(300); + var txtSnapshot = mockTextSnapshot.Object; + + // GetContainingLine() comes from ITextSnapshot.GetLineFromPosition + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromPosition(1)).Returns(GetMockedLine(txtSnapshot, 0)); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromPosition(199)).Returns(GetMockedLine(txtSnapshot, 9)); + + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromPosition(200)).Returns(GetMockedLine(txtSnapshot, 14)); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromPosition(299)).Returns(GetMockedLine(txtSnapshot, 19)); + + + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromLineNumber(5)).Returns(GetMockedLine(txtSnapshot, 5, 50, 60)); + mockTextSnapshot.Setup(textSnapshot => textSnapshot.GetLineFromLineNumber(17)).Returns(GetMockedLine(txtSnapshot, 17, 250, 260)); + + // is a normalized span collection linked to the ITextSnapshot + var normalizedSpanCollection = new NormalizedSnapshotSpanCollection(mockTextSnapshot.Object, new List { + Span.FromBounds(1, 199), + Span.FromBounds(200, 299) + }); + + var lineSpanLogic = new LineSpanLogic(); + var lineSpans = lineSpanLogic.GetLineSpans(mockBufferLineCoverage.Object, normalizedSpanCollection).ToList(); + + Assert.That(lineSpans.Count, Is.EqualTo(2)); + Assert.That(lineSpans[0].Line, Is.SameAs(firstLine)); + Assert.That(lineSpans[0].Span, Is.EqualTo(new SnapshotSpan(txtSnapshot, new Span(50, 10)))); + Assert.That(lineSpans[1].Line, Is.SameAs(secondLine)); + Assert.That(lineSpans[1].Span, Is.EqualTo(new SnapshotSpan(txtSnapshot, new Span(250, 10)))); + + } + + } + + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/Types/DummyCoverageTypeFilter.cs b/FineCodeCoverageTests/Editor/Tagging/Base/Types/DummyCoverageTypeFilter.cs new file mode 100644 index 00000000..516e5422 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/Types/DummyCoverageTypeFilter.cs @@ -0,0 +1,43 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; +using System; + +namespace FineCodeCoverageTests.Editor.Tagging.Base.Types +{ + internal class DummyCoverageTypeFilterInitializedEventArgs + { + public DummyCoverageTypeFilterInitializedEventArgs(DummyCoverageTypeFilter dummyCoverageTypeFilter) + { + DummyCoverageTypeFilter = dummyCoverageTypeFilter; + } + + public DummyCoverageTypeFilter DummyCoverageTypeFilter { get; } + } + + internal class DummyCoverageTypeFilter : ICoverageTypeFilter + { + public static event EventHandler Initialized; + + public bool Disabled { get; set; } + + public string TypeIdentifier => "Dummy"; + + public bool Show(DynamicCoverageType coverageType) + { + throw new NotImplementedException(); + } + + public Func ChangedFunc { get; set; } + public bool Changed(ICoverageTypeFilter other) + { + return ChangedFunc(other as DummyCoverageTypeFilter); + } + public IAppOptions AppOptions { get; private set; } + public void Initialize(IAppOptions appOptions) + { + AppOptions = appOptions; + Initialized?.Invoke(this, new DummyCoverageTypeFilterInitializedEventArgs(this)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/Types/DummyTag.cs b/FineCodeCoverageTests/Editor/Tagging/Base/Types/DummyTag.cs new file mode 100644 index 00000000..daaf11f8 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/Types/DummyTag.cs @@ -0,0 +1,6 @@ +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverageTests.Editor.Tagging.Base.Types +{ + internal class DummyTag : ITag { } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Base/Types/OtherCoverageTypeFilter.cs b/FineCodeCoverageTests/Editor/Tagging/Base/Types/OtherCoverageTypeFilter.cs new file mode 100644 index 00000000..7112d29c --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Base/Types/OtherCoverageTypeFilter.cs @@ -0,0 +1,29 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; +using System; + +namespace FineCodeCoverageTests.Editor.Tagging.Base.Types +{ + internal class OtherCoverageTypeFilter : ICoverageTypeFilter + { + public bool Disabled => throw new NotImplementedException(); + + public string TypeIdentifier => "Other"; + + public bool Changed(ICoverageTypeFilter other) + { + throw new NotImplementedException(); + } + + public void Initialize(IAppOptions appOptions) + { + throw new NotImplementedException(); + } + + public bool Show(DynamicCoverageType coverageType) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Classification/CoverageClassificationFilter_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Classification/CoverageClassificationFilter_Tests.cs new file mode 100644 index 00000000..91ce75fa --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Classification/CoverageClassificationFilter_Tests.cs @@ -0,0 +1,25 @@ +using FineCodeCoverage.Editor.Tagging.Classification; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.Editor.Tagging.CoverageTypeFilter; +using System; +using System.Linq.Expressions; + +namespace FineCodeCoverageTests.Editor.Tagging.Classification +{ + internal class CoverageClassificationFilter_Tests : CoverageTypeFilter_Tests_Base + { + protected override Expression> ShowCoverageExpression { get; } = appOptions => appOptions.ShowLineCoverageHighlighting; + + protected override Expression> ShowCoveredExpression { get; } = appOptions => appOptions.ShowLineCoveredHighlighting; + + protected override Expression> ShowUncoveredExpression { get; } = appOptions => appOptions.ShowLineUncoveredHighlighting; + + protected override Expression> ShowPartiallyCoveredExpression { get; } = appOptions => appOptions.ShowLinePartiallyCoveredHighlighting; + + protected override Expression> ShowDirtyExpression => appOptions => appOptions.ShowLineDirtyHighlighting; + + protected override Expression> ShowNewExpression => appOptions => appOptions.ShowLineNewHighlighting; + + protected override Expression> ShowNotIncludedExpression => appOptions => appOptions.ShowLineNotIncludedHighlighting; + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider_Tests.cs new file mode 100644 index 00000000..a77e332e --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider_Tests.cs @@ -0,0 +1,76 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.Tagging.Classification; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverageTests.Editor.Tagging.Types; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Moq; +using NUnit.Framework; +using System; +using LineSpan = FineCodeCoverageTests.Editor.Tagging.Types.LineSpan; + +namespace FineCodeCoverageTests.Editor.Tagging.Classification +{ + internal class CoverageLineClassificationTaggerProvider_Tests + { + [Test] + public void Should_Create_Tagger_From_The_ICoverageTaggerProviderFactory() + { + var mocker = new AutoMoqer(); + + var textBuffer = new Mock().Object; + var textView = new Mock().Object; + + var coverageTagger = new Mock>().Object; + var mockCoverageTaggerProvider = new Mock>(); + mockCoverageTaggerProvider.Setup(coverageTaggerProvider => coverageTaggerProvider.CreateTagger(textView, textBuffer)).Returns(coverageTagger); + + var mockCoverageTaggerProviderFactory = mocker.GetMock(); + mockCoverageTaggerProviderFactory.Setup( + coverageTaggerProviderFactory => coverageTaggerProviderFactory.Create( + It.IsAny>()) + ) + .Returns(mockCoverageTaggerProvider.Object); + + var coverageLineClassificationTaggerProvider = mocker.Create(); + + var tagger = coverageLineClassificationTaggerProvider.CreateTagger(textView, textBuffer); + + Assert.That(tagger, Is.SameAs(coverageTagger)); + } + + [TestCase(DynamicCoverageType.Covered)] + [TestCase(DynamicCoverageType.NotCovered)] + [TestCase(DynamicCoverageType.Partial)] + [TestCase(DynamicCoverageType.NewLine)] + [TestCase(DynamicCoverageType.Dirty)] + public void Should_Create_An_IClassificationTag_TagSpan_Classification_Type_From_ICoverageTypeService_For_The_Line_Coverage_Type(DynamicCoverageType coverageType) + { + var mocker = new AutoMoqer(); + var classificationType = new Mock().Object; + mocker.Setup( + coverageTypeService => coverageTypeService.GetClassificationType(coverageType)).Returns(classificationType); + + var coverageLineClassificationTaggerProvider = mocker.Create(); + + var mockCoverageTaggerProviderFactory = mocker.GetMock(); + var classificationLineSpanTagger = mockCoverageTaggerProviderFactory.Invocations[0].Arguments[0] as ILineSpanTagger; + + var snapshotSpan = SnapshotSpanFactory.Create(1); + var mockLine = new Mock(); + mockLine.SetupGet(line => line.CoverageType).Returns(coverageType); + var tagSpan = classificationLineSpanTagger.GetTagSpan(new LineSpan { Line = mockLine.Object, Span = snapshotSpan }); + + Assert.Multiple(() => + { + Assert.That(tagSpan.Span, Is.EqualTo(snapshotSpan)); + Assert.That(tagSpan.Tag.ClassificationType, Is.SameAs(classificationType)); + }); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/CoverageTypeFilter_Tests_Base.cs b/FineCodeCoverageTests/Editor/Tagging/CoverageTypeFilter_Tests_Base.cs new file mode 100644 index 00000000..11bc0e19 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/CoverageTypeFilter_Tests_Base.cs @@ -0,0 +1,307 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Options; +using Moq; +using NUnit.Framework; +using System; +using System.Linq.Expressions; + +namespace FineCodeCoverageTests.Editor.Tagging.CoverageTypeFilter +{ + internal abstract class CoverageTypeFilter_Tests_Base where TCoverageTypeFilter : ICoverageTypeFilter, new() + { + public static Action GetSetter(Expression> propertyGetExpression) + { + var entityParameterExpression = + (ParameterExpression)((MemberExpression)propertyGetExpression.Body).Expression; + var valueParameterExpression = Expression.Parameter(typeof(bool)); + + return Expression.Lambda>( + Expression.Assign(propertyGetExpression.Body, valueParameterExpression), + entityParameterExpression, + valueParameterExpression).Compile(); + } + #region expressions / actions + protected abstract Expression> ShowCoverageExpression { get; } + + protected abstract Expression> ShowCoveredExpression { get; } + protected abstract Expression> ShowUncoveredExpression { get; } + protected abstract Expression> ShowPartiallyCoveredExpression { get; } + protected abstract Expression> ShowDirtyExpression { get; } + protected abstract Expression> ShowNewExpression { get; } + protected abstract Expression> ShowNotIncludedExpression { get; } + + private Action showCoverage; + private Action ShowCoverage + { + get + { + if (showCoverage == null) + { + showCoverage = GetSetter(ShowCoverageExpression); + } + return showCoverage; + } + } + private Action showCovered; + private Action ShowCovered + { + get + { + if (showCovered == null) + { + showCovered = GetSetter(ShowCoveredExpression); + } + return showCovered; + } + } + private Action showUncovered; + private Action ShowUncovered + { + get + { + if (showUncovered == null) + { + showUncovered = GetSetter(ShowUncoveredExpression); + } + return showUncovered; + } + } + private Action showPartiallyCovered; + private Action ShowPartiallyCovered + { + get + { + if (showPartiallyCovered == null) + { + showPartiallyCovered = GetSetter(ShowPartiallyCoveredExpression); + } + return showPartiallyCovered; + } + } + private Action showDirty; + private Action ShowDirty + { + get + { + if (showDirty == null) + { + showDirty = GetSetter(ShowDirtyExpression); + } + return showDirty; + } + } + private Action showNew; + private Action ShowNew + { + get + { + if (showNew == null) + { + showNew= GetSetter(ShowNewExpression); + } + return showNew; + } + } + + private Action showNotIncluded; + private Action ShowNotIncluded + { + get + { + if (showNotIncluded == null) + { + showNotIncluded = GetSetter(ShowNotIncludedExpression); + } + return showNotIncluded; + } + } + #endregion + + [Test] + public void Should_Be_Disabled_When_ShowEditorCoverage_False() + { + var coverageTypeFilter = new TCoverageTypeFilter(); + var appOptions = GetStubbedAppOptions(); + ShowCoverage(appOptions, true); + appOptions.ShowEditorCoverage = false; + + coverageTypeFilter.Initialize(appOptions); + + Assert.True(coverageTypeFilter.Disabled); + } + + [Test] + public void Should_Be_Disabled_When_Show_Coverage_False() + { + var coverageTypeFilter = new TCoverageTypeFilter(); + var appOptions = GetStubbedAppOptions(); + ShowCoverage(appOptions, false); + appOptions.ShowEditorCoverage = true; + + coverageTypeFilter.Initialize(appOptions); + Assert.True(coverageTypeFilter.Disabled); + } + + private IAppOptions GetStubbedAppOptions() + { + return new Mock().SetupAllProperties().Object; + } + + [TestCase(true, true, true, true, false,true)] + [TestCase(false, false, false, false, true,false)] + public void Should_Show_Using_AppOptions( + bool showCovered, + bool showUncovered, + bool showPartiallyCovered, + bool showDirty, + bool showNew, + bool showNotIncluded + ) + { + var coverageTypeFilter = new TCoverageTypeFilter(); + var appOptions = new Mock().SetupAllProperties().Object; + ShowCoverage(appOptions, true); + appOptions.ShowEditorCoverage = true; + + ShowCovered(appOptions, showCovered); + ShowUncovered(appOptions, showUncovered); + ShowPartiallyCovered(appOptions, showPartiallyCovered); + ShowDirty(appOptions, showDirty); + ShowNew(appOptions, showNew); + ShowNotIncluded(appOptions, showNotIncluded); + + coverageTypeFilter.Initialize(appOptions); + + Assert.That(coverageTypeFilter.Show(DynamicCoverageType.Covered), Is.EqualTo(showCovered)); + Assert.That(coverageTypeFilter.Show(DynamicCoverageType.NotCovered), Is.EqualTo(showUncovered)); + Assert.That(coverageTypeFilter.Show(DynamicCoverageType.Partial), Is.EqualTo(showPartiallyCovered)); + Assert.That(coverageTypeFilter.Show(DynamicCoverageType.NewLine), Is.EqualTo(showNew)); + Assert.That(coverageTypeFilter.Show(DynamicCoverageType.Dirty), Is.EqualTo(showDirty)); + Assert.That(coverageTypeFilter.Show(DynamicCoverageType.NotIncluded), Is.EqualTo(showNotIncluded)); + } + + [TestCaseSource(nameof(ChangedTestSource))] + public void Should_Be_Or_Not_Be_Changed_When_AppOptions_Changed(ChangedTestArguments changedTestArguments) + { + var coverageTypeFilter = new TCoverageTypeFilter(); + coverageTypeFilter.Initialize(SetAppOptions(changedTestArguments.InitialAppOptions)); + var newCoverageTypeFilter = new TCoverageTypeFilter(); + newCoverageTypeFilter.Initialize(SetAppOptions(changedTestArguments.ChangedAppOptions)); + + Assert.That(newCoverageTypeFilter.Changed(coverageTypeFilter), Is.EqualTo(changedTestArguments.ExpectedChanged)); + } + + private IAppOptions SetAppOptions(CoverageAppOptions coverageAppOptions) + { + var appOptions = GetStubbedAppOptions(); + appOptions.ShowEditorCoverage = coverageAppOptions.ShowEditorCoverage; + ShowCoverage(appOptions, coverageAppOptions.ShowCoverage); + ShowCovered(appOptions, coverageAppOptions.ShowCovered); + ShowUncovered(appOptions, coverageAppOptions.ShowUncovered); + ShowPartiallyCovered(appOptions, coverageAppOptions.ShowPartiallyCovered); + ShowDirty(appOptions, coverageAppOptions.ShowDirty); + ShowNew(appOptions, coverageAppOptions.ShowNew); + ShowNotIncluded(appOptions, coverageAppOptions.ShowNotIncluded); + return appOptions; + } + + public class CoverageAppOptions + { + public bool ShowEditorCoverage { get; set; } + public bool ShowCoverage { get; set; } + + public bool ShowCovered { get; set; } + public bool ShowUncovered { get; set; } + public bool ShowPartiallyCovered { get; set; } + public bool ShowDirty { get; set; } + public bool ShowNew { get; set; } + public bool ShowNotIncluded { get; set; } + + public CoverageAppOptions( + bool showCovered, + bool showUncovered, + bool showPartiallyCovered, + bool showDirty, + bool showNew, + bool showNotIncluded, + bool showEditorCoverage = true, + bool showCoverage = true + ) + { + ShowCovered = showCovered; + ShowUncovered = showUncovered; + ShowPartiallyCovered = showPartiallyCovered; + ShowDirty = showDirty; + ShowNew = showNew; + ShowNotIncluded = showNotIncluded; + + ShowCoverage = showCoverage; + ShowEditorCoverage = showEditorCoverage; + } + } + + internal class ChangedTestArguments + { + public ChangedTestArguments(CoverageAppOptions initialAppOptions, CoverageAppOptions changedAppOptions, bool expectedChanged) + { + InitialAppOptions = initialAppOptions; + ChangedAppOptions = changedAppOptions; + ExpectedChanged = expectedChanged; + } + public CoverageAppOptions InitialAppOptions { get; } + public CoverageAppOptions ChangedAppOptions { get; } + public bool ExpectedChanged { get; } + } + + public static ChangedTestArguments[] ChangedTestSource = new ChangedTestArguments[]{ + new ChangedTestArguments( + new CoverageAppOptions(true,true,true, true, true, true), // changing covered + new CoverageAppOptions(false,true,true, true, true, true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true, true, true, true, true, true),// changing uncovered + new CoverageAppOptions(true,false,true,true, true,true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true, true, true, true), // changing partialy covered + new CoverageAppOptions(true,true,false, true, true, true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true, false, true, true), // changing dirty + new CoverageAppOptions(true,true,true, true, true, true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true, true, false,true), // changing new + new CoverageAppOptions(true,true,true, true, true, true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true, true, true,false), // changing not included + new CoverageAppOptions(true,true,true, true, true, true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true,true, true,true, true,true), + new CoverageAppOptions(true,true,true,true, true,true, false,true), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true,true,true,true,true,true), + new CoverageAppOptions(true,true,true,true,true,true, true,false), + true + ), + new ChangedTestArguments( + new CoverageAppOptions(true,true,true, true, true, true), + new CoverageAppOptions(true,true,true, true, true, true), + false + ) + }; + } + +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactory_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactory_Tests.cs new file mode 100644 index 00000000..71ae8d7d --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactory_Tests.cs @@ -0,0 +1,36 @@ +using FineCodeCoverage.Editor.Tagging.GlyphMargin; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; +using Moq; +using NUnit.Framework; +using System; +using System.Threading; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace FineCodeCoverageTests.Editor.Tagging.GlyphMargin +{ + internal class CoverageLineGlyphFactory_Tests + { + [Test] + public void Should_Return_Null_If_GlyphTag_Is_Not_CoverageLineGlyphTag() + { + var coverageLineGlyphFactory = new CoverageLineGlyphFactory(); + var result = coverageLineGlyphFactory.GenerateGlyph(new Mock().Object, new Mock().Object); + Assert.IsNull(result); + } + + [Apartment(ApartmentState.STA)] + [Test] + public void Should_Return_A_Solid_Colour_Rectangle_If_GlyphTag_Is_CoverageLineGlyphTag() + { + var coverageLineGlyphFactory = new CoverageLineGlyphFactory(); + var result = coverageLineGlyphFactory.GenerateGlyph(new Mock().Object, new CoverageLineGlyphTag(Colors.DeepPink)); + + var rectangle = result as Rectangle; + var fill = rectangle.Fill as SolidColorBrush; + Assert.That(fill.Color, Is.EqualTo(Colors.DeepPink)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider_Tests.cs new file mode 100644 index 00000000..544f4bfe --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider_Tests.cs @@ -0,0 +1,89 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.Tagging.GlyphMargin; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Moq; +using NUnit.Framework; +using System.Windows.Media; +using LineSpan = FineCodeCoverageTests.Editor.Tagging.Types.LineSpan; + +namespace FineCodeCoverageTests.Editor.Tagging.GlyphMargin +{ + internal class CoverageLineGlyphTaggerProvider_Tests + { + [TestCase(true)] + [TestCase(false)] + public void Should_Create_A_CoverageLineGlyphTagger_Using_The_Tagger_From_The_ICoverageTaggerProviderFactory_If_Not_Null(bool isNull) + { + var mocker = new AutoMoqer(); + + var textBuffer = new Mock().Object; + var textView = new Mock().Object; + + var coverageTagger = new Mock>().Object; + var mockCoverageTaggerProvider = new Mock>(); + var createTaggerSetup = mockCoverageTaggerProvider.Setup(coverageTaggerProvider => coverageTaggerProvider.CreateTagger(textView, textBuffer)); + if (!isNull) + { + createTaggerSetup.Returns(coverageTagger); + } + + var mockCoverageTaggerProviderFactory = mocker.GetMock(); + mockCoverageTaggerProviderFactory.Setup( + coverageTaggerProviderFactory => coverageTaggerProviderFactory.Create( + It.IsAny>()) + ) + .Returns(mockCoverageTaggerProvider.Object); + + var coverageLineGlyphTaggerProvider = mocker.Create(); + + var tagger = coverageLineGlyphTaggerProvider.CreateTagger(textView, textBuffer); + if (isNull) + { + Assert.That(tagger, Is.Null); + } + else + { + Assert.That(tagger, Is.InstanceOf()); + } + } + + [TestCase(DynamicCoverageType.Covered)] + [TestCase(DynamicCoverageType.NotCovered)] + [TestCase(DynamicCoverageType.Partial)] + [TestCase(DynamicCoverageType.NewLine)] + [TestCase(DynamicCoverageType.Dirty)] + public void Should_Create_A_CoverageLineGlyphTag_TagSpan_BackgroundColor_From_ICoverageColoursProvider_For_The_Line_Coverage_Type_And_The_Line(DynamicCoverageType coverageType) + { + var mocker = new AutoMoqer(); + var mockCoverageColours = new Mock(); + var mockItemCoverageColours = new Mock(); + mockItemCoverageColours.SetupGet(itemCoverageColours => itemCoverageColours.Background).Returns(Colors.Red); + mockCoverageColours.Setup(coverageColours => coverageColours.GetColour(coverageType)).Returns(mockItemCoverageColours.Object); + mocker.Setup( + coverageColoursProvider => coverageColoursProvider.GetCoverageColours()).Returns(mockCoverageColours.Object); + + var coverageLineGlyphTaggerProvider = mocker.Create(); + + var mockCoverageTaggerProviderFactory = mocker.GetMock(); + var classificationLineSpanTagger = mockCoverageTaggerProviderFactory.Invocations[0].Arguments[0] as ILineSpanTagger; + + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.Length).Returns(1); + var snapshotSpan = new SnapshotSpan(mockTextSnapshot.Object, new Span(0, 1)); + var mockLine = new Mock(); + mockLine.SetupGet(line => line.CoverageType).Returns(coverageType); + var tagSpan = classificationLineSpanTagger.GetTagSpan(new LineSpan { Line = mockLine.Object, Span = snapshotSpan }); + + Assert.Multiple(() => + { + Assert.That(tagSpan.Span, Is.EqualTo(snapshotSpan)); + Assert.That(tagSpan.Tag.Colour, Is.EqualTo(Colors.Red)); + }); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphTagger_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphTagger_Tests.cs new file mode 100644 index 00000000..18bf7ca5 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/CoverageLineGlyphTagger_Tests.cs @@ -0,0 +1,108 @@ +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.Tagging.GlyphMargin; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Tagging; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Editor.Tagging.GlyphMargin +{ + internal class CoverageLineGlyphTagger_Tests + { + [Test] + public void Should_Listen_For_CoverageColoursChangedMessage() + { + var autoMoqer = new AutoMoqer(); + var coverageLineGlyphTagger = autoMoqer.Create(); + + autoMoqer.Verify(eventAggregator => eventAggregator.AddListener(coverageLineGlyphTagger, null)); + } + + [Test] + public void Should_Unlisten_On_Dispose() + { + var autoMoqer = new AutoMoqer(); + var coverageLineGlyphTagger = autoMoqer.Create(); + + coverageLineGlyphTagger.Dispose(); + autoMoqer.Verify(eventAggregator => eventAggregator.RemoveListener(coverageLineGlyphTagger)); + } + + [Test] + public void Should_Dispose_The_CoverageTagger_On_Dispose() + { + var autoMoqer = new AutoMoqer(); + var coverageLineGlyphTagger = autoMoqer.Create(); + + coverageLineGlyphTagger.Dispose(); + + autoMoqer.Verify>(coverageTagger => coverageTagger.Dispose()); + } + + [Test] + public void Should_TagsChanged_When_CoverageTagger_TagsChanged() + { + var autoMoqer = new AutoMoqer(); + var coverageLineGlyphTagger = autoMoqer.Create(); + var coverageTaggerArgs = new SnapshotSpanEventArgs(new SnapshotSpan()); + SnapshotSpanEventArgs raisedArgs = null; + coverageLineGlyphTagger.TagsChanged += (sender, args) => + { + raisedArgs = args; + }; + autoMoqer.GetMock>() + .Raise(coverageTagger => coverageTagger.TagsChanged += null, coverageTaggerArgs); + + Assert.That(raisedArgs, Is.SameAs(coverageTaggerArgs)); + } + + [Test] + public void Should_Remove_TagsChanged_Handler_When_Own_Handler_Removed() + { + var autoMoqer = new AutoMoqer(); + var mockCoverageTagger = autoMoqer.GetMock>(); + + void handler(object sender, SnapshotSpanEventArgs args) { } + + var coverageLineGlyphTagger = autoMoqer.Create(); + coverageLineGlyphTagger.TagsChanged += handler; + mockCoverageTagger.VerifyAdd(coverageTagger => coverageTagger.TagsChanged += handler, Times.Once()); + coverageLineGlyphTagger.TagsChanged -= handler; + mockCoverageTagger.VerifyRemove(coverageTagger => coverageTagger.TagsChanged -= handler, Times.Once()); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_RaiseTagsChanged_On_CoverageTagger_When_Receive_CoverageColoursChangedMessage_And_There_Is_Coverage(bool hasCoverage) + { + var autoMoqer = new AutoMoqer(); + var mockCoverageTagger = autoMoqer.GetMock>(); + mockCoverageTagger.SetupGet(ct => ct.HasCoverage).Returns(hasCoverage); + + var coverageLineGlyphTagger = autoMoqer.Create(); + + coverageLineGlyphTagger.Handle(new CoverageColoursChangedMessage()); + + autoMoqer.Verify>(coverageTagger => coverageTagger.RaiseTagsChanged(), hasCoverage ? Times.Once() : Times.Never()); + } + + [Test] + public void Should_GetTags_From_The_CoverageTagger() + { + var autoMoqer = new AutoMoqer(); + var coverageLineGlyphTagger = autoMoqer.Create(); + var spans = new NormalizedSnapshotSpanCollection(); + var expectedTags = new TagSpan[0]; + autoMoqer.GetMock>() + .Setup(coverageTagger => coverageTagger.GetTags(spans)) + .Returns(expectedTags); + + var tags = coverageLineGlyphTagger.GetTags(spans); + + Assert.That(tags, Is.SameAs(expectedTags)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/GlyphFilter_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/GlyphFilter_Tests.cs new file mode 100644 index 00000000..ae033740 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/GlyphMargin/GlyphFilter_Tests.cs @@ -0,0 +1,27 @@ +using FineCodeCoverage.Editor.Tagging.GlyphMargin; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.Editor.Tagging.CoverageTypeFilter; +using System; +using System.Linq.Expressions; + +namespace FineCodeCoverageTests.Editor.Tagging.GlyphMargin +{ + internal class GlyphFilter_Tests : CoverageTypeFilter_Tests_Base + { + protected override Expression> ShowCoverageExpression { get; } = appOptions => appOptions.ShowCoverageInGlyphMargin; + + protected override Expression> ShowCoveredExpression { get; } = appOptions => appOptions.ShowCoveredInGlyphMargin; + + protected override Expression> ShowUncoveredExpression { get; } = appOptions => appOptions.ShowUncoveredInGlyphMargin; + + protected override Expression> ShowPartiallyCoveredExpression { get; } = appOptions => appOptions.ShowPartiallyCoveredInGlyphMargin; + + protected override Expression> ShowDirtyExpression => appOptions => appOptions.ShowDirtyInGlyphMargin; + + protected override Expression> ShowNewExpression => appOptions => appOptions.ShowNewInGlyphMargin; + + protected override Expression> ShowNotIncludedExpression => appOptions => appOptions.ShowNotIncludedInGlyphMargin; + } + + +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider_Tests.cs new file mode 100644 index 00000000..87ded894 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider_Tests.cs @@ -0,0 +1,74 @@ +using AutoMoq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.Tagging.OverviewMargin; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Moq; +using NUnit.Framework; +using LineSpan = FineCodeCoverageTests.Editor.Tagging.Types.LineSpan; + +namespace FineCodeCoverageTests.Editor.Tagging.OverviewMargin +{ + internal class CoverageLineOverviewMarkTaggerProvider_Tests + { + [Test] + public void Should_Create_Tagger_From_The_ICoverageTaggerProviderFactory() + { + var mocker = new AutoMoqer(); + + var textBuffer = new Mock().Object; + var textView = new Mock().Object; + + var coverageTagger = new Mock>().Object; + var mockCoverageTaggerProvider = new Mock>(); + mockCoverageTaggerProvider.Setup(coverageTaggerProvider => coverageTaggerProvider.CreateTagger(textView, textBuffer)).Returns(coverageTagger); + + var mockCoverageTaggerProviderFactory = mocker.GetMock(); + mockCoverageTaggerProviderFactory.Setup( + coverageTaggerProviderFactory => coverageTaggerProviderFactory.Create( + It.IsAny>()) + ) + .Returns(mockCoverageTaggerProvider.Object); + + var coverageLineOverviewMarkTaggerProvider = mocker.Create(); + + var tagger = coverageLineOverviewMarkTaggerProvider.CreateTagger(textView, textBuffer); + + Assert.That(tagger, Is.SameAs(coverageTagger)); + } + + [TestCase(DynamicCoverageType.Covered)] + [TestCase(DynamicCoverageType.NotCovered)] + [TestCase(DynamicCoverageType.Partial)] + [TestCase(DynamicCoverageType.NewLine)] + [TestCase(DynamicCoverageType.Dirty)] + public void Should_Create_An_OverviewMarkTag_TagSpan_MarkKindName_From_CoverageColoursEditorFormatMapNames_For_The_Line_Coverage_Type(DynamicCoverageType coverageType) + { + var mocker = new AutoMoqer(); + mocker.Setup( + coverageColoursEditorFormatMapNames => coverageColoursEditorFormatMapNames.GetEditorFormatDefinitionName(coverageType)).Returns("MarkKindName"); + + var coverageLineOverviewMarkTaggerProvider = mocker.Create(); + + var mockCoverageTaggerProviderFactory = mocker.GetMock(); + var overviewMarkLineSpanTagger = mockCoverageTaggerProviderFactory.Invocations[0].Arguments[0] as ILineSpanTagger; + + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.Length).Returns(1); + var snapshotSpan = new SnapshotSpan(mockTextSnapshot.Object, new Span(0, 1)); + var mockLine = new Mock(); + mockLine.SetupGet(line => line.CoverageType).Returns(coverageType); + var tagSpan = overviewMarkLineSpanTagger.GetTagSpan(new LineSpan { Line = mockLine.Object, Span = snapshotSpan }); + + Assert.Multiple(() => + { + Assert.That(tagSpan.Span, Is.EqualTo(snapshotSpan)); + Assert.That(tagSpan.Tag.MarkKindName, Is.EqualTo("MarkKindName")); + }); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/OverviewMargin/CoverageOverviewMargin_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/OverviewMargin/CoverageOverviewMargin_Tests.cs new file mode 100644 index 00000000..a93764b4 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/OverviewMargin/CoverageOverviewMargin_Tests.cs @@ -0,0 +1,27 @@ +using FineCodeCoverage.Editor.Tagging.OverviewMargin; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.Editor.Tagging.CoverageTypeFilter; +using System; +using System.Linq.Expressions; + +namespace FineCodeCoverageTests.Editor.Tagging.OverviewMargin +{ + internal class CoverageOverviewMargin_Tests : CoverageTypeFilter_Tests_Base + { + protected override Expression> ShowCoverageExpression { get; } = appOptions => appOptions.ShowCoverageInOverviewMargin; + + protected override Expression> ShowCoveredExpression { get; } = appOptions => appOptions.ShowCoveredInOverviewMargin; + + protected override Expression> ShowUncoveredExpression { get; } = appOptions => appOptions.ShowUncoveredInOverviewMargin; + + protected override Expression> ShowPartiallyCoveredExpression { get; } = appOptions => appOptions.ShowPartiallyCoveredInOverviewMargin; + + protected override Expression> ShowDirtyExpression => appOptions => appOptions.ShowDirtyInOverviewMargin; + + protected override Expression> ShowNewExpression => appOptions => appOptions.ShowNewInOverviewMargin; + + protected override Expression> ShowNotIncludedExpression => appOptions => appOptions.ShowNotIncludedInOverviewMargin; + } + + +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs b/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs new file mode 100644 index 00000000..6f4fa757 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/TaggerProviders_LanguageSupport_Tests.cs @@ -0,0 +1,27 @@ +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Editor.Tagging.Classification; +using FineCodeCoverage.Editor.Tagging.GlyphMargin; +using FineCodeCoverage.Editor.Tagging.OverviewMargin; +using Microsoft.VisualStudio.Utilities; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.Editor.Tagging +{ + internal class TaggerProviders_LanguageSupport_Tests + { + [Test] + public void Should_Only_Be_Interested_In_CSharp_VB_And_CPP() + { + var types = new List { typeof(CoverageLineGlyphFactoryProvider), typeof(CoverageLineGlyphTaggerProvider),typeof(CoverageLineClassificationTaggerProvider), typeof(CoverageLineOverviewMarkTaggerProvider)}; + types.ForEach(type => + { + 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 })); + }); + } + } +} diff --git a/FineCodeCoverageTests/Editor/Tagging/Types/LineSpan.cs b/FineCodeCoverageTests/Editor/Tagging/Types/LineSpan.cs new file mode 100644 index 00000000..2282ef47 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Types/LineSpan.cs @@ -0,0 +1,13 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverageTests.Editor.Tagging.Types +{ + internal class LineSpan : ILineSpan + { + public IDynamicLine Line { get; set; } + + public SnapshotSpan Span { get; set; } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/Editor/Tagging/Types/SnapshotSpanFactory.cs b/FineCodeCoverageTests/Editor/Tagging/Types/SnapshotSpanFactory.cs new file mode 100644 index 00000000..6dce4c60 --- /dev/null +++ b/FineCodeCoverageTests/Editor/Tagging/Types/SnapshotSpanFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.VisualStudio.Text; +using Moq; + +namespace FineCodeCoverageTests.Editor.Tagging.Types +{ + public static class SnapshotSpanFactory + { + public static SnapshotSpan Create(int end) + { + var mockTextSnapshot = new Mock(); + mockTextSnapshot.SetupGet(textSnapshot => textSnapshot.Length).Returns(end + 1); + return new SnapshotSpan(mockTextSnapshot.Object, new Span(0, end)); + } + } +} \ No newline at end of file diff --git a/FineCodeCoverageTests/FCCEngine_Tests.cs b/FineCodeCoverageTests/FCCEngine_Tests.cs index 58385bb9..ed49628f 100644 --- a/FineCodeCoverageTests/FCCEngine_Tests.cs +++ b/FineCodeCoverageTests/FCCEngine_Tests.cs @@ -22,12 +22,10 @@ public class FCCEngine_Tests { private AutoMoqer mocker; private FCCEngine fccEngine; - private bool updatedMarginTags; [SetUp] public void SetUp() { - updatedMarginTags = false; mocker = new AutoMoqer(); fccEngine = mocker.Create(); } @@ -73,6 +71,17 @@ public void Should_Send_NewCoverageLinesMessage_With_Null_CoverageLines_When_Cle mocker.Verify(ea => ea.SendMessage(It.Is(msg => msg.CoverageLines == null), null)); } + [Test] + public void Should_Clear_UI_When_Solution_Closes() + { + var mockSolutionEvents = mocker.GetMock(); + mocker.Setup(reportGeneratorUtil => reportGeneratorUtil.BlankReport(false)).Returns("reportHtml"); + mockSolutionEvents.Raise(s => s.AfterClosing += null, EventArgs.Empty); + + mocker.Verify(ea => ea.SendMessage(It.Is(msg => msg.CoverageLines == null), null)); + mocker.Verify(ea => ea.SendMessage(It.Is(msg => msg.Report == "reportHtml"), null)); + + } } public class FCCEngine_ReloadCoverage_Tests @@ -85,7 +94,9 @@ public void SetUp() { mocker = new AutoMoqer(); var mockDisposeAwareTaskRunner = mocker.GetMock(); - mockDisposeAwareTaskRunner.Setup(runner => runner.RunAsync(It.IsAny>())).Callback>(async taskProvider => await taskProvider()); +#pragma warning disable VSTHRD101 // Avoid unsupported async delegates + mockDisposeAwareTaskRunner.Setup(runner => runner.RunAsyncFunc(It.IsAny>())).Callback>(async taskProvider => await taskProvider()); +#pragma warning restore VSTHRD101 // Avoid unsupported async delegates fccEngine = mocker.Create(); var mockedAppOptions = mocker.GetMock(); @@ -95,21 +106,21 @@ public void SetUp() } [Test] - public async Task Should_Log_Starting_When_Initialized() + public async Task Should_Log_Starting_When_Initialized_Async() { - await ReloadInitializedCoverage(); + await ReloadInitializedCoverage_Async(); VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Start); } [Test] - public async Task Should_Prepare_For_Coverage_Suitable_CoverageProjects() + public async Task Should_Prepare_For_Coverage_Suitable_CoverageProjects_Async() { - var mockSuitableCoverageProject = await ReloadSuitableCoverageProject(); + var mockSuitableCoverageProject = await ReloadSuitableCoverageProject_Async(); mockSuitableCoverageProject.Verify(p => p.PrepareForCoverageAsync(It.IsAny(),true)); } [Test] - public async Task Should_Set_Failure_Description_For_Unsuitable_Projects() + public async Task Should_Set_Failure_Description_For_Unsuitable_Projects_Async() { SetUpSuccessfulRunReportGenerator(); @@ -122,7 +133,7 @@ public async Task Should_Set_Failure_Description_For_Unsuitable_Projects() mockDisabledProject.Setup(p => p.ProjectFile).Returns("proj.csproj"); mockDisabledProject.Setup(p => p.Settings.Enabled).Returns(false); - await ReloadInitializedCoverage(mockNullProjectFileProject.Object, mockWhitespaceProjectFileProject.Object, mockDisabledProject.Object); + await ReloadInitializedCoverage_Async(mockNullProjectFileProject.Object, mockWhitespaceProjectFileProject.Object, mockDisabledProject.Object); mockDisabledProject.VerifySet(p => p.FailureDescription = "Disabled"); mockWhitespaceProjectFileProject.VerifySet(p => p.FailureDescription = "Unsupported project type for DLL 'Whitespace_Project_File.dll'"); @@ -131,9 +142,9 @@ public async Task Should_Set_Failure_Description_For_Unsuitable_Projects() } [Test] - public async Task Should_Run_The_CoverTool_Step() + public async Task Should_Run_The_CoverTool_Step_Async() { - var mockCoverageProject = await ReloadSuitableCoverageProject(); + var mockCoverageProject = await ReloadSuitableCoverageProject_Async(); mockCoverageProject.Verify(p => p.StepAsync("Run Coverage Tool", It.IsAny>())); } @@ -141,11 +152,14 @@ public async Task Should_Run_The_CoverTool_Step() public async Task Should_Run_Coverage_ThrowingErrors_But_Safely_With_StepAsync() { ICoverageProject coverageProject = null; - await ReloadSuitableCoverageProject(mockCoverageProject => { + await ReloadSuitableCoverageProject_Async(mockCoverageProject => { coverageProject = mockCoverageProject.Object; - mockCoverageProject.Setup(p => p.StepAsync("Run Coverage Tool", It.IsAny>())).Callback>((_,runCoverTool) => + mockCoverageProject.Setup(p => p.StepAsync("Run Coverage Tool", It.IsAny>())) + .Callback>((_,runCoverTool) => { +#pragma warning disable VSTHRD110 // Observe result of async calls runCoverTool(coverageProject); +#pragma warning restore VSTHRD110 // Observe result of async calls }); }); @@ -154,7 +168,7 @@ await ReloadSuitableCoverageProject(mockCoverageProject => { } [Test] - public async Task Should_Allow_The_CoverageOutputManager_To_SetProjectCoverageOutputFolder() + public async Task Should_Allow_The_CoverageOutputManager_To_SetProjectCoverageOutputFolder_Async() { var mockCoverageToolOutputManager = mocker.GetMock(); mockCoverageToolOutputManager.Setup(om => om.SetProjectCoverageOutputFolder(It.IsAny>())). @@ -171,11 +185,13 @@ public async Task Should_Allow_The_CoverageOutputManager_To_SetProjectCoverageOu coverageProjectAfterCoverageOutputManager = cp; }); - await ReloadSuitableCoverageProject(mockCoverageProject => { + await ReloadSuitableCoverageProject_Async(mockCoverageProject => { mockCoverageProject.SetupProperty(cp => cp.CoverageOutputFolder); mockCoverageProject.Setup(p => p.StepAsync("Run Coverage Tool", It.IsAny>())).Callback>((_, runCoverTool) => { +#pragma warning disable VSTHRD110 // Observe result of async calls runCoverTool(mockCoverageProject.Object); +#pragma warning restore VSTHRD110 // Observe result of async calls }); }); @@ -183,7 +199,7 @@ await ReloadSuitableCoverageProject(mockCoverageProject => { } [Test] - public async Task Should_Run_Report_Generator_With_Output_Files_From_Coverage_For_Coverage_Projects_That_Have_Not_Failed() + public async Task Should_Run_Report_Generator_With_Output_Files_From_Coverage_For_Coverage_Projects_That_Have_Not_Failed_Async() { var failedProject = CreateSuitableProject(); failedProject.Setup(p => p.HasFailed).Returns(true); @@ -200,21 +216,21 @@ public async Task Should_Run_Report_Generator_With_Output_Files_From_Coverage_Fo It.IsAny() ).Result).Returns(new ReportGeneratorResult { }); - await ReloadInitializedCoverage(failedProject.Object, passedProject.Object); + await ReloadInitializedCoverage_Async(failedProject.Object, passedProject.Object); mocker.GetMock().VerifyAll(); } [Test] - public async Task Should_Not_Run_ReportGenerator_If_No_Successful_Projects() + public async Task Should_Not_Run_ReportGenerator_If_No_Successful_Projects_Async() { - await ReloadInitializedCoverage(); + await ReloadInitializedCoverage_Async(); mocker.Verify(rg => rg.GenerateAsync(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] - public async Task Should_Process_ReportGenerator_Output_If_Success_Raising_Events() + public async Task Should_Process_ReportGenerator_Output_If_Success_Raising_Events_Async() { var passedProject = CreateSuitableProject(); var reportGeneratorResult = new ReportGeneratorResult @@ -239,7 +255,7 @@ public async Task Should_Process_ReportGenerator_Output_If_Success_Raising_Event var fileLineCoverage = new FileLineCoverage(); mockCoberturaUtil.Setup(coberturaUtil => coberturaUtil.ProcessCoberturaXml("Unified xml file")).Returns(fileLineCoverage); - await ReloadInitializedCoverage(passedProject.Object); + await ReloadInitializedCoverage_Async(passedProject.Object); mockReportGenerator.VerifyAll(); mockReportGenerator.Verify(reportGenerator => reportGenerator.EndOfCoverageRun()); @@ -262,9 +278,9 @@ public async Task Should_Process_ReportGenerator_Output_If_Success_Raising_Event } [Test] - public async Task Should_Cancel_Running_Coverage_Logging_Cancelled_When_StopCoverage() + public async Task Should_Cancel_Running_Coverage_Logging_Cancelled_When_StopCoverage_Async() { - await StopCoverage(); + await StopCoverage_Async(); VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Cancelled); } @@ -275,7 +291,7 @@ public void Should_Not_Throw_When_StopCoverage_And_There_Is_No_Coverage_Running( } [Test] - public async Task Should_Cancel_Existing_ReloadCoverage_When_ReloadCoverage() + public async Task Should_Cancel_Existing_ReloadCoverage_When_ReloadCoverage_Async() { SetUpSuccessfulRunReportGenerator(); @@ -295,7 +311,7 @@ public async Task Should_Cancel_Existing_ReloadCoverage_When_ReloadCoverage() }).Returns(Task.FromResult(new CoverageProjectFileSynchronizationDetails())); - await ReloadInitializedCoverage(mockSuitableCoverageProject.Object); + await ReloadInitializedCoverage_Async(mockSuitableCoverageProject.Object); VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Cancelled); @@ -306,69 +322,7 @@ private void VerifyLogsReloadCoverageStatus(ReloadCoverageStatus reloadCoverageS mocker.Verify(l => l.Log(fccEngine.GetLogReloadCoverageStatusMessage(reloadCoverageStatus))); } - private async Task<(string reportGeneratedHtmlContent, FileLineCoverage updatedCoverageLines)> RunToCompletion(bool noCoverageProjects) - { - var coverageProject = CreateSuitableProject().Object; - var mockReportGenerator = mocker.GetMock(); - mockReportGenerator.Setup(rg => - rg.GenerateAsync( - It.Is>(coverOutputFiles => coverOutputFiles.Count() == 1 && coverOutputFiles.First() == coverageProject.CoverageOutputFile), - It.IsAny(), - It.IsAny() - ).Result) - .Returns( - new ReportGeneratorResult - { - UnifiedHtml = "Unified" - } - ); - - var reportGeneratedHtmlContent = ""; - mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtml("Unified", It.IsAny())).Returns(reportGeneratedHtmlContent); - var coverageLines = new FileLineCoverage(); - coverageLines.Add("test", new[] { new Line() }); - coverageLines.Completed(); - mocker.GetMock().Setup(coberturaUtil => coberturaUtil.ProcessCoberturaXml(It.IsAny())).Returns(coverageLines); - if (noCoverageProjects) - { - await ReloadInitializedCoverage(); - } - else - { - await ReloadInitializedCoverage(coverageProject); - } - - return (reportGeneratedHtmlContent, coverageLines); - - } - - private async Task ThrowReadingReportHtml() - { - var passedProject = CreateSuitableProject(); - - var mockReportGenerator = mocker.GetMock(); - mockReportGenerator.Setup(rg => - rg.GenerateAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny() - ).Result) - .Returns( - new ReportGeneratorResult - { - } - ); - - var coverageLines = new FileLineCoverage(); - coverageLines.Add("test", new[] { new Line() }); - coverageLines.Completed(); - mocker.GetMock().Setup(coberturaUtil => coberturaUtil.ProcessCoberturaXml(It.IsAny())).Returns(coverageLines); - - await ReloadInitializedCoverage(passedProject.Object); - - } - - private async Task StopCoverage() + private async Task StopCoverage_Async() { var mockSuitableCoverageProject = new Mock(); mockSuitableCoverageProject.Setup(p => p.ProjectFile).Returns("Defined.csproj"); @@ -380,7 +334,7 @@ private async Task StopCoverage() }).Returns(Task.FromResult(new CoverageProjectFileSynchronizationDetails())); - await ReloadInitializedCoverage(mockSuitableCoverageProject.Object); + await ReloadInitializedCoverage_Async(mockSuitableCoverageProject.Object); } private void SetUpSuccessfulRunReportGenerator() @@ -390,16 +344,18 @@ private void SetUpSuccessfulRunReportGenerator() It.IsAny>(), It.IsAny(), It.IsAny() - ).Result) - .Returns(new ReportGeneratorResult { }); + )) + .ReturnsAsync(new ReportGeneratorResult { }); } - private async Task ReloadInitializedCoverage(params ICoverageProject[] coverageProjects) + private async Task ReloadInitializedCoverage_Async(params ICoverageProject[] coverageProjects) { var projectsFromTask = Task.FromResult(coverageProjects.ToList()); fccEngine.Initialize(CancellationToken.None); +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks fccEngine.ReloadCoverage(() => projectsFromTask); await fccEngine.reloadCoverageTask; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks } private Mock CreateSuitableProject() { @@ -410,12 +366,12 @@ private Mock CreateSuitableProject() mockSuitableCoverageProject.Setup(p => p.StepAsync("Run Coverage Tool", It.IsAny>())).Returns(Task.CompletedTask); return mockSuitableCoverageProject; } - private async Task> ReloadSuitableCoverageProject(Action> setUp = null) + private async Task> ReloadSuitableCoverageProject_Async(Action> setUp = null) { var mockSuitableCoverageProject = CreateSuitableProject(); setUp?.Invoke(mockSuitableCoverageProject); SetUpSuccessfulRunReportGenerator(); - await ReloadInitializedCoverage(mockSuitableCoverageProject.Object); + await ReloadInitializedCoverage_Async(mockSuitableCoverageProject.Object); return mockSuitableCoverageProject; } diff --git a/FineCodeCoverageTests/FileLineCoverage_Tests.cs b/FineCodeCoverageTests/FileLineCoverage_Tests.cs index 7e8d087f..3cc6ea46 100644 --- a/FineCodeCoverageTests/FileLineCoverage_Tests.cs +++ b/FineCodeCoverageTests/FileLineCoverage_Tests.cs @@ -1,25 +1,69 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FineCodeCoverage.Engine.Model; +using Moq; using NUnit.Framework; namespace FineCodeCoverageTests { internal class FileLineCoverage_Tests { - [TestCaseSource(nameof(Cases))] + private static ILine CreateLine(int lineNumber, CoverageType coverageType = CoverageType.Covered) + { + var mockLine = new Mock(); + mockLine.Setup(l => l.Number).Returns(lineNumber); + mockLine.Setup(l => l.CoverageType).Returns(coverageType); + return mockLine.Object; + } + + [TestCaseSource(nameof(Cases))] public void GetLines_Test(IEnumerable lineNumbers, int startLineNumber, int endLineNumber, IEnumerable expectedLineNumbers) { var fileLineCoverage = new FileLineCoverage(); - fileLineCoverage.Add("fp", lineNumbers.Select(n => new FineCodeCoverage.Engine.Cobertura.Line { Number = n })); - fileLineCoverage.Completed(); + fileLineCoverage.Add("fp", lineNumbers.Select((n => CreateLine(n)))); + fileLineCoverage.Sort(); var lines = fileLineCoverage.GetLines("fp", startLineNumber, endLineNumber); Assert.That(lines.Select(l => l.Number), Is.EqualTo(expectedLineNumbers)); } - static object[] Cases = + [Test] + public void Should_Get_Empty_Lines_For_File_Not_In_Report() + { + var fileLineCoverage = new FileLineCoverage(); + + var lines = fileLineCoverage.GetLines("", 1, 2); + + Assert.That(lines, Is.Empty); + } + + [Test] + public void Should_Should_Not_Throw_When_File_Renamed_Not_In_Report() + { + var fileLineCoverage = new FileLineCoverage(); + fileLineCoverage.UpdateRenamed("old", "new"); + } + + [Test] + public void Should_Rename_When_FileName_Changes() + { + var fileLineCoverage = new FileLineCoverage(); + var lines = new[] { CreateLine(1), CreateLine(2) }; + fileLineCoverage.Add("old", lines); + AssertLines("old"); + + fileLineCoverage.UpdateRenamed("old", "new"); + AssertLines("new"); + Assert.That(fileLineCoverage.GetLines("old"), Is.Empty); + + void AssertLines(string fileName) + { + var allLines = fileLineCoverage.GetLines(fileName); + Assert.That(allLines, Is.EqualTo(lines)); + } + } + + static readonly object[] Cases = { new object[] { new int[] { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}, 19, 20, new int[]{ 19,20} }, new object[] { new int[] { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}, 12, 13, new int[]{ 12,13} }, diff --git a/FineCodeCoverageTests/FineCodeCoverageTests.csproj b/FineCodeCoverageTests/FineCodeCoverageTests.csproj index ce836d65..20ad6b01 100644 --- a/FineCodeCoverageTests/FineCodeCoverageTests.csproj +++ b/FineCodeCoverageTests/FineCodeCoverageTests.csproj @@ -1,7 +1,5 @@  - - Debug @@ -37,75 +35,33 @@ 4 - - ..\packages\AutoMoq.2.0.0\lib\net45\AutoMoq.dll - - - ..\packages\Castle.Core.4.4.1\lib\net45\Castle.Core.dll - - - True - - - ..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll - - - ..\packages\Unity.4.0.1\lib\net45\Microsoft.Practices.Unity.dll - - - ..\packages\Unity.4.0.1\lib\net45\Microsoft.Practices.Unity.Configuration.dll - - - ..\packages\Unity.4.0.1\lib\net45\Microsoft.Practices.Unity.RegistrationByConvention.dll - - - ..\packages\Microsoft.TestPlatform.ObjectModel.11.0.0\lib\net35\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll - - - ..\packages\Microsoft.VisualStudio.TestWindow.Interfaces.11.0.61030\lib\net45\Microsoft.VisualStudio.TestWindow.Interfaces.dll - True - - - ..\packages\Moq.4.16.0\lib\net45\Moq.dll - - - ..\packages\NuGet.Frameworks.5.11.0\lib\net472\NuGet.Frameworks.dll - - - ..\packages\NUnit.3.13.1\lib\net45\nunit.framework.dll - - - ..\packages\StructureMap.4.7.1\lib\net45\StructureMap.dll - + + + - - ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll - - - ..\packages\System.Reflection.Metadata.1.6.0\lib\netstandard2.0\System.Reflection.Metadata.dll - - - - ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll - + + + + + - - ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - + + + - - - ..\packages\XMLUnit.Core.2.9.0\lib\net35\xmlunit-core.dll - + + + + @@ -114,6 +70,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -135,9 +147,13 @@ - - - + + + + + + + @@ -162,23 +178,62 @@ - - - {31c104bb-d294-4942-b206-896aa7a5fcb9} - FineCodeCoverage + + {59a22196-a750-4ba4-b30e-be1422e68b8e} + FineCodeCoverage2022 + + + 2.0.0 + + + 4.4.1 + + + 1.2.0 + + + 4.8.0 + + + 4.8.0 + + + 11.0.0 + + + 17.1.32210.191 + + + 11.0.61030 + + + 4.16.0 + + + 5.11.0 + + + 3.13.1 + + + 3.17.0 + + + 4.7.1 + + + 4.0.1 + + + 2.9.0 + + true - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - \ No newline at end of file diff --git a/FineCodeCoverageTests/FirstTimeToolWindowOpener_Tests.cs b/FineCodeCoverageTests/FirstTimeToolWindowOpener_Tests.cs index 920c28b0..be0f0677 100644 --- a/FineCodeCoverageTests/FirstTimeToolWindowOpener_Tests.cs +++ b/FineCodeCoverageTests/FirstTimeToolWindowOpener_Tests.cs @@ -23,7 +23,7 @@ public void SetUp() { [TestCase(true, true, false)] [TestCase(false, false, false)] [TestCase(false, true, false)] - public async Task It_Should_Open_If_Have_Never_Shown_The_ToolWindow_And_InitializedFromTestContainerDiscoverer( + public async Task It_Should_Open_If_Have_Never_Shown_The_ToolWindow_And_InitializedFromTestContainerDiscoverer_Async( bool initializedFromTestContainerDiscoverer, bool hasShownToolWindow, bool expectedShown diff --git a/FineCodeCoverageTests/Initializer_Tests.cs b/FineCodeCoverageTests/Initializer_Tests.cs index ba4c9cb7..08cf04e8 100644 --- a/FineCodeCoverageTests/Initializer_Tests.cs +++ b/FineCodeCoverageTests/Initializer_Tests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; using System.Threading; using System.Threading.Tasks; using AutoMoq; @@ -19,9 +21,19 @@ public class Initializer_Tests public void SetUp() { mocker = new AutoMoqer(); + mocker.SetInstance(new IInitializable[] { }); initializer = mocker.Create(); } + [Test] + public void Should_ImportMany_IInitializable() + { + var constructor = typeof(Initializer).GetConstructors().Single(); + var initializablesConstructorParameter = constructor.GetParameters().Single(p => p.ParameterType == typeof(IInitializable[])); + var hasImportManyAttribute = initializablesConstructorParameter.GetCustomAttributes(typeof(ImportManyAttribute), false).Any(); + Assert.True(hasImportManyAttribute); + } + [Test] public void Should_Have_Initial_InitializeStatus_As_Initializing() { @@ -29,7 +41,7 @@ public void Should_Have_Initial_InitializeStatus_As_Initializing() } [Test] - public async Task Should_Log_Initializing_When_Initialize() + public async Task Should_Log_Initializing_When_Initialize_Async() { await initializer.InitializeAsync(CancellationToken.None); mocker.Verify(l => l.Log("Initializing")); @@ -45,21 +57,21 @@ private async Task InitializeWithExceptionAsync(Action callback = nul } [Test] - public async Task Should_Set_InitializeStatus_To_Error_If_Exception_When_Initialize() + public async Task Should_Set_InitializeStatus_To_Error_If_Exception_When_Initialize_Async() { await InitializeWithExceptionAsync(); Assert.AreEqual(InitializeStatus.Error, initializer.InitializeStatus); } [Test] - public async Task Should_Set_InitializeExceptionMessage_If_Exception_When_Initialize() + public async Task Should_Set_InitializeExceptionMessage_If_Exception_When_Initialize_Async() { await InitializeWithExceptionAsync(); Assert.AreEqual("initialize exception", initializer.InitializeExceptionMessage); } [Test] - public async Task Should_Log_Failed_Initialization_With_Exception_if_Exception_When_Initialize() + public async Task Should_Log_Failed_Initialization_With_Exception_if_Exception_When_Initialize_Async() { Exception initializeException = null; await InitializeWithExceptionAsync(exc => initializeException = exc); @@ -67,21 +79,21 @@ public async Task Should_Log_Failed_Initialization_With_Exception_if_Exception_W } [Test] - public async Task Should_Set_InitializeStatus_To_Initialized_When_Successfully_Completed() + public async Task Should_Set_InitializeStatus_To_Initialized_When_Successfully_Completed_Async() { await initializer.InitializeAsync(CancellationToken.None); Assert.AreEqual(InitializeStatus.Initialized, initializer.InitializeStatus); } [Test] - public async Task Should_Log_Initialized_When_Successfully_Completed() + public async Task Should_Log_Initialized_When_Successfully_Completed_Async() { await initializer.InitializeAsync(CancellationToken.None); mocker.Verify(l => l.Log("Initialized")); } [Test] - public async Task Should_Initialize_Dependencies_In_Order() + public async Task Should_Initialize_Dependencies_In_Order_Async() { var disposalToken = CancellationToken.None; List callOrder = new List(); diff --git a/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_Collect_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_Collect_Tests.cs index 456056df..a1019c17 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_Collect_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_Collect_Tests.cs @@ -11,7 +11,7 @@ using FineCodeCoverage.Engine; using System.Threading.Tasks; using FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using FineCodeCoverage.Engine.ReportGenerator; using FineCodeCoverage.Engine.Model; using FineCodeCoverage.Options; @@ -22,7 +22,7 @@ namespace FineCodeCoverageTests.MsCodeCoverage internal class MsCodeCoverageRunSettingsService_Test_Execution_Not_Finished_Tests { [Test] - public void Should_Set_To_Not_Collecting() + public async Task Should_Set_To_Not_Collecting_Async() { var autoMocker = new AutoMoqer(); var msCodeCoverageRunSettingsService = autoMocker.Create(); @@ -31,13 +31,13 @@ public void Should_Set_To_Not_Collecting() var mockTestOperation = new Mock(); mockTestOperation.Setup(testOperation => testOperation.GetCoverageProjectsAsync()).ReturnsAsync(new List()); - msCodeCoverageRunSettingsService.TestExecutionNotFinishedAsync(mockTestOperation.Object); + await msCodeCoverageRunSettingsService.TestExecutionNotFinishedAsync(mockTestOperation.Object); Assert.That(msCodeCoverageRunSettingsService.collectionStatus, Is.EqualTo(MsCodeCoverageCollectionStatus.NotCollecting)); } [Test] - public async Task Should_Clean_Up_RunSettings_Coverage_Projects() + public async Task Should_Clean_Up_RunSettings_Coverage_Projects_Async() { var autoMocker = new AutoMoqer(); var msCodeCoverageRunSettingsService = autoMocker.Create(); @@ -87,7 +87,7 @@ internal class MsCodeCoverageRunSettingsService_Collect_Tests private MsCodeCoverageRunSettingsService msCodeCoverageRunSettingsService; [Test] - public async Task Should_Set_To_Not_Collecting() + public async Task Should_Set_To_Not_Collecting_Async() { var resultsUris = new List() { @@ -103,7 +103,7 @@ public async Task Should_Set_To_Not_Collecting() } [Test] - public async Task Should_FCCEngine_RunAndProcessReport_With_CoberturaResults() + public async Task Should_FCCEngine_RunAndProcessReport_With_CoberturaResults_Async() { var resultsUris = new List() { @@ -117,13 +117,13 @@ public async Task Should_FCCEngine_RunAndProcessReport_With_CoberturaResults() } [Test] - public async Task Should_Not_Throw_If_No_Results() + public async Task Should_Not_Throw_If_No_Results_Async() { await RunAndProcessReportAsync(null, Array.Empty()); } [Test] - public async Task Should_Combined_Log_When_No_Cobertura_Files() + public async Task Should_Combined_Log_When_No_Cobertura_Files_Async() { await RunAndProcessReportAsync(null, Array.Empty()); autoMocker.Verify(logger => logger.Log("No cobertura files for ms code coverage.")); @@ -133,7 +133,7 @@ public async Task Should_Combined_Log_When_No_Cobertura_Files() } [Test] - public async Task Should_Clean_Up_RunSettings_Coverage_Projects_From_IsCollecting() + public async Task Should_Clean_Up_RunSettings_Coverage_Projects_From_IsCollecting_Async() { await RunAndProcessReportAsync(null, Array.Empty()); autoMocker.Verify( diff --git a/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IRunSettingsService_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IRunSettingsService_Tests.cs index 52774d2c..97746f4d 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IRunSettingsService_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IRunSettingsService_Tests.cs @@ -44,7 +44,7 @@ public void Should_Not_Delegate_To_UserRunSettingsService_When_Not_Test_Executio [TestCase(MsCodeCoverageCollectionStatus.Error)] public void Should_Not_Delegate_To_UserRunSettingsService_When_Is_Not_Collecting(MsCodeCoverageCollectionStatus status) { - msCodeCoverageRunSettingsService.collectionStatus = MsCodeCoverageCollectionStatus.NotCollecting; + msCodeCoverageRunSettingsService.collectionStatus = status; SetuserRunSettingsProjectDetailsLookup(false); ShouldNotDelegateToUserRunSettingsService(RunSettingConfigurationInfoState.Execution); diff --git a/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IsCollecting_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IsCollecting_Tests.cs index 7f39d1da..b626fcc2 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IsCollecting_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/MsCodeCoverageRunSettingsService_IsCollecting_Tests.cs @@ -10,7 +10,7 @@ using FineCodeCoverage.Engine; using System.Linq; using System.Threading; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; using FineCodeCoverage.Engine.ReportGenerator; using FineCodeCoverage.Core.Utilities; using System.IO; @@ -87,7 +87,7 @@ public void SetupSut() } [Test] - public async Task Should_Not_Be_Collecting_If_RunMsCodeCoverage_No() + public async Task Should_Not_Be_Collecting_If_RunMsCodeCoverage_No_Async() { SetupAppOptionsProvider(RunMsCodeCoverage.No); var testOperation = SetUpTestOperation(new List { }); @@ -98,7 +98,7 @@ public async Task Should_Not_Be_Collecting_If_RunMsCodeCoverage_No() [TestCase(true)] [TestCase(false)] - public async Task Should_Try_Analyse_Projects_With_Runsettings(bool useMsCodeCoverageOption) + public async Task Should_Try_Analyse_Projects_With_Runsettings_Async(bool useMsCodeCoverageOption) { var runMsCodeCoverage = useMsCodeCoverageOption ? RunMsCodeCoverage.Yes : RunMsCodeCoverage.IfInRunSettings; SetupAppOptionsProvider(runMsCodeCoverage); @@ -122,7 +122,7 @@ public async Task Should_Try_Analyse_Projects_With_Runsettings(bool useMsCodeCov } [Test] // in case shutdown visual studio before normal clean up operation - public async Task Should_CleanUp_Projects_With_RunSettings_First() + public async Task Should_CleanUp_Projects_With_RunSettings_First_Async() { var coverageProjectWithRunSettings = CreateCoverageProject(".runsettings"); var coverageProjects = new List { coverageProjectWithRunSettings, CreateCoverageProject(null) }; @@ -155,37 +155,37 @@ public async Task Should_CleanUp_Projects_With_RunSettings_First() } [Test] - public async Task Should_Log_Exception_From_UserRunSettingsService_Analyse() + public async Task Should_Log_Exception_From_UserRunSettingsService_Analyse_Async() { var exception = new Exception("Msg"); - await Throw_Exception_From_UserRunSettingsService_Analyse(exception); + await Throw_Exception_From_UserRunSettingsService_Analyse_Async(exception); VerifyLogException("Exception analysing runsettings files", exception); } [Test] - public async Task Should_Have_Status_Error_When_Exception_From_UserRunSettingsService_Analyse() + public async Task Should_Have_Status_Error_When_Exception_From_UserRunSettingsService_Analyse_Async() { var exception = new Exception("Msg"); - var status = await Throw_Exception_From_UserRunSettingsService_Analyse(exception); + var status = await Throw_Exception_From_UserRunSettingsService_Analyse_Async(exception); Assert.AreEqual(MsCodeCoverageCollectionStatus.Error, status); } [Test] - public async Task Should_Report_End_Of_CoverageRun_If_Error() + public async Task Should_Report_End_Of_CoverageRun_If_Error_Async() { var exception = new Exception("Msg"); - await Throw_Exception_From_UserRunSettingsService_Analyse(exception); + await Throw_Exception_From_UserRunSettingsService_Analyse_Async(exception); autoMocker.Verify(reportGeneratorUtil => reportGeneratorUtil.EndOfCoverageRun()); } - private Task Throw_Exception_From_UserRunSettingsService_Analyse(Exception exception) + private Task Throw_Exception_From_UserRunSettingsService_Analyse_Async(Exception exception) { SetupIUserRunSettingsServiceAnalyseAny().Throws(exception); return msCodeCoverageRunSettingsService.IsCollectingAsync(SetUpTestOperation()); } [Test] - public async Task Should_Prepare_Coverage_Projects_When_Suitable() + public async Task Should_Prepare_Coverage_Projects_When_Suitable_Async() { SetupAppOptionsProvider(RunMsCodeCoverage.IfInRunSettings); @@ -210,7 +210,7 @@ public async Task Should_Prepare_Coverage_Projects_When_Suitable() } [Test] - public async Task Should_Set_UserRunSettingsProjectDetailsLookup_For_IRunSettingsService_When_Suitable() + public async Task Should_Set_UserRunSettingsProjectDetailsLookup_For_IRunSettingsService_When_Suitable_Async() { SetupAppOptionsProvider(RunMsCodeCoverage.IfInRunSettings); @@ -246,20 +246,20 @@ public async Task Should_Set_UserRunSettingsProjectDetailsLookup_For_IRunSetting } [Test] - public async Task Should_Be_Collecting_When_Suitable_RunSettings_And_No_Templates() + public async Task Should_Be_Collecting_When_Suitable_RunSettings_And_No_Templates_Async() { - var status = await IsCollecting_With_Suitable_RunSettings_Only(); + var status = await IsCollecting_With_Suitable_RunSettings_Only_Async(); Assert.AreEqual(MsCodeCoverageCollectionStatus.Collecting, status); } [Test] - public async Task Should_Combined_Log_Collecting_With_RunSettings_When_Only_Suitable_RunSettings() + public async Task Should_Combined_Log_Collecting_With_RunSettings_When_Only_Suitable_RunSettings_Async() { - await IsCollecting_With_Suitable_RunSettings_Only(); + await IsCollecting_With_Suitable_RunSettings_Only_Async(); VerifyCombinedLogMessage("Ms code coverage with user runsettings"); } - private Task IsCollecting_With_Suitable_RunSettings_Only() + private Task IsCollecting_With_Suitable_RunSettings_Only_Async() { var testOperation = SetUpTestOperation(); SetupIUserRunSettingsServiceAnalyseAny().Returns(new UserRunSettingsAnalysisResult(true, false)); @@ -267,7 +267,7 @@ private Task IsCollecting_With_Suitable_RunSetti } [Test] - public async Task Should_Not_Be_Collecting_If_User_RunSettings_Are_Not_Suitable() + public async Task Should_Not_Be_Collecting_If_User_RunSettings_Are_Not_Suitable_Async() { var testOperation = SetUpTestOperation(); SetupIUserRunSettingsServiceAnalyseAny().Returns(new UserRunSettingsAnalysisResult()); @@ -277,18 +277,18 @@ public async Task Should_Not_Be_Collecting_If_User_RunSettings_Are_Not_Suitable( } [Test] - public Task Should_Generate_RunSettings_From_Templates_When_MsCodeCoverage_Option_is_True() + public Task Should_Generate_RunSettings_From_Templates_When_MsCodeCoverage_Option_is_True_Async() { - return GenerateRunSettingsFromTemplate(true, false); + return GenerateRunSettingsFromTemplate_Async(true, false); } [Test] - public Task Should_Generate_RunSettings_From_Templates_When_RunSettings_SpecifiedMsCodeCoverage() + public Task Should_Generate_RunSettings_From_Templates_When_RunSettings_SpecifiedMsCodeCoverage_Async() { - return GenerateRunSettingsFromTemplate(false, true); + return GenerateRunSettingsFromTemplate_Async(false, true); } - public async Task GenerateRunSettingsFromTemplate(bool msCodeCoverageOptions, bool runSettingsSpecifiedMsCodeCoverage) + public async Task GenerateRunSettingsFromTemplate_Async(bool msCodeCoverageOptions, bool runSettingsSpecifiedMsCodeCoverage) { var runMsCodeCoverage = msCodeCoverageOptions ? RunMsCodeCoverage.Yes : RunMsCodeCoverage.IfInRunSettings; SetupAppOptionsProvider(runMsCodeCoverage); @@ -319,24 +319,24 @@ public async Task GenerateRunSettingsFromTemplate(bool msCodeCoverageOptions, bo } [Test] - public Task Should_Combined_Log_When_Successfully_Generate_RunSettings_From_Templates() + public Task Should_Combined_Log_When_Successfully_Generate_RunSettings_From_Templates_Async() { - return Successful_RunSettings_From_Templates_CombinedLog_Test( + return Successful_RunSettings_From_Templates_CombinedLog_Test_Async( new List { }, new List { "Ms code coverage" } ); } [Test] - public Task Should_Combined_Log_With_Custom_Template_Paths_When_Successfully_Generate_RunSettings_From_Templates() + public Task Should_Combined_Log_With_Custom_Template_Paths_When_Successfully_Generate_RunSettings_From_Templates_Async() { - return Successful_RunSettings_From_Templates_CombinedLog_Test( + return Successful_RunSettings_From_Templates_CombinedLog_Test_Async( new List { "Custom path 1", "Custom path 2","Custom path 2" }, new List { "Ms code coverage - custom template paths","Custom path 1", "Custom path 2"} ); } - private async Task Successful_RunSettings_From_Templates_CombinedLog_Test(List customTemplatePaths,List expectedLoggerMessages) + private async Task Successful_RunSettings_From_Templates_CombinedLog_Test_Async(List customTemplatePaths,List expectedLoggerMessages) { SetupIUserRunSettingsServiceAnalyseAny().Returns(new UserRunSettingsAnalysisResult(true, true)); @@ -359,22 +359,22 @@ private async Task Successful_RunSettings_From_Templates_CombinedLog_Test(List ExceptionWhenGenerateRunSettingsFromTemplates(Exception exception) + private Task ExceptionWhenGenerateRunSettingsFromTemplates_Async(Exception exception) { SetupIUserRunSettingsServiceAnalyseAny().Returns(new UserRunSettingsAnalysisResult(true, true)); @@ -400,7 +400,7 @@ private Task ExceptionWhenGenerateRunSettingsFro } [Test] - public async Task Should_Not_Be_Collecting_When_Template_Projects_And_Do_Not_Ms_Collect() + public async Task Should_Not_Be_Collecting_When_Template_Projects_And_Do_Not_Ms_Collect_Async() { SetupAppOptionsProvider(RunMsCodeCoverage.IfInRunSettings); SetupIUserRunSettingsServiceAnalyseAny().Returns(new UserRunSettingsAnalysisResult(true, false)); @@ -417,7 +417,7 @@ public async Task Should_Not_Be_Collecting_When_Template_Projects_And_Do_Not_Ms_ } [Test] - public async Task Should_Shim_Copy_From_RunSettingsProjects_And_Template_Projects_That_Require_It() + public async Task Should_Shim_Copy_From_RunSettingsProjects_And_Template_Projects_That_Require_It_Async() { var shimPath = InitializeShimPath(); diff --git a/FineCodeCoverageTests/MsCodeCoverage/ProjectRunSettingsGenerator_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/ProjectRunSettingsGenerator_Tests.cs index ee8edae3..d1926736 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/ProjectRunSettingsGenerator_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/ProjectRunSettingsGenerator_Tests.cs @@ -65,7 +65,7 @@ public void Setup() } [Test] - public async Task Should_Write_All_Project_Run_Settings_File_Path_With_The_VsRunSettingsWriter() + public async Task Should_Write_All_Project_Run_Settings_File_Path_With_The_VsRunSettingsWriter_Async() { await projectRunSettingsGenerator.WriteProjectsRunSettingsAsync(coverageProjectsRunSettings); @@ -77,7 +77,7 @@ public async Task Should_Write_All_Project_Run_Settings_File_Path_With_The_VsRun [TestCase(true)] [TestCase(false)] - public async Task Should_Write_RunSettings_In_Project_Output_Folder_If_The_VsRunSettingsWriter_Is_Successful(bool success) + public async Task Should_Write_RunSettings_In_Project_Output_Folder_If_The_VsRunSettingsWriter_Is_Successful_Async(bool success) { var mockVsRunSettingsWriter = autoMocker.GetMock(); mockVsRunSettingsWriter.Setup(rsw => rsw.WriteRunSettingsFilePathAsync(projectId1, generatedRunSettingsInOutputFolderPath1)).ReturnsAsync(success); @@ -99,7 +99,7 @@ public async Task Should_Write_RunSettings_In_Project_Output_Folder_If_The_VsRun } [Test] - public async Task Should_Remove_Generated_Run_Settings_File_Path_With_The_VsRunSettingsWriter() + public async Task Should_Remove_Generated_Run_Settings_File_Path_With_The_VsRunSettingsWriter_Async() { var mockProjectWithGeneratedRunSettings = new Mock(); var mockProjectWithoutRunSettings = new Mock(); diff --git a/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs index e607e3f2..6e68fd11 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplateReplacementsFactory_Tests.cs @@ -695,6 +695,8 @@ internal class TestCoverageProjectOptions : IAppOptions public bool ShowCoveredInOverviewMargin { get; set; } public bool ShowUncoveredInOverviewMargin { get; set; } public bool ShowPartiallyCoveredInOverviewMargin { get; set; } + public bool ShowDirtyInOverviewMargin { get; set; } + public bool ShowNewInOverviewMargin { get; set; } public bool ShowToolWindowToolbar { get; set; } public bool Hide0Coverable { get; set; } public bool Hide0Coverage { get; set; } @@ -705,5 +707,23 @@ internal class TestCoverageProjectOptions : IAppOptions public OpenCoverRegister OpenCoverRegister { get; set; } public string OpenCoverTarget { get; set; } public string OpenCoverTargetArgs { get; set; } + public bool ShowCoverageInGlyphMargin { get; set; } + public bool ShowCoveredInGlyphMargin { get; set; } + public bool ShowUncoveredInGlyphMargin { get; set; } + public bool ShowPartiallyCoveredInGlyphMargin {get; set; } + public bool ShowDirtyInGlyphMargin { get; set; } + public bool ShowNewInGlyphMargin { get; set ; } + public bool ShowLineCoverageHighlighting { get; set; } + public bool ShowLineCoveredHighlighting { get; set; } + public bool ShowLineUncoveredHighlighting { get; set; } + public bool ShowLinePartiallyCoveredHighlighting { get; set; } + public bool ShowLineDirtyHighlighting { get; set; } + public bool ShowLineNewHighlighting { get; set; } + public bool ShowEditorCoverage { get; set; } + public bool UseEnterpriseFontsAndColors { get; set; } + public EditorCoverageColouringMode EditorCoverageColouringMode { get; set; } + public bool ShowNotIncludedInOverviewMargin { get; set; } + public bool ShowNotIncludedInGlyphMargin { get; set; } + public bool ShowLineNotIncludedHighlighting { get; set; } } } diff --git a/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplate_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplate_Tests.cs index 7a1cf606..ae82ff87 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplate_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/RunSettingsTemplate_Tests.cs @@ -2,7 +2,7 @@ using FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage; using System.Xml.Linq; using System.Xml.XPath; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; namespace FineCodeCoverageTests.MsCodeCoverage { diff --git a/FineCodeCoverageTests/MsCodeCoverage/TemplatedRunSettingsService_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/TemplatedRunSettingsService_Tests.cs index 3700264f..70d718c2 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/TemplatedRunSettingsService_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/TemplatedRunSettingsService_Tests.cs @@ -24,7 +24,7 @@ public void SetupSut() [TestCase(true)] [TestCase(false)] - public async Task Should_Create_Run_Settings_From_Template(bool isDotNetFramework) + public async Task Should_Create_Run_Settings_From_Template_Async(bool isDotNetFramework) { var mockCoverageProject = new Mock(); mockCoverageProject.SetupGet(cp => cp.IsDotNetFramework).Returns(isDotNetFramework); @@ -52,7 +52,7 @@ public async Task Should_Create_Run_Settings_From_Template(bool isDotNetFramewor } [Test] - public async Task Should_Create_Run_Settings_From_Configured_Custom_Template_If_Available() + public async Task Should_Create_Run_Settings_From_Configured_Custom_Template_If_Available_Async() { Mock mockCustomRunSettingsTemplateProvider = autoMocker.GetMock(); mockCustomRunSettingsTemplateProvider.Setup( @@ -80,7 +80,7 @@ public async Task Should_Create_Run_Settings_From_Configured_Custom_Template_If_ } [Test] - public async Task Should_Return_ExceptionReason_Result_If_Throws_Creating_RunSettings() + public async Task Should_Return_ExceptionReason_Result_If_Throws_Creating_RunSettings_Async() { var mockCoverageProject = new Mock(); mockCoverageProject.Setup(cp => cp.ProjectFile).Returns(@"C:\SomeProject\SomeProject.csproj"); @@ -97,7 +97,7 @@ public async Task Should_Return_ExceptionReason_Result_If_Throws_Creating_RunSet } [Test] - public async Task Should_Write_Generated_RunSettings() + public async Task Should_Write_Generated_RunSettings_Async() { SetupReplaceResult(new TemplateReplaceResult { Replaced = "RunSettings" }); @@ -110,7 +110,7 @@ public async Task Should_Write_Generated_RunSettings() } [Test] - public async Task Should_Return_ExceptionReason_Result_If_Throws_Writing_Generated_RunSettings() + public async Task Should_Return_ExceptionReason_Result_If_Throws_Writing_Generated_RunSettings_Async() { SetupReplaceResult(new TemplateReplaceResult { Replaced = "RunSettings" }); @@ -129,7 +129,7 @@ public async Task Should_Return_ExceptionReason_Result_If_Throws_Writing_Generat } [Test] - public async Task Should_Return_A_Result_With_No_ExceptionReason_When_No_Exception() + public async Task Should_Return_A_Result_With_No_ExceptionReason_When_No_Exception_Async() { var mockCoverageProject1 = new Mock(); mockCoverageProject1.Setup(cp => cp.ProjectFile).Returns(@"C:\SomeProject\SomeProject.csproj"); @@ -174,7 +174,7 @@ public async Task Should_Return_A_Result_With_No_ExceptionReason_When_No_Excepti } [Test] - public async Task Clean_Up_Should_Remove_Generated_Project_RunSettings() + public async Task Clean_Up_Should_Remove_Generated_Project_RunSettings_Async() { var coverageProjects = new List { new Mock().Object}; await templatedRunSettingsService.CleanUpAsync(coverageProjects); diff --git a/FineCodeCoverageTests/MsCodeCoverage/UserRunSettingsService_AddFCCSettings_Tests.cs b/FineCodeCoverageTests/MsCodeCoverage/UserRunSettingsService_AddFCCSettings_Tests.cs index 9c24d5ac..ad679dbc 100644 --- a/FineCodeCoverageTests/MsCodeCoverage/UserRunSettingsService_AddFCCSettings_Tests.cs +++ b/FineCodeCoverageTests/MsCodeCoverage/UserRunSettingsService_AddFCCSettings_Tests.cs @@ -5,7 +5,7 @@ using Moq; using Microsoft.VisualStudio.TestWindow.Extensibility; using System.Collections.Generic; -using FineCodeCoverageTests.Test_helpers; +using FineCodeCoverageTests.TestHelpers; namespace FineCodeCoverageTests.MsCodeCoverage { diff --git a/FineCodeCoverageTests/OpenCoverUtil_Tests.cs b/FineCodeCoverageTests/OpenCoverUtil_Tests.cs index e4f2f947..cdde77fb 100644 --- a/FineCodeCoverageTests/OpenCoverUtil_Tests.cs +++ b/FineCodeCoverageTests/OpenCoverUtil_Tests.cs @@ -50,7 +50,7 @@ private void Initialize() [TestCase(true)] [TestCase(false)] - public async Task Should_Delete_The_Test_Pdb_When_RunOpenCoverAsync_And_IncludeTestAssembly_Is_False(bool includeTestAssembly) + public async Task Should_Delete_The_Test_Pdb_When_RunOpenCoverAsync_And_IncludeTestAssembly_Is_False_Async(bool includeTestAssembly) { var ct = CancellationToken.None; mocker.Setup>(openCoverExeArgumentsProvider => openCoverExeArgumentsProvider.Provide( diff --git a/FineCodeCoverageTests/PackageLoader_Tests.cs b/FineCodeCoverageTests/PackageLoader_Tests.cs index 5f000aae..d425e582 100644 --- a/FineCodeCoverageTests/PackageLoader_Tests.cs +++ b/FineCodeCoverageTests/PackageLoader_Tests.cs @@ -21,7 +21,9 @@ public void SetUp() [Test] - public void Should_Not_Be_InitializedFromTestContainerDiscoverer_If_LoadPackageAsync() +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods + public void Should_Not_Be_InitializedFromTestContainerDiscoverer_If_Not_LoadPackageAsync() +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods { Assert.That(packageLoader.InitializedFromTestContainerDiscoverer, Is.False); } diff --git a/FineCodeCoverageTests/Properties/AssemblyInfo.cs b/FineCodeCoverageTests/Properties/AssemblyInfo.cs index 5f7527fa..5186fd03 100644 --- a/FineCodeCoverageTests/Properties/AssemblyInfo.cs +++ b/FineCodeCoverageTests/Properties/AssemblyInfo.cs @@ -34,3 +34,5 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/FineCodeCoverageTests/ScriptManager_Tests.cs b/FineCodeCoverageTests/ScriptManager_Tests.cs index 59d19cf4..567a44ba 100644 --- a/FineCodeCoverageTests/ScriptManager_Tests.cs +++ b/FineCodeCoverageTests/ScriptManager_Tests.cs @@ -52,10 +52,12 @@ public void DocumentFocused_Should_Send_Message() } [Test] - public async Task Should_Call_SourceFileOpender_When_OpenFile() + public async Task Should_Call_SourceFileOpender_When_OpenFile_Async() { scriptManager.OpenFile("aname", "q.cname", 2, 3); +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks await scriptManager.openFileTask; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks sourceFileOpener.Verify(engine => engine.OpenFileAsync("aname", "q.cname", 2, 3)); } } diff --git a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs index effaa881..033c0c8c 100644 --- a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs +++ b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs @@ -124,7 +124,9 @@ public void SetUp() testContainerDiscoverer = mocker.Create(); testContainerDiscoverer.RunAsync = (taskProvider) => { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits taskProvider().Wait(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits }; var mockTestOperationStateInvocationManager = mocker.GetMock(); mockTestOperationStateInvocationManager.Setup(testOperationStateInvocationManager => testOperationStateInvocationManager.CanInvoke(It.IsAny())).Returns(true); @@ -172,7 +174,9 @@ public void Should_Stop_Ms_CodeCoverage_When_TestExecutionStarting_And_Ms_Code_C [TestCase(MsCodeCoverageCollectionStatus.Collecting, false)] [TestCase(MsCodeCoverageCollectionStatus.NotCollecting, false)] [TestCase(MsCodeCoverageCollectionStatus.Error, false)] +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods public void Should_Notify_MsCodeCoverage_When_Test_Execution_Not_Finished_IfCollectingAsync(MsCodeCoverageCollectionStatus status, bool cancelling) +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods { var mockMsCodeCoverageRunSettingsService = SetMsCodeCoverageCollecting(status); var operation = new Mock().Object; @@ -263,7 +267,7 @@ public void Should_Collect_Ms_Code_Coverage_When_TestExecutionFinished_And_Ms_Co } [Test] - public async Task Should_ReloadCoverage_When_TestExecutionStarting_And_Settings_RunInParallel_Is_True() + public async Task Should_ReloadCoverage_When_TestExecutionStarting_And_Settings_RunInParallel_Is_True_Async() { SetUpOptions(mockAppOptions => { @@ -313,7 +317,7 @@ public void Should_ReloadCoverage_When_TestExecutionStarting_And_Settings_RunInP [TestCase(false, 10, 1, 0, false, Description = "Should not run when tests fail if settings RunWhenTestsFail is false")] [TestCase(false, 0, 1, 1, false, Description = "Should not run when total tests does not exceed the RunWhenTestsExceed setting")] [TestCase(false, 0, 1, 0, true, Description = "Should run when total tests does not exceed the RunWhenTestsExceed setting")] - public async Task Conditional_Run_Coverage_When_TestExecutionFinished(bool runWhenTestsFail, long numberFailedTests, long totalTests, int runWhenTestsExceed, bool expectReloadedCoverage) + public async Task Conditional_Run_Coverage_When_TestExecutionFinished_Async(bool runWhenTestsFail, long numberFailedTests, long totalTests, int runWhenTestsExceed, bool expectReloadedCoverage) { var (operation, coverageProjects, mockTestOperation) = SetUpForProceedPath(); mockTestOperation.Setup(o => o.FailedTests).Returns(numberFailedTests); diff --git a/FineCodeCoverageTests/TestHelpers/ExportsInitializable.cs b/FineCodeCoverageTests/TestHelpers/ExportsInitializable.cs new file mode 100644 index 00000000..2a18f90f --- /dev/null +++ b/FineCodeCoverageTests/TestHelpers/ExportsInitializable.cs @@ -0,0 +1,18 @@ +using FineCodeCoverage.Core.Initialization; +using NUnit.Framework; +using System; +using System.ComponentModel.Composition; +using System.Linq; + +namespace FineCodeCoverageTests.Test_helpers +{ + internal static class ExportsInitializable + { + public static void Should_Export_IInitializable(Type type) + { + var exportsIInitializable = type.GetCustomAttributes(typeof(ExportAttribute), false).Any(ea => (ea as ExportAttribute).ContractType == typeof(IInitializable)); + Assert.That(exportsIInitializable, Is.True); + Assert.That(type.GetInterfaces().Any(i => i == typeof(IInitializable)), Is.True); + } + } +} diff --git a/FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs b/FineCodeCoverageTests/TestHelpers/MefOrderAssertions.cs similarity index 80% rename from FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs rename to FineCodeCoverageTests/TestHelpers/MefOrderAssertions.cs index b2154e39..3d70e05c 100644 --- a/FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs +++ b/FineCodeCoverageTests/TestHelpers/MefOrderAssertions.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using FineCodeCoverage.Core.Utilities; using NUnit.Framework; -namespace FineCodeCoverageTests.Test_helpers +namespace FineCodeCoverageTests.TestHelpers { public static class MefOrderAssertions { @@ -26,11 +23,7 @@ public static void InterfaceExportsHaveConsistentOrder(Type interfaceType) var derivations = types.Where(t => t != interfaceType && interfaceType.IsAssignableFrom(t)); var orders = derivations.Select(d => { - var orderAttribute = GetOrderAtrribute(d); - if (orderAttribute == null) - { - throw new Exception("Missing mef attribute"); - } + var orderAttribute = GetOrderAtrribute(d) ?? throw new Exception("Missing mef attribute"); if (orderAttribute.ContractType != interfaceType) { throw new Exception("Incorrect contract type"); diff --git a/FineCodeCoverageTests/TestHelpers/MoqAssertionsHelper.cs b/FineCodeCoverageTests/TestHelpers/MoqAssertionsHelper.cs new file mode 100644 index 00000000..32ff43dc --- /dev/null +++ b/FineCodeCoverageTests/TestHelpers/MoqAssertionsHelper.cs @@ -0,0 +1,9 @@ +using Moq; + +namespace FineCodeCoverageTests.TestHelpers +{ + internal static class MoqAssertionsHelper + { + public static Times ExpectedTimes(bool expected) => expected ? Times.Once() : Times.Never(); + } +} diff --git a/FineCodeCoverageTests/TestHelpers/MoqExtensions.cs b/FineCodeCoverageTests/TestHelpers/MoqExtensions.cs new file mode 100644 index 00000000..5cb21310 --- /dev/null +++ b/FineCodeCoverageTests/TestHelpers/MoqExtensions.cs @@ -0,0 +1,24 @@ +using Moq; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverageTests.TestHelpers +{ + internal static class MoqExtensions + { + public static IEnumerable GetMethodInvocations(this IInvocationList invocationList, string methodName) + { + return invocationList.Where(invocation => invocation.Method.Name == methodName); + } + + public static IEnumerable> GetMethodInvocationArguments(this IInvocationList invocationList, string methodName) + { + return invocationList.GetMethodInvocations(methodName).Select(invocation => invocation.Arguments); + } + + public static IEnumerable GetMethodInvocationSingleArgument(this IInvocationList invocationList, string methodName) + { + return invocationList.GetMethodInvocationArguments(methodName).Select(args => (T)args.Single()); + } + } +} diff --git a/FineCodeCoverageTests/TestHelpers/MoqMatchers.cs b/FineCodeCoverageTests/TestHelpers/MoqMatchers.cs new file mode 100644 index 00000000..1a1759ca --- /dev/null +++ b/FineCodeCoverageTests/TestHelpers/MoqMatchers.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; + +namespace FineCodeCoverageTests.TestHelpers +{ + internal static class MoqMatchers + { + internal static IEnumerable EnumerableExpected(IEnumerable expected, Func comparer) + { + return Match.Create>(enumerableArg => + { + var list = enumerableArg.ToList(); + var expectedList = expected.ToList(); + if (list.Count != expectedList.Count) + { + return false; + } + for (var i = 0; i < list.Count; i++) + { + if (!comparer(list[i], expectedList[i])) + { + return false; + } + } + return true; + }); + } + } +} diff --git a/FineCodeCoverageTests/Test helpers/TestThreadHelper.cs b/FineCodeCoverageTests/TestHelpers/TestThreadHelper.cs similarity index 57% rename from FineCodeCoverageTests/Test helpers/TestThreadHelper.cs rename to FineCodeCoverageTests/TestHelpers/TestThreadHelper.cs index e0ef7dbd..5dc143ba 100644 --- a/FineCodeCoverageTests/Test helpers/TestThreadHelper.cs +++ b/FineCodeCoverageTests/TestHelpers/TestThreadHelper.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; -namespace FineCodeCoverageTests.Test_helpers +namespace FineCodeCoverageTests.TestHelpers { internal class TestThreadHelper : IThreadHelper { @@ -14,7 +14,16 @@ internal class TestJoinableTaskFactory : IJoinableTaskFactory { public void Run(Func asyncMethod) { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits asyncMethod().Wait(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } + + public T Run(Func> asyncMethod) + { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + return asyncMethod().GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits } public Task SwitchToMainThreadAsync(CancellationToken cancellationToken = default) diff --git a/FineCodeCoverageTests/Test helpers/XmlAssert.cs b/FineCodeCoverageTests/TestHelpers/XmlAssert.cs similarity index 89% rename from FineCodeCoverageTests/Test helpers/XmlAssert.cs rename to FineCodeCoverageTests/TestHelpers/XmlAssert.cs index e1b24c40..b30a0c79 100644 --- a/FineCodeCoverageTests/Test helpers/XmlAssert.cs +++ b/FineCodeCoverageTests/TestHelpers/XmlAssert.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Org.XmlUnit.Builder; -namespace FineCodeCoverageTests.Test_helpers +namespace FineCodeCoverageTests.TestHelpers { internal static class XmlAssert { diff --git a/FineCodeCoverageTests/app.config b/FineCodeCoverageTests/app.config index 0a78aff9..0ebfba3c 100644 --- a/FineCodeCoverageTests/app.config +++ b/FineCodeCoverageTests/app.config @@ -8,12 +8,72 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FineCodeCoverageTests/packages.config b/FineCodeCoverageTests/packages.config deleted file mode 100644 index b7904659..00000000 --- a/FineCodeCoverageTests/packages.config +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 825c97a2..840b2cc8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Fine Code Coverage +[![Highlights video](https://img.youtube.com/vi/CvrySUcTi7I/0.jpg)](https://youtu.be/CvrySUcTi7I) + [![Build status](https://ci.appveyor.com/api/projects/status/yq8s0ridnphpx4ig?svg=true)](https://ci.appveyor.com/project/FortuneN/finecodecoverage) Download this extension from the [Visual Studio Market Place ( vs 2019 )](https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage), [Visual Studio Market Place ( vs 2022 )](https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage2022) @@ -24,11 +26,61 @@ Assembly level exclusions and inclusions can be achieved - see ExcludeAssemblies Configuration is ( mostly ) determined from Visual Studio options, finecodecoverage-settings.xml files and project msbuild properties. All of these settings are optional. For options that have a project scope, these settings form a hierarchy where lower levels override or, for collections, override or merge with the level above. This is described in detail further on. -Regardless of the coverage tool employed the process begins with FCC reacting to the test explorer in visual studio. One of the 3 coverage tools provides the coverage results that are presented as a single unified report in the Fine Code Coverage Tool Window. The report shows line and branch coverage and risk hotspots with the facility to open your class files, that will have coloured margins to indicate uncovered or partially covered code. +Regardless of the coverage tool employed the process begins with FCC reacting to the test explorer in visual studio. One of the 3 coverage tools provides the coverage results and the results can be opened from buttons on the Fine Code Coverage Tool Window. This coverage is not dynamic and represents the coverage obtained from the last time you executed tests. When the coverage becomes outdated, you can click the 'FCC Clear UI' button in Tools or run coverage again. Details of how FCC is progressing with code coverage can be found in the Coverage Log tab in the Fine Code Coverage Tool Window with more detailed logs in the FCC Output Window Pane. If you experience issues then providing the logs from the output window will help to understand the nature of the problem. +### Coverage Result Presentation +### Report +Present a single unified report in the Fine Code Coverage Tool Window. The report shows line and branch coverage and risk hotspots with the facility to open your class files. + +### Editor + +Coloured margins to indicate the coverage status of your code. Instrumented ( included and analysable) lines of code are either covered, uncovered or partially covered which means that not all branches were executed. + +FCC provides the concept of dirty regions where previously instrumented code will no longer show instrumented status once you have change the code. + +For C# and Visual Basic provides further coverage information : + +FCC also allows you to see code that was not included in coverage and new lines that have been added since the last coverage run. + +Both dirty and new line colouring needs to be turned on in options. + +If desired, lines can be highlighted too by setting the available Visual Studio options. Read on for more details. + +The colours can be controlled via Visual Studio / Tools / Options / Environment / Fonts and Colors / Text Editor / Display Items : + +For Visual Studio Community, Professional and Enterprise you can use the settings + +Coverage Touched Area FCC + +Coverage Partially Touched Area FCC + +Coverage Not Touched Area FCC + +Coverage Dirty Area FCC + +Coverage New Lines Area FCC + +Coverage Not Included Area FCC + + +For versions that supply the items below FCC will use these by default over the equivalent FCC items so that colours defined in themes can be used. +If you wish to be consistent for the 5 available items you can set UseEnterpriseFontsAndColors to false. + + +Coverage Not Touched Area + +Coverage Partially Touched Area + +Coverage Touched Area + + +You can turn off editor colouring by setting the visual studio option EditorCoverageColouringMode to Off. +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. + ## 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. @@ -90,21 +142,19 @@ This can be changed with the ToolsDirectory Visual Studio option. Ensure that t --- -### Watch Introduction Video - ### Highlights unit test code coverage Run a(some) unit test(s) and ... #### Get highlights on the code being tested and the code doing the testing ![Highlights](Art/preview-coverage.png) -#### See Coverage View +#### Report Coverage View ![Coverage View](Art/Output-Coverage.png) -#### See Summary View +#### Report Summary View ![Summary View](Art/Output-Summary.png) -#### See Risk Hotspots View +#### Report Risk Hotspots View ![Risk Hotspots View](Art/Output-RiskHotspots.png) ## Project configuration @@ -231,13 +281,29 @@ If you are using option 1) then project and global options will only be used whe |Option |Description| |--|---| |**Common**|| -|CoverageColoursFromFontsAndColours|Specify true to use Environment / Fonts and Colors / Text Editor for editor Coverage colouring ( if present). Coverage Touched Area / Coverage Not Touched Area / Coverage Partially Touched Area. When false colours used are Green, Red and Gold.| +|EditorCoverageColouringMode|Set to Off, or Set to DoNotUseRoslynWhenTextChanges if there is a performance issue| +|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| +|ShowUncoveredInGlyphMargin|Set to false to prevent uncovered marks in the glyph margin| +|ShowPartiallyCoveredInGlyphMargin|Set to false to prevent partially covered marks in the glyph margin| +|ShowDirtyInGlyphMargin|Set to true to show dirty marks in the glyph margin| +|ShowNewInGlyphMargin|Set to true to show new line marks in the glyph margin| |ShowCoverageInOverviewMargin|Set to false to prevent coverage marks in the overview margin| |ShowCoveredInOverviewMargin|Set to false to prevent covered marks in the overview margin| |ShowUncoveredInOverviewMargin|Set to false to prevent uncovered marks in the overview margin| |ShowPartiallyCoveredInOverviewMargin|Set to false to prevent partially covered marks in the overview margin| +|ShowDirtyInOverviewMargin|Set to true to show dirty marks in the overview margin| +|ShowNewInOverviewMargin|Set to true to show new line marks in the overview margin| +|ShowLineCoverageHighlighting|Set to true to allow coverage line highlighting| +|ShowLineCoveredHighlighting|Set to false to prevent covered line highlighting| +|ShowLineUncoveredHighlighting|Set to false to prevent uncovered line highlighting| +|ShowLinePartiallyCoveredHighlighting|Set to false to prevent partially covered line highlighting| +|ShowLineDirtyHighlighting|Set to true to show dirty line highlighting| +|ShowLineNewHighlighting|Set to true to show new line highlighting| +|UseEnterpriseFontsAndColors|Set to false to use FCC Fonts And Colors items| |ShowToolWindowToolbar|Set to false to hide the toolbar on the tool window. Requires restarting Visual Studio. The toolbar has buttons for viewing the Cobertura xml and the risk hotspots.| -|FCCSolutionOutputDirectoryName|To have fcc output visible in a sub folder of your solution provide this name| +|FCC Solution Output Directory Name|To have fcc output visible in a sub folder of your solution provide this name| |ToolsDirectory|Folder to which copy tools subfolder. Must alredy exist. Requires restart of VS.| |ThresholdForCyclomaticComplexity| When [cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) exceeds this value for a method then the method will be present in the risk hotspots tab. | |StickyCoverageTable|Set to true for coverage table to have a sticky thead.| @@ -269,8 +335,8 @@ If you are using option 1) then project and global options will only be used whe |ModulePathsInclude|Include - Matches assemblies specified by assembly name or file path.| |CompanyNamesExclude|Exclude - Matches assemblies by the Company attribute.| |CompanyNamesInclude|Include - Matches assemblies by the Company attribute.| -|PublicKeyTokensExclude|Exclude - Matches signed assemblies by the public key token.| -|PublicKeyTokensInclude|Include - Matches signed assemblies by the public key token.| +|PublicKeyTokens Exclude|Exclude - Matches signed assemblies by the public key token.| +|PublicKeyTokens Include|Include - Matches signed assemblies by the public key token.| |SourcesExclude|Exclude - Matches elements by the path name of the source file in which they're defined.| |SourcesInclude|Include - Matches elements by the path name of the source file in which they're defined.| |AttributesExclude|Exclude - Matches elements that have the specified attribute. Specify the full name of the attribute| @@ -366,4 +432,4 @@ used by this project. | Provider | Type | Link | |:---------|:---------:|:---------------------------------------------------------------------------------------------------------------------------------:| | Paypal | Once | [](https://paypal.me/FortuneNgwenya) | -| Librepay | Recurring | [Donate using Liberapay](https://liberapay.com/FortuneN/donate) | +| Liberapay | Recurring | [Donate using Liberapay](https://liberapay.com/FortuneN/donate) | diff --git a/SharedProject/Core/Cobertura/Classes.cs b/SharedProject/Core/Cobertura/Classes.cs deleted file mode 100644 index 6254093d..00000000 --- a/SharedProject/Core/Cobertura/Classes.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Xml.Serialization; - -// Generated from cobertura XML schema - -namespace FineCodeCoverage.Engine.Cobertura -{ - [XmlRoot(ElementName = "classes")] - [ExcludeFromCodeCoverage] - public class Classes - { - [XmlElement(ElementName = "class")] - public List Class { get; set; } - } -} \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/CoberturaDerializer.cs b/SharedProject/Core/Cobertura/CoberturaDerializer.cs new file mode 100644 index 00000000..baf75469 --- /dev/null +++ b/SharedProject/Core/Cobertura/CoberturaDerializer.cs @@ -0,0 +1,23 @@ +using System.Xml.Serialization; +using System.Xml; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; + +namespace FineCodeCoverage.Engine.Cobertura +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ICoberturaDeserializer))] + internal class CoberturaDerializer : ICoberturaDeserializer + { + private readonly XmlSerializer xmlSerializer = new XmlSerializer(typeof(CoverageReport)); + private readonly XmlReaderSettings xmlReaderSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; + public CoverageReport Deserialize(string xmlFile) + { + using (var reader = XmlReader.Create(xmlFile, xmlReaderSettings)) + { + var report = (CoverageReport)xmlSerializer.Deserialize(reader); + return report; + } + } + } +} diff --git a/SharedProject/Core/Cobertura/CoberturaUtil.cs b/SharedProject/Core/Cobertura/CoberturaUtil.cs index 6c162f2b..40c06a54 100644 --- a/SharedProject/Core/Cobertura/CoberturaUtil.cs +++ b/SharedProject/Core/Cobertura/CoberturaUtil.cs @@ -1,75 +1,124 @@ -using System.Xml; -using System.Linq; -using System.Xml.Serialization; +using System.Linq; using System.Collections.Generic; using FineCodeCoverage.Engine.Model; using System.ComponentModel.Composition; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Engine.Cobertura { - [Export(typeof(ICoberturaUtil))] internal class CoberturaUtil:ICoberturaUtil - { - private readonly XmlSerializer SERIALIZER = new XmlSerializer(typeof(CoverageReport)); - private readonly XmlReaderSettings READER_SETTINGS = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; - private CoverageReport coverageReport; + { + private readonly ICoberturaDeserializer coberturaDeserializer; + private readonly IFileLineCoverageFactory fileLineCoverageFactory; + private CoverageReport coverageReport; + private IFileLineCoverage fileLineCoverage; - private CoverageReport LoadReport(string xmlFile) - { - using (var reader = XmlReader.Create(xmlFile, READER_SETTINGS)) + private class FileLine : ILine + { + public FileLine(Line line) { - var report = (CoverageReport)SERIALIZER.Deserialize(reader); - return report; - } - } + CoverageType = GetCoverageType(line); + Number = line.Number; + } + + private static CoverageType GetCoverageType(Line line) + { + var lineConditionCoverage = line.ConditionCoverage?.Trim(); + + var coverageType = CoverageType.NotCovered; - public FileLineCoverage ProcessCoberturaXml(string xmlFile) + if (line.Hits > 0) + { + coverageType = CoverageType.Covered; + + if (!string.IsNullOrWhiteSpace(lineConditionCoverage) && !lineConditionCoverage.StartsWith("100")) + { + coverageType = CoverageType.Partial; + } + } + return coverageType; + } + public int Number { get; } + public CoverageType CoverageType { get; } + } + + [ImportingConstructor] + public CoberturaUtil( + ICoberturaDeserializer coberturaDeserializer, + IFileRenameListener fileRenameListener, + IFileLineCoverageFactory fileLineCoverageFactory + ) { - var fileLineCoverage = new FileLineCoverage(); + fileRenameListener.ListenForFileRename((oldFile, newFile) => + { + fileLineCoverage?.UpdateRenamed(oldFile, newFile); + }); + this.coberturaDeserializer = coberturaDeserializer; + this.fileLineCoverageFactory = fileLineCoverageFactory; + } - coverageReport = LoadReport(xmlFile); - foreach (var package in coverageReport.Packages.Package) - { - foreach (var classs in package.Classes.Class) - { - fileLineCoverage.Add(classs.Filename, classs.Lines.Line); - } - } + public IFileLineCoverage ProcessCoberturaXml(string xmlFile) + { + fileLineCoverage = fileLineCoverageFactory.Create(); + + coverageReport = coberturaDeserializer.Deserialize(xmlFile); - fileLineCoverage.Completed(); + AddThenSort(); return fileLineCoverage; } + private void AddThenSort() + { + foreach (var package in coverageReport.Packages) + { + foreach (var classs in package.Classes) + { + fileLineCoverage.Add(classs.Filename, classs.Lines.Select(l => new FileLine(l)).Cast()); + } + } + + fileLineCoverage.Sort(); + } + + private Package GetPackage(string assemblyName) + { + return coverageReport.Packages.SingleOrDefault(package => package.Name.Equals(assemblyName)); + } + public string[] GetSourceFiles(string assemblyName, string qualifiedClassName, int file) { // Note : There may be more than one file; e.g. in the case of partial classes // For riskhotspots the file parameter is available ( otherwise is -1 ) - var package = coverageReport - .Packages.Package - .SingleOrDefault(x => x.Name.Equals(assemblyName)); + var package = GetPackage(assemblyName); + return package == null ? new string[0] : GetSourceFilesFromPackage(package, qualifiedClassName, file); + } - if (package == null) - { - return new string[0]; - } + private static string[] GetSourceFilesFromPackage(Package package, string qualifiedClassName, int file) + { + var classes = GetClasses(package, qualifiedClassName); + return GetSourceFiles(classes, file); + } - var classes = package - .Classes.Class - .Where(x => x.Name.Equals(qualifiedClassName)); + private static IEnumerable GetClasses(Package package, string qualifiedClassName) + { + return package.Classes.Where(x => x.Name.Equals(qualifiedClassName)); + } - if (file != -1) - { - classes = new List { classes.ElementAt(file) }; - } + private static string[] GetSourceFiles(IEnumerable classes, int file) + { + if (file != -1) + { + classes = new List { classes.ElementAt(file) }; + } - var classFiles = classes - .Select(x => x.Filename) - .ToArray(); + var classFiles = classes + .Select(x => x.Filename) + .ToArray(); - return classFiles; - } - } + return classFiles; + } + } } \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/CoverageType.cs b/SharedProject/Core/Cobertura/CoverageType.cs new file mode 100644 index 00000000..a3dee285 --- /dev/null +++ b/SharedProject/Core/Cobertura/CoverageType.cs @@ -0,0 +1,4 @@ +namespace FineCodeCoverage.Engine.Model +{ + public enum CoverageType { Covered, Partial, NotCovered } +} diff --git a/SharedProject/Core/Cobertura/FileLineCoverage.cs b/SharedProject/Core/Cobertura/FileLineCoverage.cs new file mode 100644 index 00000000..822b1c97 --- /dev/null +++ b/SharedProject/Core/Cobertura/FileLineCoverage.cs @@ -0,0 +1,62 @@ +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Impl; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverage.Engine.Model +{ + // FileLineCoverage maps from a filename to the list of lines in the file + internal class FileLineCoverage : IFileLineCoverage + { + private readonly Dictionary> m_coverageLines = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public void Add(string filename, IEnumerable lines) + { + if (!m_coverageLines.TryGetValue(filename, out var fileCoverageLines)) + { + fileCoverageLines = new List(); + m_coverageLines.Add(filename, fileCoverageLines); + } + + fileCoverageLines.AddRange(lines); + } + + public void Sort() + { + foreach (var lines in m_coverageLines.Values) + lines.Sort((a, b) => a.Number - b.Number); + } + + public IEnumerable GetLines(string filePath) + { + if (!m_coverageLines.TryGetValue(filePath, out var lines)) + { + lines = Enumerable.Empty().ToList(); + } + return lines; + + } + public IEnumerable GetLines(string filePath, int startLineNumber, int endLineNumber) + { + if (!m_coverageLines.TryGetValue(filePath, out var lines)) + yield break; + + int first = lines.LowerBound(line => startLineNumber - line.Number); + if (first != -1) + { + for (int it = first; it < lines.Count && lines[it].Number <= endLineNumber; ++it) + yield return lines[it]; + } + } + + public void UpdateRenamed(string oldFilePath, string newFilePath) + { + if(m_coverageLines.TryGetValue(oldFilePath, out var lines)) + { + m_coverageLines.Add(newFilePath, lines); + m_coverageLines.Remove(oldFilePath); + } + } + } +} diff --git a/SharedProject/Core/Cobertura/FileLineCoverageFactory.cs b/SharedProject/Core/Cobertura/FileLineCoverageFactory.cs new file mode 100644 index 00000000..2b95a939 --- /dev/null +++ b/SharedProject/Core/Cobertura/FileLineCoverageFactory.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Engine.Cobertura +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IFileLineCoverageFactory))] + internal class FileLineCoverageFactory : IFileLineCoverageFactory + { + public IFileLineCoverage Create() => new FileLineCoverage(); + } +} diff --git a/SharedProject/Core/Cobertura/ICoberturaDeserializer.cs b/SharedProject/Core/Cobertura/ICoberturaDeserializer.cs new file mode 100644 index 00000000..e99fba2e --- /dev/null +++ b/SharedProject/Core/Cobertura/ICoberturaDeserializer.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Engine.Cobertura +{ + internal interface ICoberturaDeserializer + { + CoverageReport Deserialize(string xmlFile); + } +} diff --git a/SharedProject/Core/Cobertura/ICoberturaUtil.cs b/SharedProject/Core/Cobertura/ICoberturaUtil.cs index 41b1717a..c30f5d8d 100644 --- a/SharedProject/Core/Cobertura/ICoberturaUtil.cs +++ b/SharedProject/Core/Cobertura/ICoberturaUtil.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Engine.Model; namespace FineCodeCoverage.Engine.Cobertura { interface ICoberturaUtil { - FileLineCoverage ProcessCoberturaXml(string xmlFile); + IFileLineCoverage ProcessCoberturaXml(string xmlFile); string[] GetSourceFiles(string assemblyName, string qualifiedClassName, int file); } } \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/IFileLineCoverage.cs b/SharedProject/Core/Cobertura/IFileLineCoverage.cs new file mode 100644 index 00000000..b9eafc7d --- /dev/null +++ b/SharedProject/Core/Cobertura/IFileLineCoverage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Engine.Model +{ + internal interface IFileLineCoverage + { + void Add(string filename, IEnumerable line); + IEnumerable GetLines(string filePath, int startLineNumber, int endLineNumber); + IEnumerable GetLines(string filePath); + void Sort(); + void UpdateRenamed(string oldFile, string newFile); + } +} diff --git a/SharedProject/Core/Cobertura/IFileLineCoverageFactory.cs b/SharedProject/Core/Cobertura/IFileLineCoverageFactory.cs new file mode 100644 index 00000000..e78f01d4 --- /dev/null +++ b/SharedProject/Core/Cobertura/IFileLineCoverageFactory.cs @@ -0,0 +1,9 @@ +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Engine.Cobertura +{ + internal interface IFileLineCoverageFactory + { + IFileLineCoverage Create(); + } +} diff --git a/SharedProject/Core/Cobertura/ILine.cs b/SharedProject/Core/Cobertura/ILine.cs new file mode 100644 index 00000000..6916e1db --- /dev/null +++ b/SharedProject/Core/Cobertura/ILine.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Engine.Model +{ + internal interface ILine + { + int Number { get; } + CoverageType CoverageType { get; } + } +} diff --git a/SharedProject/Core/Cobertura/Lines.cs b/SharedProject/Core/Cobertura/Lines.cs deleted file mode 100644 index ce4ab136..00000000 --- a/SharedProject/Core/Cobertura/Lines.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Xml.Serialization; - -// Generated from cobertura XML schema - -namespace FineCodeCoverage.Engine.Cobertura -{ - [XmlRoot(ElementName = "lines")] - [ExcludeFromCodeCoverage] - public class Lines - { - [XmlElement(ElementName = "line")] - public List Line { get; set; } - } -} \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/Methods.cs b/SharedProject/Core/Cobertura/Methods.cs deleted file mode 100644 index ecb0199a..00000000 --- a/SharedProject/Core/Cobertura/Methods.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Xml.Serialization; - -// Generated from cobertura XML schema - -namespace FineCodeCoverage.Engine.Cobertura -{ - [XmlRoot(ElementName = "methods")] - [ExcludeFromCodeCoverage] - public class Methods - { - [XmlElement(ElementName = "method")] - public List Method { get; set; } - } -} \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/Packages.cs b/SharedProject/Core/Cobertura/Packages.cs deleted file mode 100644 index c1108bd1..00000000 --- a/SharedProject/Core/Cobertura/Packages.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Xml.Serialization; - -// Generated from cobertura XML schema - -namespace FineCodeCoverage.Engine.Cobertura -{ - [XmlRoot(ElementName = "packages")] - [ExcludeFromCodeCoverage] - public class Packages - { - [XmlElement(ElementName = "package")] - public List Package { get; set; } - } -} \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/Class.cs b/SharedProject/Core/Cobertura/Report/Class.cs similarity index 65% rename from SharedProject/Core/Cobertura/Class.cs rename to SharedProject/Core/Cobertura/Report/Class.cs index 9ec23ddc..042babec 100644 --- a/SharedProject/Core/Cobertura/Class.cs +++ b/SharedProject/Core/Cobertura/Report/Class.cs @@ -1,19 +1,20 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; -// Generated from cobertura XML schema - namespace FineCodeCoverage.Engine.Cobertura { [XmlRoot(ElementName = "class")] [ExcludeFromCodeCoverage] public class Class { - [XmlElement(ElementName = "methods")] - public Methods Methods { get; set; } + [XmlArray(ElementName = "methods")] + [XmlArrayItem(ElementName = "method")] + public List Methods { get; set; } - [XmlElement(ElementName = "lines")] - public Lines Lines { get; set; } + [XmlArray(ElementName = "lines")] + [XmlArrayItem(ElementName = "line")] + public List Lines { get; set; } [XmlAttribute(AttributeName = "name")] public string Name { get; set; } diff --git a/SharedProject/Core/Cobertura/Condition.cs b/SharedProject/Core/Cobertura/Report/Condition.cs similarity index 100% rename from SharedProject/Core/Cobertura/Condition.cs rename to SharedProject/Core/Cobertura/Report/Condition.cs diff --git a/SharedProject/Core/Cobertura/Conditions.cs b/SharedProject/Core/Cobertura/Report/Conditions.cs similarity index 100% rename from SharedProject/Core/Cobertura/Conditions.cs rename to SharedProject/Core/Cobertura/Report/Conditions.cs diff --git a/SharedProject/Core/Cobertura/CoverageReport.cs b/SharedProject/Core/Cobertura/Report/CoverageReport.cs similarity index 83% rename from SharedProject/Core/Cobertura/CoverageReport.cs rename to SharedProject/Core/Cobertura/Report/CoverageReport.cs index 5c7fdef9..fe07a2fc 100644 --- a/SharedProject/Core/Cobertura/CoverageReport.cs +++ b/SharedProject/Core/Cobertura/Report/CoverageReport.cs @@ -1,8 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; -// Generated from cobertura XML schema - namespace FineCodeCoverage.Engine.Cobertura { [XmlRoot(ElementName = "coverage")] @@ -12,8 +11,9 @@ public class CoverageReport [XmlElement(ElementName = "sources")] public Sources Sources { get; set; } - [XmlElement(ElementName = "packages")] - public Packages Packages { get; set; } + [XmlArray(ElementName = "packages")] + [XmlArrayItem(ElementName = "package")] + public List Packages { get; set; } [XmlAttribute(AttributeName = "line-rate")] public float LineRate { get; set; } diff --git a/SharedProject/Core/Cobertura/Line.cs b/SharedProject/Core/Cobertura/Report/Line.cs similarity index 97% rename from SharedProject/Core/Cobertura/Line.cs rename to SharedProject/Core/Cobertura/Report/Line.cs index 2059f708..9415bed8 100644 --- a/SharedProject/Core/Cobertura/Line.cs +++ b/SharedProject/Core/Cobertura/Report/Line.cs @@ -23,5 +23,7 @@ public class Line [XmlAttribute(AttributeName = "condition-coverage")] public string ConditionCoverage { get; set; } - } + + } + } \ No newline at end of file diff --git a/SharedProject/Core/Cobertura/Method.cs b/SharedProject/Core/Cobertura/Report/Method.cs similarity index 74% rename from SharedProject/Core/Cobertura/Method.cs rename to SharedProject/Core/Cobertura/Report/Method.cs index 9315a654..178a1a8a 100644 --- a/SharedProject/Core/Cobertura/Method.cs +++ b/SharedProject/Core/Cobertura/Report/Method.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; // Generated from cobertura XML schema @@ -9,8 +10,9 @@ namespace FineCodeCoverage.Engine.Cobertura [ExcludeFromCodeCoverage] public class Method { - [XmlElement(ElementName = "lines")] - public Lines Lines { get; set; } + [XmlArray(ElementName = "lines")] + [XmlArrayItem(ElementName = "line")] + public List Lines { get; set; } [XmlAttribute(AttributeName = "name")] public string Name { get; set; } diff --git a/SharedProject/Core/Cobertura/Package.cs b/SharedProject/Core/Cobertura/Report/Package.cs similarity index 72% rename from SharedProject/Core/Cobertura/Package.cs rename to SharedProject/Core/Cobertura/Report/Package.cs index 9f12af24..0f509a67 100644 --- a/SharedProject/Core/Cobertura/Package.cs +++ b/SharedProject/Core/Cobertura/Report/Package.cs @@ -1,16 +1,16 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; -// Generated from cobertura XML schema - namespace FineCodeCoverage.Engine.Cobertura { [XmlRoot(ElementName = "package")] [ExcludeFromCodeCoverage] public class Package { - [XmlElement(ElementName = "classes")] - public Classes Classes { get; set; } + [XmlArray(ElementName = "classes")] + [XmlArrayItem(ElementName = "class")] + public List Classes { get; set; } [XmlAttribute(AttributeName = "name")] public string Name { get; set; } diff --git a/SharedProject/Core/Cobertura/Sources.cs b/SharedProject/Core/Cobertura/Report/Sources.cs similarity index 100% rename from SharedProject/Core/Cobertura/Sources.cs rename to SharedProject/Core/Cobertura/Report/Sources.cs diff --git a/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs b/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs index 4adce7dd..148032c4 100644 --- a/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs +++ b/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs @@ -96,7 +96,7 @@ internal interface ICoverletConsoleExecuteRequestProvider [Export(typeof(ICoverletConsoleExecuteRequestProvider))] internal class CoverletConsoleExecuteRequestProvider : ICoverletConsoleExecuteRequestProvider { - private List executors; + private readonly List executors; [ImportingConstructor] public CoverletConsoleExecuteRequestProvider( @@ -140,7 +140,6 @@ internal class CoverletConsoleUtil : ICoverletConsoleUtil private readonly ICoverletConsoleExecuteRequestProvider coverletConsoleExecuteRequestProvider; private readonly IFCCCoverletConsoleExecutor fccExecutor; private readonly ICoverletExeArgumentsProvider coverletExeArgumentsProvider; - private readonly List executors; [ImportingConstructor] public CoverletConsoleUtil( diff --git a/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorGeneratedCobertura.cs b/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorGeneratedCobertura.cs index 48bc0100..7d747377 100644 --- a/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorGeneratedCobertura.cs +++ b/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorGeneratedCobertura.cs @@ -21,11 +21,7 @@ private FileInfo GetCoberturaFile(string coverageOutputFolder) } public void CorrectPath(string coverageOutputFolder, string coverageOutputFile) { - var coberturaFile = GetCoberturaFile(coverageOutputFolder); - if (coberturaFile == null) - { - throw new Exception($"Data collector did not generate {collectorGeneratedCobertura}"); - } + var coberturaFile = GetCoberturaFile(coverageOutputFolder) ?? throw new Exception($"Data collector did not generate {collectorGeneratedCobertura}"); var guidDirectoryToDelete = coberturaFile.Directory; coberturaFile.MoveTo(coverageOutputFile); diff --git a/SharedProject/Core/FCCEngine.cs b/SharedProject/Core/FCCEngine.cs index d706f99c..fb40e0ee 100644 --- a/SharedProject/Core/FCCEngine.cs +++ b/SharedProject/Core/FCCEngine.cs @@ -3,7 +3,6 @@ using System.ComponentModel.Composition; using System.Linq; using System.Threading; -using FineCodeCoverage.Core.Initialization; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Cobertura; using FineCodeCoverage.Engine.Model; @@ -17,9 +16,9 @@ namespace FineCodeCoverage.Engine { internal enum ReloadCoverageStatus { Start, Done, Cancelled, Error, Initializing }; - internal class NewCoverageLinesMessage + internal sealed class NewCoverageLinesMessage { - public FileLineCoverage CoverageLines { get; set; } + public IFileLineCoverage CoverageLines { get; set; } } internal class CoverageTaskState @@ -30,7 +29,7 @@ internal class CoverageTaskState internal class ReportResult { - public FileLineCoverage FileLineCoverage { get; set; } + public IFileLineCoverage FileLineCoverage { get; set; } public string ProcessedReport { get; set; } public string HotspotsFile { get; set; } public string CoberturaFile { get; set; } @@ -88,7 +87,7 @@ IDisposeAwareTaskRunner disposeAwareTaskRunner this.solutionEvents = solutionEvents; this.eventAggregator = eventAggregator; this.disposeAwareTaskRunner = disposeAwareTaskRunner; - solutionEvents.AfterClosing += (s,args) => ClearOutputWindow(false); + solutionEvents.AfterClosing += (s,args) => ClearUI(false); appOptionsProvider.OptionsChanged += (appOptions) => { if (!appOptions.Enabled) @@ -126,10 +125,10 @@ public void Initialize(CancellationToken cancellationToken) msCodeCoverageRunSettingsService.Initialize(AppDataFolderPath, this,cancellationToken); } - public void ClearUI() + public void ClearUI(bool clearOutputWindowHistory = true) { ClearCoverageLines(); - ClearOutputWindow(true); + ClearOutputWindow(clearOutputWindowHistory); } private void ClearOutputWindow(bool withHistory) @@ -213,12 +212,12 @@ private void ClearCoverageLines() RaiseCoverageLines(null); } - private void RaiseCoverageLines(FileLineCoverage coverageLines) + private void RaiseCoverageLines(IFileLineCoverage coverageLines) { eventAggregator.SendMessage(new NewCoverageLinesMessage { CoverageLines = coverageLines}); } - private void UpdateUI(FileLineCoverage coverageLines, string reportHtml) + private void UpdateUI(IFileLineCoverage coverageLines, string reportHtml) { RaiseCoverageLines(coverageLines); if (reportHtml == null) @@ -300,10 +299,12 @@ private void CoverageTaskCompletion(System.Threading.Tasks.Task t, break; case System.Threading.Tasks.TaskStatus.RanToCompletion: LogReloadCoverageStatus(ReloadCoverageStatus.Done); +#pragma warning disable IDE0079 // Remove unnecessary suppression #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits UpdateUI(t.Result.FileLineCoverage, t.Result.ProcessedReport); RaiseReportFiles(t.Result); #pragma warning restore VSTHRD002 // Avoid problematic synchronous waits +#pragma warning restore IDE0079 // Remove unnecessary suppression break; } @@ -340,7 +341,7 @@ private void RunCancellableCoverageTask( { var vsLinkedCancellationTokenSource = Reset(); var vsShutdownLinkedCancellationToken = vsLinkedCancellationTokenSource.Token; - disposeAwareTaskRunner.RunAsync(() => + disposeAwareTaskRunner.RunAsyncFunc(() => { reloadCoverageTask = System.Threading.Tasks.Task.Run(async () => { diff --git a/SharedProject/Core/IFCCEngine.cs b/SharedProject/Core/IFCCEngine.cs index 4cb2b128..e3a2c464 100644 --- a/SharedProject/Core/IFCCEngine.cs +++ b/SharedProject/Core/IFCCEngine.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using FineCodeCoverage.Core.Initialization; using FineCodeCoverage.Engine.Model; namespace FineCodeCoverage.Engine @@ -12,7 +11,7 @@ internal interface IFCCEngine void StopCoverage(); void ReloadCoverage(Func>> coverageRequestCallback); void RunAndProcessReport(string[] coberturaFiles,Action cleanUp = null); - void ClearUI(); + void ClearUI(bool clearOutputWindowHistory = true); } } \ No newline at end of file diff --git a/SharedProject/Core/Initialization/Initializer.cs b/SharedProject/Core/Initialization/Initializer.cs index 1dcbb5fd..5ed0fb54 100644 --- a/SharedProject/Core/Initialization/Initializer.cs +++ b/SharedProject/Core/Initialization/Initializer.cs @@ -7,6 +7,8 @@ namespace FineCodeCoverage.Core.Initialization { + interface IInitializable { } + [Export(typeof(IInitializer))] [Export(typeof(IInitializeStatusProvider))] internal class Initializer : IInitializer @@ -24,7 +26,9 @@ public Initializer( IFCCEngine fccEngine, ILogger logger, ICoverageProjectFactory coverageProjectFactory, - IFirstTimeToolWindowOpener firstTimeToolWindowOpener + IFirstTimeToolWindowOpener firstTimeToolWindowOpener, + [ImportMany] + IInitializable[] initializables ) { this.fccEngine = fccEngine; diff --git a/SharedProject/Core/Initialization/PackageLoader.cs b/SharedProject/Core/Initialization/PackageLoader.cs index e8a3ecc9..c6d0d3f6 100644 --- a/SharedProject/Core/Initialization/PackageLoader.cs +++ b/SharedProject/Core/Initialization/PackageLoader.cs @@ -15,7 +15,7 @@ internal interface IShellPackageLoader [Export(typeof(IShellPackageLoader))] internal class ShellPackageLoader : IShellPackageLoader { - private IServiceProvider serviceProvider; + private readonly IServiceProvider serviceProvider; [ImportingConstructor] public ShellPackageLoader( diff --git a/SharedProject/Core/Model/CoverageProject.cs b/SharedProject/Core/Model/CoverageProject.cs index 9ca483da..b6409f49 100644 --- a/SharedProject/Core/Model/CoverageProject.cs +++ b/SharedProject/Core/Model/CoverageProject.cs @@ -172,10 +172,12 @@ public IAppOptions Settings { if (settings == null) { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously ThreadHelper.JoinableTaskFactory.Run(async () => { settings = await settingsManager.GetSettingsAsync(this); }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously } return settings; } @@ -291,12 +293,7 @@ private void SetExcludedReferencedProjects(List referencedPro private async Task> GetReferencedProjectsAsync() { - List referencedProjects = await SafeGetReferencedProjectsFromDteAsync(); - - if (referencedProjects == null) - { - referencedProjects = await GetReferencedProjectsFromProjectFileAsync(); - } + List referencedProjects = await SafeGetReferencedProjectsFromDteAsync() ?? await GetReferencedProjectsFromProjectFileAsync(); return referencedProjects; } diff --git a/SharedProject/Core/Model/CoverageProjectSettingsProvider.cs b/SharedProject/Core/Model/CoverageProjectSettingsProvider.cs index 922ba425..f5dc01f0 100644 --- a/SharedProject/Core/Model/CoverageProjectSettingsProvider.cs +++ b/SharedProject/Core/Model/CoverageProjectSettingsProvider.cs @@ -19,11 +19,7 @@ IVsBuildFCCSettingsProvider vsBuildFCCSettingsProvider } public async Task ProvideAsync(ICoverageProject coverageProject) { - var settingsElement = ProjectSettingsElementFromFCCLabelledPropertyGroup(coverageProject); - if (settingsElement == null) - { - settingsElement = await vsBuildFCCSettingsProvider.GetSettingsAsync(coverageProject.Id); - } + var settingsElement = ProjectSettingsElementFromFCCLabelledPropertyGroup(coverageProject) ?? await vsBuildFCCSettingsProvider.GetSettingsAsync(coverageProject.Id); return settingsElement; } diff --git a/SharedProject/Core/Model/FileLineCoverage.cs b/SharedProject/Core/Model/FileLineCoverage.cs deleted file mode 100644 index f7e409d7..00000000 --- a/SharedProject/Core/Model/FileLineCoverage.cs +++ /dev/null @@ -1,46 +0,0 @@ -using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Engine.Cobertura; -using System; -using System.Collections.Generic; - -namespace FineCodeCoverage.Engine.Model -{ - // FileLineCoverage maps from a filename to the list of lines in the file - internal class FileLineCoverage - { - private Dictionary> m_coverageLines = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - public void Add(string filename, IEnumerable lines) - { - if (!m_coverageLines.TryGetValue(filename, out var fileCoverageLines)) - { - fileCoverageLines = new List(); - m_coverageLines.Add(filename, fileCoverageLines); - } - - fileCoverageLines.AddRange(lines); - } - - public void Completed() - { - foreach (var lines in m_coverageLines.Values) - lines.Sort((a, b) => a.Number - b.Number); - } - - public IEnumerable GetLines(string filePath, int startLineNumber, int endLineNumber) - { - if (!m_coverageLines.TryGetValue(filePath, out var lines)) - yield break; - - int first = lines.LowerBound(line => startLineNumber - line.Number); - if (first != -1) - { - for (int it = first; it < lines.Count && lines[it].Number <= endLineNumber; ++it) - yield return lines[it]; - } - - } - } - - -} diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs index 8d70cf67..468676ad 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs @@ -5,8 +5,8 @@ namespace FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage { public class MsTemplateReplacementException : Exception { - private XmlException innerException; - private string replacedRunSettingsTemplate; + private readonly XmlException innerException; + private readonly string replacedRunSettingsTemplate; public MsTemplateReplacementException(XmlException innerException, string replacedRunSettingsTemplate) { this.innerException = innerException; diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplate.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplate.cs index 0b4e63b6..bcf6f2fb 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplate.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplate.cs @@ -43,8 +43,8 @@ private class ReplacementLookups : IRunSettingsTemplateReplacements public string MsDataCollectorElement { get; } private const string fccMarkerElementName = "FCCGenerated"; - private string msDataCollectorConfigurationElement; - private string msDataCollectorCodeCoverageElement; + private readonly string msDataCollectorConfigurationElement; + private readonly string msDataCollectorCodeCoverageElement; private readonly List<(string elementName, string value)> recommendedYouDoNotChangeElementsNetCore = new List<(string elementName, string value)> { @@ -187,7 +187,7 @@ bool isNetFramework private string AddRecommendedYouDoNotChangeElementsIfNotProvided(string replacedRunSettingsTemplate, bool isNetFramework) { - XDocument templateDocument = null; + XDocument templateDocument; try { templateDocument = XDocument.Parse(replacedRunSettingsTemplate); diff --git a/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs b/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs index c44a3d6f..21d7f35a 100644 --- a/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs +++ b/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs @@ -166,7 +166,7 @@ public static RGB HSLtoRGB(double h, double s, double l) public static HSL RGBtoHSL(int red, int green, int blue) { - double h = 0, s = 0, l = 0; + double h = 0, s = 0; // normalize red, green, blue values double r = (double)red / 255.0; @@ -199,7 +199,7 @@ public static HSL RGBtoHSL(int red, int green, int blue) } // luminance - l = (max + min) / 2.0; + double l = (max + min) / 2.0; // saturation if (l == 0 || max == min) diff --git a/SharedProject/Core/ReportGenerator/JsThemeStyling.cs b/SharedProject/Core/ReportGenerator/JsThemeStyling.cs index 1016dd67..30e06ece 100644 --- a/SharedProject/Core/ReportGenerator/JsThemeStyling.cs +++ b/SharedProject/Core/ReportGenerator/JsThemeStyling.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Security.Permissions; -using System.Text; +using System.Security.Permissions; namespace FineCodeCoverage.Engine.ReportGenerator { @@ -9,8 +6,6 @@ namespace FineCodeCoverage.Engine.ReportGenerator [System.Runtime.InteropServices.ComVisible(true)] public class JsThemeStyling { -//#pragma warning disable IDE0079 // Remove unnecessary suppression -//#pragma warning disable SA1401 // Fields should be private public string BackgroundColour; public string FontColour; public string TableBorderColour; @@ -56,7 +51,5 @@ public class JsThemeStyling public string ButtonFocusedTextColour; public string ButtonHoverTextColour; public string ButtonPressedTextColour; - //#pragma warning restore SA1401 // Fields should be private - //#pragma warning restore IDE0079 // Remove unnecessary suppression } } diff --git a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs index 84e2416e..a3174aee 100644 --- a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -90,7 +90,7 @@ private IReportColours ReportColours private string FontSize => environmentFontDetails == null ? "12px" : $"{environmentFontDetails.Size * dpiScale.DpiScaleX}px"; private string FontName => environmentFontDetails == null ? "Arial" : environmentFontDetails.Family.Source; - private HotspotReader hotspotsReader = new HotspotReader(); + private readonly HotspotReader hotspotsReader = new HotspotReader(); [ImportingConstructor] public ReportGeneratorUtil( @@ -152,7 +152,7 @@ public async Task GenerateAsync(IEnumerable cover reportGeneratorSettings.Add($@"""-targetdir:{reportOutputFolder}"""); - async Task run(string outputReportType, string inputReports) + async Task RunAsync(string outputReportType, string inputReports) { var reportTypeSettings = reportGeneratorSettings.ToArray().ToList(); @@ -205,7 +205,7 @@ async Task run(string outputReportType, string inputReports) var startTime = DateTime.Now; LogCoverageProcess("Generating cobertura report"); - await run("Cobertura", string.Join(";", coverOutputFiles)); + await RunAsync("Cobertura", string.Join(";", coverOutputFiles)); var duration = DateTime.Now - startTime; var coberturaDurationMesage = $"Cobertura report generation duration - {duration}"; @@ -213,7 +213,7 @@ async Task run(string outputReportType, string inputReports) startTime = DateTime.Now; LogCoverageProcess("Generating html report"); - await run("HtmlInline_AzurePipelines", unifiedXmlFile); + await RunAsync("HtmlInline_AzurePipelines", unifiedXmlFile); duration = DateTime.Now - startTime; cancellationToken.ThrowIfCancellationRequested(); var htmlReportDurationMessage = $"Html report generation duration - {duration}"; @@ -917,8 +917,8 @@ private string ObserveAndHideNamespaceWhenGroupingByNamespace(IAppOptions appOpt { return ""; } - var fullyQualifiedToName = ""; - switch(appOptions.NamespaceQualification) + string fullyQualifiedToName; + switch (appOptions.NamespaceQualification) { case NamespaceQualification.AlwaysUnqualified: case NamespaceQualification.UnqualifiedByNamespace: @@ -1105,13 +1105,13 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo var assemblies = JArray.Parse(assembliesToReplace); var groupingLevel = 0; - foreach (JObject assembly in assemblies) + foreach (var assembly in assemblies.Cast()) { var assemblyName = assembly["name"]; var classes = assembly["classes"] as JArray; var autoGeneratedRemovals = new List(); - foreach (JObject @class in classes) + foreach (var @class in classes.Cast()) { var className = @class["name"].ToString(); if (className == "AutoGeneratedProgram") @@ -1156,7 +1156,7 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo rhToReplace = rhToReplace.Substring(0, rhEndIndex + 1); var riskHotspots = JArray.Parse(rhToReplace); - foreach (JObject riskHotspot in riskHotspots) + foreach (var riskHotspot in riskHotspots.Cast()) { var assembly = riskHotspot["assembly"].ToString(); var qualifiedClassName = riskHotspot["class"].ToString(); @@ -1756,7 +1756,12 @@ public string BlankReport(bool withHistory) { logs.Clear(); } - return ProcessUnifiedHtml(resourceProvider.ReadResource("dummyReportToProcess.html"),null); + return ProcessUnifiedHtml(GetDummyReportToProcess(),null); + } + + private string GetDummyReportToProcess() + { + return resourceProvider.ReadResource("dummyReportToProcess.html"); } public void LogCoverageProcess(string message) @@ -1780,6 +1785,10 @@ public void UpdateReportWithDpiFontChanges() private void ReprocessReport() { + if(unprocessedReport == null) + { + unprocessedReport = GetDummyReportToProcess(); + } var newReport = ProcessUnifiedHtml(unprocessedReport, previousReportOutputFolder); eventAggregator.SendMessage(new NewReportMessage { Report = newReport }); } diff --git a/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs b/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs index be68a6e6..2285e8f6 100644 --- a/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs +++ b/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs @@ -1,9 +1,7 @@ using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using System; -using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Text; using System.Threading; using Task = System.Threading.Tasks.Task; @@ -12,9 +10,7 @@ namespace FineCodeCoverage.Core.Utilities internal interface IDisposeAwareTaskRunner { -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - void RunAsync(Func taskProvider); -#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods + void RunAsyncFunc(Func taskProvider); CancellationToken DisposalToken { get; } } @@ -52,9 +48,11 @@ protected virtual void Dispose(bool disposing) try { // Block Dispose until all async work has completed. +#pragma warning disable IDE0079 // Remove unnecessary suppression #pragma warning disable VSTHRD102 // Implement internal logic asynchronously ThreadHelper.JoinableTaskFactory.Run(this.JoinableTaskCollection.JoinTillEmptyAsync); #pragma warning restore VSTHRD102 // Implement internal logic asynchronously +#pragma warning restore IDE0079 // Remove unnecessary suppression } catch (OperationCanceledException) { @@ -72,7 +70,7 @@ protected virtual void Dispose(bool disposing) } } - public void RunAsync(Func taskProvider) + public void RunAsyncFunc(Func taskProvider) { _ = JoinableTaskFactory.RunAsync(taskProvider); } diff --git a/SharedProject/Core/Utilities/FileRenameListener.cs b/SharedProject/Core/Utilities/FileRenameListener.cs new file mode 100644 index 00000000..63a6cd53 --- /dev/null +++ b/SharedProject/Core/Utilities/FileRenameListener.cs @@ -0,0 +1,122 @@ +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Core.Utilities +{ + [Export(typeof(IFileRenameListener))] + internal class FileRenameListener : IFileRenameListener, IVsTrackProjectDocumentsEvents2 + { + private readonly List> callbacks = new List>(); + private readonly System.IServiceProvider serviceProvider; + private bool listening; + + [ImportingConstructor] + public FileRenameListener( + [Import(typeof(SVsServiceProvider))] System.IServiceProvider serviceProvider + ) + { + this.serviceProvider = serviceProvider; + } + + private void EnsureListening() + { + if (!listening) + { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously + ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var trackProjectDocuments = serviceProvider.GetService(typeof(SVsTrackProjectDocuments)) as IVsTrackProjectDocuments2; + trackProjectDocuments.AdviseTrackProjectDocumentsEvents(this, out var cookie); + }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously + listening = true; + } + } + + public void ListenForFileRename(Action callback) + { + callbacks.Add(callback); + EnsureListening(); + } + + public int OnQueryAddFiles(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, VSQUERYADDFILEFLAGS[] rgFlags, VSQUERYADDFILERESULTS[] pSummaryResult, VSQUERYADDFILERESULTS[] rgResults) + { + return VSConstants.S_OK; + } + + public int OnAfterAddFilesEx(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSADDFILEFLAGS[] rgFlags) + { + return VSConstants.S_OK; + } + + public int OnAfterAddDirectoriesEx(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSADDDIRECTORYFLAGS[] rgFlags) + { + return VSConstants.S_OK; + } + + public int OnAfterRemoveFiles(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSREMOVEFILEFLAGS[] rgFlags) + { + return VSConstants.S_OK; + } + + public int OnAfterRemoveDirectories(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSREMOVEDIRECTORYFLAGS[] rgFlags) + { + return VSConstants.S_OK; + } + + public int OnQueryRenameFiles(IVsProject pProject, int cFiles, string[] rgszMkOldNames, string[] rgszMkNewNames, VSQUERYRENAMEFILEFLAGS[] rgFlags, VSQUERYRENAMEFILERESULTS[] pSummaryResult, VSQUERYRENAMEFILERESULTS[] rgResults) + { + return VSConstants.S_OK; + } + + public int OnQueryRenameDirectories(IVsProject pProject, int cDirs, string[] rgszMkOldNames, string[] rgszMkNewNames, VSQUERYRENAMEDIRECTORYFLAGS[] rgFlags, VSQUERYRENAMEDIRECTORYRESULTS[] pSummaryResult, VSQUERYRENAMEDIRECTORYRESULTS[] rgResults) + { + return VSConstants.S_OK; + } + + public int OnAfterRenameDirectories(int cProjects, int cDirs, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgszMkOldNames, string[] rgszMkNewNames, VSRENAMEDIRECTORYFLAGS[] rgFlags) + { + return VSConstants.S_OK; + } + + public int OnQueryAddDirectories(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, VSQUERYADDDIRECTORYFLAGS[] rgFlags, VSQUERYADDDIRECTORYRESULTS[] pSummaryResult, VSQUERYADDDIRECTORYRESULTS[] rgResults) + { + return VSConstants.S_OK; + } + + public int OnQueryRemoveFiles(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, VSQUERYREMOVEFILEFLAGS[] rgFlags, VSQUERYREMOVEFILERESULTS[] pSummaryResult, VSQUERYREMOVEFILERESULTS[] rgResults) + { + return VSConstants.S_OK; + } + + public int OnQueryRemoveDirectories(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, VSQUERYREMOVEDIRECTORYFLAGS[] rgFlags, VSQUERYREMOVEDIRECTORYRESULTS[] pSummaryResult, VSQUERYREMOVEDIRECTORYRESULTS[] rgResults) + { + return VSConstants.S_OK; + } + + public int OnAfterSccStatusChanged(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgdwSccStatus) + { + return VSConstants.S_OK; + } + + public int OnAfterRenameFiles(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgszMkOldNames, string[] rgszMkNewNames, VSRENAMEFILEFLAGS[] rgFlags) + { + for (var i = 0; i < cFiles; i++) + { + Callback(rgszMkOldNames[i], rgszMkNewNames[i]); + } + return VSConstants.S_OK; + } + + private void Callback(string oldFilePath, string newFilePath) + { + callbacks.ForEach(callback => callback(oldFilePath, newFilePath)); + } + } + +} diff --git a/SharedProject/Core/Utilities/IFileRenameListener.cs b/SharedProject/Core/Utilities/IFileRenameListener.cs new file mode 100644 index 00000000..2a890c16 --- /dev/null +++ b/SharedProject/Core/Utilities/IFileRenameListener.cs @@ -0,0 +1,10 @@ +using System; + +namespace FineCodeCoverage.Core.Utilities +{ + interface IFileRenameListener + { + void ListenForFileRename(Action callback); + } + +} diff --git a/SharedProject/Core/Utilities/IJsonConvertService.cs b/SharedProject/Core/Utilities/IJsonConvertService.cs new file mode 100644 index 00000000..9461d0b1 --- /dev/null +++ b/SharedProject/Core/Utilities/IJsonConvertService.cs @@ -0,0 +1,11 @@ +using System; + +namespace FineCodeCoverage.Core.Utilities +{ + internal interface IJsonConvertService + { + object DeserializeObject(string serialized, Type propertyType); + T DeserializeObject(string serialized); + string SerializeObject(object objValue); + } +} diff --git a/SharedProject/Options/JsonConvertService.cs b/SharedProject/Core/Utilities/JsonConvertService.cs similarity index 53% rename from SharedProject/Options/JsonConvertService.cs rename to SharedProject/Core/Utilities/JsonConvertService.cs index 8a73876a..df460fcf 100644 --- a/SharedProject/Options/JsonConvertService.cs +++ b/SharedProject/Core/Utilities/JsonConvertService.cs @@ -1,17 +1,23 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; -namespace FineCodeCoverage.Options +namespace FineCodeCoverage.Core.Utilities { [ExcludeFromCodeCoverage] [Export(typeof(IJsonConvertService))] public class JsonConvertService : IJsonConvertService { - public object DeserializeObject(string value, Type propertyType) + public object DeserializeObject(string serialized, Type propertyType) { - return JsonConvert.DeserializeObject(value, propertyType); + return JsonConvert.DeserializeObject(serialized, propertyType); + } + + public T DeserializeObject(string serialized) + { + return JsonConvert.DeserializeObject(serialized); } public string SerializeObject(object value) diff --git a/SharedProject/Core/Utilities/LinqExtensions.cs b/SharedProject/Core/Utilities/LinqExtensions.cs index 76fd7508..10b52dd5 100644 --- a/SharedProject/Core/Utilities/LinqExtensions.cs +++ b/SharedProject/Core/Utilities/LinqExtensions.cs @@ -21,5 +21,21 @@ public static TTransformed SelectFirstNonNull(this IEnumerable< } return null; } + + public static IEnumerable TakeUntil(this IEnumerable source, System.Func predicate) + => source == null + ? throw new ArgumentNullException(nameof(source)) + : predicate == null ? throw new ArgumentNullException(nameof(predicate)) : + TakeUntilIterator(source, predicate); + + private static IEnumerable TakeUntilIterator(IEnumerable source, Func predicate) + { + foreach (T item in source) + { + yield return item; + if (predicate(item)) + yield break; + } + } } } diff --git a/SharedProject/Core/Utilities/ListExtensions.cs b/SharedProject/Core/Utilities/ListExtensions.cs index 04977eab..3d116fec 100644 --- a/SharedProject/Core/Utilities/ListExtensions.cs +++ b/SharedProject/Core/Utilities/ListExtensions.cs @@ -3,7 +3,7 @@ namespace FineCodeCoverage.Core.Utilities { - public static class ListExtensions + public static class List { // To be performed on a sorted list // Returns -1 for empty list or when all elements are outside the lower bounds diff --git a/SharedProject/Core/Utilities/SVsColorThemeService.cs b/SharedProject/Core/Utilities/SVsColorThemeService.cs new file mode 100644 index 00000000..290e74b9 --- /dev/null +++ b/SharedProject/Core/Utilities/SVsColorThemeService.cs @@ -0,0 +1,148 @@ +using Microsoft.VisualStudio.Shell; +using System; +using System.ComponentModel.Composition; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace FineCodeCoverage.Core.Utilities +{ + [ComImport] + [Guid("0D915B59-2ED7-472A-9DE8-9161737EA1C5")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [TypeIdentifier] +#pragma warning disable IDE1006 // Naming Styles + public interface SVsColorThemeService +#pragma warning restore IDE1006 // Naming Styles + { + } + + public interface IVsColorTheme + { + event EventHandler ThemeChanged; + VsColorEntry GetColorEntry(ColorName colorName); + string CurrentThemeName { get; } + } + + public class ColorName + { + public ColorName(Guid category, string name) + { + Category = category; + Name = name; + } + public Guid Category { get; } + + public string Name { get; } + } + + public class VsColorEntry + { + public VsColorEntry(object iVsColorEntry, ColorName colorName) + { + var d = iVsColorEntry as dynamic; + BackgroundType = d.BackgroundType; + ForegroundType = d.ForegroundType; + Background = d.Background; + Foreground = d.Foreground; + BackgroundSource = d.BackgroundSource; + ForegroundSource = d.ForegroundSource; + ColorName = colorName; + } + + ColorName ColorName { get; } + + byte BackgroundType { get; } + + byte ForegroundType { get; } + + uint Background { get; } + + uint Foreground { get; } + + uint BackgroundSource { get; } + + uint ForegroundSource { get; } + } + + [Export(typeof(IVsColorTheme))] + public class VsColorTheme : IVsColorTheme + { + private object currentTheme; + private readonly object colorThemeService; + + private PropertyInfo indexer; + private Type colorNameType; + + [ImportingConstructor] + public VsColorTheme( + [Import(typeof(SVsServiceProvider))] System.IServiceProvider serviceProvider + ) + { + colorThemeService = serviceProvider.GetService(typeof(SVsColorThemeService)); + } + + private object CurrentTheme + { + get + { + if (currentTheme == null) + { + SetCurrentTheme(); + } + return currentTheme; + } + } + + public string CurrentThemeName + { + get => (CurrentTheme as dynamic).Name; + + } + +#pragma warning disable IDE1006 // Naming Styles + private event EventHandler themeChanged; +#pragma warning restore IDE1006 // Naming Styles + + public event EventHandler ThemeChanged + { + add + { + themeChanged = value; + Microsoft.VisualStudio.PlatformUI.VSColorTheme.ThemeChanged += (e) => + { + SetCurrentTheme(); + themeChanged?.Invoke(this, EventArgs.Empty); + + }; + } + remove + { + themeChanged = null; + } + } + + private void SetCurrentTheme() + { + var currentTheme = (colorThemeService as dynamic).CurrentTheme; + this.currentTheme = currentTheme; + } + + public VsColorEntry GetColorEntry(ColorName colorName) + { + if (indexer == null) + { + indexer = CurrentTheme.GetType().GetProperty("Item"); + colorNameType = indexer.GetIndexParameters()[0].ParameterType; + } + + var vsColorName = Activator.CreateInstance(colorNameType, true); + (vsColorName as dynamic).Category = colorName.Category; + (vsColorName as dynamic).Name = colorName.Name; + + var colorEntry = indexer.GetValue(CurrentTheme, new object[] { vsColorName }); + if (colorEntry == null) return null; + return new VsColorEntry(colorEntry, colorName); + } + } + +} \ No newline at end of file diff --git a/SharedProject/Core/Utilities/ServiceProviderExtensions.cs b/SharedProject/Core/Utilities/ServiceProviderExtensions.cs new file mode 100644 index 00000000..c23a9964 --- /dev/null +++ b/SharedProject/Core/Utilities/ServiceProviderExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace FineCodeCoverage.Core.Utilities +{ + internal static class ServiceProviderExtensions + { + public static T GetService(this IServiceProvider serviceProvider) + { + return (T)serviceProvider.GetService(typeof(T)); + } + } +} diff --git a/SharedProject/Core/Utilities/ShownToolWindowHistory.cs b/SharedProject/Core/Utilities/ShownToolWindowHistory.cs index c943fc80..4f57800b 100644 --- a/SharedProject/Core/Utilities/ShownToolWindowHistory.cs +++ b/SharedProject/Core/Utilities/ShownToolWindowHistory.cs @@ -18,14 +18,14 @@ public ShownToolWindowHistory(IFCCEngine fccEngine, IFileUtil fileUtil) this.fccEngine = fccEngine; this.fileUtil = fileUtil; } - private string shownToolWindowFilePath => Path.Combine(fccEngine.AppDataFolderPath, "outputWindowInitialized"); + private string ShownToolWindowFilePath => Path.Combine(fccEngine.AppDataFolderPath, "outputWindowInitialized"); public bool HasShownToolWindow { get { if (!hasShownToolWindow && !checkedFileExists) { - hasShownToolWindow = fileUtil.Exists(shownToolWindowFilePath); + hasShownToolWindow = fileUtil.Exists(ShownToolWindowFilePath); checkedFileExists = true; } return hasShownToolWindow; @@ -37,7 +37,7 @@ public void ShowedToolWindow() if (!hasShownToolWindow) { hasShownToolWindow = true; - fileUtil.WriteAllText(shownToolWindowFilePath, string.Empty); + fileUtil.WriteAllText(ShownToolWindowFilePath, string.Empty); } } } diff --git a/SharedProject/Core/Utilities/SolutionEvents.cs b/SharedProject/Core/Utilities/SolutionEvents.cs index b74c2c88..07be7f7e 100644 --- a/SharedProject/Core/Utilities/SolutionEvents.cs +++ b/SharedProject/Core/Utilities/SolutionEvents.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.Shell.Interop; using System; using System.ComponentModel.Composition; +using Task = System.Threading.Tasks.Task; namespace FineCodeCoverage.Core.Utilities { @@ -19,6 +20,7 @@ public SolutionEvents( IServiceProvider serviceProvider ) { +#pragma warning disable VSTHRD104 // Offer async methods ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -26,6 +28,7 @@ IServiceProvider serviceProvider Assumes.Present(vsSolution); vsSolution.AdviseSolutionEvents(this, out uint _); }); +#pragma warning restore VSTHRD104 // Offer async methods } public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) diff --git a/SharedProject/Core/Utilities/ThrowIf.cs b/SharedProject/Core/Utilities/ThrowIf.cs new file mode 100644 index 00000000..964617f7 --- /dev/null +++ b/SharedProject/Core/Utilities/ThrowIf.cs @@ -0,0 +1,15 @@ +using System; + +namespace FineCodeCoverage.Core.Utilities +{ + internal static class ThrowIf + { + public static void Null(T value, string name) where T : class + { + if (value == null) + { + throw new ArgumentNullException(name); + } + } + } +} diff --git a/SharedProject/Core/Utilities/VsThreading/IThreadHelper.cs b/SharedProject/Core/Utilities/VsThreading/IThreadHelper.cs index 937e9643..afdf9517 100644 --- a/SharedProject/Core/Utilities/VsThreading/IThreadHelper.cs +++ b/SharedProject/Core/Utilities/VsThreading/IThreadHelper.cs @@ -1,6 +1,8 @@ using Microsoft.VisualStudio.Shell; using System; +using System.ComponentModel.Composition; using System.Threading; +using System.Threading.Tasks; using Task = System.Threading.Tasks.Task; namespace FineCodeCoverage.Core.Utilities.VsThreading @@ -13,6 +15,7 @@ internal interface IThreadHelper internal interface IJoinableTaskFactory { void Run(Func asyncMethod); + T Run(Func> asyncMethod); Task SwitchToMainThreadAsync(CancellationToken cancellationToken = default); } @@ -20,7 +23,16 @@ internal class VsJoinableTaskFactory : IJoinableTaskFactory { public void Run(Func asyncMethod) { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously ThreadHelper.JoinableTaskFactory.Run(asyncMethod); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously + } + + public T Run(Func> asyncMethod) + { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously + return ThreadHelper.JoinableTaskFactory.Run(asyncMethod); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously } public async Task SwitchToMainThreadAsync(CancellationToken cancellationToken = default) @@ -29,6 +41,7 @@ public async Task SwitchToMainThreadAsync(CancellationToken cancellationToken = } } + [Export(typeof(IThreadHelper))] internal class VsThreadHelper : IThreadHelper { public IJoinableTaskFactory JoinableTaskFactory { get; } = new VsJoinableTaskFactory(); diff --git a/SharedProject/Editor/.editorconfig b/SharedProject/Editor/.editorconfig new file mode 100644 index 00000000..b77da5b6 --- /dev/null +++ b/SharedProject/Editor/.editorconfig @@ -0,0 +1,231 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = true:error +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_method = true:error +dotnet_style_qualification_for_property = true:error + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:error +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true:error +dotnet_style_collection_initializer = true:error +dotnet_style_explicit_tuple_names = true:error +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true:error +dotnet_style_object_initializer = true:error +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:error +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true:error +dotnet_style_prefer_conditional_expression_over_assignment = true:error +dotnet_style_prefer_conditional_expression_over_return = true:error +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true:error +dotnet_style_prefer_inferred_tuple_names = true:error +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error +dotnet_style_prefer_simplified_boolean_expressions = true:error +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true:error + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:error + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = false:error +dotnet_style_allow_statement_immediately_after_block_experimental = false:error + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:error +csharp_style_var_for_built_in_types = false:error +csharp_style_var_when_type_is_apparent = true:error + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:error +csharp_style_expression_bodied_constructors = true:error +csharp_style_expression_bodied_indexers = true:error +csharp_style_expression_bodied_lambdas = true:error +csharp_style_expression_bodied_local_functions = true:error +csharp_style_expression_bodied_methods = true:error +csharp_style_expression_bodied_operators = true:error +csharp_style_expression_bodied_properties = true:error + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true:error +csharp_style_prefer_pattern_matching = true:error +csharp_style_prefer_switch_expression = true:error + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:error + +# Modifier preferences +csharp_prefer_static_local_function = true:error +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true:error +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = when_multiline:error +csharp_prefer_simple_using_statement = true:error +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true:error +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:error +csharp_style_deconstructed_variable_declaration = true:error +csharp_style_implicit_object_creation_when_type_is_apparent = true:error +csharp_style_inlined_variable_declaration = true:error +csharp_style_prefer_index_operator = true:error +csharp_style_prefer_local_over_anonymous_function = true:error +csharp_style_prefer_null_check_over_type_check = true:error +csharp_style_prefer_range_operator = true:error +csharp_style_prefer_tuple_swap = true:error +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true:error +csharp_style_unused_value_assignment_preference = discard_variable:error +csharp_style_unused_value_expression_statement_preference = discard_variable:error + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:error + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:error +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:error +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:error +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:error +csharp_style_allow_embedded_statements_on_same_line_experimental = true:error + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/SharedProject/Editor/DynamicCoverage/Common/DynamicCoverageTypeConverter.cs b/SharedProject/Editor/DynamicCoverage/Common/DynamicCoverageTypeConverter.cs new file mode 100644 index 00000000..007a9bc6 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Common/DynamicCoverageTypeConverter.cs @@ -0,0 +1,39 @@ +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal static class DynamicCoverageTypeConverter + { + public static DynamicCoverageType Convert(CoverageType coverageType) + { + DynamicCoverageType dynamicCoverageType = DynamicCoverageType.Covered; + switch (coverageType) + { + case CoverageType.NotCovered: + dynamicCoverageType = DynamicCoverageType.NotCovered; + break; + case CoverageType.Partial: + dynamicCoverageType = DynamicCoverageType.Partial; + break; + } + + return dynamicCoverageType; + } + + public static CoverageType Convert(DynamicCoverageType coverageType) + { + CoverageType converted = CoverageType.Covered; + switch (coverageType) + { + case DynamicCoverageType.NotCovered: + converted = CoverageType.NotCovered; + break; + case DynamicCoverageType.Partial: + converted = CoverageType.Partial; + break; + } + + return converted; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Common/DynamicLine.cs b/SharedProject/Editor/DynamicCoverage/Common/DynamicLine.cs new file mode 100644 index 00000000..1af54e0c --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Common/DynamicLine.cs @@ -0,0 +1,15 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class DynamicLine : IDynamicLine + { + public DynamicLine(int lineNumber, DynamicCoverageType dynamicCoverageType) + { + this.Number = lineNumber; + this.CoverageType = dynamicCoverageType; + } + + public int Number { get; set; } + + public DynamicCoverageType CoverageType { get; set; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Common/TrackingLine.cs b/SharedProject/Editor/DynamicCoverage/Common/TrackingLine.cs new file mode 100644 index 00000000..bd7eaf90 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Common/TrackingLine.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackingLine : ITrackingLine + { + private readonly ITrackingSpan startTrackingSpan; + private readonly ILineTracker lineTracker; + private readonly DynamicCoverageType dynamicCoverageType; + + public IDynamicLine Line { get; private set; } + public TrackingLine( + ITrackingSpan startTrackingSpan, + ITextSnapshot currentSnapshot, + ILineTracker lineTracker, + DynamicCoverageType dynamicCoverageType) + { + this.startTrackingSpan = startTrackingSpan; + this.lineTracker = lineTracker; + this.dynamicCoverageType = dynamicCoverageType; + this.SetLine(currentSnapshot); + } + + private void SetLine(ITextSnapshot currentSnapshot) + { + int startLineNumber = this.lineTracker.GetLineNumber(this.startTrackingSpan, currentSnapshot, false); + + this.Line = new DynamicLine(startLineNumber, this.dynamicCoverageType); + } + + public List Update(ITextSnapshot currentSnapshot) + { + int currentFirstLineNumber = this.Line.Number; + this.SetLine(currentSnapshot); + bool updated = currentFirstLineNumber != this.Line.Number; + return updated ? new List { currentFirstLineNumber, this.Line.Number } : new List(); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/CoverageLine.cs b/SharedProject/Editor/DynamicCoverage/Coverage/CoverageLine.cs new file mode 100644 index 00000000..2fb84934 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/CoverageLine.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class CoverageLine : ICoverageLine + { + private readonly ITrackingSpan trackingSpan; + private readonly ILineTracker lineTracker; + private readonly TrackedLineLine line; + public IDynamicLine Line => this.line; + + public CoverageLine(ITrackingSpan trackingSpan, ILine line, ILineTracker lineTracker) + { + this.line = new TrackedLineLine(line); + this.trackingSpan = trackingSpan; + this.lineTracker = lineTracker; + } + + public List Update(ITextSnapshot currentSnapshot) + { + int previousLineNumber = this.Line.Number; + int newLineNumber = this.lineTracker.GetLineNumber(this.trackingSpan, currentSnapshot, true); + if (newLineNumber != previousLineNumber) + { + this.line.Number = newLineNumber; + return new List { previousLineNumber, newLineNumber }; + + } + + return Enumerable.Empty().ToList(); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/CoverageLineFactory.cs b/SharedProject/Editor/DynamicCoverage/Coverage/CoverageLineFactory.cs new file mode 100644 index 00000000..5337e55f --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/CoverageLineFactory.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ICoverageLineFactory))] + internal class CoverageLineFactory : ICoverageLineFactory + { + private readonly ILineTracker lineTracker; + + [ImportingConstructor] + public CoverageLineFactory(ILineTracker lineTracker) => this.lineTracker = lineTracker; + public ICoverageLine Create(ITrackingSpan trackingSpan, ILine line) => new CoverageLine(trackingSpan, line, this.lineTracker); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/ICoverageLine.cs b/SharedProject/Editor/DynamicCoverage/Coverage/ICoverageLine.cs new file mode 100644 index 00000000..a340d845 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/ICoverageLine.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ICoverageLine + { + List Update(ITextSnapshot currentSnapshot); + IDynamicLine Line { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/ICoverageLineFactory.cs b/SharedProject/Editor/DynamicCoverage/Coverage/ICoverageLineFactory.cs new file mode 100644 index 00000000..e95ad0f5 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/ICoverageLineFactory.cs @@ -0,0 +1,10 @@ +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ICoverageLineFactory + { + ICoverageLine Create(ITrackingSpan trackingSpan, ILine line); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/ITrackedCoverageLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/Coverage/ITrackedCoverageLinesFactory.cs new file mode 100644 index 00000000..0dcb343f --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/ITrackedCoverageLinesFactory.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackedCoverageLinesFactory + { + ITrackedCoverageLines Create(List coverageLines); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/TrackedCoverageLines.cs b/SharedProject/Editor/DynamicCoverage/Coverage/TrackedCoverageLines.cs new file mode 100644 index 00000000..f1516f2b --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/TrackedCoverageLines.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackedCoverageLines : ITrackedCoverageLines + { + private readonly List coverageLines; + + public IEnumerable Lines => this.coverageLines.Select(coverageLine => coverageLine.Line); + public TrackedCoverageLines(List coverageLines) => this.coverageLines = coverageLines; + + public IEnumerable GetUpdatedLineNumbers(ITextSnapshot currentSnapshot) + => this.coverageLines.SelectMany(coverageLine => coverageLine.Update(currentSnapshot)); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/TrackedCoverageLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/Coverage/TrackedCoverageLinesFactory.cs new file mode 100644 index 00000000..e5786d89 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/TrackedCoverageLinesFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ITrackedCoverageLinesFactory))] + internal class TrackedCoverageLinesFactory : ITrackedCoverageLinesFactory + { + public ITrackedCoverageLines Create(List coverageLines) => new TrackedCoverageLines(coverageLines); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Coverage/TrackedLineLine.cs b/SharedProject/Editor/DynamicCoverage/Coverage/TrackedLineLine.cs new file mode 100644 index 00000000..2a5f5dd3 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Coverage/TrackedLineLine.cs @@ -0,0 +1,16 @@ +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackedLineLine : IDynamicLine + { + public TrackedLineLine(ILine line) + { + this.Number = line.Number - 1; + this.CoverageType = DynamicCoverageTypeConverter.Convert(line.CoverageType); + } + + public int Number { get; set; } + public DynamicCoverageType CoverageType { get; private set; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Dirty/DirtyLineFactory.cs b/SharedProject/Editor/DynamicCoverage/Dirty/DirtyLineFactory.cs new file mode 100644 index 00000000..d96ebfe3 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Dirty/DirtyLineFactory.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IDirtyLineFactory))] + internal class DirtyLineFactory : IDirtyLineFactory + { + private readonly ILineTracker lineTracker; + + [ImportingConstructor] + public DirtyLineFactory(ILineTracker lineTracker) => this.lineTracker = lineTracker; + public ITrackingLine Create(ITrackingSpan trackingSpan, ITextSnapshot snapshot) + => new TrackingLine(trackingSpan, snapshot, this.lineTracker, DynamicCoverageType.Dirty); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Dirty/IDirtyLineFactory.cs b/SharedProject/Editor/DynamicCoverage/Dirty/IDirtyLineFactory.cs new file mode 100644 index 00000000..737f0a4b --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Dirty/IDirtyLineFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IDirtyLineFactory + { + ITrackingLine Create(ITrackingSpan trackingSpan, ITextSnapshot snapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs new file mode 100644 index 00000000..662bb236 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverage.cs @@ -0,0 +1,179 @@ +using System; +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.Options; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class BufferLineCoverage : IBufferLineCoverage, 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 ITextBuffer2 textBuffer; + private ITrackedLines trackedLines; + private bool? editorCoverageModeOff; + private IFileLineCoverage fileLineCoverage; + public BufferLineCoverage( + IFileLineCoverage fileLineCoverage, + ITextInfo textInfo, + IEventAggregator eventAggregator, + ITrackedLinesFactory trackedLinesFactory, + IDynamicCoverageStore dynamicCoverageStore, + IAppOptionsProvider appOptionsProvider + ) + { + this.fileLineCoverage = fileLineCoverage; + this.language = SupportedContentTypeLanguages.GetLanguage(textInfo.TextBuffer.ContentType.TypeName); + this.textBuffer = textInfo.TextBuffer; + this.textInfo = textInfo; + this.eventAggregator = eventAggregator; + this.trackedLinesFactory = trackedLinesFactory; + this.dynamicCoverageStore = dynamicCoverageStore; + this.appOptionsProvider = appOptionsProvider; + void AppOptionsChanged(IAppOptions appOptions) + { + bool newEditorCoverageModeOff = appOptions.EditorCoverageColouringMode == EditorCoverageColouringMode.Off; + if (this.trackedLines != null && newEditorCoverageModeOff && this.editorCoverageModeOff != newEditorCoverageModeOff) + { + this.trackedLines = null; + this.SendCoverageChangedMessage(); + } + + this.editorCoverageModeOff = newEditorCoverageModeOff; + } + + appOptionsProvider.OptionsChanged += AppOptionsChanged; + if (fileLineCoverage != null) + { + this.CreateTrackedLinesIfRequired(true); + } + + _ = eventAggregator.AddListener(this); + this.textBuffer.ChangedOnBackground += this.TextBuffer_ChangedOnBackground; + void textViewClosedHandler(object s, EventArgs e) + { + this.UpdateDynamicCoverageStore(); + this.textBuffer.Changed -= this.TextBuffer_ChangedOnBackground; + textInfo.TextView.Closed -= textViewClosedHandler; + appOptionsProvider.OptionsChanged -= AppOptionsChanged; + _ = eventAggregator.RemoveListener(this); + } + + textInfo.TextView.Closed += textViewClosedHandler; + } + + private void UpdateDynamicCoverageStore() + { + if (this.trackedLines != null) + { + this.dynamicCoverageStore.SaveSerializedCoverage(this.textInfo.FilePath, this.trackedLinesFactory.Serialize(this.trackedLines)); + } + else + { + this.dynamicCoverageStore.RemoveSerializedCoverage(this.textInfo.FilePath); + } + } + + private void CreateTrackedLinesIfRequired(bool initial) + { + if (this.EditorCoverageColouringModeOff()) + { + this.trackedLines = null; + } + else + { + this.CreateTrackedLines(initial); + } + } + + private void CreateTrackedLinesIfRequiredWithMessage() + { + bool hadTrackedLines = this.trackedLines != null; + this.CreateTrackedLinesIfRequired(false); + bool hasTrackedLines = this.trackedLines != null; + if (hadTrackedLines || hasTrackedLines) + { + this.SendCoverageChangedMessage(); + } + } + + private void CreateTrackedLines(bool initial) + { + ITextSnapshot currentSnapshot = this.textBuffer.CurrentSnapshot; + if (initial) + { + string serializedCoverage = this.dynamicCoverageStore.GetSerializedCoverage(this.textInfo.FilePath); + if (serializedCoverage != null) + { + this.trackedLines = this.trackedLinesFactory.Create(serializedCoverage, currentSnapshot, this.language); + return; + } + } + + var lines = this.fileLineCoverage.GetLines(this.textInfo.FilePath).ToList(); + this.trackedLines = this.trackedLinesFactory.Create(lines, currentSnapshot, this.language); + } + + private bool EditorCoverageColouringModeOff() + { + this.editorCoverageModeOff = this.appOptionsProvider.Get().EditorCoverageColouringMode == EditorCoverageColouringMode.Off; + return this.editorCoverageModeOff.Value; + } + + private void TextBuffer_ChangedOnBackground(object sender, TextContentChangedEventArgs e) + { + if (this.trackedLines != null) + { + this.UpdateTrackedLines(e); + } + } + + private void UpdateTrackedLines(TextContentChangedEventArgs e) + { + IEnumerable changedLineNumbers = this.trackedLines.GetChangedLineNumbers(e.After, e.Changes.Select(change => change.NewSpan).ToList()); + this.SendCoverageChangedMessageIfChanged(changedLineNumbers); + } + + private void SendCoverageChangedMessageIfChanged(IEnumerable changedLineNumbers) + { + if (changedLineNumbers.Any()) + { + this.SendCoverageChangedMessage(changedLineNumbers); + } + } + + private void SendCoverageChangedMessage(IEnumerable changedLineNumbers = null) + => 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); + + public void Handle(NewCoverageLinesMessage message) + { + this.fileLineCoverage = message.CoverageLines; + + bool hadTrackedLines = this.trackedLines != null; + if (this.fileLineCoverage == null) + { + this.trackedLines = null; + if (hadTrackedLines) + { + this.SendCoverageChangedMessage(); + } + } + else + { + this.CreateTrackedLinesIfRequiredWithMessage(); + } + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs new file mode 100644 index 00000000..897c4750 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/BufferLineCoverageFactory.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IBufferLineCoverageFactory))] + internal class BufferLineCoverageFactory : IBufferLineCoverageFactory + { + private readonly IDynamicCoverageStore dynamicCoverageStore; + private readonly IAppOptionsProvider appOptionsProvider; + + [ImportingConstructor] + public BufferLineCoverageFactory( + IDynamicCoverageStore dynamicCoverageStore, + IAppOptionsProvider appOptionsProvider + ) + { + this.appOptionsProvider = appOptionsProvider; + this.dynamicCoverageStore = dynamicCoverageStore; + } + + public IBufferLineCoverage Create( + IFileLineCoverage fileLineCoverage, + ITextInfo textInfo, + IEventAggregator eventAggregator, + ITrackedLinesFactory trackedLinesFactory + ) => new BufferLineCoverage( + fileLineCoverage, + textInfo, + eventAggregator, + trackedLinesFactory, + this.dynamicCoverageStore, + this.appOptionsProvider); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs b/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs new file mode 100644 index 00000000..7794ad8a --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/CoverageChangedMessage.cs @@ -0,0 +1,19 @@ + +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class CoverageChangedMessage + { + public IBufferLineCoverage BufferLineCoverage { get; } + public string AppliesTo { get; } + public IEnumerable ChangedLineNumbers { get; } + + public CoverageChangedMessage(IBufferLineCoverage bufferLineCoverage, string appliesTo, IEnumerable changedLineNumbers) + { + this.BufferLineCoverage = bufferLineCoverage; + this.AppliesTo = appliesTo; + this.ChangedLineNumbers = changedLineNumbers; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs b/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs new file mode 100644 index 00000000..f1a60292 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageManager.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.Composition; +using FineCodeCoverage.Core.Initialization; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [Export(typeof(IInitializable))] + [Export(typeof(IDynamicCoverageManager))] + internal class DynamicCoverageManager : IDynamicCoverageManager, IListener, IInitializable + { + private readonly IEventAggregator eventAggregator; + private readonly ITrackedLinesFactory trackedLinesFactory; + private readonly IBufferLineCoverageFactory bufferLineCoverageFactory; + private IFileLineCoverage lastCoverageLines; + + [ImportingConstructor] + public DynamicCoverageManager( + IEventAggregator eventAggregator, + ITrackedLinesFactory trackedLinesFactory, + IBufferLineCoverageFactory bufferLineCoverageFactory) + { + this.bufferLineCoverageFactory = bufferLineCoverageFactory; + _ = eventAggregator.AddListener(this); + this.eventAggregator = eventAggregator; + this.trackedLinesFactory = trackedLinesFactory; + } + public void Handle(NewCoverageLinesMessage message) => this.lastCoverageLines = message.CoverageLines; + + public IBufferLineCoverage Manage(ITextInfo textInfo) + => textInfo.TextBuffer.Properties.GetOrCreateSingletonProperty( + () => this.bufferLineCoverageFactory.Create(this.lastCoverageLines, textInfo, this.eventAggregator, this.trackedLinesFactory) + ); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageType.cs b/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageType.cs new file mode 100644 index 00000000..fa875488 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/DynamicCoverageType.cs @@ -0,0 +1,10 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal enum DynamicCoverageType + { + Covered, Partial, NotCovered, + Dirty, + NewLine, + NotIncluded + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverage.cs b/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverage.cs new file mode 100644 index 00000000..71d46b5e --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverage.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IBufferLineCoverage + { + IEnumerable GetLines(int startLineNumber, int endLineNumber); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs b/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs new file mode 100644 index 00000000..1e66e629 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/IBufferLineCoverageFactory.cs @@ -0,0 +1,12 @@ +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IBufferLineCoverageFactory + { + IBufferLineCoverage Create( + IFileLineCoverage fileLineCoverage, ITextInfo textInfo, IEventAggregator eventAggregator, ITrackedLinesFactory trackedLinesFactory + ); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/IDynamicCoverageManager.cs b/SharedProject/Editor/DynamicCoverage/Management/IDynamicCoverageManager.cs new file mode 100644 index 00000000..6e0f5628 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/IDynamicCoverageManager.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IDynamicCoverageManager + { + IBufferLineCoverage Manage(ITextInfo textInfo); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/IDynamicLine.cs b/SharedProject/Editor/DynamicCoverage/Management/IDynamicLine.cs new file mode 100644 index 00000000..11a06aaf --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/IDynamicLine.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IDynamicLine + { + int Number { get; } + DynamicCoverageType CoverageType { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/ITrackedLines.cs b/SharedProject/Editor/DynamicCoverage/Management/ITrackedLines.cs new file mode 100644 index 00000000..f6e33d9f --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/ITrackedLines.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackedLines + { + IEnumerable GetLines(int startLineNumber, int endLineNumber); + IEnumerable GetChangedLineNumbers(ITextSnapshot currentSnapshot, List newSpanChanges); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs new file mode 100644 index 00000000..6a52ce9e --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Management/ITrackedLinesFactory.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; + +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); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs new file mode 100644 index 00000000..698fe299 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/INewCodeTrackerFactory.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface INewCodeTrackerFactory + { + INewCodeTracker Create(bool isCSharp); + INewCodeTracker Create(bool isCSharp, IEnumerable lineNumbers, ITextSnapshot textSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/ITrackedNewCodeLine.cs b/SharedProject/Editor/DynamicCoverage/NewCode/ITrackedNewCodeLine.cs new file mode 100644 index 00000000..9aba9e9e --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/ITrackedNewCodeLine.cs @@ -0,0 +1,11 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackedNewCodeLine + { + TrackedNewCodeLineUpdate Update(ITextSnapshot currentSnapshot); + string GetText(ITextSnapshot currentSnapshot); + IDynamicLine Line { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/ITrackedNewCodeLineFactory.cs b/SharedProject/Editor/DynamicCoverage/NewCode/ITrackedNewCodeLineFactory.cs new file mode 100644 index 00000000..35287476 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/ITrackedNewCodeLineFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackedNewCodeLineFactory + { + ITrackedNewCodeLine Create(ITextSnapshot textSnapshot, SpanTrackingMode spanTrackingMode, int lineNumber); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs new file mode 100644 index 00000000..7d0bdcf8 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTracker.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text; + +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) + { + 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) + { + _ = this.AddTrackedNewCodeLineIfNotExcluded(currentSnapshot, lineNumber); + } + } + + public IEnumerable Lines => this.trackedNewCodeLines.OrderBy(l => l.Line.Number).Select(l => l.Line); + + public IEnumerable GetChangedLineNumbers( + ITextSnapshot currentSnapshot, + List potentialNewLines, + IEnumerable newCodeCodeRanges + ) => newCodeCodeRanges != null + ? this.ProcessNewCodeCodeRanges(newCodeCodeRanges, currentSnapshot) + : this.ProcessSpanAndLineRanges(potentialNewLines, currentSnapshot); + + #region NewCodeCodeRanges + + private List ProcessNewCodeCodeRanges(IEnumerable newCodeCodeRanges, ITextSnapshot textSnapshot) + { + var startLineNumbers = newCodeCodeRanges.Select(newCodeCodeRange => newCodeCodeRange.StartLine).ToList(); + IEnumerable removed = this.RemoveAndReduceByLineNumbers(startLineNumbers); + this.CreateTrackedNewCodeLines(startLineNumbers, textSnapshot); + return removed.Concat(startLineNumbers).ToList(); + } + + private IEnumerable RemoveAndReduceByLineNumbers(List startLineNumbers) + { + var removals = this.trackedNewCodeLines.Where( + trackedNewCodeLine => !startLineNumbers.Remove(trackedNewCodeLine.Line.Number)).ToList(); + + removals.ForEach(removal => this.trackedNewCodeLines.Remove(removal)); + return removals.Select(removal => removal.Line.Number); + } + + private void CreateTrackedNewCodeLines(IEnumerable lineNumbers, ITextSnapshot currentSnapshot) + { + foreach (int lineNumber in lineNumbers) + { + ITrackedNewCodeLine trackedNewCodeLine = this.CreateTrackedNewCodeLine(currentSnapshot, lineNumber); + this.trackedNewCodeLines.Add(trackedNewCodeLine); + } + } + #endregion + #region SpanAndLineRanges + private List ProcessSpanAndLineRanges(List potentialNewLines, ITextSnapshot currentSnapshot) + { + (IEnumerable updatedLineNumbers, List updatedPotentialNewLines) = + this.UpdateAndReduceBySpanAndLineRanges(currentSnapshot, potentialNewLines); + IEnumerable addedLineNumbers = this.AddTrackedNewCodeLinesIfNotExcluded(updatedPotentialNewLines, currentSnapshot); + return updatedLineNumbers.Concat(addedLineNumbers).ToList(); + } + + private (IEnumerable updatedLinesNumbers, List potentialNewLines) UpdateAndReduceBySpanAndLineRanges( + ITextSnapshot currentSnapshot, + List potentialNewLines + ) + { + var updatedLineNumbers = new List(); + var removals = new List(); + foreach (ITrackedNewCodeLine trackedNewCodeLine in this.trackedNewCodeLines) + { + (List lineNumberUpdates, List reducedPotentialNewLines) = this.UpdateAndReduce( + trackedNewCodeLine, currentSnapshot, potentialNewLines, removals); + updatedLineNumbers.AddRange(lineNumberUpdates); + potentialNewLines = reducedPotentialNewLines; + }; + removals.ForEach(removal => this.trackedNewCodeLines.Remove(removal)); + return (updatedLineNumbers.Distinct().ToList(), potentialNewLines); + } + + private (List lineNumberUpdates, List reducedPotentialNewLines) UpdateAndReduce( + ITrackedNewCodeLine trackedNewCodeLine, + ITextSnapshot currentSnapshot, + List potentialNewLines, + List removals + ) + { + var lineNumberUpdates = new List(); + TrackedNewCodeLineUpdate trackedNewCodeLineUpdate = trackedNewCodeLine.Update(currentSnapshot); + + List reducedPotentialNewLines = this.ReducePotentialNewLines(potentialNewLines, trackedNewCodeLineUpdate.NewLineNumber); + + bool excluded = this.RemoveTrackedNewCodeLineIfExcluded(removals, trackedNewCodeLine, trackedNewCodeLineUpdate.Text); + if (excluded) + { + lineNumberUpdates.Add(trackedNewCodeLineUpdate.OldLineNumber); + } + else + { + if (trackedNewCodeLineUpdate.NewLineNumber != trackedNewCodeLineUpdate.OldLineNumber) + { + lineNumberUpdates.Add(trackedNewCodeLineUpdate.OldLineNumber); + lineNumberUpdates.Add(trackedNewCodeLineUpdate.NewLineNumber); + } + } + + return (lineNumberUpdates, reducedPotentialNewLines); + } + + private List ReducePotentialNewLines(List potentialNewLines, int updatedLineNumber) + => potentialNewLines.Where(spanAndLineRange => spanAndLineRange.StartLineNumber != updatedLineNumber).ToList(); + + private bool RemoveTrackedNewCodeLineIfExcluded( + List removals, + ITrackedNewCodeLine trackedNewCodeLine, + string newText) + { + bool excluded = false; + if (this.codeLineExcluder.ExcludeIfNotCode(newText, this.isCSharp)) + { + excluded = true; + removals.Add(trackedNewCodeLine); + } + + return excluded; + } + private IEnumerable AddTrackedNewCodeLinesIfNotExcluded(IEnumerable potentialNewLines, ITextSnapshot currentSnapshot) + => this.GetDistinctStartLineNumbers(potentialNewLines) + .Where(lineNumber => this.AddTrackedNewCodeLineIfNotExcluded(currentSnapshot, lineNumber)); + + private IEnumerable GetDistinctStartLineNumbers(IEnumerable potentialNewLines) + => potentialNewLines.Select(spanAndLineNumber => spanAndLineNumber.StartLineNumber).Distinct(); + + #endregion + + private bool AddTrackedNewCodeLineIfNotExcluded(ITextSnapshot currentSnapshot, int lineNumber) + { + bool added = false; + ITrackedNewCodeLine trackedNewCodeLine = this.CreateTrackedNewCodeLine(currentSnapshot, lineNumber); + string text = trackedNewCodeLine.GetText(currentSnapshot); + if (!this.codeLineExcluder.ExcludeIfNotCode(text, this.isCSharp)) + { + this.trackedNewCodeLines.Add(trackedNewCodeLine); + added = true; + } + + return added; + } + + private ITrackedNewCodeLine CreateTrackedNewCodeLine(ITextSnapshot currentSnapshot, int lineNumber) + => this.trackedNewCodeLineFactory.Create(currentSnapshot, SpanTrackingMode.EdgeExclusive, lineNumber); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs new file mode 100644 index 00000000..2e1d1dc3 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/NewCodeTrackerFactory.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(INewCodeTrackerFactory))] + 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); + + public INewCodeTracker Create(bool isCSharp, IEnumerable lineNumbers, ITextSnapshot textSnapshot) + => new NewCodeTracker(isCSharp, this.trackedNewCodeLineFactory, this.codeLineExcluder, lineNumbers, textSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewCodeLine.cs b/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewCodeLine.cs new file mode 100644 index 00000000..6b6bef7e --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewCodeLine.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackedNewCodeLine : ITrackedNewCodeLine + { + private readonly ITrackingSpan trackingSpan; + private readonly DynamicLine line; + private readonly ILineTracker lineTracker; + + public TrackedNewCodeLine(ITrackingSpan trackingSpan, int lineNumber, ILineTracker lineTracker) + { + this.line = new DynamicLine(lineNumber, DynamicCoverageType.NewLine); + this.lineTracker = lineTracker; + this.trackingSpan = trackingSpan; + } + + public IDynamicLine Line => this.line; + + public string GetText(ITextSnapshot currentSnapshot) + => this.lineTracker.GetTrackedLineInfo(this.trackingSpan, currentSnapshot, true).LineText; + + public TrackedNewCodeLineUpdate Update(ITextSnapshot currentSnapshot) + { + int oldLineNumber = this.line.Number; + TrackedLineInfo trackedLineInfo = this.lineTracker.GetTrackedLineInfo(this.trackingSpan, currentSnapshot, true); + this.line.Number = trackedLineInfo.LineNumber; + return new TrackedNewCodeLineUpdate(trackedLineInfo.LineText, this.line.Number, oldLineNumber); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewCodeLineUpdate.cs b/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewCodeLineUpdate.cs new file mode 100644 index 00000000..64939702 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewCodeLineUpdate.cs @@ -0,0 +1,15 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal readonly struct TrackedNewCodeLineUpdate + { + public TrackedNewCodeLineUpdate(string text, int newLineNumber, int oldLineNumber) + { + this.Text = text; + this.NewLineNumber = newLineNumber; + this.OldLineNumber = oldLineNumber; + } + public string Text { get; } + public int NewLineNumber { get; } + public int OldLineNumber { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewLineFactory.cs b/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewLineFactory.cs new file mode 100644 index 00000000..0d17ef19 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NewCode/TrackedNewLineFactory.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ITrackedNewCodeLineFactory))] + internal class TrackedNewLineFactory : ITrackedNewCodeLineFactory + { + private readonly ITrackingLineFactory trackingLineFactory; + private readonly ILineTracker lineTracker; + + [ImportingConstructor] + public TrackedNewLineFactory( + ITrackingLineFactory trackingLineFactory, + ILineTracker lineTracker + ) + { + this.trackingLineFactory = trackingLineFactory; + this.lineTracker = lineTracker; + } + public ITrackedNewCodeLine Create(ITextSnapshot textSnapshot, SpanTrackingMode spanTrackingMode, int lineNumber) + { + ITrackingSpan trackingSpan = this.trackingLineFactory.CreateTrackingSpan(textSnapshot, lineNumber, spanTrackingMode); + return new TrackedNewCodeLine(trackingSpan, lineNumber, this.lineTracker); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NotIncluded/INotIncludedLineFactory.cs b/SharedProject/Editor/DynamicCoverage/NotIncluded/INotIncludedLineFactory.cs new file mode 100644 index 00000000..2c0d417d --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NotIncluded/INotIncludedLineFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface INotIncludedLineFactory + { + ITrackingLine Create(ITrackingSpan startTrackingSpan, ITextSnapshot currentSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/NotIncluded/NotIncludedLineFactory.cs b/SharedProject/Editor/DynamicCoverage/NotIncluded/NotIncludedLineFactory.cs new file mode 100644 index 00000000..49c8b8bb --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/NotIncluded/NotIncludedLineFactory.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(INotIncludedLineFactory))] + internal class NotIncludedLineFactory : INotIncludedLineFactory + { + private readonly ILineTracker lineTracker; + + [ImportingConstructor] + public NotIncludedLineFactory( + ILineTracker lineTracker + ) => this.lineTracker = lineTracker; + + public ITrackingLine Create(ITrackingSpan startTrackingSpan, ITextSnapshot currentSnapshot) + => new TrackingLine(startTrackingSpan, currentSnapshot, this.lineTracker, DynamicCoverageType.NotIncluded); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs b/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs new file mode 100644 index 00000000..ba9665dd --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Store/DynamicCoverageStore.cs @@ -0,0 +1,88 @@ +using System.ComponentModel.Composition; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Options; +using Microsoft.VisualStudio.Settings; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [Export(typeof(IDynamicCoverageStore))] + internal class DynamicCoverageStore : IDynamicCoverageStore, IListener + { + private readonly IWritableUserSettingsStoreProvider writableUserSettingsStoreProvider; + private const string dynamicCoverageStoreCollectionName = "FCC.DynamicCoverageStore"; + private WritableSettingsStore writableUserSettingsStore; + private WritableSettingsStore WritableUserSettingsStore + { + get + { + if (this.writableUserSettingsStore == null) + { + this.writableUserSettingsStore = this.writableUserSettingsStoreProvider.Provide(); + } + + return this.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 + [ImportingConstructor] + public DynamicCoverageStore( + IWritableUserSettingsStoreProvider writableUserSettingsStoreProvider, + IFileRenameListener fileRenameListener, + IEventAggregator eventAggregator + ) + { + _ = eventAggregator.AddListener(this); + this.writableUserSettingsStoreProvider = writableUserSettingsStoreProvider; + fileRenameListener.ListenForFileRename((oldFileName, newFileName) => + { + bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); + if (collectionExists) + { + if (this.WritableUserSettingsStore.PropertyExists(dynamicCoverageStoreCollectionName, oldFileName)) + { + string serialized = this.WritableUserSettingsStore.GetString(dynamicCoverageStoreCollectionName, oldFileName); + this.WritableUserSettingsStore.SetString(dynamicCoverageStoreCollectionName, newFileName, serialized); + _ = this.WritableUserSettingsStore.DeleteProperty(dynamicCoverageStoreCollectionName, oldFileName); + } + } + }); + } + + public string GetSerializedCoverage(string filePath) + { + bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); + return !collectionExists + ? null + : this.WritableUserSettingsStore.PropertyExists(dynamicCoverageStoreCollectionName, filePath) + ? this.WritableUserSettingsStore.GetString(dynamicCoverageStoreCollectionName, filePath) + : null; + } + + public void SaveSerializedCoverage(string filePath, string serializedCoverage) + { + bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); + if (!collectionExists) + { + this.WritableUserSettingsStore.CreateCollection(dynamicCoverageStoreCollectionName); + } + + this.WritableUserSettingsStore.SetString(dynamicCoverageStoreCollectionName, filePath, serializedCoverage); + } + + public void Handle(NewCoverageLinesMessage message) + { + bool collectionExists = this.WritableUserSettingsStore.CollectionExists(dynamicCoverageStoreCollectionName); + if (collectionExists) + { + _ = this.WritableUserSettingsStore.DeleteCollection(dynamicCoverageStoreCollectionName); + } + } + + public void RemoveSerializedCoverage(string filePath) + => _ = this.WritableUserSettingsStore.DeleteProperty(dynamicCoverageStoreCollectionName, filePath); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs b/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs new file mode 100644 index 00000000..029059b0 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Store/IDynamicCoverageStore.cs @@ -0,0 +1,9 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IDynamicCoverageStore + { + string GetSerializedCoverage(string filePath); + void RemoveSerializedCoverage(string filePath); + void SaveSerializedCoverage(string filePath, string serialized); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs new file mode 100644 index 00000000..87ac9a37 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackedLinesFactory.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IContainingCodeTrackedLinesFactory))] + internal class ContainingCodeTrackedLinesFactory : IContainingCodeTrackedLinesFactory + { + public TrackedLines Create( + List containingCodeTrackers, + INewCodeTracker newCodeTracker, + IFileCodeSpanRangeService fileCodeSpanRangeService + ) => new TrackedLines(containingCodeTrackers, newCodeTracker, fileCodeSpanRangeService); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackerState.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackerState.cs new file mode 100644 index 00000000..5149ff17 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackerState.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class ContainingCodeTrackerState + { + public ContainingCodeTrackerState( + ContainingCodeTrackerType type, + CodeSpanRange codeSpanRange, + IEnumerable lines + ) + { + this.Type = type; + this.CodeSpanRange = codeSpanRange; + this.Lines = lines; + } + + public ContainingCodeTrackerType Type { get; } + public CodeSpanRange CodeSpanRange { get; } + public IEnumerable Lines { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackerType.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackerType.cs new file mode 100644 index 00000000..41358303 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/ContainingCodeTrackerType.cs @@ -0,0 +1,4 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal enum ContainingCodeTrackerType { CoverageLines, NotIncluded, OtherLines } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTracker.cs new file mode 100644 index 00000000..5b441d1e --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTracker.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IContainingCodeTracker + { + IContainingCodeTrackerProcessResult ProcessChanges(ITextSnapshot currentSnapshot, List newSpanAndLineRanges); + ContainingCodeTrackerState GetState(); + + IEnumerable Lines { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTrackerProcessResult.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTrackerProcessResult.cs new file mode 100644 index 00000000..4c12ee2c --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/IContainingCodeTrackerProcessResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IContainingCodeTrackerProcessResult + { + bool IsEmpty { get; } + IEnumerable ChangedLines { get; } + List UnprocessedSpans { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/IFileCodeSpanRangeService.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/IFileCodeSpanRangeService.cs new file mode 100644 index 00000000..a68ff7a9 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/IFileCodeSpanRangeService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IFileCodeSpanRangeService + { + List GetFileCodeSpanRanges(ITextSnapshot snapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/INewCodeTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/INewCodeTracker.cs new file mode 100644 index 00000000..c69169c2 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/INewCodeTracker.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface INewCodeTracker + { + IEnumerable Lines { get; } + + IEnumerable GetChangedLineNumbers( + ITextSnapshot currentSnapshot, + List newSpanChanges, + IEnumerable newCodeCodeRanges); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/SpanAndLineRange.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/SpanAndLineRange.cs new file mode 100644 index 00000000..30174e88 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/SpanAndLineRange.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class SpanAndLineRange + { + public SpanAndLineRange(Span span, int startLineNumber, int endLineNumber) + { + this.Span = span; + this.StartLineNumber = startLineNumber; + this.EndLineNumber = endLineNumber; + } + + public Span Span { get; } + public int StartLineNumber { get; } + public int EndLineNumber { get; } + + [ExcludeFromCodeCoverage] + public override bool Equals(object obj) + => obj is SpanAndLineRange other && + other.Span.Equals(this.Span) && other.StartLineNumber == this.StartLineNumber && other.EndLineNumber == this.EndLineNumber; + + [ExcludeFromCodeCoverage] + public override int GetHashCode() + { + int hashCode = -414942; + hashCode = (hashCode * -1521134295) + this.Span.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.StartLineNumber.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.EndLineNumber.GetHashCode(); + return hashCode; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs b/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs new file mode 100644 index 00000000..9ba84175 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLines/TrackedLines.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using System.Linq; +using FineCodeCoverage.Core.Utilities; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackedLines : ITrackedLines + { + private readonly List containingCodeTrackers; + private readonly INewCodeTracker newCodeTracker; + private readonly IFileCodeSpanRangeService fileCodeSpanRangeService; + + public IReadOnlyList ContainingCodeTrackers => this.containingCodeTrackers; + private readonly bool useFileCodeSpanRangeService; + + public TrackedLines( + List containingCodeTrackers, + INewCodeTracker newCodeTracker, + IFileCodeSpanRangeService roslynService) + { + this.containingCodeTrackers = containingCodeTrackers; + this.newCodeTracker = newCodeTracker; + this.fileCodeSpanRangeService = roslynService; + this.useFileCodeSpanRangeService = this.fileCodeSpanRangeService != null && newCodeTracker != null; + } + + private List GetSpanAndLineRanges(ITextSnapshot currentSnapshot, List newSpanChanges) + => newSpanChanges.Select( + newSpanChange => new SpanAndLineRange( + newSpanChange, + currentSnapshot.GetLineNumberFromPosition(newSpanChange.Start), + currentSnapshot.GetLineNumberFromPosition(newSpanChange.End) + )).ToList(); + + private (IEnumerable, List) ProcessContainingCodeTrackers( + ITextSnapshot currentSnapshot, + List spanAndLineRanges + ) + { + var removals = new List(); + var allChangedLines = new List(); + foreach (IContainingCodeTracker containingCodeTracker in this.containingCodeTrackers) + { + (IEnumerable changedLines, List unprocessedSpans) = + this.ProcessContainingCodeTracker(removals, containingCodeTracker, currentSnapshot, spanAndLineRanges); + allChangedLines.AddRange(changedLines); + spanAndLineRanges = unprocessedSpans; + } + + removals.ForEach(removal => this.containingCodeTrackers.Remove(removal)); + + return (allChangedLines, spanAndLineRanges); + } + + private (IEnumerable changedLines, List unprocessedSpans) ProcessContainingCodeTracker( + List removals, + IContainingCodeTracker containingCodeTracker, + ITextSnapshot currentSnapshot, + List spanAndLineRanges + ) + { + IContainingCodeTrackerProcessResult processResult = containingCodeTracker.ProcessChanges(currentSnapshot, spanAndLineRanges); + if (processResult.IsEmpty) + { + removals.Add(containingCodeTracker); + } + + return (processResult.ChangedLines, processResult.UnprocessedSpans); + } + + // normalized spans + public IEnumerable GetChangedLineNumbers(ITextSnapshot currentSnapshot, List newSpanChanges) + { + List spanAndLineRanges = this.GetSpanAndLineRanges(currentSnapshot, newSpanChanges); + (IEnumerable changedLines, List unprocessedSpans) = + this.ProcessContainingCodeTrackers(currentSnapshot, spanAndLineRanges); + IEnumerable newCodeTrackerChangedLines = this.GetNewCodeTrackerChangedLineNumbers(currentSnapshot, unprocessedSpans); + return changedLines.Concat(newCodeTrackerChangedLines).Distinct(); + } + + private IEnumerable GetNewCodeTrackerChangedLineNumbers(ITextSnapshot currentSnapshot, List 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); + } + + private List GetNewCodeCodeRanges(ITextSnapshot currentSnapshot) + => this.useFileCodeSpanRangeService ? this.GetNewCodeCodeRangesActual(currentSnapshot) : null; + + private List GetNewCodeCodeRangesActual(ITextSnapshot currentSnapshot) + => this.GetNewCodeCodeRanges(currentSnapshot, this.GetContainingCodeTrackersCodeSpanRanges()).ToList(); + + private List GetContainingCodeTrackersCodeSpanRanges() + => this.containingCodeTrackers.Select(ct => ct.GetState().CodeSpanRange).ToList(); + + private List GetNewCodeCodeRanges( + ITextSnapshot currentSnapshot, + List containingCodeTrackersCodeSpanRanges) + { + List fileCodeSpanRanges = this.fileCodeSpanRangeService.GetFileCodeSpanRanges(currentSnapshot); + var newCodeCodeRanges = new List(); + int i = 0, j = 0; + + while (i < fileCodeSpanRanges.Count && j < containingCodeTrackersCodeSpanRanges.Count) + { + CodeSpanRange fileRange = fileCodeSpanRanges[i]; + CodeSpanRange trackerRange = containingCodeTrackersCodeSpanRanges[j]; + + if (fileRange.EndLine < trackerRange.StartLine) + { + // fileRange does not intersect with trackerRange, add it to the result + newCodeCodeRanges.Add(fileRange); + i++; + } + else if (fileRange.StartLine > trackerRange.EndLine) + { + // fileRange is after trackerRange, move to the next trackerRange + j++; + } + else + { + // fileRange intersects with trackerRange, skip it + i++; + } + } + + // Add remaining fileCodeSpanRanges that come after the last trackerRange + while (i < fileCodeSpanRanges.Count) + { + newCodeCodeRanges.Add(fileCodeSpanRanges[i]); + i++; + } + + return newCodeCodeRanges; + } + + private (bool done, IEnumerable lines) GetLines(IEnumerable dynamicLines, int startLineNumber, int endLineNumber) + { + IEnumerable linesApplicableToStartLineNumber = this.LinesApplicableToStartLineNumber(dynamicLines, startLineNumber); + var lines = linesApplicableToStartLineNumber.TakeWhile(l => l.Number <= endLineNumber).ToList(); + bool done = lines.Count != linesApplicableToStartLineNumber.Count(); + return (done, lines); + } + + private IEnumerable LinesApplicableToStartLineNumber(IEnumerable dynamicLines, int startLineNumber) + => dynamicLines.Where(l => l.Number >= startLineNumber); + + private IEnumerable GetLinesFromContainingCodeTrackers(int startLineNumber, int endLineNumber) + => 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 GetNewLines(int startLineNumber, int endLineNumber) + => this.LinesApplicableToStartLineNumber(this.NewCodeTrackerLines(), startLineNumber) + .TakeWhile(l => l.Number <= endLineNumber); + + public IEnumerable GetLines(int startLineNumber, int endLineNumber) + => this.GetLinesFromContainingCodeTrackers(startLineNumber, endLineNumber) + .Concat(this.GetNewLines(startLineNumber, endLineNumber)) + .Distinct(new DynamicLineByLineNumberComparer()).ToList(); + + private class DynamicLineByLineNumberComparer : IEqualityComparer + { + public bool Equals(IDynamicLine x, IDynamicLine y) => x.Number == y.Number; + public int GetHashCode(IDynamicLine obj) => obj.Number; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs new file mode 100644 index 00000000..456d398d --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRange.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class CodeSpanRange + { + public CodeSpanRange(int startLine, int endLine) + { + this.StartLine = startLine; + this.EndLine = endLine; + } + public static CodeSpanRange SingleLine(int lineNumber) => new CodeSpanRange(lineNumber, lineNumber); + public int StartLine { get; set; } + public int EndLine { get; set; } + + public override bool Equals(object obj) + => obj is CodeSpanRange codeSpanRange && codeSpanRange.StartLine == this.StartLine && codeSpanRange.EndLine == this.EndLine; + + [ExcludeFromCodeCoverage] + public override int GetHashCode() + { + int hashCode = -1763436595; + hashCode = (hashCode * -1521134295) + this.StartLine.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.EndLine.GetHashCode(); + return hashCode; + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRangeContainingCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRangeContainingCodeTrackerFactory.cs new file mode 100644 index 00000000..5a29ec4b --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/CodeSpanRangeContainingCodeTrackerFactory.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [Export(typeof(ICodeSpanRangeContainingCodeTrackerFactory))] + internal class CodeSpanRangeContainingCodeTrackerFactory : ICodeSpanRangeContainingCodeTrackerFactory + { + private readonly ITrackingLineFactory trackingLineFactory; + private readonly ITrackingSpanRangeFactory trackingSpanRangeFactory; + private readonly ITrackedCoverageLinesFactory trackedCoverageLinesFactory; + private readonly ICoverageLineFactory coverageLineFactory; + private readonly ITrackingSpanRangeContainingCodeTrackerFactory trackedContainingCodeTrackerFactory; + private readonly INotIncludedLineFactory notIncludedLineFactory; + + [ImportingConstructor] + public CodeSpanRangeContainingCodeTrackerFactory( + ITrackingLineFactory trackingLineFactory, + ITrackingSpanRangeFactory trackingSpanRangeFactory, + ITrackedCoverageLinesFactory trackedCoverageLinesFactory, + ICoverageLineFactory coverageLineFactory, + ITrackingSpanRangeContainingCodeTrackerFactory trackedContainingCodeTrackerFactory, + INotIncludedLineFactory notIncludedLineFactory + ) + { + this.trackingLineFactory = trackingLineFactory; + this.trackingSpanRangeFactory = trackingSpanRangeFactory; + this.trackedCoverageLinesFactory = trackedCoverageLinesFactory; + this.coverageLineFactory = coverageLineFactory; + this.trackedContainingCodeTrackerFactory = trackedContainingCodeTrackerFactory; + this.notIncludedLineFactory = notIncludedLineFactory; + } + + public IContainingCodeTracker CreateNotIncluded(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + { + ITrackingSpanRange trackingSpanRange = this.CreateTrackingSpanRange(textSnapshot, containingRange, spanTrackingMode); + ITrackingLine notIncludedLine = this.notIncludedLineFactory.Create(trackingSpanRange.GetFirstTrackingSpan(), textSnapshot); + return this.trackedContainingCodeTrackerFactory.CreateNotIncluded(notIncludedLine, trackingSpanRange); + } + + public IContainingCodeTracker CreateCoverageLines( + ITextSnapshot textSnapshot, + List lines, + CodeSpanRange containingRange, + SpanTrackingMode spanTrackingMode + ) => this.trackedContainingCodeTrackerFactory.CreateCoverageLines( + this.CreateTrackingSpanRange(textSnapshot, containingRange, spanTrackingMode), + this.CreateTrackedCoverageLines(textSnapshot, lines, spanTrackingMode) + ); + + public IContainingCodeTracker CreateOtherLines(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + { + ITrackingSpanRange trackingSpanRange = this.CreateTrackingSpanRange(textSnapshot, containingRange, spanTrackingMode); + return this.trackedContainingCodeTrackerFactory.CreateOtherLines(trackingSpanRange); + } + + private ITrackingSpanRange CreateTrackingSpanRange(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode) + { + ITrackingSpan startTrackingSpan = this.trackingLineFactory.CreateTrackingSpan(textSnapshot, containingRange.StartLine, spanTrackingMode); + ITrackingSpan endTrackingSpan = this.trackingLineFactory.CreateTrackingSpan(textSnapshot, containingRange.EndLine, spanTrackingMode); + return this.trackingSpanRangeFactory.Create(startTrackingSpan, endTrackingSpan, textSnapshot); + } + + private ITrackedCoverageLines CreateTrackedCoverageLines(ITextSnapshot textSnapshot, List lines, SpanTrackingMode spanTrackingMode) + { + var coverageLines = lines.Select(line => this.coverageLineFactory.Create( + this.trackingLineFactory.CreateTrackingSpan(textSnapshot, line.Number - 1, spanTrackingMode), line) + ).ToList(); + return this.trackedCoverageLinesFactory.Create(coverageLines.ToList()); + } + + public IContainingCodeTracker CreateDirty( + ITextSnapshot currentSnapshot, + CodeSpanRange containingRange, + SpanTrackingMode spanTrackingMode + ) => this.trackedContainingCodeTrackerFactory.CreateDirty( + this.CreateTrackingSpanRange(currentSnapshot, containingRange, spanTrackingMode), + currentSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs new file mode 100644 index 00000000..f524a330 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ContainingCodeTrackedLinesBuilder.cs @@ -0,0 +1,318 @@ +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.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 + { + private readonly IRoslynService roslynService; + 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; + + [ImportingConstructor] + public ContainingCodeTrackedLinesBuilder( + IRoslynService roslynService, + ICodeSpanRangeContainingCodeTrackerFactory containingCodeTrackerFactory, + IContainingCodeTrackedLinesFactory containingCodeTrackedLinesFactory, + INewCodeTrackerFactory newCodeTrackerFactory, + IThreadHelper threadHelper, + ITextSnapshotLineExcluder textSnapshotLineExcluder, + IJsonConvertService jsonConvertService, + IAppOptionsProvider appOptionsProvider + ) + { + this.roslynService = roslynService; + this.containingCodeTrackerFactory = containingCodeTrackerFactory; + this.containingCodeTrackedLinesFactory = containingCodeTrackedLinesFactory; + this.newCodeTrackerFactory = newCodeTrackerFactory; + this.threadHelper = threadHelper; + this.textSnapshotLineExcluder = textSnapshotLineExcluder; + this.jsonConvertService = jsonConvertService; + this.appOptionsProvider = appOptionsProvider; + } + + private bool UseRoslynWhenTextChanges() + => this.appOptionsProvider.Get().EditorCoverageColouringMode == EditorCoverageColouringMode.UseRoslynWhenTextChanges; + + 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 ITrackedLines Create(List lines, ITextSnapshot textSnapshot, Language language) + { + 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); + } + + private IFileCodeSpanRangeService GetFileCodeSpanRangeService(Language language) + => language == Language.CPP ? null : this.GetRoslynFileCodeSpanRangeService(this.UseRoslynWhenTextChanges()); + + private IFileCodeSpanRangeService GetRoslynFileCodeSpanRangeService(bool useRoslynWhenTextChanges) + => useRoslynWhenTextChanges ? this : null; + + private List CreateContainingCodeTrackers(List lines, ITextSnapshot textSnapshot, Language language) + { + if (language == Language.CPP) + { + /* + 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(); + } + + return this.CreateRoslynContainingCodeTrackers(lines, textSnapshot, language == Language.CSharp); + } + + private IContainingCodeTracker CreateSingleLineContainingCodeTracker(ITextSnapshot textSnapshot, ILine line) + => this.CreateCoverageLines(textSnapshot, new List { line }, CodeSpanRange.SingleLine(line.Number - 1)); + + private IContainingCodeTracker CreateOtherLines(ITextSnapshot textSnapshot, CodeSpanRange codeSpanRange) + => this.containingCodeTrackerFactory.CreateOtherLines( + textSnapshot, + codeSpanRange, + SpanTrackingMode.EdgeNegative + ); + + private IContainingCodeTracker CreateCoverageLines(ITextSnapshot textSnapshot, List lines, CodeSpanRange containingRange) + => this.containingCodeTrackerFactory.CreateCoverageLines(textSnapshot, lines, containingRange, SpanTrackingMode.EdgeExclusive); + + private IContainingCodeTracker CreateNotIncluded(ITextSnapshot textSnapshot, CodeSpanRange containingRange) + => this.containingCodeTrackerFactory.CreateNotIncluded(textSnapshot, containingRange, SpanTrackingMode.EdgeExclusive); + + private List CreateRoslynContainingCodeTrackers(List lines, ITextSnapshot textSnapshot, bool isCSharp) + { + 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() + { + currentCodeSpanIndex++; + CodeSpanRange previousCodeSpanRange = currentCodeSpanRange; + currentCodeSpanRange = currentCodeSpanIndex < codeSpanRanges.Count + ? codeSpanRanges[currentCodeSpanIndex] + : null; + } + + void TrackOtherLines() + { + int to = currentCodeSpanRange.StartLine - 1; + TrackOtherLinesTo(to); + currentLine = currentCodeSpanRange.EndLine + 1; + } + + void TrackOtherLinesTo(int to) + { + 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) + { + containingCodeTrackers.Add( + this.CreateOtherLines( + textSnapshot, + CodeSpanRange.SingleLine(otherCodeLine) + ) + ); + } + } + + 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) + { + if (currentCodeSpanRange == null) + { + CreateSingleLineContainingCodeTrackerInCase(line); + } + else + { + int adjustedLine = line.Number - 1; + if (adjustedLine < currentCodeSpanRange.StartLine) + { + CreateSingleLineContainingCodeTrackerInCase(line); + } + else if (adjustedLine > currentCodeSpanRange.EndLine) + { + CreateRangeContainingCodeTracker(); + + LineAction(line); + + } + else + { + containedLines.Add(line); + } + } + } + + foreach (ILine line in lines) // these are in order` + { + LineAction(line); + } + + while (currentCodeSpanRange != null) + { + CreateRangeContainingCodeTracker(); + } + + 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); + } + + private IEnumerable StatesWithinSnapshot(IEnumerable states, ITextSnapshot currentSnapshot) + { + int numLines = currentSnapshot.LineCount; + return states.Where(state => state.CodeSpanRange.EndLine < numLines); + } + + private IContainingCodeTracker RecreateCoverageLines(SerializedState state, ITextSnapshot currentSnapshot) + { + CodeSpanRange codeSpanRange = state.CodeSpanRange; + return state.Lines[0].CoverageType == DynamicCoverageType.Dirty + ? this.containingCodeTrackerFactory.CreateDirty(currentSnapshot, codeSpanRange, SpanTrackingMode.EdgeExclusive) + : this.CreateCoverageLines(currentSnapshot, this.AdjustCoverageLines(state.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, + ITextSnapshot currentSnapshot + ) => states.Where(state => codeSpanRanges.Remove(state.CodeSpanRange)) + .Select(state => this.RecreateContainingCodeTracker(state, currentSnapshot)).ToList(); + + private IContainingCodeTracker RecreateContainingCodeTracker(SerializedState state, ITextSnapshot currentSnapshot) + { + CodeSpanRange codeSpanRange = state.CodeSpanRange; + IContainingCodeTracker containingCodeTracker = null; + switch (state.Type) + { + case ContainingCodeTrackerType.OtherLines: + containingCodeTracker = this.CreateOtherLines(currentSnapshot, codeSpanRange); + break; + case ContainingCodeTrackerType.NotIncluded: + containingCodeTracker = this.CreateNotIncluded(currentSnapshot, codeSpanRange); + break; + case ContainingCodeTrackerType.CoverageLines: + containingCodeTracker = this.RecreateCoverageLines(state, currentSnapshot); + break; + } + + return containingCodeTracker; + } + + private ITrackedLines RecreateTrackedLinesFromRoslynState(List states, ITextSnapshot currentSnapshot, bool isCharp) + { + 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); + } + + private IEnumerable GetRecreateNewCodeLineNumbers(List newCodeCodeRanges, bool useRoslynWhenTextChanges) + => useRoslynWhenTextChanges + ? this.StartLines(newCodeCodeRanges) + : this.EveryLineInCodeSpanRanges(newCodeCodeRanges); + + private IEnumerable StartLines(List newCodeCodeRanges) + => newCodeCodeRanges.Select(newCodeCodeRange => newCodeCodeRange.StartLine); + private IEnumerable EveryLineInCodeSpanRanges(List newCodeCodeRanges) + => newCodeCodeRanges.SelectMany( + newCodeCodeRange => Enumerable.Range( + newCodeCodeRange.StartLine, + newCodeCodeRange.EndLine - newCodeCodeRange.StartLine + 1) + ); + public ITrackedLines Create(string serializedCoverage, ITextSnapshot currentSnapshot, Language language) + { + List states = this.jsonConvertService.DeserializeObject>(serializedCoverage); + return language == Language.CPP + ? this.RecreateTrackedLinesFromCPPStates(states, currentSnapshot) + : this.RecreateTrackedLinesFromRoslynState(states, currentSnapshot, language == Language.CSharp); + } + + public string Serialize(ITrackedLines trackedLines) + { + var trackedLinesImpl = trackedLines as TrackedLines; + List states = this.GetSerializedStates(trackedLinesImpl); + return this.jsonConvertService.SerializeObject(states); + } + + private List GetSerializedStates(TrackedLines trackedLines) + => trackedLines.ContainingCodeTrackers.Select( + containingCodeTracker => SerializedState.From(containingCodeTracker.GetState())).ToList(); + + public List GetFileCodeSpanRanges(ITextSnapshot snapshot) => this.GetRoslynCodeSpanRanges(snapshot); + + private class AdjustedLine : ILine + { + public AdjustedLine(IDynamicLine dynamicLine) + { + this.Number = dynamicLine.Number + 1; + this.CoverageType = DynamicCoverageTypeConverter.Convert(dynamicLine.CoverageType); + } + + public int Number { get; } + + public CoverageType CoverageType { get; } + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ICodeSpanRangeContainingCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ICodeSpanRangeContainingCodeTrackerFactory.cs new file mode 100644 index 00000000..7fcd3627 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ICodeSpanRangeContainingCodeTrackerFactory.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ICodeSpanRangeContainingCodeTrackerFactory + { + IContainingCodeTracker CreateNotIncluded(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode); + IContainingCodeTracker CreateCoverageLines(ITextSnapshot textSnapshot, List lines, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode); + IContainingCodeTracker CreateOtherLines(ITextSnapshot textSnapshot, CodeSpanRange containingRange, SpanTrackingMode spanTrackingMode); + IContainingCodeTracker CreateDirty(ITextSnapshot currentSnapshot, CodeSpanRange codeSpanRange, SpanTrackingMode spanTrackingMode); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs new file mode 100644 index 00000000..cc490418 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/IContainingCodeTrackedLinesFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IContainingCodeTrackedLinesFactory + { + TrackedLines Create( + List containingCodeTrackers, + INewCodeTracker newCodeTracker, + IFileCodeSpanRangeService fileCodeSpanRangeService + ); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ITrackingSpanRangeContainingCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ITrackingSpanRangeContainingCodeTrackerFactory.cs new file mode 100644 index 00000000..3dd92ba9 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ITrackingSpanRangeContainingCodeTrackerFactory.cs @@ -0,0 +1,12 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackingSpanRangeContainingCodeTrackerFactory + { + IContainingCodeTracker CreateCoverageLines(ITrackingSpanRange trackingSpanRange, ITrackedCoverageLines trackedCoverageLines); + IContainingCodeTracker CreateDirty(ITrackingSpanRange trackingSpanRange, ITextSnapshot textSnapshot); + IContainingCodeTracker CreateNotIncluded(ITrackingLine trackingLine, ITrackingSpanRange trackingSpanRange); + IContainingCodeTracker CreateOtherLines(ITrackingSpanRange trackingSpanRange); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ITrackingSpanRangeFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ITrackingSpanRangeFactory.cs new file mode 100644 index 00000000..1baed8d5 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/ITrackingSpanRangeFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackingSpanRangeFactory + { + ITrackingSpanRange Create(ITrackingSpan startTrackingSpan, ITrackingSpan endTrackingSpan, ITextSnapshot currentSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedState.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedState.cs new file mode 100644 index 00000000..eff5485c --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/SerializedState.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class SerializedState + { + public SerializedState(CodeSpanRange codeSpanRange, ContainingCodeTrackerType type, List dynamicLines) + { + this.CodeSpanRange = codeSpanRange; + this.Type = type; + this.Lines = dynamicLines; + } + + public static SerializedState From(ContainingCodeTrackerState containingCodeTrackerState) + => new SerializedState( + containingCodeTrackerState.CodeSpanRange, + containingCodeTrackerState.Type, + containingCodeTrackerState.Lines.Select(line => new DynamicLine(line.Number, line.CoverageType)).ToList() + ); + + public CodeSpanRange CodeSpanRange { get; set; } + public ContainingCodeTrackerType Type { get; set; } + public List Lines { get; set; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRange.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRange.cs new file mode 100644 index 00000000..7d600b82 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRange.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackingSpanRange : ITrackingSpanRange + { + private readonly ITrackingSpan startTrackingSpan; + private readonly ITrackingSpan endTrackingSpan; + private readonly ILineTracker lineTracker; + private string lastRangeText; + private CodeSpanRange codeSpanRange; + + public TrackingSpanRange( + ITrackingSpan startTrackingSpan, + ITrackingSpan endTrackingSpan, + ITextSnapshot currentSnapshot, + ILineTracker lineTracker + ) + { + this.startTrackingSpan = startTrackingSpan; + this.endTrackingSpan = endTrackingSpan; + this.lineTracker = lineTracker; + (SnapshotSpan currentStartSpan, SnapshotSpan currentEndSpan) = this.GetCurrentRange(currentSnapshot); + this.SetRangeText(currentSnapshot, currentStartSpan, currentEndSpan); + } + + private (SnapshotSpan, SnapshotSpan) GetCurrentRange(ITextSnapshot currentSnapshot) + { + SnapshotSpan currentStartSpan = this.startTrackingSpan.GetSpan(currentSnapshot); + SnapshotSpan currentEndSpan = this.endTrackingSpan.GetSpan(currentSnapshot); + int startLineNumber = this.lineTracker.GetLineNumber(this.startTrackingSpan, currentSnapshot, false); + int endLineNumber = this.lineTracker.GetLineNumber(this.endTrackingSpan, currentSnapshot, true); + this.codeSpanRange = new CodeSpanRange(startLineNumber, endLineNumber); + return (currentStartSpan, currentEndSpan); + } + + private void SetRangeText(ITextSnapshot currentSnapshot, SnapshotSpan currentFirstSpan, SnapshotSpan currentEndSpan) + => this.lastRangeText = currentSnapshot.GetText(new Span(currentFirstSpan.Start, currentEndSpan.End - currentFirstSpan.Start)); + + public TrackingSpanRangeProcessResult Process(ITextSnapshot currentSnapshot, List newSpanAndLineRanges) + { + (SnapshotSpan currentFirstSpan, SnapshotSpan currentEndSpan) = this.GetCurrentRange(currentSnapshot); + (bool isEmpty, bool textChanged) = this.GetTextChangeInfo(currentSnapshot, currentFirstSpan, currentEndSpan); + List nonIntersecting = this.GetNonIntersecting(currentSnapshot, currentFirstSpan, currentEndSpan, newSpanAndLineRanges); + return new TrackingSpanRangeProcessResult(this, nonIntersecting, isEmpty, textChanged); + } + + private (bool isEmpty, bool textChanged) GetTextChangeInfo(ITextSnapshot currentSnapshot, SnapshotSpan currentFirstSpan, SnapshotSpan currentEndSpan) + { + string previousRangeText = this.lastRangeText; + this.SetRangeText(currentSnapshot, currentFirstSpan, currentEndSpan); + bool textChanged = previousRangeText != this.lastRangeText; + bool isEmpty = string.IsNullOrWhiteSpace(this.lastRangeText); + return (isEmpty, textChanged); + + } + + private List GetNonIntersecting( + ITextSnapshot currentSnapshot, SnapshotSpan currentFirstSpan, SnapshotSpan currentEndSpan, List newSpanAndLineRanges) + { + int currentFirstTrackedLineNumber = currentSnapshot.GetLineNumberFromPosition(currentFirstSpan.End); + int currentEndTrackedLineNumber = currentSnapshot.GetLineNumberFromPosition(currentEndSpan.End); + return newSpanAndLineRanges.Where( + spanAndLineNumber => this.OutsideRange( + currentFirstTrackedLineNumber, + currentEndTrackedLineNumber, + spanAndLineNumber.StartLineNumber) + && + this.OutsideRange(currentFirstTrackedLineNumber, currentEndTrackedLineNumber, spanAndLineNumber.EndLineNumber)).ToList(); + } + + private bool OutsideRange(int firstLineNumber, int endLineNumber, int spanLineNumber) + => spanLineNumber < firstLineNumber || spanLineNumber > endLineNumber; + + public ITrackingSpan GetFirstTrackingSpan() => this.startTrackingSpan; + + public CodeSpanRange ToCodeSpanRange() => this.codeSpanRange; + + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRangeFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRangeFactory.cs new file mode 100644 index 00000000..158b3786 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRangeFactory.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ITrackingSpanRangeFactory))] + internal class TrackingSpanRangeFactory : ITrackingSpanRangeFactory + { + private readonly ILineTracker lineTracker; + + [ImportingConstructor] + public TrackingSpanRangeFactory(ILineTracker lineTracker) => this.lineTracker = lineTracker; + + public ITrackingSpanRange Create(ITrackingSpan startTrackingSpan, ITrackingSpan endTrackingSpan, ITextSnapshot currentSnapshot) + => new TrackingSpanRange(startTrackingSpan, endTrackingSpan, currentSnapshot, this.lineTracker); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRangeProcessResult.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRangeProcessResult.cs new file mode 100644 index 00000000..10910c6b --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/Construction/TrackingSpanRangeProcessResult.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackingSpanRangeProcessResult + { + public TrackingSpanRangeProcessResult(ITrackingSpanRange trackingSpanRange, List nonIntersectingSpans, bool isEmpty, bool textChanged) + { + this.TrackingSpanRange = trackingSpanRange; + this.NonIntersectingSpans = nonIntersectingSpans; + this.IsEmpty = isEmpty; + this.TextChanged = textChanged; + } + public ITrackingSpanRange TrackingSpanRange { get; } + public List NonIntersectingSpans { get; } + public bool IsEmpty { get; } + public bool TextChanged { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ContainingCodeTrackerProcessResult.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ContainingCodeTrackerProcessResult.cs new file mode 100644 index 00000000..345baaa5 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ContainingCodeTrackerProcessResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class ContainingCodeTrackerProcessResult : IContainingCodeTrackerProcessResult + { + public ContainingCodeTrackerProcessResult(IEnumerable changedLines, List unprocessedSpans, bool isEmpty) + { + this.ChangedLines = changedLines; + this.UnprocessedSpans = unprocessedSpans; + this.IsEmpty = isEmpty; + } + public bool IsEmpty { get; } + public IEnumerable ChangedLines { get; set; } + + public List UnprocessedSpans { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs new file mode 100644 index 00000000..3f4a0a4d --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/CoverageCodeTracker.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class CoverageCodeTracker : IUpdatableDynamicLines + { + private readonly ITrackedCoverageLines trackedCoverageLines; + private readonly IDirtyLineFactory dirtyLineFactory; + private ITrackingLine dirtyLine; + + public CoverageCodeTracker( + ITrackedCoverageLines trackedCoverageLines, + IDirtyLineFactory dirtyLineFactory + ) + { + this.trackedCoverageLines = trackedCoverageLines; + this.dirtyLineFactory = dirtyLineFactory; + } + + private List CreateDirtyLineIfRequired( + List newSpanChanges, + List nonIntersecting, + bool textChanged, + ITextSnapshot currentSnapshot, + ITrackingSpanRange trackingSpanRange + ) => this.dirtyLine == null && textChanged && this.Intersected(newSpanChanges, nonIntersecting) + ? this.CreateDirtyLine(currentSnapshot, trackingSpanRange) + : null; + + private List CreateDirtyLine(ITextSnapshot currentSnapshot, ITrackingSpanRange trackingSpanRange) + { + ITrackingSpan firstTrackingSpan = trackingSpanRange.GetFirstTrackingSpan(); + this.dirtyLine = this.dirtyLineFactory.Create(firstTrackingSpan, currentSnapshot); + return new int[] { this.dirtyLine.Line.Number }.Concat(this.trackedCoverageLines.Lines.Select(l => l.Number)).ToList(); + } + + private bool Intersected( + List newSpanChanges, + List nonIntersecting + ) => nonIntersecting.Count < newSpanChanges.Count; + + public IEnumerable GetUpdatedLineNumbers( + TrackingSpanRangeProcessResult trackingSpanRangeProcessResult, + ITextSnapshot currentSnapshot, + List newSpanAndLIneRanges + ) + { + List changedLineNumbers = this.CreateDirtyLineIfRequired( + newSpanAndLIneRanges, + trackingSpanRangeProcessResult.NonIntersectingSpans, + trackingSpanRangeProcessResult.TextChanged, + currentSnapshot, + trackingSpanRangeProcessResult.TrackingSpanRange + ); + return changedLineNumbers ?? this.UpdateLines(currentSnapshot); + } + + private IEnumerable UpdateLines(ITextSnapshot currentSnapshot) + => this.dirtyLine != null + ? this.dirtyLine.Update(currentSnapshot) + : this.trackedCoverageLines.GetUpdatedLineNumbers(currentSnapshot); + + public IEnumerable Lines => this.dirtyLine != null ? new List { this.dirtyLine.Line } : this.trackedCoverageLines.Lines; + + public ContainingCodeTrackerType Type => ContainingCodeTrackerType.CoverageLines; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackedCoverageLines.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackedCoverageLines.cs new file mode 100644 index 00000000..a2d96766 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackedCoverageLines.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackedCoverageLines + { + IEnumerable Lines { get; } + IEnumerable GetUpdatedLineNumbers(ITextSnapshot currentSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingLine.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingLine.cs new file mode 100644 index 00000000..c5731e0b --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingLine.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackingLine + { + IDynamicLine Line { get; } + + List Update(ITextSnapshot currentSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs new file mode 100644 index 00000000..cf121f22 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/ITrackingSpanRange.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackingSpanRange + { + TrackingSpanRangeProcessResult Process(ITextSnapshot currentSnapshot, List newSpanAndLIneRanges); + ITrackingSpan GetFirstTrackingSpan(); + CodeSpanRange ToCodeSpanRange(); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/IUpdatableDynamicLines.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/IUpdatableDynamicLines.cs new file mode 100644 index 00000000..a92460ed --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/IUpdatableDynamicLines.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface IUpdatableDynamicLines + { + IEnumerable Lines { get; } + ContainingCodeTrackerType Type { get; } + + IEnumerable GetUpdatedLineNumbers( + TrackingSpanRangeProcessResult trackingSpanRangeProcessResult, + ITextSnapshot currentSnapshot, + List newSpanAndLineRanges); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/OtherLinesTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/OtherLinesTracker.cs new file mode 100644 index 00000000..c98e9899 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/OtherLinesTracker.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class OtherLinesTracker : IUpdatableDynamicLines + { + public IEnumerable Lines { get; } = Enumerable.Empty(); + + public ContainingCodeTrackerType Type => ContainingCodeTrackerType.OtherLines; + + public IEnumerable GetUpdatedLineNumbers( + TrackingSpanRangeProcessResult trackingSpanRangeProcessResult, + ITextSnapshot currentSnapshot, + List newSpanAndLineRanges) => Enumerable.Empty(); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingLineTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingLineTracker.cs new file mode 100644 index 00000000..ec615604 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingLineTracker.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackingLineTracker : IUpdatableDynamicLines + { + private readonly ITrackingLine trackingLine; + + public TrackingLineTracker( + ITrackingLine trackingLine, ContainingCodeTrackerType containingCodeTrackerType + ) + { + this.trackingLine = trackingLine; + this.Type = containingCodeTrackerType; + } + + public IEnumerable Lines => new List { this.trackingLine.Line }; + + public ContainingCodeTrackerType Type { get; } + + public IEnumerable GetUpdatedLineNumbers( + TrackingSpanRangeProcessResult trackingSpanRangeProcessResult, + ITextSnapshot currentSnapshot, + List newSpanAndLineRanges) => this.trackingLine.Update(currentSnapshot); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingSpanRangeContainingCodeTrackerFactory.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingSpanRangeContainingCodeTrackerFactory.cs new file mode 100644 index 00000000..b2422648 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingSpanRangeContainingCodeTrackerFactory.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ITrackingSpanRangeContainingCodeTrackerFactory))] + internal class TrackingSpanRangeContainingCodeTrackerFactory : ITrackingSpanRangeContainingCodeTrackerFactory + { + private readonly IDirtyLineFactory dirtyLineFactory; + + [ImportingConstructor] + public TrackingSpanRangeContainingCodeTrackerFactory( + IDirtyLineFactory dirtyLineFactory + ) => this.dirtyLineFactory = dirtyLineFactory; + + public IContainingCodeTracker CreateCoverageLines(ITrackingSpanRange trackingSpanRange, ITrackedCoverageLines trackedCoverageLines) + => this.Wrap(trackingSpanRange, new CoverageCodeTracker(trackedCoverageLines, this.dirtyLineFactory)); + + public IContainingCodeTracker CreateDirty(ITrackingSpanRange trackingSpanRange, ITextSnapshot textSnapshot) + => this.Wrap(trackingSpanRange, new TrackingLineTracker( + this.dirtyLineFactory.Create(trackingSpanRange.GetFirstTrackingSpan(), textSnapshot), + ContainingCodeTrackerType.CoverageLines) + ); + + public IContainingCodeTracker CreateNotIncluded(ITrackingLine trackingLine, ITrackingSpanRange trackingSpanRange) + => this.Wrap(trackingSpanRange, new TrackingLineTracker(trackingLine, ContainingCodeTrackerType.NotIncluded)); + + public IContainingCodeTracker CreateOtherLines(ITrackingSpanRange trackingSpanRange) + => this.Wrap(trackingSpanRange, new OtherLinesTracker()); + + private IContainingCodeTracker Wrap(ITrackingSpanRange trackingSpanRange, IUpdatableDynamicLines updatableDynamicLines) + => new TrackingSpanRangeUpdatingTracker(trackingSpanRange, updatableDynamicLines); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingSpanRangeUpdatingTracker.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingSpanRangeUpdatingTracker.cs new file mode 100644 index 00000000..c4d18f6f --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/ContainingCodeTracker/TrackingSpanRangeUpdatingTracker.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TrackingSpanRangeUpdatingTracker : IContainingCodeTracker + { + private readonly ITrackingSpanRange trackingSpanRange; + private readonly IUpdatableDynamicLines updatableDynamicLines; + + public TrackingSpanRangeUpdatingTracker( + ITrackingSpanRange trackingSpanRange, + IUpdatableDynamicLines updatableDynamicLines + ) + { + this.trackingSpanRange = trackingSpanRange; + this.updatableDynamicLines = updatableDynamicLines; + } + + public IEnumerable Lines => this.updatableDynamicLines.Lines; + + public ContainingCodeTrackerState GetState() + => new ContainingCodeTrackerState(this.updatableDynamicLines.Type, this.trackingSpanRange.ToCodeSpanRange(), this.Lines); + + public IContainingCodeTrackerProcessResult ProcessChanges(ITextSnapshot currentSnapshot, List newSpanAndLineRanges) + { + TrackingSpanRangeProcessResult trackingSpanRangeProcessResult = this.trackingSpanRange.Process(currentSnapshot, newSpanAndLineRanges); + List nonIntersectingSpans = trackingSpanRangeProcessResult.NonIntersectingSpans; + if (trackingSpanRangeProcessResult.IsEmpty) + { + IEnumerable lines = this.updatableDynamicLines.Lines.Select(l => l.Number); + return new ContainingCodeTrackerProcessResult(lines, nonIntersectingSpans, true); + } + + IEnumerable changedLines = this.updatableDynamicLines.GetUpdatedLineNumbers(trackingSpanRangeProcessResult, currentSnapshot, newSpanAndLineRanges); + return new ContainingCodeTrackerProcessResult(changedLines, nonIntersectingSpans, false); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs new file mode 100644 index 00000000..576ae603 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ILineExcluder.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ILineExcluder + { + bool ExcludeIfNotCode(string text, bool isCSharp); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ITextSnapshotLineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ITextSnapshotLineExcluder.cs new file mode 100644 index 00000000..593a2658 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/ITextSnapshotLineExcluder.cs @@ -0,0 +1,9 @@ +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 new file mode 100644 index 00000000..372b9492 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/LineExcluder.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.Composition; +using System.Linq; + +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", "'", "#" }; + + public bool ExcludeIfNotCode(string text, bool isCSharp) + { + string trimmedLineText = text.Trim(); + return trimmedLineText.Length == 0 || this.StartsWithExclusion(trimmedLineText, isCSharp); + } + + private bool StartsWithExclusion(string text, bool isCSharp) + { + string[] languageExclusions = isCSharp ? cSharpExclusions : vbExclusions; + return languageExclusions.Any(languageExclusion => text.StartsWith(languageExclusion)); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/TextSnapshotLineExcluder.cs b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/TextSnapshotLineExcluder.cs new file mode 100644 index 00000000..2bcee75a --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/TrackedLinesImpl/LineExclusion/TextSnapshotLineExcluder.cs @@ -0,0 +1,21 @@ +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/ILineTracker.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ILineTracker.cs new file mode 100644 index 00000000..b6d8631f --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ILineTracker.cs @@ -0,0 +1,11 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ILineTracker + { + int GetLineNumber(ITrackingSpan trackingSpan, ITextSnapshot currentSnapshot, bool lineFromEnd); + + TrackedLineInfo GetTrackedLineInfo(ITrackingSpan trackingSpan, ITextSnapshot currentSnapshot, bool lineFromEnd); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs new file mode 100644 index 00000000..caaf1f08 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfo.cs @@ -0,0 +1,12 @@ +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITextInfo + { + string FilePath { get; } + ITextBuffer2 TextBuffer { get; } + ITextView TextView { get; } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs new file mode 100644 index 00000000..09f4a800 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ITextInfoFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITextInfoFactory + { + ITextInfo Create(ITextView textView, ITextBuffer textBuffer); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ITextSnapshotText.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ITextSnapshotText.cs new file mode 100644 index 00000000..89cc9e45 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ITextSnapshotText.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITextSnapshotText + { + string GetLineText(ITextSnapshot textSnapshot, int lineNumber); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/ITrackingLineFactory.cs b/SharedProject/Editor/DynamicCoverage/Utilities/ITrackingLineFactory.cs new file mode 100644 index 00000000..a8bcf100 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/ITrackingLineFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal interface ITrackingLineFactory + { + ITrackingSpan CreateTrackingSpan(ITextSnapshot textSnapshot, int lineNumber, SpanTrackingMode spanTrackingMode); + } +} \ No newline at end of file diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/LineTracker.cs b/SharedProject/Editor/DynamicCoverage/Utilities/LineTracker.cs new file mode 100644 index 00000000..42f33c60 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/LineTracker.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [Export(typeof(ITrackingLineFactory))] + [Export(typeof(ILineTracker))] + internal class LineTracker : ILineTracker, ITrackingLineFactory + { + public int GetLineNumber(ITrackingSpan trackingSpan, ITextSnapshot currentSnapshot, bool lineFromEnd) + { + SnapshotPoint position = this.GetPoint(trackingSpan, currentSnapshot, lineFromEnd); + return currentSnapshot.GetLineNumberFromPosition(position); + } + + private SnapshotPoint GetPoint(ITrackingSpan trackingSpan, ITextSnapshot currentSnapshot, bool lineFromEnd) + => lineFromEnd ? trackingSpan.GetEndPoint(currentSnapshot) : trackingSpan.GetStartPoint(currentSnapshot); + + public TrackedLineInfo GetTrackedLineInfo(ITrackingSpan trackingSpan, ITextSnapshot currentSnapshot, bool lineFromEnd) + { + SnapshotPoint position = this.GetPoint(trackingSpan, currentSnapshot, lineFromEnd); + + ITextSnapshotLine line = currentSnapshot.GetLineFromPosition(position); + int lineNumber = line.LineNumber; + string text = currentSnapshot.GetText(line.Extent); + + return new TrackedLineInfo(lineNumber, text); + } + + public ITrackingSpan CreateTrackingSpan(ITextSnapshot textSnapshot, int lineNumber, SpanTrackingMode spanTrackingMode) + { + SnapshotSpan span = textSnapshot.GetLineFromLineNumber(lineNumber).Extent; + return textSnapshot.CreateTrackingSpan(span, spanTrackingMode); + } + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs new file mode 100644 index 00000000..76a47d93 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfo.cs @@ -0,0 +1,36 @@ +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal class TextInfo : ITextInfo + { + private bool triedGetProperty; + private ITextDocument document; + private ITextDocument TextDocument + { + get + { + if (!this.triedGetProperty) + { + this.triedGetProperty = true; + if (this.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out ITextDocument document)) + { + this.document = document; + } + } + + return this.document; + } + } + public TextInfo(ITextView textView, ITextBuffer textBuffer) + { + this.TextView = textView; + this.TextBuffer = textBuffer as ITextBuffer2; + } + + public ITextView TextView { get; } + public ITextBuffer2 TextBuffer { get; } + public string FilePath => this.TextDocument?.FilePath; + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs new file mode 100644 index 00000000..507b6ea0 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/TextInfoFactory.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ITextInfoFactory))] + internal class TextInfoFactory : ITextInfoFactory + { + public ITextInfo Create(ITextView textView, ITextBuffer textBuffer) => new TextInfo(textView, textBuffer); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/TextSnapshotText.cs b/SharedProject/Editor/DynamicCoverage/Utilities/TextSnapshotText.cs new file mode 100644 index 00000000..719f97a8 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/TextSnapshotText.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + [Export(typeof(ITextSnapshotText))] + [ExcludeFromCodeCoverage] + internal class TextSnapshotText : ITextSnapshotText + { + public string GetLineText(ITextSnapshot textSnapshot, int lineNumber) + => textSnapshot.GetLineFromLineNumber(lineNumber).Extent.GetText(); + } +} diff --git a/SharedProject/Editor/DynamicCoverage/Utilities/TrackedLineInfo.cs b/SharedProject/Editor/DynamicCoverage/Utilities/TrackedLineInfo.cs new file mode 100644 index 00000000..7f459492 --- /dev/null +++ b/SharedProject/Editor/DynamicCoverage/Utilities/TrackedLineInfo.cs @@ -0,0 +1,13 @@ +namespace FineCodeCoverage.Editor.DynamicCoverage +{ + internal readonly struct TrackedLineInfo + { + public TrackedLineInfo(int lineNumber, string lineText) + { + this.LineNumber = lineNumber; + this.LineText = lineText; + } + public int LineNumber { get; } + public string LineText { get; } + } +} diff --git a/SharedProject/Editor/Management/ColoursClassificationFormatDefinition.cs b/SharedProject/Editor/Management/ColoursClassificationFormatDefinition.cs new file mode 100644 index 00000000..19b29447 --- /dev/null +++ b/SharedProject/Editor/Management/ColoursClassificationFormatDefinition.cs @@ -0,0 +1,14 @@ +using System.Windows.Media; +using Microsoft.VisualStudio.Text.Classification; + +namespace FineCodeCoverage.Editor.Management +{ + internal class ColoursClassificationFormatDefinition : ClassificationFormatDefinition + { + public ColoursClassificationFormatDefinition(Color foregroundColor, Color backgroundColor) + { + this.ForegroundColor = foregroundColor; + this.BackgroundColor = backgroundColor; + } + } +} diff --git a/SharedProject/Editor/Management/CoverageClassificationTypeService.cs b/SharedProject/Editor/Management/CoverageClassificationTypeService.cs new file mode 100644 index 00000000..49a40368 --- /dev/null +++ b/SharedProject/Editor/Management/CoverageClassificationTypeService.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Utilities; + +namespace FineCodeCoverage.Editor.Management +{ + [Export(typeof(ICoverageTypeService))] + [Export(typeof(ICoverageColoursEditorFormatMapNames))] + [Export(typeof(ICoverageClassificationColourService))] + internal class CoverageClassificationTypeService : + ICoverageClassificationColourService, ICoverageColoursEditorFormatMapNames, ICoverageTypeService + { + public const string FCCCoveredClassificationTypeName = "FCCCovered"; + public const string FCCNotCoveredClassificationTypeName = "FCCNotCovered"; + public const string FCCPartiallyCoveredClassificationTypeName = "FCCPartial"; + public const string FCCDirtyClassificationTypeName = "FCCDirty"; + public const string FCCNewLineClassificationTypeName = "FCCNewLine"; + public const string FCCNotIncludedClassificationTypeName = "FCCNotIncluded"; + private readonly Dictionary editorFormatNames = new Dictionary + { + {DynamicCoverageType.Partial, FCCPartiallyCoveredClassificationTypeName }, + {DynamicCoverageType.NotCovered, FCCNotCoveredClassificationTypeName }, + {DynamicCoverageType.Covered, FCCCoveredClassificationTypeName }, + {DynamicCoverageType.Dirty, FCCDirtyClassificationTypeName }, + {DynamicCoverageType.NewLine, FCCNewLineClassificationTypeName }, + {DynamicCoverageType.NotIncluded, FCCNotIncludedClassificationTypeName } + }; + + private readonly IClassificationFormatMap classificationFormatMap; + private readonly ReadOnlyDictionary classificationTypes; + private readonly IClassificationType highestPriorityClassificationType; + + [ExcludeFromCodeCoverage] + [Export] + [Name(FCCNotCoveredClassificationTypeName)] + public ClassificationTypeDefinition FCCNotCoveredTypeDefinition { get; set; } + + [ExcludeFromCodeCoverage] + [Export] + [Name(FCCCoveredClassificationTypeName)] + public ClassificationTypeDefinition FCCCoveredTypeDefinition { get; set; } + + [ExcludeFromCodeCoverage] + [Export] + [Name(FCCPartiallyCoveredClassificationTypeName)] + public ClassificationTypeDefinition FCCPartiallyCoveredTypeDefinition { get; set; } + + [ExcludeFromCodeCoverage] + [Export] + [Name(FCCDirtyClassificationTypeName)] + public ClassificationTypeDefinition FCCDirtyTypeDefinition { get; set; } + + [ExcludeFromCodeCoverage] + [Export] + [Name(FCCNewLineClassificationTypeName)] + public ClassificationTypeDefinition FCCNewLineTypeDefinition { get; set; } + + [ExcludeFromCodeCoverage] + [Export] + [Name(FCCNotIncludedClassificationTypeName)] + public ClassificationTypeDefinition FCCNotIncludedTypeDefinition { get; set; } + + [ImportingConstructor] + public CoverageClassificationTypeService( + IClassificationFormatMapService classificationFormatMapService, + IClassificationTypeRegistryService classificationTypeRegistryService + ) + { + this.classificationFormatMap = classificationFormatMapService.GetClassificationFormatMap("text"); + this.highestPriorityClassificationType = this.classificationFormatMap.CurrentPriorityOrder.Where(ct => ct != null).Last(); + + IClassificationType notCoveredClassificationType = classificationTypeRegistryService.GetClassificationType(FCCNotCoveredClassificationTypeName); + IClassificationType coveredClassificationType = classificationTypeRegistryService.GetClassificationType(FCCCoveredClassificationTypeName); + IClassificationType partiallyCoveredClassificationType = classificationTypeRegistryService.GetClassificationType(FCCPartiallyCoveredClassificationTypeName); + IClassificationType dirtyClassificationType = classificationTypeRegistryService.GetClassificationType(FCCDirtyClassificationTypeName); + IClassificationType newCodeClassificationType = classificationTypeRegistryService.GetClassificationType(FCCNewLineClassificationTypeName); + IClassificationType notIncludedClassificationType = classificationTypeRegistryService.GetClassificationType(FCCNotIncludedClassificationTypeName); + + this.classificationTypes = new ReadOnlyDictionary( + new Dictionary + { + { DynamicCoverageType.Covered, coveredClassificationType }, + { DynamicCoverageType.NotCovered, notCoveredClassificationType }, + { DynamicCoverageType.Partial, partiallyCoveredClassificationType }, + { DynamicCoverageType.Dirty, dirtyClassificationType }, + { DynamicCoverageType.NewLine, newCodeClassificationType }, + { DynamicCoverageType.NotIncluded, notIncludedClassificationType } + }); + } + + private void BatchUpdateIfRequired(Action action) + { + if (this.classificationFormatMap.IsInBatchUpdate) + { + action(); + } + else + { + this.classificationFormatMap.BeginBatchUpdate(); + action(); + this.classificationFormatMap.EndBatchUpdate(); + } + } + + public string GetEditorFormatDefinitionName(DynamicCoverageType coverageType) => this.editorFormatNames[coverageType]; + + public IClassificationType GetClassificationType(DynamicCoverageType coverageType) => this.classificationTypes[coverageType]; + + public void SetCoverageColours(IEnumerable coverageTypeColours) + => this.BatchUpdateIfRequired(() => + { + foreach (ICoverageTypeColour coverageTypeColour in coverageTypeColours) + { + this.SetCoverageColour(coverageTypeColour); + } + }); + + private void SetCoverageColour(ICoverageTypeColour coverageTypeColour) + { + IClassificationType classificationType = this.classificationTypes[coverageTypeColour.CoverageType]; + this.classificationFormatMap.AddExplicitTextProperties( + classificationType, coverageTypeColour.TextFormattingRunProperties, this.highestPriorityClassificationType); + } + } +} diff --git a/SharedProject/Editor/Management/CoverageColours.cs b/SharedProject/Editor/Management/CoverageColours.cs new file mode 100644 index 00000000..1197732a --- /dev/null +++ b/SharedProject/Editor/Management/CoverageColours.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.DynamicCoverage; + +namespace FineCodeCoverage.Editor.Management +{ + internal class CoverageColours : ICoverageColours + { + private readonly Dictionary coverageTypeToFontAndColorsInfo; + public CoverageColours( + IFontAndColorsInfo coverageTouchedInfo, + IFontAndColorsInfo coverageNotTouchedInfo, + IFontAndColorsInfo coveragePartiallyTouchedInfo, + IFontAndColorsInfo dirtyInfo, + IFontAndColorsInfo newLineInfo, + IFontAndColorsInfo notIncludedInfo + ) => this.coverageTypeToFontAndColorsInfo = new Dictionary + { + { DynamicCoverageType.Covered, coverageTouchedInfo}, + { DynamicCoverageType.NotCovered, coverageNotTouchedInfo }, + { DynamicCoverageType.Partial, coveragePartiallyTouchedInfo}, + { DynamicCoverageType.Dirty, dirtyInfo}, + { DynamicCoverageType.NewLine, newLineInfo}, + { DynamicCoverageType.NotIncluded, notIncludedInfo} + }; + + internal Dictionary GetChanges(CoverageColours lastCoverageColours) + => lastCoverageColours == null + ? this.coverageTypeToFontAndColorsInfo + : this.GetChanges(lastCoverageColours.coverageTypeToFontAndColorsInfo); + + private Dictionary GetChanges(Dictionary lastCoverageTypeToFontAndColorsInfo) + { + var changes = new Dictionary(); + foreach (KeyValuePair kvp in lastCoverageTypeToFontAndColorsInfo) + { + if (!this.coverageTypeToFontAndColorsInfo[kvp.Key].Equals(kvp.Value)) + { + changes.Add(kvp.Key, this.coverageTypeToFontAndColorsInfo[kvp.Key]); + } + } + + return changes; + } + + public IItemCoverageColours GetColour(DynamicCoverageType coverageType) + => this.coverageTypeToFontAndColorsInfo[coverageType].ItemCoverageColours; + } +} diff --git a/SharedProject/Editor/Management/CoverageColoursChangedMessage.cs b/SharedProject/Editor/Management/CoverageColoursChangedMessage.cs new file mode 100644 index 00000000..3de1c676 --- /dev/null +++ b/SharedProject/Editor/Management/CoverageColoursChangedMessage.cs @@ -0,0 +1,6 @@ +namespace FineCodeCoverage.Editor.Management +{ + internal class CoverageColoursChangedMessage + { + } +} diff --git a/SharedProject/Editor/Management/CoverageColoursManager.cs b/SharedProject/Editor/Management/CoverageColoursManager.cs new file mode 100644 index 00000000..4370a086 --- /dev/null +++ b/SharedProject/Editor/Management/CoverageColoursManager.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Windows.Media; +using FineCodeCoverage.Core.Initialization; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Utilities; + +namespace FineCodeCoverage.Editor.Management +{ + [Export(typeof(IInitializable))] + internal class CoverageColoursManager : IInitializable + { + private readonly ICoverageClassificationColourService coverageClassificationColourService; + private readonly IFontAndColorsInfosProvider fontAndColorsInfosProvider; + private readonly IEditorFormatMapTextSpecificListener editorFormatMapTextSpecificListener; + private readonly ITextFormattingRunPropertiesFactory textFormattingRunPropertiesFactory; + + #region format definitions + private const string partiallyCoveredEditorFormatDefinitionName = "Coverage Partially Touched Area FCC"; + private const string notCoveredEditorFormatDefinitionName = "Coverage Not Touched Area FCC"; + private const string coveredEditorFormatDefinitionName = "Coverage Touched Area FCC"; + private const string newLinesEditorFormatDefinitionName = "Coverage New Lines Area FCC"; + private const string dirtyEditorFormatDefinitionName = "Coverage Dirty Area FCC"; + private const string notIncludedEditorFormatDefintionName = "Coverage Not Included Area FCC"; + + [Export] + [Name(notIncludedEditorFormatDefintionName)] + [UserVisible(true)] + public EditorFormatDefinition NotIncludedEditorFormatDefinition { get; } = new ColoursClassificationFormatDefinition(Colors.Black, Colors.LightPink); + + [Export] + [Name(newLinesEditorFormatDefinitionName)] + [UserVisible(true)] + public EditorFormatDefinition NewLinesEditorFormatDefinition { get; } = new ColoursClassificationFormatDefinition(Colors.Black, Colors.Yellow); + + [Export] + [Name(dirtyEditorFormatDefinitionName)] + [UserVisible(true)] + public EditorFormatDefinition DirtyEditorFormatDefinition { get; } = new ColoursClassificationFormatDefinition(Colors.White, Colors.Brown); + + [Export] + [Name(coveredEditorFormatDefinitionName)] + [UserVisible(true)] + public EditorFormatDefinition CoveredEditorFormatDefinition { get; } = new ColoursClassificationFormatDefinition(Colors.Black, Color.FromRgb(16, 135, 24)); + + [Export] + [Name(notCoveredEditorFormatDefinitionName)] + [UserVisible(true)] + public EditorFormatDefinition NotCoveredEditorFormatDefinition { get; } = new ColoursClassificationFormatDefinition(Colors.White, Colors.Red); + + [Export] + [Name(partiallyCoveredEditorFormatDefinitionName)] + [UserVisible(true)] + public EditorFormatDefinition PartiallyCoveredEditorFormatDefinition { get; } = new ColoursClassificationFormatDefinition(Colors.Black, Color.FromRgb(255, 165, 0)); + + #endregion + + [ImportingConstructor] + public CoverageColoursManager( + IVsHasCoverageMarkersLogic vsHasCoverageMarkersLogic, + ICoverageClassificationColourService coverageClassificationColourService, + IFontAndColorsInfosProvider fontAndColorsInfosProvider, + IEditorFormatMapTextSpecificListener editorFormatMapTextSpecificListener, + ITextFormattingRunPropertiesFactory textFormattingRunPropertiesFactory, + IDelayedMainThreadInvocation delayedMainThreadInvocation, + ICoverageFontAndColorsCategoryItemNamesManager coverageFontAndColorsCategoryItemNamesManager + ) + { + this.coverageClassificationColourService = coverageClassificationColourService; + this.fontAndColorsInfosProvider = fontAndColorsInfosProvider; + this.editorFormatMapTextSpecificListener = editorFormatMapTextSpecificListener; + this.textFormattingRunPropertiesFactory = textFormattingRunPropertiesFactory; + + coverageFontAndColorsCategoryItemNamesManager.Initialize( + new FCCEditorFormatDefinitionNames( + coveredEditorFormatDefinitionName, + notCoveredEditorFormatDefinitionName, + partiallyCoveredEditorFormatDefinitionName, + newLinesEditorFormatDefinitionName, + dirtyEditorFormatDefinitionName, + notIncludedEditorFormatDefintionName + )); + coverageFontAndColorsCategoryItemNamesManager.Changed += (sender, args) => this.Changed(); + fontAndColorsInfosProvider.CoverageFontAndColorsCategoryItemNames = coverageFontAndColorsCategoryItemNamesManager.CategoryItemNames; + + this.editorFormatMapTextSpecificListener.ListenFor( + new List { + MarkerTypeNames.Covered, + MarkerTypeNames.NotCovered, + MarkerTypeNames.PartiallyCovered, + coveredEditorFormatDefinitionName, + notCoveredEditorFormatDefinitionName, + partiallyCoveredEditorFormatDefinitionName, + + newLinesEditorFormatDefinitionName, + dirtyEditorFormatDefinitionName, + notIncludedEditorFormatDefintionName + }, + () => this.Changed()); + + delayedMainThreadInvocation.DelayedInvoke(this.InitializeColours); + } + + private void InitializeColours() + { + Dictionary coverageColors = this.fontAndColorsInfosProvider.GetFontAndColorsInfos(); + this.SetClassificationTypeColoursIfChanged(coverageColors); + } + + private void Changed() + { + Dictionary changedColours = this.fontAndColorsInfosProvider.GetChangedFontAndColorsInfos(); + this.SetClassificationTypeColoursIfChanged(changedColours); + } + + private void SetClassificationTypeColoursIfChanged(Dictionary changes) + { + if (changes.Any()) + { + this.editorFormatMapTextSpecificListener.PauseListeningWhenExecuting( + () => this.SetClassificationTypeColours(changes) + ); + } + } + + private void SetClassificationTypeColours(Dictionary changes) + { + IEnumerable coverageTypeColours = changes.Select( + change => new CoverageTypeColour(change.Key, this.textFormattingRunPropertiesFactory.Create(change.Value)) + ); + this.coverageClassificationColourService.SetCoverageColours(coverageTypeColours); + } + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Management/CoverageFontAndColorsCategoryItemNamesManager.cs b/SharedProject/Editor/Management/CoverageFontAndColorsCategoryItemNamesManager.cs new file mode 100644 index 00000000..8135f53c --- /dev/null +++ b/SharedProject/Editor/Management/CoverageFontAndColorsCategoryItemNamesManager.cs @@ -0,0 +1,123 @@ +using System; +using System.ComponentModel.Composition; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Management +{ + [Export(typeof(ICoverageFontAndColorsCategoryItemNames))] + [Export(typeof(ICoverageFontAndColorsCategoryItemNamesManager))] + internal class CoverageFontAndColorsCategoryItemNamesManager : ICoverageFontAndColorsCategoryItemNames, ICoverageFontAndColorsCategoryItemNamesManager + { + private readonly Guid EditorTextMarkerFontAndColorCategory = new Guid("FF349800-EA43-46C1-8C98-878E78F46501"); + private readonly Guid EditorMEFCategory = new Guid("75A05685-00A8-4DED-BAE5-E7A50BFA929A"); + private readonly bool hasCoverageMarkers; + private readonly IAppOptionsProvider appOptionsProvider; + private FCCEditorFormatDefinitionNames fCCEditorFormatDefinitionNames; + private bool usingEnterprise = false; + private bool initialized = false; + + public event EventHandler Changed; + + [ImportingConstructor] + public CoverageFontAndColorsCategoryItemNamesManager( + IVsHasCoverageMarkersLogic vsHasCoverageMarkersLogic, + IAppOptionsProvider appOptionsProvider + ) + { + appOptionsProvider.OptionsChanged += this.AppOptionsProvider_OptionsChanged; + this.hasCoverageMarkers = vsHasCoverageMarkersLogic.HasCoverageMarkers(); + this.appOptionsProvider = appOptionsProvider; + } + + private void AppOptionsProvider_OptionsChanged(IAppOptions appOptions) + { + if (this.initialized) + { + this.ReactToAppOptionsChanging(appOptions); + } + } + + private void ReactToAppOptionsChanging(IAppOptions appOptions) + { + bool preUsingEnterprise = this.usingEnterprise; + this.Set(() => appOptions.UseEnterpriseFontsAndColors); + if (this.usingEnterprise != preUsingEnterprise) + { + Changed?.Invoke(this, new EventArgs()); + } + } + + public void Initialize(FCCEditorFormatDefinitionNames fCCEditorFormatDefinitionNames) + { + this.fCCEditorFormatDefinitionNames = fCCEditorFormatDefinitionNames; + this.Set(); + this.initialized = true; + } + + private void Set() => this.Set(() => this.appOptionsProvider.Get().UseEnterpriseFontsAndColors); + + private void Set(Func getUseEnterprise) + { + if (!this.hasCoverageMarkers) + { + this.SetMarkersFromFCC(); + } + else + { + + this.SetPossiblyEnterprise(getUseEnterprise()); + } + + this.SetFCCOnly(); + } + + private void SetPossiblyEnterprise(bool useEnterprise) + { + this.usingEnterprise = useEnterprise; + if (useEnterprise) + { + this.SetMarkersFromEnterprise(); + } + else + { + this.SetMarkersFromFCC(); + } + } + + private void SetFCCOnly() + { + this.NewLines = this.CreateMef(this.fCCEditorFormatDefinitionNames.NewLines); + this.Dirty = this.CreateMef(this.fCCEditorFormatDefinitionNames.Dirty); + this.NotIncluded = this.CreateMef(this.fCCEditorFormatDefinitionNames.NotIncluded); + } + + private void SetMarkersFromFCC() + { + this.Covered = this.CreateMef(this.fCCEditorFormatDefinitionNames.Covered); + this.NotCovered = this.CreateMef(this.fCCEditorFormatDefinitionNames.NotCovered); + this.PartiallyCovered = this.CreateMef(this.fCCEditorFormatDefinitionNames.PartiallyCovered); + } + + private void SetMarkersFromEnterprise() + { + this.Covered = this.CreateEnterprise(MarkerTypeNames.Covered); + this.NotCovered = this.CreateEnterprise(MarkerTypeNames.NotCovered); + this.PartiallyCovered = this.CreateEnterprise(MarkerTypeNames.PartiallyCovered); + } + + private FontAndColorsCategoryItemName CreateMef(string itemName) + => new FontAndColorsCategoryItemName(itemName, this.EditorMEFCategory); + + private FontAndColorsCategoryItemName CreateEnterprise(string itemName) + => new FontAndColorsCategoryItemName(itemName, this.EditorTextMarkerFontAndColorCategory); + + public FontAndColorsCategoryItemName Covered { get; private set; } + public FontAndColorsCategoryItemName NotCovered { get; private set; } + public FontAndColorsCategoryItemName PartiallyCovered { get; private set; } + public FontAndColorsCategoryItemName NewLines { get; private set; } + public FontAndColorsCategoryItemName Dirty { get; private set; } + + public FontAndColorsCategoryItemName NotIncluded { get; private set; } + public ICoverageFontAndColorsCategoryItemNames CategoryItemNames => this; + } +} diff --git a/SharedProject/Editor/Management/CoverageTypeColour.cs b/SharedProject/Editor/Management/CoverageTypeColour.cs new file mode 100644 index 00000000..408cc5e8 --- /dev/null +++ b/SharedProject/Editor/Management/CoverageTypeColour.cs @@ -0,0 +1,17 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text.Formatting; + +namespace FineCodeCoverage.Editor.Management +{ + internal class CoverageTypeColour : ICoverageTypeColour + { + public CoverageTypeColour(DynamicCoverageType coverageType, TextFormattingRunProperties textFormattingRunProperties) + { + this.CoverageType = coverageType; + this.TextFormattingRunProperties = textFormattingRunProperties; + } + + public DynamicCoverageType CoverageType { get; } + public TextFormattingRunProperties TextFormattingRunProperties { get; } + } +} diff --git a/SharedProject/Editor/Management/DelayedMainThreadInvocation.cs b/SharedProject/Editor/Management/DelayedMainThreadInvocation.cs new file mode 100644 index 00000000..f44156a1 --- /dev/null +++ b/SharedProject/Editor/Management/DelayedMainThreadInvocation.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; + +namespace FineCodeCoverage.Editor.Management +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IDelayedMainThreadInvocation))] + internal class DelayedMainThreadInvocation : IDelayedMainThreadInvocation + { + public void DelayedInvoke(Action action) + => _ = System.Threading.Tasks.Task.Delay(0).ContinueWith(_ => + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + action(); + }), TaskScheduler.Default); + } +} diff --git a/SharedProject/Editor/Management/EditorFormatMapTextSpecificListener.cs b/SharedProject/Editor/Management/EditorFormatMapTextSpecificListener.cs new file mode 100644 index 00000000..f4fe81ae --- /dev/null +++ b/SharedProject/Editor/Management/EditorFormatMapTextSpecificListener.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using Microsoft.VisualStudio.Text.Classification; + +namespace FineCodeCoverage.Editor.Management +{ + [Export(typeof(IEditorFormatMapTextSpecificListener))] + internal class EditorFormatMapTextSpecificListener : IEditorFormatMapTextSpecificListener + { + private List keys; + private Action callback; + [ImportingConstructor] + public EditorFormatMapTextSpecificListener( + IEditorFormatMapService editorFormatMapService + ) => editorFormatMapService.GetEditorFormatMap("text").FormatMappingChanged += this.EditorFormatMap_FormatMappingChanged; + + private void EditorFormatMap_FormatMappingChanged(object sender, FormatItemsEventArgs e) + { + var watchedItems = e.ChangedItems.Where(changedItem => this.keys.Contains(changedItem)).ToList(); + if (this.listening && watchedItems.Any()) + { + this.callback(); + } + } + + private bool listening; + + public void ListenFor(List keys, Action callback) + { + this.keys = keys; + this.callback = callback; + this.listening = true; + } + + public void PauseListeningWhenExecuting(Action action) + { + this.listening = false; + action(); + this.listening = true; + } + } +} diff --git a/SharedProject/Editor/Management/FCCEditorFormatDefinitionNames.cs b/SharedProject/Editor/Management/FCCEditorFormatDefinitionNames.cs new file mode 100644 index 00000000..7e5fa1b1 --- /dev/null +++ b/SharedProject/Editor/Management/FCCEditorFormatDefinitionNames.cs @@ -0,0 +1,24 @@ +namespace FineCodeCoverage.Editor.Management +{ + internal readonly struct FCCEditorFormatDefinitionNames + { + public FCCEditorFormatDefinitionNames( + string covered, string notCovered, string partiallyCovered, string newLines, string dirty, string notIncluded + ) + { + this.Covered = covered; + this.NotCovered = notCovered; + this.PartiallyCovered = partiallyCovered; + this.NewLines = newLines; + this.Dirty = dirty; + this.NotIncluded = notIncluded; + } + + public string Covered { get; } + public string NotCovered { get; } + public string PartiallyCovered { get; } + public string NewLines { get; } + public string Dirty { get; } + public string NotIncluded { get; } + } +} diff --git a/SharedProject/Editor/Management/FontAndColorsCategoryItemName.cs b/SharedProject/Editor/Management/FontAndColorsCategoryItemName.cs new file mode 100644 index 00000000..34d727fd --- /dev/null +++ b/SharedProject/Editor/Management/FontAndColorsCategoryItemName.cs @@ -0,0 +1,15 @@ +using System; + +namespace FineCodeCoverage.Editor.Management +{ + internal readonly struct FontAndColorsCategoryItemName + { + public FontAndColorsCategoryItemName(string itemName, Guid category) + { + this.Category = category; + this.ItemName = itemName; + } + public Guid Category { get; } + public string ItemName { get; } + } +} diff --git a/SharedProject/Editor/Management/FontAndColorsInfo.cs b/SharedProject/Editor/Management/FontAndColorsInfo.cs new file mode 100644 index 00000000..63f33df3 --- /dev/null +++ b/SharedProject/Editor/Management/FontAndColorsInfo.cs @@ -0,0 +1,16 @@ +namespace FineCodeCoverage.Editor.Management +{ + internal class FontAndColorsInfo : IFontAndColorsInfo + { + public FontAndColorsInfo(IItemCoverageColours itemCoverageColours, bool isBold) + { + this.ItemCoverageColours = itemCoverageColours; + this.IsBold = isBold; + } + + public IItemCoverageColours ItemCoverageColours { get; } + public bool IsBold { get; } + + public bool Equals(IFontAndColorsInfo other) => this.IsBold == other.IsBold && this.ItemCoverageColours.Equals(other.ItemCoverageColours); + } +} diff --git a/SharedProject/Editor/Management/FontAndColorsInfosProvider.cs b/SharedProject/Editor/Management/FontAndColorsInfosProvider.cs new file mode 100644 index 00000000..53a7db8d --- /dev/null +++ b/SharedProject/Editor/Management/FontAndColorsInfosProvider.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading.Tasks; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Core.Utilities.VsThreading; +using FineCodeCoverage.Editor.DynamicCoverage; + +namespace FineCodeCoverage.Editor.Management +{ + [Export(typeof(ICoverageColoursProvider))] + [Export(typeof(IFontAndColorsInfosProvider))] + internal class FontAndColorsInfosProvider : ICoverageColoursProvider, IFontAndColorsInfosProvider + { + private readonly IEventAggregator eventAggregator; + private readonly IFontsAndColorsHelper fontsAndColorsHelper; + + private readonly IThreadHelper threadHelper; + private CoverageColours lastCoverageColours; + private ICoverageFontAndColorsCategoryItemNames coverageFontAndColorsCategoryItemNames; + + public ICoverageFontAndColorsCategoryItemNames CoverageFontAndColorsCategoryItemNames + { + set => this.coverageFontAndColorsCategoryItemNames = value; + } + + private readonly struct NameIndex + { + public NameIndex(string name, int index) + { + this.Name = name; + this.Index = index; + } + public string Name { get; } + public int Index { get; } + } + + private class CategoryNameIndices + { + public CategoryNameIndices(Guid category) => this.Category = category; + public Guid Category { get; } + public List NameIndices { get; } = new List(); + } + + [ImportingConstructor] + public FontAndColorsInfosProvider( + IEventAggregator eventAggregator, + IFontsAndColorsHelper fontsAndColorsHelper, + IThreadHelper threadHelper + ) + { + this.eventAggregator = eventAggregator; + this.fontsAndColorsHelper = fontsAndColorsHelper; + this.threadHelper = threadHelper; + } + + private List<(FontAndColorsCategoryItemName, int)> IndexedFontAndColorsCategoryItemNames() + => new List<(FontAndColorsCategoryItemName, int)> + { + (this.coverageFontAndColorsCategoryItemNames.Covered, 0), + (this.coverageFontAndColorsCategoryItemNames.NotCovered, 1), + (this.coverageFontAndColorsCategoryItemNames.PartiallyCovered, 2), + (this.coverageFontAndColorsCategoryItemNames.Dirty,3), + (this.coverageFontAndColorsCategoryItemNames.NewLines,4), + (this.coverageFontAndColorsCategoryItemNames.NotIncluded,5) + }; + + private List GetCategoryNameIndices() + { + var lookup = new Dictionary(); + + List<(FontAndColorsCategoryItemName, int)> indexedFontAndColorsCategoryItemNames = this.IndexedFontAndColorsCategoryItemNames(); + + foreach ((FontAndColorsCategoryItemName fontAndColorsCategoryItemName, int index) in indexedFontAndColorsCategoryItemNames) + { + if (!lookup.TryGetValue(fontAndColorsCategoryItemName.Category, out CategoryNameIndices categoryNameIndices)) + { + categoryNameIndices = new CategoryNameIndices(fontAndColorsCategoryItemName.Category); + lookup.Add(fontAndColorsCategoryItemName.Category, categoryNameIndices); + } + + categoryNameIndices.NameIndices.Add(new NameIndex(fontAndColorsCategoryItemName.ItemName, index)); + } + + return lookup.Values.ToList(); + } + + public ICoverageColours GetCoverageColours() => this.GetCoverageColoursIfRequired(); + + private CoverageColours GetCoverageColoursIfRequired() + { + if (this.lastCoverageColours == null) + { + this.lastCoverageColours = this.GetCoverageColoursFromFontsAndColors(); + } + + return this.lastCoverageColours; + } + + private CoverageColours GetCoverageColoursFromFontsAndColors() + { + List fromFontsAndColors = this.GetItemCoverageInfosFromFontsAndColors(); + return new CoverageColours( + fromFontsAndColors[0],//touched + fromFontsAndColors[1],//not touched + fromFontsAndColors[2],//partial + fromFontsAndColors[3],//dirty + fromFontsAndColors[4],//newlines + fromFontsAndColors[5]//not included + ); + } + + private List GetItemCoverageInfosFromFontsAndColors() + => this.threadHelper.JoinableTaskFactory.Run(() => this.GetItemCoverageInfosFromFontsAndColorsAsync()); + + private async Task> GetItemCoverageInfosFromFontsAndColorsAsync() + { + List<(IFontAndColorsInfo fontAndColorsInfo, int nameIndex)>[] results = await this.GetItemCoverageInfosWithNameIndexFromFontsAndColorsAsync(); + return results.SelectMany(r => r).OrderBy(r => r.nameIndex).Select(r => r.fontAndColorsInfo).ToList(); + } + + private Task[]> GetItemCoverageInfosWithNameIndexFromFontsAndColorsAsync() + { + List allCategoryNameIndices = this.GetCategoryNameIndices(); + return Task.WhenAll( + allCategoryNameIndices.Select(categoryNameIndices => this.GetAsync(categoryNameIndices)) + ); + } + + private async Task> GetAsync(CategoryNameIndices categoryNameIndices) + { + List fontAndColorsInfos = await this.fontsAndColorsHelper.GetInfosAsync( + categoryNameIndices.Category, + categoryNameIndices.NameIndices.Select(ni => ni.Name).ToArray()); + return fontAndColorsInfos.Select((fontAndColorsInfo, i) => (fontAndColorsInfo, categoryNameIndices.NameIndices[i].Index)).ToList(); + } + + public Dictionary GetChangedFontAndColorsInfos() + { + CoverageColours currentColors = this.GetCoverageColoursFromFontsAndColors(); + Dictionary changes = currentColors.GetChanges(this.lastCoverageColours); + this.lastCoverageColours = currentColors; + if (changes.Any()) + { + this.eventAggregator.SendMessage(new CoverageColoursChangedMessage()); + } + + return changes; + } + + public Dictionary GetFontAndColorsInfos() + => this.GetCoverageColoursIfRequired().GetChanges(null); + } +} diff --git a/SharedProject/Editor/Management/FontsAndColorsHelper.cs b/SharedProject/Editor/Management/FontsAndColorsHelper.cs new file mode 100644 index 00000000..b64e455a --- /dev/null +++ b/SharedProject/Editor/Management/FontsAndColorsHelper.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading.Tasks; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Core.Utilities.VsThreading; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.TextManager.Interop; + +namespace FineCodeCoverage.Editor.Management +{ + [Export(typeof(IFontsAndColorsHelper))] + internal class FontsAndColorsHelper : IFontsAndColorsHelper + { + private readonly uint storeFlags = (uint)(__FCSTORAGEFLAGS.FCSF_READONLY | __FCSTORAGEFLAGS.FCSF_LOADDEFAULTS | __FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS | __FCSTORAGEFLAGS.FCSF_PROPAGATECHANGES); + private readonly System.IServiceProvider serviceProvider; + private readonly IThreadHelper threadHelper; + + [ImportingConstructor] + public FontsAndColorsHelper( + [Import(typeof(SVsServiceProvider))] System.IServiceProvider serviceProvider, + IThreadHelper threadHelper + ) + { + this.serviceProvider = serviceProvider; + this.threadHelper = threadHelper; + } + + private System.Windows.Media.Color ParseColor(uint color) + { + System.Drawing.Color dcolor = System.Drawing.ColorTranslator.FromOle(Convert.ToInt32(color)); + return System.Windows.Media.Color.FromArgb(dcolor.A, dcolor.R, dcolor.G, dcolor.B); + } + + private IVsFontAndColorStorage vsFontAndColorStorage; + private async Task GetVsFontAndColorStorageAsync() + { + if (this.vsFontAndColorStorage == null) + { + await this.threadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + this.vsFontAndColorStorage = this.serviceProvider.GetService(); + } + + return this.vsFontAndColorStorage; + } + + private IFontAndColorsInfo GetInfo(string displayName, IVsFontAndColorStorage fontAndColorStorage) + { + var touchAreaInfo = new ColorableItemInfo[1]; +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + int getItemSuccess = fontAndColorStorage.GetItem(displayName, touchAreaInfo); +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread + + if (getItemSuccess == VSConstants.S_OK) + { + System.Windows.Media.Color bgColor = this.ParseColor(touchAreaInfo[0].crBackground); + System.Windows.Media.Color fgColor = this.ParseColor(touchAreaInfo[0].crForeground); + return new FontAndColorsInfo(new ItemCoverageColours(fgColor, bgColor), touchAreaInfo[0].dwFontFlags == (uint)FONTFLAGS.FF_BOLD); + } + + return null; + } + + public async System.Threading.Tasks.Task> GetInfosAsync(Guid category, IEnumerable names) + { + var infos = new List(); + await this.OpenCloseCategoryAsync( + category, + fontAndColorStorage => infos = names.Select(name => this.GetInfo(name, fontAndColorStorage)).Where(color => color != null).ToList() + ); + return infos; + } + + private async System.Threading.Tasks.Task OpenCloseCategoryAsync(Guid category, Action action) + { + IVsFontAndColorStorage fontAndColorStorage = await this.GetVsFontAndColorStorageAsync(); + await this.threadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + int success = fontAndColorStorage.OpenCategory(ref category, this.storeFlags); + + if (success == VSConstants.S_OK) + { + action(fontAndColorStorage); + } + + _ = fontAndColorStorage.CloseCategory(); +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread + } + } +} diff --git a/SharedProject/Editor/Management/ICoverageClassificationColourService.cs b/SharedProject/Editor/Management/ICoverageClassificationColourService.cs new file mode 100644 index 00000000..a59efec1 --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageClassificationColourService.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageClassificationColourService : ICoverageTypeService + { + void SetCoverageColours(IEnumerable coverageTypeColours); + } +} diff --git a/SharedProject/Editor/Management/ICoverageColours.cs b/SharedProject/Editor/Management/ICoverageColours.cs new file mode 100644 index 00000000..62f69277 --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageColours.cs @@ -0,0 +1,9 @@ +using FineCodeCoverage.Editor.DynamicCoverage; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageColours + { + IItemCoverageColours GetColour(DynamicCoverageType coverageType); + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Management/ICoverageColoursEditorFormatMapNames.cs b/SharedProject/Editor/Management/ICoverageColoursEditorFormatMapNames.cs new file mode 100644 index 00000000..a3b462ee --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageColoursEditorFormatMapNames.cs @@ -0,0 +1,9 @@ +using FineCodeCoverage.Editor.DynamicCoverage; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageColoursEditorFormatMapNames + { + string GetEditorFormatDefinitionName(DynamicCoverageType coverageType); + } +} diff --git a/SharedProject/Editor/Management/ICoverageColoursProvider.cs b/SharedProject/Editor/Management/ICoverageColoursProvider.cs new file mode 100644 index 00000000..5349e9ae --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageColoursProvider.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageColoursProvider + { + ICoverageColours GetCoverageColours(); + } +} diff --git a/SharedProject/Editor/Management/ICoverageFontAndColorsCategoryItemNames.cs b/SharedProject/Editor/Management/ICoverageFontAndColorsCategoryItemNames.cs new file mode 100644 index 00000000..17fed809 --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageFontAndColorsCategoryItemNames.cs @@ -0,0 +1,12 @@ +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageFontAndColorsCategoryItemNames + { + FontAndColorsCategoryItemName Covered { get; } + FontAndColorsCategoryItemName Dirty { get; } + FontAndColorsCategoryItemName NewLines { get; } + FontAndColorsCategoryItemName NotCovered { get; } + FontAndColorsCategoryItemName PartiallyCovered { get; } + FontAndColorsCategoryItemName NotIncluded { get; } + } +} diff --git a/SharedProject/Editor/Management/ICoverageFontAndColorsCategoryItemNamesManager.cs b/SharedProject/Editor/Management/ICoverageFontAndColorsCategoryItemNamesManager.cs new file mode 100644 index 00000000..be304b1c --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageFontAndColorsCategoryItemNamesManager.cs @@ -0,0 +1,11 @@ +using System; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageFontAndColorsCategoryItemNamesManager + { + event EventHandler Changed; + void Initialize(FCCEditorFormatDefinitionNames fCCEditorFormatDefinitionNames); + ICoverageFontAndColorsCategoryItemNames CategoryItemNames { get; } + } +} diff --git a/SharedProject/Editor/Management/ICoverageTypeColour.cs b/SharedProject/Editor/Management/ICoverageTypeColour.cs new file mode 100644 index 00000000..8f93ead9 --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageTypeColour.cs @@ -0,0 +1,11 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text.Formatting; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageTypeColour + { + DynamicCoverageType CoverageType { get; } + TextFormattingRunProperties TextFormattingRunProperties { get; } + } +} diff --git a/SharedProject/Editor/Management/ICoverageTypeService.cs b/SharedProject/Editor/Management/ICoverageTypeService.cs new file mode 100644 index 00000000..ec548dab --- /dev/null +++ b/SharedProject/Editor/Management/ICoverageTypeService.cs @@ -0,0 +1,10 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text.Classification; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ICoverageTypeService + { + IClassificationType GetClassificationType(DynamicCoverageType coverageType); + } +} diff --git a/SharedProject/Editor/Management/IDelayedMainThreadInvocation.cs b/SharedProject/Editor/Management/IDelayedMainThreadInvocation.cs new file mode 100644 index 00000000..19341882 --- /dev/null +++ b/SharedProject/Editor/Management/IDelayedMainThreadInvocation.cs @@ -0,0 +1,9 @@ +using System; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface IDelayedMainThreadInvocation + { + void DelayedInvoke(Action action); + } +} diff --git a/SharedProject/Editor/Management/IEditorFormatMapTextSpecificListener.cs b/SharedProject/Editor/Management/IEditorFormatMapTextSpecificListener.cs new file mode 100644 index 00000000..2aabb585 --- /dev/null +++ b/SharedProject/Editor/Management/IEditorFormatMapTextSpecificListener.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface IEditorFormatMapTextSpecificListener + { + void ListenFor(List keys, Action callback); + void PauseListeningWhenExecuting(Action value); + } +} diff --git a/SharedProject/Editor/Management/IFontAndColorsInfo.cs b/SharedProject/Editor/Management/IFontAndColorsInfo.cs new file mode 100644 index 00000000..b3c1d9f4 --- /dev/null +++ b/SharedProject/Editor/Management/IFontAndColorsInfo.cs @@ -0,0 +1,10 @@ +using System; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface IFontAndColorsInfo : IEquatable + { + IItemCoverageColours ItemCoverageColours { get; } + bool IsBold { get; } + } +} diff --git a/SharedProject/Editor/Management/IFontAndColorsInfosProvider.cs b/SharedProject/Editor/Management/IFontAndColorsInfosProvider.cs new file mode 100644 index 00000000..7082f9d5 --- /dev/null +++ b/SharedProject/Editor/Management/IFontAndColorsInfosProvider.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.DynamicCoverage; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface IFontAndColorsInfosProvider + { + Dictionary GetChangedFontAndColorsInfos(); + Dictionary GetFontAndColorsInfos(); + ICoverageFontAndColorsCategoryItemNames CoverageFontAndColorsCategoryItemNames { set; } + } +} diff --git a/SharedProject/Editor/Management/IFontsAndColorsHelper.cs b/SharedProject/Editor/Management/IFontsAndColorsHelper.cs new file mode 100644 index 00000000..4ca43c9a --- /dev/null +++ b/SharedProject/Editor/Management/IFontsAndColorsHelper.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface IFontsAndColorsHelper + { + System.Threading.Tasks.Task> GetInfosAsync(Guid category, IEnumerable names); + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Management/IItemCoverageColours.cs b/SharedProject/Editor/Management/IItemCoverageColours.cs new file mode 100644 index 00000000..376b8bf2 --- /dev/null +++ b/SharedProject/Editor/Management/IItemCoverageColours.cs @@ -0,0 +1,11 @@ +using System; +using System.Windows.Media; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface IItemCoverageColours : IEquatable + { + Color Foreground { get; } + Color Background { get; } + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Management/ITextFormattingRunPropertiesFactory.cs b/SharedProject/Editor/Management/ITextFormattingRunPropertiesFactory.cs new file mode 100644 index 00000000..86838b71 --- /dev/null +++ b/SharedProject/Editor/Management/ITextFormattingRunPropertiesFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text.Formatting; + +namespace FineCodeCoverage.Editor.Management +{ + internal interface ITextFormattingRunPropertiesFactory + { + TextFormattingRunProperties Create(IFontAndColorsInfo fontAndColorsInfo); + } +} diff --git a/SharedProject/Editor/Management/IVsHasCoverageMarkersLogic.cs b/SharedProject/Editor/Management/IVsHasCoverageMarkersLogic.cs new file mode 100644 index 00000000..486d7f19 --- /dev/null +++ b/SharedProject/Editor/Management/IVsHasCoverageMarkersLogic.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Editor.Management +{ + internal interface IVsHasCoverageMarkersLogic + { + bool HasCoverageMarkers(); + } +} diff --git a/SharedProject/Editor/Management/ItemCoverageColours.cs b/SharedProject/Editor/Management/ItemCoverageColours.cs new file mode 100644 index 00000000..db5166b2 --- /dev/null +++ b/SharedProject/Editor/Management/ItemCoverageColours.cs @@ -0,0 +1,18 @@ +using System.Windows.Media; + +namespace FineCodeCoverage.Editor.Management +{ + internal class ItemCoverageColours : IItemCoverageColours + { + public ItemCoverageColours(Color foreground, Color background) + { + this.Foreground = foreground; + this.Background = background; + } + + public Color Foreground { get; } + public Color Background { get; } + + public bool Equals(IItemCoverageColours other) => this.Foreground == other.Foreground && this.Background == other.Background; + } +} diff --git a/SharedProject/Editor/Management/MarkerTypeNames.cs b/SharedProject/Editor/Management/MarkerTypeNames.cs new file mode 100644 index 00000000..8620c498 --- /dev/null +++ b/SharedProject/Editor/Management/MarkerTypeNames.cs @@ -0,0 +1,9 @@ +namespace FineCodeCoverage.Editor.Management +{ + public class MarkerTypeNames + { + public const string Covered = "Coverage Touched Area"; + public const string NotCovered = "Coverage Not Touched Area"; + public const string PartiallyCovered = "Coverage Partially Touched Area"; + } +} diff --git a/SharedProject/Editor/Management/TextFormattingRunPropertiesFactory.cs b/SharedProject/Editor/Management/TextFormattingRunPropertiesFactory.cs new file mode 100644 index 00000000..15b76a5a --- /dev/null +++ b/SharedProject/Editor/Management/TextFormattingRunPropertiesFactory.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.Composition; +using System.Windows.Media; +using Microsoft.VisualStudio.Text.Formatting; + +namespace FineCodeCoverage.Editor.Management +{ + // todo - consider a MEF export to allow other extensions to change the formatting + [Export(typeof(ITextFormattingRunPropertiesFactory))] + internal class TextFormattingRunPropertiesFactory : ITextFormattingRunPropertiesFactory + { + public TextFormattingRunProperties Create(IFontAndColorsInfo fontAndColorsInfo) + { + IItemCoverageColours coverageColours = fontAndColorsInfo.ItemCoverageColours; + return TextFormattingRunProperties.CreateTextFormattingRunProperties( + new SolidColorBrush(coverageColours.Foreground), new SolidColorBrush(coverageColours.Background), + null, // Typeface + null, // size + null, // hinting size + /* + TextDecorationCollection + https://docs.microsoft.com/en-us/dotnet/api/system.windows.textdecorations?view=windowsdesktop-8.0 + https://learn.microsoft.com/en-us/dotnet/api/system.windows.textdecorations?view=windowsdesktop-8.0 + */ + null, + // TextEffectCollection https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.texteffect?view=windowsdesktop-8.0 + null, // + null // CultureInfo + ).SetBold(fontAndColorsInfo.IsBold); + } + } +} diff --git a/SharedProject/Editor/Management/VsHasCoverageMarkersLogic.cs b/SharedProject/Editor/Management/VsHasCoverageMarkersLogic.cs new file mode 100644 index 00000000..0bf77466 --- /dev/null +++ b/SharedProject/Editor/Management/VsHasCoverageMarkersLogic.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Management +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IVsHasCoverageMarkersLogic))] + internal class VsHasCoverageMarkersLogic : IVsHasCoverageMarkersLogic + { + private readonly IReadOnlyConfigSettingsStoreProvider readOnlyConfigSettingsStoreProvider; + + [ImportingConstructor] + public VsHasCoverageMarkersLogic( + IReadOnlyConfigSettingsStoreProvider readOnlyConfigSettingsStoreProvider + ) => this.readOnlyConfigSettingsStoreProvider = readOnlyConfigSettingsStoreProvider; + + public bool HasCoverageMarkers() + { + Microsoft.VisualStudio.Settings.SettingsStore readOnlySettingsStore = this.readOnlyConfigSettingsStoreProvider.Provide(); + return readOnlySettingsStore.CollectionExists(@"Text Editor\External Markers\{b4ee9ead-e105-11d7-8a44-00065bbd20a4}"); + } + } +} diff --git a/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs b/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs new file mode 100644 index 00000000..35837965 --- /dev/null +++ b/SharedProject/Editor/Roslyn/CSharpContainingCodeVisitor.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal class CSharpContainingCodeVisitor : CSharpSyntaxVisitor, ILanguageContainingCodeVisitor + { + private readonly List spans = new List(); + public List GetSpans(SyntaxNode rootNode) + { + this.Visit(rootNode); + return this.spans; + } + +#if VS2022 + public override void VisitFileScopedNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax node) + => this.VisitMembers(node.Members); +#endif + public override void VisitCompilationUnit(CompilationUnitSyntax node) => this.VisitMembers(node.Members); + + public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) => this.VisitMembers(node.Members); + + public override void VisitClassDeclaration(ClassDeclarationSyntax node) => this.VisitMembers(node.Members); + + public override void VisitStructDeclaration(StructDeclarationSyntax node) => this.VisitMembers(node.Members); + + public override void VisitRecordDeclaration(RecordDeclarationSyntax node) => this.VisitMembers(node.Members); + + public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) => this.VisitMembers(node.Members); + + public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => this.AddIfHasBody(node); + + public override void VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) => this.AddIfHasBody(node); + + public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node) => this.AddIfHasBody(node); + + public override void VisitMethodDeclaration(MethodDeclarationSyntax node) => this.AddIfHasBody(node); + + public override void VisitOperatorDeclaration(OperatorDeclarationSyntax node) => this.AddIfHasBody(node); + + private bool HasBody(BaseMethodDeclarationSyntax node) => node.Body != null || node.ExpressionBody != null; + private void AddIfHasBody(BaseMethodDeclarationSyntax node) + { + if (this.HasBody(node)) + { + this.AddNode(node); + } + } + + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) => this.VisitBasePropertyDeclaration(node); + + public override void VisitEventDeclaration(EventDeclarationSyntax node) => this.VisitBasePropertyDeclaration(node); + + public override void VisitIndexerDeclaration(IndexerDeclarationSyntax node) => this.VisitBasePropertyDeclaration(node); + + private void VisitBasePropertyDeclaration(BasePropertyDeclarationSyntax node) + { + if (!this.IsAbstract(node.Modifiers)) + { + this.VisitNonAbstractBasePropertyDeclaration(node); + } + } + + private void AddIfPropertyDeclaration(BasePropertyDeclarationSyntax node) + { + if (node is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + this.AddNode(propertyDeclarationSyntax); + } + } + + private void VisitNonAbstractBasePropertyDeclaration(BasePropertyDeclarationSyntax node) + { + if (node.AccessorList == null) + { + this.AddIfPropertyDeclaration(node); + } + else + { + this.AddAccessors(node.AccessorList.Accessors, node.Parent is InterfaceDeclarationSyntax); + } + } + + private void AddAccessors(SyntaxList accessors, bool typeIsInterface) + => accessors.Where(accessor => !typeIsInterface || this.AccessorHasBody(accessor)).ToList().ForEach(this.AddNode); + + private bool AccessorHasBody(AccessorDeclarationSyntax accessor) => accessor.Body != null || accessor.ExpressionBody != null; + + private void VisitMembers(SyntaxList members) + { + foreach (MemberDeclarationSyntax member in members) + { + this.Visit(member); + } + } + + private bool IsAbstract(SyntaxTokenList modifiers) => modifiers.Any(modifier => modifier.IsKind(SyntaxKind.AbstractKeyword)); + + private void AddNode(SyntaxNode node) => this.spans.Add(node.Span); + } +} diff --git a/SharedProject/Editor/Roslyn/ILanguageContainingCodeVisitor.cs b/SharedProject/Editor/Roslyn/ILanguageContainingCodeVisitor.cs new file mode 100644 index 00000000..2d4d7e0a --- /dev/null +++ b/SharedProject/Editor/Roslyn/ILanguageContainingCodeVisitor.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal interface ILanguageContainingCodeVisitor + { + List GetSpans(SyntaxNode rootNode); + } +} diff --git a/SharedProject/Editor/Roslyn/ILanguageContainingCodeVisitorFactory.cs b/SharedProject/Editor/Roslyn/ILanguageContainingCodeVisitorFactory.cs new file mode 100644 index 00000000..c9dda073 --- /dev/null +++ b/SharedProject/Editor/Roslyn/ILanguageContainingCodeVisitorFactory.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Editor.Roslyn +{ + internal interface ILanguageContainingCodeVisitorFactory + { + ILanguageContainingCodeVisitor Create(bool isCSharp); + } +} diff --git a/SharedProject/Editor/Roslyn/IRoslynService.cs b/SharedProject/Editor/Roslyn/IRoslynService.cs new file mode 100644 index 00000000..8706b939 --- /dev/null +++ b/SharedProject/Editor/Roslyn/IRoslynService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal interface IRoslynService + { + Task> GetContainingCodeSpansAsync(ITextSnapshot textSnapshot); + } +} diff --git a/SharedProject/Editor/Roslyn/ITextSnapshotToSyntaxService.cs b/SharedProject/Editor/Roslyn/ITextSnapshotToSyntaxService.cs new file mode 100644 index 00000000..f021fb4a --- /dev/null +++ b/SharedProject/Editor/Roslyn/ITextSnapshotToSyntaxService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal interface ITextSnapshotToSyntaxService + { + Task GetRootAndLanguageAsync(ITextSnapshot textSnapshot); + } +} diff --git a/SharedProject/Editor/Roslyn/LanguageContainingCodeVisitorFactory.cs b/SharedProject/Editor/Roslyn/LanguageContainingCodeVisitorFactory.cs new file mode 100644 index 00000000..726a3afe --- /dev/null +++ b/SharedProject/Editor/Roslyn/LanguageContainingCodeVisitorFactory.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Editor.Roslyn +{ + [Export(typeof(ILanguageContainingCodeVisitorFactory))] + internal class LanguageContainingCodeVisitorFactory : ILanguageContainingCodeVisitorFactory + { + public ILanguageContainingCodeVisitor Create(bool isCSharp) + => isCSharp ? new CSharpContainingCodeVisitor() as ILanguageContainingCodeVisitor : new VBContainingCodeVisitor(); + } +} diff --git a/SharedProject/Editor/Roslyn/RootNodeAndLanguage.cs b/SharedProject/Editor/Roslyn/RootNodeAndLanguage.cs new file mode 100644 index 00000000..cc1558a6 --- /dev/null +++ b/SharedProject/Editor/Roslyn/RootNodeAndLanguage.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal class RootNodeAndLanguage + { + public SyntaxNode Root { get; } + public string Language { get; } + + public RootNodeAndLanguage(SyntaxNode root, string language) + { + this.Root = root; + this.Language = language; + } + } +} diff --git a/SharedProject/Editor/Roslyn/RoslynService.cs b/SharedProject/Editor/Roslyn/RoslynService.cs new file mode 100644 index 00000000..0e74cba2 --- /dev/null +++ b/SharedProject/Editor/Roslyn/RoslynService.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Roslyn +{ + [Export(typeof(IRoslynService))] + internal class RoslynService : IRoslynService + { + private readonly ILanguageContainingCodeVisitorFactory languageContainingCodeVisitorFactory; + private readonly ITextSnapshotToSyntaxService textSnapshotToSyntaxService; + + [ImportingConstructor] + public RoslynService( + ILanguageContainingCodeVisitorFactory languageContainingCodeVisitorFactory, + ITextSnapshotToSyntaxService textSnapshotToSyntaxService) + { + this.languageContainingCodeVisitorFactory = languageContainingCodeVisitorFactory; + this.textSnapshotToSyntaxService = textSnapshotToSyntaxService; + } + public async Task> GetContainingCodeSpansAsync(ITextSnapshot textSnapshot) + { + RootNodeAndLanguage rootNodeAndLanguage = await this.textSnapshotToSyntaxService.GetRootAndLanguageAsync(textSnapshot); + if (rootNodeAndLanguage == null) + { + return Enumerable.Empty().ToList(); + } + + bool isCSharp = rootNodeAndLanguage.Language == LanguageNames.CSharp; + ILanguageContainingCodeVisitor languageContainingCodeVisitor = this.languageContainingCodeVisitorFactory.Create(isCSharp); + return languageContainingCodeVisitor.GetSpans(rootNodeAndLanguage.Root); + } + } +} diff --git a/SharedProject/Editor/Roslyn/TextSnapshotToSyntaxService.cs b/SharedProject/Editor/Roslyn/TextSnapshotToSyntaxService.cs new file mode 100644 index 00000000..fc24612f --- /dev/null +++ b/SharedProject/Editor/Roslyn/TextSnapshotToSyntaxService.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Roslyn +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ITextSnapshotToSyntaxService))] + internal class TextSnapshotToSyntaxService : ITextSnapshotToSyntaxService + { + public async Task GetRootAndLanguageAsync(ITextSnapshot textSnapshot) + { + Microsoft.CodeAnalysis.Document document = textSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) + { + string language = document.Project.Language; + Microsoft.CodeAnalysis.SyntaxNode root = await document.GetSyntaxRootAsync(); + return new RootNodeAndLanguage(root, language); + } + + return null; + } + } +} diff --git a/SharedProject/Editor/Roslyn/VBContainingCodeVisitor.cs b/SharedProject/Editor/Roslyn/VBContainingCodeVisitor.cs new file mode 100644 index 00000000..b35bb7f4 --- /dev/null +++ b/SharedProject/Editor/Roslyn/VBContainingCodeVisitor.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace FineCodeCoverage.Editor.Roslyn +{ + internal class VBContainingCodeVisitor : VisualBasicSyntaxVisitor, ILanguageContainingCodeVisitor + { + private readonly List spans = new List(); + public List GetSpans(SyntaxNode rootNode) + { + this.Visit(rootNode); + return this.spans; + } + public override void VisitCompilationUnit(CompilationUnitSyntax node) => this.VisitMembers(node.Members); + + public override void VisitNamespaceBlock(NamespaceBlockSyntax node) => this.VisitMembers(node.Members); + + private void VisitMembers(SyntaxList members) + { + foreach (StatementSyntax member in members) + { + this.Visit(member); + } + } + + public override void VisitClassBlock(ClassBlockSyntax node) => this.VisitMembers(node.Members); + + public override void VisitStructureBlock(StructureBlockSyntax node) => this.VisitMembers(node.Members); + + public override void VisitModuleBlock(ModuleBlockSyntax node) => this.VisitMembers(node.Members); + + public override void VisitConstructorBlock(ConstructorBlockSyntax node) => this.AddNode(node); + + public override void VisitMethodBlock(MethodBlockSyntax node) + { + if (!this.IsPartial(node.SubOrFunctionStatement.Modifiers)) + { + this.AddNode(node); + } + } + + private bool IsPartial(SyntaxTokenList modifiers) => modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)); + + private bool IsAbstract(SyntaxTokenList modifiers) => modifiers.Any(modifier => modifier.IsKind(SyntaxKind.MustOverrideKeyword)); + + public override void VisitOperatorBlock(OperatorBlockSyntax node) => this.AddNode(node); + + public override void VisitPropertyBlock(PropertyBlockSyntax node) => this.VisitAccessors(node.Accessors); + + // Coverlet instruments C# auto properties but not VB. May be able to remove this + public override void VisitPropertyStatement(PropertyStatementSyntax node) + { + if (!this.IsAbstract(node.Modifiers)) + { + this.AddNode(node); + } + } + + public override void VisitEventBlock(EventBlockSyntax node) => this.VisitAccessors(node.Accessors); + + private void VisitAccessors(SyntaxList accessors) + { + foreach (AccessorBlockSyntax accessor in accessors) + { + this.Visit(accessor); + } + } + + public override void VisitAccessorBlock(AccessorBlockSyntax node) => this.AddNode(node); + + private void AddNode(SyntaxNode node) => this.spans.Add(node.Span); + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTagger.cs b/SharedProject/Editor/Tagging/Base/CoverageTagger.cs new file mode 100644 index 00000000..2d99ce3a --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/CoverageTagger.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal class CoverageTagger : + ICoverageTagger, + IListener, + IListener, + IDisposable, + ITagger + where TTag : ITag + + { + private readonly ITextInfo textInfo; + private readonly ITextBuffer textBuffer; + private IBufferLineCoverage bufferLineCoverage; + private ICoverageTypeFilter coverageTypeFilter; + private readonly IEventAggregator eventAggregator; + private readonly ILineSpanLogic lineSpanLogic; + private readonly ILineSpanTagger lineSpanTagger; + + public event EventHandler TagsChanged; + + public CoverageTagger( + ITextInfo textInfo, + IBufferLineCoverage bufferLineCoverage, + ICoverageTypeFilter coverageTypeFilter, + IEventAggregator eventAggregator, + ILineSpanLogic lineSpanLogic, + ILineSpanTagger lineSpanTagger + ) + { + ThrowIf.Null(textInfo, nameof(textInfo)); + ThrowIf.Null(coverageTypeFilter, nameof(coverageTypeFilter)); + ThrowIf.Null(eventAggregator, nameof(eventAggregator)); + ThrowIf.Null(lineSpanLogic, nameof(lineSpanLogic)); + ThrowIf.Null(lineSpanTagger, nameof(lineSpanTagger)); + this.textInfo = textInfo; + this.textBuffer = textInfo.TextBuffer; + this.bufferLineCoverage = bufferLineCoverage; + this.coverageTypeFilter = coverageTypeFilter; + this.eventAggregator = eventAggregator; + this.lineSpanLogic = lineSpanLogic; + this.lineSpanTagger = lineSpanTagger; + _ = eventAggregator.AddListener(this); + } + + public bool HasCoverage => this.bufferLineCoverage != null; + + public void RaiseTagsChanged() => this.RaiseTagsChangedLinesOrAll(); + + private void RaiseTagsChangedLinesOrAll(IEnumerable changedLines = null) + { + ITextSnapshot currentSnapshot = this.textBuffer.CurrentSnapshot; + SnapshotSpan snapshotSpan; + if (changedLines != null) + { + Span span = changedLines.Select(changedLine => currentSnapshot.GetLineFromLineNumber(changedLine).Extent.Span) + .Aggregate((acc, next) => Span.FromBounds(Math.Min(acc.Start, next.Start), Math.Max(acc.End, next.End))); + snapshotSpan = new SnapshotSpan(currentSnapshot, span); + } + else + { + snapshotSpan = new SnapshotSpan(currentSnapshot, 0, currentSnapshot.Length); + } + + var spanEventArgs = new SnapshotSpanEventArgs(snapshotSpan); + TagsChanged?.Invoke(this, spanEventArgs); + } + + public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + => this.CanGetTagsFromCoverageLines + ? this.GetTagsFromCoverageLines(spans) + : Enumerable.Empty>(); + + private bool CanGetTagsFromCoverageLines => this.bufferLineCoverage != null && !this.coverageTypeFilter.Disabled; + + private IEnumerable> GetTagsFromCoverageLines(NormalizedSnapshotSpanCollection spans) + { + IEnumerable lineSpans = this.lineSpanLogic.GetLineSpans(this.bufferLineCoverage, spans); + return this.GetTags(lineSpans); + } + + private IEnumerable> GetTags(IEnumerable lineSpans) + => lineSpans.Where(lineSpan => this.coverageTypeFilter.Show(lineSpan.Line.CoverageType)) + .Select(lineSpan => this.lineSpanTagger.GetTagSpan(lineSpan)); + + public void Dispose() => _ = this.eventAggregator.RemoveListener(this); + + public void Handle(CoverageChangedMessage message) + { + if (this.IsOwnChange(message)) + { + this.HandleOwnChange(message); + } + } + + private bool IsOwnChange(CoverageChangedMessage message) => message.AppliesTo == this.textInfo.FilePath; + + private void HandleOwnChange(CoverageChangedMessage message) + { + this.bufferLineCoverage = message.BufferLineCoverage; + this.RaiseTagsChangedLinesOrAll(message.ChangedLineNumbers); + } + + public void Handle(CoverageTypeFilterChangedMessage message) + { + if (message.Filter.TypeIdentifier == this.coverageTypeFilter.TypeIdentifier) + { + this.coverageTypeFilter = message.Filter; + if (this.HasCoverage) + { + this.RaiseTagsChanged(); + } + } + } + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs b/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs new file mode 100644 index 00000000..d980b138 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/CoverageTaggerProvider.cs @@ -0,0 +1,75 @@ +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Options; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal class CoverageTaggerProvider : ICoverageTaggerProvider + where TTag : ITag where TCoverageTypeFilter : ICoverageTypeFilter, new() + { + protected readonly IEventAggregator eventAggregator; + private readonly ILineSpanLogic lineSpanLogic; + private readonly ILineSpanTagger coverageTagger; + private readonly IDynamicCoverageManager dynamicCoverageManager; + private readonly ITextInfoFactory textInfoFactory; + private TCoverageTypeFilter coverageTypeFilter; + + public CoverageTaggerProvider( + IEventAggregator eventAggregator, + IAppOptionsProvider appOptionsProvider, + ILineSpanLogic lineSpanLogic, + ILineSpanTagger coverageTagger, + IDynamicCoverageManager dynamicCoverageManager, + ITextInfoFactory textInfoFactory) + { + this.dynamicCoverageManager = dynamicCoverageManager; + this.textInfoFactory = textInfoFactory; + IAppOptions appOptions = appOptionsProvider.Get(); + this.coverageTypeFilter = this.CreateFilter(appOptions); + appOptionsProvider.OptionsChanged += this.AppOptionsProvider_OptionsChanged; + this.eventAggregator = eventAggregator; + this.lineSpanLogic = lineSpanLogic; + this.coverageTagger = coverageTagger; + } + + private TCoverageTypeFilter CreateFilter(IAppOptions appOptions) + { + var newCoverageTypeFilter = new TCoverageTypeFilter(); + newCoverageTypeFilter.Initialize(appOptions); + return newCoverageTypeFilter; + } + + private void AppOptionsProvider_OptionsChanged(IAppOptions appOptions) + { + TCoverageTypeFilter newCoverageTypeFilter = this.CreateFilter(appOptions); + if (newCoverageTypeFilter.Changed(this.coverageTypeFilter)) + { + this.coverageTypeFilter = newCoverageTypeFilter; + var message = new CoverageTypeFilterChangedMessage(newCoverageTypeFilter); + this.eventAggregator.SendMessage(message); + } + } + + public ICoverageTagger CreateTagger(ITextView textView, ITextBuffer textBuffer) + { + ITextInfo textInfo = this.textInfoFactory.Create(textView, textBuffer); + string filePath = textInfo.FilePath; + if (filePath == null) + { + return null; + } + + IBufferLineCoverage bufferLineCoverage = this.dynamicCoverageManager.Manage(textInfo); + return new CoverageTagger( + textInfo, + bufferLineCoverage, + this.coverageTypeFilter, + this.eventAggregator, + this.lineSpanLogic, + this.coverageTagger); + } + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs b/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs new file mode 100644 index 00000000..2764ace7 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/CoverageTaggerProviderFactory.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Options; +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + [ExcludeFromCodeCoverage] + [Export(typeof(ICoverageTaggerProviderFactory))] + internal class CoverageTaggerProviderFactory : ICoverageTaggerProviderFactory + { + private readonly IEventAggregator eventAggregator; + private readonly IAppOptionsProvider appOptionsProvider; + private readonly ILineSpanLogic lineSpanLogic; + private readonly IDynamicCoverageManager dynamicCoverageManager; + private readonly ITextInfoFactory textInfoFactory; + + [ImportingConstructor] + public CoverageTaggerProviderFactory( + IEventAggregator eventAggregator, + IAppOptionsProvider appOptionsProvider, + ILineSpanLogic lineSpanLogic, + IDynamicCoverageManager dynamicCoverageManager, + ITextInfoFactory textInfoFactory + ) + { + this.eventAggregator = eventAggregator; + this.appOptionsProvider = appOptionsProvider; + this.lineSpanLogic = lineSpanLogic; + this.dynamicCoverageManager = dynamicCoverageManager; + this.textInfoFactory = textInfoFactory; + } + public ICoverageTaggerProvider Create(ILineSpanTagger tagger) + where TTag : ITag + where TCoverageTypeFilter : ICoverageTypeFilter, new() + => new CoverageTaggerProvider( + this.eventAggregator, + this.appOptionsProvider, + this.lineSpanLogic, + tagger, + this.dynamicCoverageManager, + this.textInfoFactory + ); + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/CoverageTypeFilterBase.cs b/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/CoverageTypeFilterBase.cs new file mode 100644 index 00000000..1a18e793 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/CoverageTypeFilterBase.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal abstract class CoverageTypeFilterBase : ICoverageTypeFilter + { + private static readonly Dictionary doNotShowLookup = new Dictionary() + { + { DynamicCoverageType.Covered, false }, + { DynamicCoverageType.Partial, false }, + { DynamicCoverageType.NotCovered, false }, + { DynamicCoverageType.Dirty, false }, + { DynamicCoverageType.NewLine, false }, + { DynamicCoverageType.NotIncluded, false } + }; + private Dictionary showLookup = doNotShowLookup; + + public void Initialize(IAppOptions appOptions) + { + if (this.ShouldGetShowLookup(appOptions)) + { + this.showLookup = this.GetShowLookup(appOptions); + this.ThrowIfInvalidShowLookup(); + } + } + + private bool ShouldGetShowLookup(IAppOptions appOptions) => appOptions.ShowEditorCoverage && this.EnabledPrivate(appOptions); + + private void ThrowIfInvalidShowLookup() + { + if (this.showLookup == null || this.showLookup.Count != 6) + { + throw new InvalidOperationException("Invalid showLookup"); + } + } + + private bool EnabledPrivate(IAppOptions appOptions) + { + bool enabled = this.Enabled(appOptions); + this.Disabled = !enabled; + return enabled; + } + + protected abstract bool Enabled(IAppOptions appOptions); + protected abstract Dictionary GetShowLookup(IAppOptions appOptions); + + public abstract string TypeIdentifier { get; } + + public bool Disabled { get; set; } = true; + + public bool Show(DynamicCoverageType coverageType) => this.showLookup[coverageType]; + + public bool Changed(ICoverageTypeFilter other) + { + this.ThrowIfIncorrectCoverageTypeFilter(other); + + return this.CompareLookups((other as CoverageTypeFilterBase).showLookup); + } + + private bool CompareLookups(Dictionary otherShowLookup) + => Enum.GetValues(typeof(DynamicCoverageType)).Cast() + .Any(coverageType => this.showLookup[coverageType] != otherShowLookup[coverageType]); + + private void ThrowIfIncorrectCoverageTypeFilter(ICoverageTypeFilter other) + { + if (other.TypeIdentifier != this.TypeIdentifier) + { + throw new ArgumentException("Argument of incorrect type", nameof(other)); + } + } + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/CoverageTypeFilterChangedMessage.cs b/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/CoverageTypeFilterChangedMessage.cs new file mode 100644 index 00000000..528d330f --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/CoverageTypeFilterChangedMessage.cs @@ -0,0 +1,9 @@ +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal class CoverageTypeFilterChangedMessage + { + public CoverageTypeFilterChangedMessage(ICoverageTypeFilter filter) => this.Filter = filter; + + public ICoverageTypeFilter Filter { get; } + } +} diff --git a/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/ICoverageTypeFilter.cs b/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/ICoverageTypeFilter.cs new file mode 100644 index 00000000..71612838 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/CoverageTypeFilter/ICoverageTypeFilter.cs @@ -0,0 +1,14 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ICoverageTypeFilter + { + void Initialize(IAppOptions appOptions); + bool Disabled { get; } + bool Show(DynamicCoverageType coverageType); + string TypeIdentifier { get; } + bool Changed(ICoverageTypeFilter other); + } +} diff --git a/SharedProject/Editor/Tagging/Base/ICoverageTagger.cs b/SharedProject/Editor/Tagging/Base/ICoverageTagger.cs new file mode 100644 index 00000000..411ca758 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/ICoverageTagger.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ICoverageTagger : ITagger, IDisposable where T : ITag + { + void RaiseTagsChanged(); + bool HasCoverage { get; } + } +} diff --git a/SharedProject/Editor/Tagging/Base/ICoverageTaggerProvider.cs b/SharedProject/Editor/Tagging/Base/ICoverageTaggerProvider.cs new file mode 100644 index 00000000..b6242860 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/ICoverageTaggerProvider.cs @@ -0,0 +1,11 @@ +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ICoverageTaggerProvider where TTag : ITag + { + ICoverageTagger CreateTagger(ITextView textView, ITextBuffer textBuffer); + } +} diff --git a/SharedProject/Editor/Tagging/Base/ICoverageTaggerProviderFactory.cs b/SharedProject/Editor/Tagging/Base/ICoverageTaggerProviderFactory.cs new file mode 100644 index 00000000..692ad8fc --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/ICoverageTaggerProviderFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ICoverageTaggerProviderFactory + { + ICoverageTaggerProvider Create(ILineSpanTagger tagger) + where TTag : ITag where TCoverageTypeFilter : ICoverageTypeFilter, new(); + } +} diff --git a/SharedProject/Editor/Tagging/Base/ILineSpan.cs b/SharedProject/Editor/Tagging/Base/ILineSpan.cs new file mode 100644 index 00000000..57657363 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/ILineSpan.cs @@ -0,0 +1,11 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ILineSpan + { + IDynamicLine Line { get; } + SnapshotSpan Span { get; } + } +} diff --git a/SharedProject/Editor/Tagging/Base/ILineSpanLogic.cs b/SharedProject/Editor/Tagging/Base/ILineSpanLogic.cs new file mode 100644 index 00000000..19b2d53d --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/ILineSpanLogic.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ILineSpanLogic + { + IEnumerable GetLineSpans(IBufferLineCoverage bufferLineCoverage, NormalizedSnapshotSpanCollection spans); + } +} diff --git a/SharedProject/Editor/Tagging/Base/ILineSpanTagger.cs b/SharedProject/Editor/Tagging/Base/ILineSpanTagger.cs new file mode 100644 index 00000000..29bae34c --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/ILineSpanTagger.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal interface ILineSpanTagger where TTag : ITag + { + TagSpan GetTagSpan(ILineSpan lineSpan); + } +} diff --git a/SharedProject/Editor/Tagging/Base/LineSpan.cs b/SharedProject/Editor/Tagging/Base/LineSpan.cs new file mode 100644 index 00000000..4ad37631 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/LineSpan.cs @@ -0,0 +1,17 @@ +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + internal class LineSpan : ILineSpan + { + public LineSpan(IDynamicLine line, SnapshotSpan span) + { + this.Line = line; + this.Span = span; + } + public IDynamicLine Line { get; } + + public SnapshotSpan Span { get; } + } +} diff --git a/SharedProject/Editor/Tagging/Base/LineSpanLogic.cs b/SharedProject/Editor/Tagging/Base/LineSpanLogic.cs new file mode 100644 index 00000000..eab0842b --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/LineSpanLogic.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using FineCodeCoverage.Editor.DynamicCoverage; +using Microsoft.VisualStudio.Text; + +namespace FineCodeCoverage.Editor.Tagging.Base +{ + [Export(typeof(ILineSpanLogic))] + internal class LineSpanLogic : ILineSpanLogic + { + public IEnumerable GetLineSpans( + IBufferLineCoverage bufferLineCoverage, + NormalizedSnapshotSpanCollection normalizedSnapshotSpanCollection + ) => normalizedSnapshotSpanCollection.SelectMany(snapshotSpan => GetApplicableLineSpans(snapshotSpan, bufferLineCoverage)); + + private static IEnumerable GetApplicableLineSpans(SnapshotSpan snapshotSpan, IBufferLineCoverage bufferLineCoverage) + { + IEnumerable applicableCoverageLines = GetApplicableCoverageLines(bufferLineCoverage, snapshotSpan); + return applicableCoverageLines.Select( + applicableCoverageLine => new LineSpan(applicableCoverageLine, GetLineSnapshotSpan(applicableCoverageLine.Number, snapshotSpan))); + } + + private static IEnumerable GetApplicableCoverageLines(IBufferLineCoverage bufferLineCoverage, SnapshotSpan span) + { + (int coverageStartLineNumber, int coverageEndLineNumber) = GetStartEndCoverageLineNumbers(span); + return bufferLineCoverage.GetLines(coverageStartLineNumber, coverageEndLineNumber); + } + + private static (int, int) GetStartEndCoverageLineNumbers(SnapshotSpan span) + { + int startLineNumber = span.Start.GetContainingLine().LineNumber; + int endLineNumber = span.End.GetContainingLine().LineNumber; + return (startLineNumber, endLineNumber); + } + + private static SnapshotSpan GetLineSnapshotSpan(int lineNumber, SnapshotSpan originalSpan) + { + ITextSnapshotLine line = originalSpan.Snapshot.GetLineFromLineNumber(lineNumber); + + SnapshotPoint startPoint = line.Start; + SnapshotPoint endPoint = line.End; + + return new SnapshotSpan(startPoint, endPoint); + } + } +} diff --git a/SharedProject/Editor/Tagging/Base/SupportedContentTypeLanguages.cs b/SharedProject/Editor/Tagging/Base/SupportedContentTypeLanguages.cs new file mode 100644 index 00000000..b7702967 --- /dev/null +++ b/SharedProject/Editor/Tagging/Base/SupportedContentTypeLanguages.cs @@ -0,0 +1,12 @@ +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/CoverageClassificationFilter.cs b/SharedProject/Editor/Tagging/Classification/CoverageClassificationFilter.cs new file mode 100644 index 00000000..8d899dbe --- /dev/null +++ b/SharedProject/Editor/Tagging/Classification/CoverageClassificationFilter.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Tagging.Classification +{ + internal class CoverageClassificationFilter : CoverageTypeFilterBase + { + public override string TypeIdentifier => "Classification"; + + protected override bool Enabled(IAppOptions appOptions) => appOptions.ShowLineCoverageHighlighting; + + protected override Dictionary GetShowLookup(IAppOptions appOptions) + => new Dictionary() + { + { DynamicCoverageType.Covered, appOptions.ShowLineCoveredHighlighting }, + { DynamicCoverageType.Partial, appOptions.ShowLinePartiallyCoveredHighlighting }, + { DynamicCoverageType.NotCovered, appOptions.ShowLineUncoveredHighlighting }, + { DynamicCoverageType.Dirty, appOptions.ShowLineDirtyHighlighting }, + { DynamicCoverageType.NewLine, appOptions.ShowLineNewHighlighting }, + { DynamicCoverageType.NotIncluded, appOptions.ShowLineNotIncludedHighlighting }, + }; + } +} diff --git a/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs b/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs new file mode 100644 index 00000000..c2785f59 --- /dev/null +++ b/SharedProject/Editor/Tagging/Classification/CoverageLineClassificationTaggerProvider.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.Composition; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace FineCodeCoverage.Editor.Tagging.Classification +{ + [ContentType(SupportedContentTypeLanguages.CSharp)] + [ContentType(SupportedContentTypeLanguages.VisualBasic)] + [ContentType(SupportedContentTypeLanguages.CPP)] + [TagType(typeof(IClassificationTag))] + [Name("FCC.CoverageLineClassificationTaggerProvider")] + [Export(typeof(IViewTaggerProvider))] + internal class CoverageLineClassificationTaggerProvider : IViewTaggerProvider, ILineSpanTagger + { + private readonly ICoverageTypeService coverageTypeService; + private readonly ICoverageTaggerProvider coverageTaggerProvider; + + [ImportingConstructor] + public CoverageLineClassificationTaggerProvider( + ICoverageTypeService coverageTypeService, + ICoverageTaggerProviderFactory coverageTaggerProviderFactory + ) + { + this.coverageTypeService = coverageTypeService; + this.coverageTaggerProvider = coverageTaggerProviderFactory.Create(this); + } + + public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag + => this.coverageTaggerProvider.CreateTagger(textView, buffer) as ITagger; + + public TagSpan GetTagSpan(ILineSpan lineSpan) + { + IClassificationType ct = this.coverageTypeService.GetClassificationType(lineSpan.Line.CoverageType); + return new TagSpan(lineSpan.Span, new ClassificationTag(ct)); + } + } +} diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactory.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactory.cs new file mode 100644 index 00000000..3978df6a --- /dev/null +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactory.cs @@ -0,0 +1,23 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; + +namespace FineCodeCoverage.Editor.Tagging.GlyphMargin +{ + internal class CoverageLineGlyphFactory : IGlyphFactory + { + public UIElement GenerateGlyph(IWpfTextViewLine textViewLine, IGlyphTag glyphTag) + => glyphTag is CoverageLineGlyphTag tag + ? this.GetColouredRectange(tag.Colour) + : (UIElement)null; + + private Rectangle GetColouredRectange(Color colour) => new Rectangle + { + Fill = new SolidColorBrush(colour), + Width = 3, + Height = 16 + }; + } +} diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs new file mode 100644 index 00000000..9b424fb5 --- /dev/null +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphFactoryProvider.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using FineCodeCoverage.Editor.Tagging.Base; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; +using OrderAttribute = Microsoft.VisualStudio.Utilities.OrderAttribute; + +namespace FineCodeCoverage.Editor.Tagging.GlyphMargin +{ + [ExcludeFromCodeCoverage] + [ContentType(SupportedContentTypeLanguages.CSharp)] + [ContentType(SupportedContentTypeLanguages.VisualBasic)] + [ContentType(SupportedContentTypeLanguages.CPP)] + [TagType(typeof(CoverageLineGlyphTag))] + [Order(Before = "VsTextMarker")] + [Name(Vsix.GlyphFactoryProviderName)] + [Export(typeof(IGlyphFactoryProvider))] + internal class CoverageLineGlyphFactoryProvider : IGlyphFactoryProvider + { + public IGlyphFactory GetGlyphFactory(IWpfTextView textView, IWpfTextViewMargin textViewMargin) => new CoverageLineGlyphFactory(); + + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTag.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTag.cs new file mode 100644 index 00000000..001f46c3 --- /dev/null +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTag.cs @@ -0,0 +1,12 @@ +using System.Windows.Media; +using Microsoft.VisualStudio.Text.Editor; + +namespace FineCodeCoverage.Editor.Tagging.GlyphMargin +{ + internal class CoverageLineGlyphTag : IGlyphTag + { + public Color Colour { get; } + + public CoverageLineGlyphTag(Color colour) => this.Colour = colour; + } +} diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTagger.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTagger.cs new file mode 100644 index 00000000..a8d43490 --- /dev/null +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTagger.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Tagging; + +namespace FineCodeCoverage.Editor.Tagging.GlyphMargin +{ + internal class CoverageLineGlyphTagger : ITagger, IDisposable, IListener + { + private readonly IEventAggregator eventAggregator; + private readonly ICoverageTagger coverageTagger; + + public CoverageLineGlyphTagger(IEventAggregator eventAggregator, ICoverageTagger coverageTagger) + { + ThrowIf.Null(coverageTagger, nameof(coverageTagger)); + _ = eventAggregator.AddListener(this); + this.eventAggregator = eventAggregator; + this.coverageTagger = coverageTagger; + + } + public event EventHandler TagsChanged + { + add => this.coverageTagger.TagsChanged += value; + remove => this.coverageTagger.TagsChanged -= value; + } + + public void Dispose() + { + this.coverageTagger.Dispose(); + _ = this.eventAggregator.RemoveListener(this); + } + + public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + => this.coverageTagger.GetTags(spans); + + public void Handle(CoverageColoursChangedMessage message) + { + if (this.coverageTagger.HasCoverage) + { + this.coverageTagger.RaiseTagsChanged(); + } + } + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs new file mode 100644 index 00000000..e06c252e --- /dev/null +++ b/SharedProject/Editor/Tagging/GlyphMargin/CoverageLineGlyphTaggerProvider.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.Composition; +using System.Windows.Media; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace FineCodeCoverage.Editor.Tagging.GlyphMargin +{ + [ContentType(SupportedContentTypeLanguages.CSharp)] + [ContentType(SupportedContentTypeLanguages.VisualBasic)] + [ContentType(SupportedContentTypeLanguages.CPP)] + [TagType(typeof(CoverageLineGlyphTag))] + [Name(Vsix.TaggerProviderName)] + [Export(typeof(IViewTaggerProvider))] + internal class CoverageLineGlyphTaggerProvider : IViewTaggerProvider, ILineSpanTagger + { + private readonly ICoverageTaggerProvider coverageTaggerProvider; + private readonly IEventAggregator eventAggregator; + private readonly ICoverageColoursProvider coverageColoursProvider; + + [ImportingConstructor] + public CoverageLineGlyphTaggerProvider( + IEventAggregator eventAggregator, + ICoverageColoursProvider coverageColoursProvider, + ICoverageTaggerProviderFactory coverageTaggerProviderFactory + ) + { + this.coverageTaggerProvider = coverageTaggerProviderFactory.Create(this); + this.eventAggregator = eventAggregator; + this.coverageColoursProvider = coverageColoursProvider; + } + + public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag + { + ICoverageTagger coverageTagger = this.coverageTaggerProvider.CreateTagger(textView, buffer); + return coverageTagger == null ? null : new CoverageLineGlyphTagger(this.eventAggregator, coverageTagger) as ITagger; + } + + public TagSpan GetTagSpan(ILineSpan lineSpan) + { + IDynamicLine coverageLine = lineSpan.Line; + ICoverageColours coverageColours = this.coverageColoursProvider.GetCoverageColours(); + Color colour = coverageColours.GetColour(coverageLine.CoverageType).Background; + return new TagSpan(lineSpan.Span, new CoverageLineGlyphTag(colour)); + } + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Tagging/GlyphMargin/GlyphFilter.cs b/SharedProject/Editor/Tagging/GlyphMargin/GlyphFilter.cs new file mode 100644 index 00000000..debcdc00 --- /dev/null +++ b/SharedProject/Editor/Tagging/GlyphMargin/GlyphFilter.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Tagging.GlyphMargin +{ + internal class GlyphFilter : CoverageTypeFilterBase + { + public override string TypeIdentifier => "Glyph"; + + protected override bool Enabled(IAppOptions appOptions) => appOptions.ShowCoverageInGlyphMargin; + + protected override Dictionary GetShowLookup(IAppOptions appOptions) + => new Dictionary + { + { DynamicCoverageType.Covered, appOptions.ShowCoveredInGlyphMargin }, + { DynamicCoverageType.Partial, appOptions.ShowPartiallyCoveredInGlyphMargin }, + { DynamicCoverageType.NotCovered, appOptions.ShowUncoveredInGlyphMargin }, + { DynamicCoverageType.Dirty, appOptions.ShowDirtyInGlyphMargin }, + { DynamicCoverageType.NewLine, appOptions.ShowNewInGlyphMargin }, + { DynamicCoverageType.NotIncluded, appOptions.ShowNotIncludedInGlyphMargin }, + }; + } +} diff --git a/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs b/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs new file mode 100644 index 00000000..b700096b --- /dev/null +++ b/SharedProject/Editor/Tagging/OverviewMargin/CoverageLineOverviewMarkTaggerProvider.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.Composition; +using FineCodeCoverage.Editor.Management; +using FineCodeCoverage.Editor.Tagging.Base; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace FineCodeCoverage.Editor.Tagging.OverviewMargin +{ + [ContentType(SupportedContentTypeLanguages.CSharp)] + [ContentType(SupportedContentTypeLanguages.VisualBasic)] + [ContentType(SupportedContentTypeLanguages.CPP)] + [TagType(typeof(OverviewMarkTag))] + [Name("FCC.CoverageLineOverviewMarkTaggerProvider")] + [Export(typeof(IViewTaggerProvider))] + internal class CoverageLineOverviewMarkTaggerProvider : IViewTaggerProvider, ILineSpanTagger + { + private readonly ICoverageTaggerProvider coverageTaggerProvider; + private readonly ICoverageColoursEditorFormatMapNames coverageColoursEditorFormatMapNames; + + [ImportingConstructor] + public CoverageLineOverviewMarkTaggerProvider( + ICoverageTaggerProviderFactory coverageTaggerProviderFactory, + ICoverageColoursEditorFormatMapNames coverageColoursEditorFormatMapNames, + ILineSpanLogic lineSpanLogic + ) + { + this.coverageTaggerProvider = coverageTaggerProviderFactory.Create(this); + this.coverageColoursEditorFormatMapNames = coverageColoursEditorFormatMapNames; + } + + public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag + => this.coverageTaggerProvider.CreateTagger(textView, buffer) as ITagger; + + public TagSpan GetTagSpan(ILineSpan lineSpan) + { + string editorFormatDefinitionName = this.coverageColoursEditorFormatMapNames.GetEditorFormatDefinitionName( + lineSpan.Line.CoverageType); + return new TagSpan(lineSpan.Span, new OverviewMarkTag(editorFormatDefinitionName)); + } + } +} \ No newline at end of file diff --git a/SharedProject/Editor/Tagging/OverviewMargin/CoverageOverviewMarginFilter.cs b/SharedProject/Editor/Tagging/OverviewMargin/CoverageOverviewMarginFilter.cs new file mode 100644 index 00000000..70d18499 --- /dev/null +++ b/SharedProject/Editor/Tagging/OverviewMargin/CoverageOverviewMarginFilter.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using FineCodeCoverage.Editor.DynamicCoverage; +using FineCodeCoverage.Editor.Tagging.Base; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Editor.Tagging.OverviewMargin +{ + internal class CoverageOverviewMarginFilter : CoverageTypeFilterBase + { + public override string TypeIdentifier => "OverviewMargin"; + + protected override bool Enabled(IAppOptions appOptions) => appOptions.ShowCoverageInOverviewMargin; + + protected override Dictionary GetShowLookup(IAppOptions appOptions) + => new Dictionary + { + { DynamicCoverageType.Covered, appOptions.ShowCoveredInOverviewMargin }, + { DynamicCoverageType.NotCovered, appOptions.ShowUncoveredInOverviewMargin }, + { DynamicCoverageType.Partial, appOptions.ShowPartiallyCoveredInOverviewMargin }, + { DynamicCoverageType.Dirty, appOptions.ShowDirtyInOverviewMargin}, + { DynamicCoverageType.NewLine, appOptions.ShowNewInOverviewMargin}, + { DynamicCoverageType.NotIncluded, appOptions.ShowNotIncludedInOverviewMargin}, + }; + } +} diff --git a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphFactory.cs b/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphFactory.cs deleted file mode 100644 index 61de8ed8..00000000 --- a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphFactory.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Windows; -using System.Windows.Media; -using System.Windows.Shapes; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Formatting; - -namespace FineCodeCoverage.Impl -{ - internal class CoverageLineGlyphFactory : IGlyphFactory - { - private readonly ICoverageColours coverageColours; - - public CoverageLineGlyphFactory(ICoverageColours coverageColours) - { - this.coverageColours = coverageColours; - } - - public UIElement GenerateGlyph(IWpfTextViewLine textViewLine, IGlyphTag glyphTag) - { - if (!(glyphTag is CoverageLineGlyphTag tag)) - { - return null; - } - - var coverageType = tag.CoverageLine.GetCoverageType(); - - var result = new Rectangle(); - result.Width = 3; - result.Height = 16; - result.Fill = GetBrush(coverageType); - - return result; - } - - private Brush GetBrush(CoverageType coverageType) - { - Color color = default; - switch (coverageType) - { - case CoverageType.Partial: - color = coverageColours.CoveragePartiallyTouchedArea; - break; - case CoverageType.NotCovered: - color = coverageColours.CoverageNotTouchedArea; - break; - case CoverageType.Covered: - color = coverageColours.CoverageTouchedArea; - break; - } - return new SolidColorBrush(color); - } - } -} diff --git a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphFactoryProvider.cs b/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphFactoryProvider.cs deleted file mode 100644 index 214ff03f..00000000 --- a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphFactoryProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.VisualStudio.Utilities; -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Tagging; - -namespace FineCodeCoverage.Impl -{ - [ContentType("code")] - [TagType(typeof(CoverageLineGlyphTag))] - [Order(Before = "VsTextMarker")] - [Name(Vsix.GlyphFactoryProviderName)] - [Export(typeof(IGlyphFactoryProvider))] - internal class CoverageLineGlyphFactoryProvider: IGlyphFactoryProvider - { - private readonly ICoverageColours coverageColours; - - [ImportingConstructor] - public CoverageLineGlyphFactoryProvider(ICoverageColours coverageColours) - { - this.coverageColours = coverageColours; - } - public IGlyphFactory GetGlyphFactory(IWpfTextView textView, IWpfTextViewMargin textViewMargin) - { - return new CoverageLineGlyphFactory(coverageColours); - } - } -} \ No newline at end of file diff --git a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTag.cs b/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTag.cs deleted file mode 100644 index 5ca9d8a7..00000000 --- a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTag.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FineCodeCoverage.Engine.Cobertura; -using FineCodeCoverage.Engine.Model; -using Microsoft.VisualStudio.Text.Editor; - -namespace FineCodeCoverage.Impl -{ - internal class CoverageLineGlyphTag : IGlyphTag - { - public Line CoverageLine { get; } - - public CoverageLineGlyphTag(Line coverageLine) - { - CoverageLine = coverageLine; - } - } -} diff --git a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTagger.cs b/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTagger.cs deleted file mode 100644 index 88ec3098..00000000 --- a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTagger.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Tagging; -using FineCodeCoverage.Engine.Model; -using FineCodeCoverage.Core.Utilities; - -namespace FineCodeCoverage.Impl -{ - internal class CoverageLineGlyphTagger : CoverageLineTaggerBase, IListener - { - public CoverageLineGlyphTagger(ITextBuffer textBuffer, FileLineCoverage lastCoverageLines) : base(textBuffer, lastCoverageLines) - { - } - - public void Handle(RefreshCoverageGlyphsMessage message) - { - RaiseTagsChanged(); - } - - protected override TagSpan GetTagSpan(Engine.Cobertura.Line coverageLine, SnapshotSpan span) - { - return new TagSpan(span, new CoverageLineGlyphTag(coverageLine)); - } - } -} \ No newline at end of file diff --git a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTaggerProvider.cs b/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTaggerProvider.cs deleted file mode 100644 index 3a48ec35..00000000 --- a/SharedProject/Impl/CoverageColour/GlyphMargin/CoverageLineGlyphTaggerProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Utilities; -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Text.Tagging; -using Microsoft.VisualStudio.Shell; -using FineCodeCoverage.Core.Utilities; -using System.Collections.Generic; -using FineCodeCoverage.Engine.Model; - -namespace FineCodeCoverage.Impl -{ - [ContentType("code")] - [TagType(typeof(CoverageLineGlyphTag))] - [Name(Vsix.TaggerProviderName)] - [Export(typeof(ITaggerProvider))] - internal class CoverageLineGlyphTaggerProvider : CoverageLineTaggerProviderBase - { - private readonly ICoverageColoursProvider coverageColoursProvider; - private RefreshCoverageGlyphsMessage refreshCoverageGlyphsMessage = new RefreshCoverageGlyphsMessage(); - [ImportingConstructor] - public CoverageLineGlyphTaggerProvider( - IEventAggregator eventAggregator, - ICoverageColoursProvider coverageColoursProvider, - ICoverageColours coverageColours - ) : base(eventAggregator) - { - this.coverageColoursProvider = coverageColoursProvider; - coverageColours.ColoursChanged += CoverageColours_ColoursChanged; - } - - private void CoverageColours_ColoursChanged(object sender, System.EventArgs e) - { - eventAggregator.SendMessage(refreshCoverageGlyphsMessage); - } - - protected override void NewCoverageLinesMessageReceived() - { - ThreadHelper.JoinableTaskFactory.Run(async () => - { - await coverageColoursProvider.PrepareAsync(); - }); - } - - protected override CoverageLineGlyphTagger CreateTagger(ITextBuffer textBuffer, FileLineCoverage lastCoverageLines) - { - return new CoverageLineGlyphTagger(textBuffer, lastCoverageLines); - } - } -} \ No newline at end of file diff --git a/SharedProject/Impl/CoverageColour/GlyphMargin/RefreshCoverageGlyphsMessage.cs b/SharedProject/Impl/CoverageColour/GlyphMargin/RefreshCoverageGlyphsMessage.cs deleted file mode 100644 index e8756c17..00000000 --- a/SharedProject/Impl/CoverageColour/GlyphMargin/RefreshCoverageGlyphsMessage.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace FineCodeCoverage.Impl -{ - internal class RefreshCoverageGlyphsMessage - { - } -} diff --git a/SharedProject/Impl/CoverageColour/MarginBase/CoverageLineTaggerBase.cs b/SharedProject/Impl/CoverageColour/MarginBase/CoverageLineTaggerBase.cs deleted file mode 100644 index 638854f5..00000000 --- a/SharedProject/Impl/CoverageColour/MarginBase/CoverageLineTaggerBase.cs +++ /dev/null @@ -1,84 +0,0 @@ -using FineCodeCoverage.Engine; -using FineCodeCoverage.Engine.Model; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Tagging; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace FineCodeCoverage.Impl -{ - internal abstract class CoverageLineTaggerBase : ICoverageLineTagger where TTag : ITag - { - private readonly ITextBuffer _textBuffer; - private FileLineCoverage coverageLines; - - public event EventHandler TagsChanged; - - public CoverageLineTaggerBase(ITextBuffer textBuffer, FileLineCoverage lastCoverageLines) - { - _textBuffer = textBuffer; - coverageLines = lastCoverageLines; - if (lastCoverageLines != null) - { - RaiseTagsChanged(); - } - } - - protected virtual void RaiseTagsChanged() - { - var span = new SnapshotSpan(_textBuffer.CurrentSnapshot, 0, _textBuffer.CurrentSnapshot.Length); - var spanEventArgs = new SnapshotSpanEventArgs(span); - TagsChanged?.Invoke(this, spanEventArgs); - } - - private IEnumerable GetApplicableLines(string filePath, int startLineNumber, int endLineNumber) - => coverageLines.GetLines(filePath, startLineNumber, endLineNumber); - - public void Handle(NewCoverageLinesMessage message) - { - coverageLines = message.CoverageLines; - RaiseTagsChanged(); - } - - public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) - { - var result = new List>(); - - if (spans == null || coverageLines == null) - { - return result; - } - - AddTags(spans, result); - return result; - } - - protected virtual void AddTags(NormalizedSnapshotSpanCollection spans, List> result) - { - foreach (var span in spans) - { - if (!span.Snapshot.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out ITextDocument document)) - { - continue; - } - - var startLineNumber = span.Start.GetContainingLine().LineNumber + 1; - var endLineNumber = span.End.GetContainingLine().LineNumber + 1; - - var applicableCoverageLines = GetApplicableLines(document.FilePath, startLineNumber, endLineNumber); - - foreach (var applicableCoverageLine in applicableCoverageLines) - { - var tagSpan = GetTagSpan(applicableCoverageLine, span); - if (tagSpan != null) - { - result.Add(GetTagSpan(applicableCoverageLine, span)); - } - } - } - } - - protected abstract TagSpan GetTagSpan(Engine.Cobertura.Line coverageLine, SnapshotSpan span); - } -} diff --git a/SharedProject/Impl/CoverageColour/MarginBase/CoverageLineTaggerProviderBase.cs b/SharedProject/Impl/CoverageColour/MarginBase/CoverageLineTaggerProviderBase.cs deleted file mode 100644 index 33ed9f91..00000000 --- a/SharedProject/Impl/CoverageColour/MarginBase/CoverageLineTaggerProviderBase.cs +++ /dev/null @@ -1,45 +0,0 @@ -using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Engine; -using FineCodeCoverage.Engine.Model; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Tagging; -using System.Collections.Generic; - -namespace FineCodeCoverage.Impl -{ - internal abstract class CoverageLineTaggerProviderBase : ITaggerProvider, IListener - where TTaggerListener : ITagger, IListener - where TTag : ITag - { - protected readonly IEventAggregator eventAggregator; - private FileLineCoverage lastCoverageLines; - - public CoverageLineTaggerProviderBase( - IEventAggregator eventAggregator - ) - { - eventAggregator.AddListener(this); - this.eventAggregator = eventAggregator; - } - - public ITagger CreateTagger(ITextBuffer textBuffer) where T : ITag - { - var tagger = CreateTagger(textBuffer, lastCoverageLines); - eventAggregator.AddListener(tagger, false); - return tagger as ITagger; - } - - protected abstract TTaggerListener CreateTagger(ITextBuffer textBuffer, FileLineCoverage lastCoverageLines); - - public void Handle(NewCoverageLinesMessage message) - { - lastCoverageLines = message.CoverageLines; - NewCoverageLinesMessageReceived(); - } - - protected virtual void NewCoverageLinesMessageReceived() - { - - } - } -} diff --git a/SharedProject/Impl/CoverageColour/MarginBase/ICoverageLineTagger.cs b/SharedProject/Impl/CoverageColour/MarginBase/ICoverageLineTagger.cs deleted file mode 100644 index 6d01308b..00000000 --- a/SharedProject/Impl/CoverageColour/MarginBase/ICoverageLineTagger.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Engine; -using Microsoft.VisualStudio.Text.Tagging; - -namespace FineCodeCoverage.Impl -{ - internal interface ICoverageLineTagger : ITagger, IListener where TTag : ITag - { - - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageEditorFormatDefinition.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageEditorFormatDefinition.cs deleted file mode 100644 index 5962fac8..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageEditorFormatDefinition.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.VisualStudio.Text.Classification; -using System.Windows.Media; - -namespace FineCodeCoverage.Impl -{ - internal abstract class CoverageEditorFormatDefinition : EditorFormatDefinition, ICoverageEditorFormatDefinition - { - public CoverageEditorFormatDefinition( - string identifier, - IEditorFormatMapCoverageColoursManager editorFormatMapCoverageColoursManager, - CoverageType coverageType) - { - Identifier = identifier; - CoverageType = coverageType; - editorFormatMapCoverageColoursManager.Register(this); - } - - public string Identifier { get; private set; } - - public void SetBackgroundColor(Color backgroundColor) - { - BackgroundColor = backgroundColor; - } - - public CoverageType CoverageType { get; } - } - -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageLineMarkTagger.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageLineMarkTagger.cs deleted file mode 100644 index 93fd70d0..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageLineMarkTagger.cs +++ /dev/null @@ -1,64 +0,0 @@ -using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Engine.Cobertura; -using FineCodeCoverage.Engine.Model; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Tagging; -using System.Collections.Generic; - -namespace FineCodeCoverage.Impl -{ - internal class CoverageLineMarkTagger : CoverageLineTaggerBase, IListener - { - private ICoverageMarginOptions coverageMarginOptions; - public CoverageLineMarkTagger(ITextBuffer textBuffer, FileLineCoverage lastCoverageLines, ICoverageMarginOptions coverageMarginOptions) : - base(textBuffer, lastCoverageLines) - { - this.coverageMarginOptions = coverageMarginOptions; - } - - public void Handle(CoverageMarginOptionsChangedMessage message) - { - coverageMarginOptions = message.Options; - RaiseTagsChanged(); - } - - protected override TagSpan GetTagSpan(Engine.Cobertura.Line coverageLine, SnapshotSpan span) - { - var coverageType = coverageLine.GetCoverageType(); - var shouldShow = coverageMarginOptions.Show(coverageType); - if (!shouldShow) return null; - - var newSnapshotSpan = GetLineSnapshotSpan(coverageLine.Number, span); - return new TagSpan(newSnapshotSpan, new OverviewMarkTag(GetMarkKindName(coverageLine))); - } - - private SnapshotSpan GetLineSnapshotSpan(int lineNumber, SnapshotSpan originalSpan) - { - var line = originalSpan.Snapshot.GetLineFromLineNumber(lineNumber - 1); - - var startPoint = line.Start; - var endPoint = line.End; - - return new SnapshotSpan(startPoint, endPoint); - } - - private string GetMarkKindName(Line line) - { - var lineHitCount = line?.Hits ?? 0; - var lineConditionCoverage = line?.ConditionCoverage?.Trim(); - - var markKindName = NotCoveredEditorFormatDefinition.ResourceName; - - if (lineHitCount > 0) - { - markKindName = CoveredEditorFormatDefinition.ResourceName; - - if (!string.IsNullOrWhiteSpace(lineConditionCoverage) && !lineConditionCoverage.StartsWith("100")) - { - markKindName = PartiallyCoveredEditorFormatDefinition.ResourceName; - } - } - return markKindName; - } - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageLineMarkTaggerProvider.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageLineMarkTaggerProvider.cs deleted file mode 100644 index e58c7350..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageLineMarkTaggerProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -using FineCodeCoverage.Core.Utilities; -using FineCodeCoverage.Engine.Model; -using FineCodeCoverage.Options; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Tagging; -using Microsoft.VisualStudio.Utilities; -using System.Collections.Generic; -using System.ComponentModel.Composition; - -namespace FineCodeCoverage.Impl -{ - [ContentType("code")] - [TagType(typeof(OverviewMarkTag))] - [Name("FCC.CoverageLineMarkTaggerProvider")] - [Export(typeof(ITaggerProvider))] - internal class CoverageLineMarkTaggerProvider : CoverageLineTaggerProviderBase - { - private CoverageMarginOptions coverageMarginOptions; - [ImportingConstructor] - public CoverageLineMarkTaggerProvider( - IEventAggregator eventAggregator, - IAppOptionsProvider appOptionsProvider - ) : base(eventAggregator) - { - var appOptions = appOptionsProvider.Get(); - coverageMarginOptions = CoverageMarginOptions.Create(appOptions); - appOptionsProvider.OptionsChanged += AppOptionsProvider_OptionsChanged; - } - - private void AppOptionsProvider_OptionsChanged(IAppOptions appOptions) - { - var newCoverageMarginOptions = CoverageMarginOptions.Create(appOptions); - if (!newCoverageMarginOptions.AreEqual(coverageMarginOptions)) - { - coverageMarginOptions = newCoverageMarginOptions; - eventAggregator.SendMessage(new CoverageMarginOptionsChangedMessage(coverageMarginOptions)); - } - } - - protected override CoverageLineMarkTagger CreateTagger(ITextBuffer textBuffer, FileLineCoverage lastCoverageLines) - { - return new CoverageLineMarkTagger(textBuffer, lastCoverageLines, coverageMarginOptions); - } - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageMarginOptions.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageMarginOptions.cs deleted file mode 100644 index 04282e8c..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageMarginOptions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using FineCodeCoverage.Options; - -namespace FineCodeCoverage.Impl -{ - internal class CoverageMarginOptions : ICoverageMarginOptions - { - public bool ShowCoveredInOverviewMargin { get; set; } - public bool ShowPartiallyCoveredInOverviewMargin { get; set; } - public bool ShowUncoveredInOverviewMargin { get; set; } - - public bool AreEqual(CoverageMarginOptions options) - { - return ShowUncoveredInOverviewMargin == options.ShowUncoveredInOverviewMargin && - ShowPartiallyCoveredInOverviewMargin == options.ShowPartiallyCoveredInOverviewMargin && - ShowCoveredInOverviewMargin == options.ShowCoveredInOverviewMargin; - } - - public static CoverageMarginOptions Create(IAppOptions appOptions) - { - if (!appOptions.ShowCoverageInOverviewMargin) - { - return new CoverageMarginOptions(); - } - return new CoverageMarginOptions - { - ShowCoveredInOverviewMargin = appOptions.ShowCoveredInOverviewMargin, - ShowPartiallyCoveredInOverviewMargin = appOptions.ShowPartiallyCoveredInOverviewMargin, - ShowUncoveredInOverviewMargin = appOptions.ShowUncoveredInOverviewMargin, - }; - } - - public bool Show(CoverageType coverageType) - { - var shouldShow = false; - switch (coverageType) - { - case CoverageType.Covered: - shouldShow = ShowCoveredInOverviewMargin; - break; - case CoverageType.NotCovered: - shouldShow = ShowUncoveredInOverviewMargin; - break; - case CoverageType.Partial: - shouldShow = ShowPartiallyCoveredInOverviewMargin; - break; - } - return shouldShow; - } - } - -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageMarginOptionsChangedMessage.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageMarginOptionsChangedMessage.cs deleted file mode 100644 index da2d6100..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/CoverageMarginOptionsChangedMessage.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FineCodeCoverage.Impl -{ - internal class CoverageMarginOptionsChangedMessage - { - public CoverageMarginOptionsChangedMessage(ICoverageMarginOptions options) - { - Options = options; - } - public ICoverageMarginOptions Options { get; } - } - -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/CoveredEditorFormatDefinition.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/CoveredEditorFormatDefinition.cs deleted file mode 100644 index 13ad368e..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/CoveredEditorFormatDefinition.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.VisualStudio.Text.Classification; -using Microsoft.VisualStudio.Utilities; -using System.ComponentModel.Composition; - -namespace FineCodeCoverage.Impl -{ - [Export(typeof(EditorFormatDefinition))] - [Name(CoveredEditorFormatDefinition.ResourceName)] - [UserVisible(true)] - internal class CoveredEditorFormatDefinition : CoverageEditorFormatDefinition - { - public const string ResourceName = "FCCCovered"; - [ImportingConstructor] - public CoveredEditorFormatDefinition( - IEditorFormatMapCoverageColoursManager editorFormatMapCoverageColoursManager - ) : base(ResourceName, editorFormatMapCoverageColoursManager, CoverageType.Covered) - { - } - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/EditorFormatMapCoverageColoursManager.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/EditorFormatMapCoverageColoursManager.cs deleted file mode 100644 index 5e56f1eb..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/EditorFormatMapCoverageColoursManager.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Text.Classification; -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Windows.Media; - -namespace FineCodeCoverage.Impl -{ - [Export(typeof(IEditorFormatMapCoverageColoursManager))] - internal class EditorFormatMapCoverageColoursManager : IEditorFormatMapCoverageColoursManager - { - private bool prepared = false; - private readonly ICoverageColoursProvider coverageColoursProvider; - private readonly ICoverageColours coverageColours; - private readonly IEditorFormatMap editorFormatMap; - private List coverageEditorFormatDefinitions = new List(); - - [ImportingConstructor] - public EditorFormatMapCoverageColoursManager( - ICoverageColoursProvider coverageColoursProvider, - ICoverageColours coverageColours, - IEditorFormatMapService editorFormatMapService - ) - { - this.coverageColoursProvider = coverageColoursProvider; - this.coverageColours = coverageColours; - editorFormatMap = editorFormatMapService.GetEditorFormatMap("text"); - coverageColours.ColoursChanged += CoverageColours_ColoursChanged; - } - - private void CoverageColours_ColoursChanged(object sender, EventArgs e) - { - if (prepared) - { - editorFormatMap.BeginBatchUpdate(); - foreach (var coverageEditorFormatDefinition in coverageEditorFormatDefinitions) - { - var newBackgroundColor = GetBackgroundColor(coverageEditorFormatDefinition.CoverageType); - coverageEditorFormatDefinition.SetBackgroundColor(newBackgroundColor); - editorFormatMap.AddProperties(coverageEditorFormatDefinition.Identifier, coverageEditorFormatDefinition.CreateResourceDictionary()); - } - editorFormatMap.EndBatchUpdate(); - } - } - - public void Register(ICoverageEditorFormatDefinition coverageEditorFormatDefinition) - { - coverageEditorFormatDefinitions.Add(coverageEditorFormatDefinition); - if (!prepared) - { - ThreadHelper.JoinableTaskFactory.Run(coverageColoursProvider.PrepareAsync); - prepared = true; - } - Color backgroundColor = GetBackgroundColor(coverageEditorFormatDefinition.CoverageType); - coverageEditorFormatDefinition.SetBackgroundColor(backgroundColor); - } - - private Color GetBackgroundColor(CoverageType coverageType) - { - Color backgroundColor = default(Color); - switch (coverageType) - { - case CoverageType.Covered: - backgroundColor = coverageColours.CoverageTouchedArea; - break; - case CoverageType.NotCovered: - backgroundColor = coverageColours.CoverageNotTouchedArea; - break; - case CoverageType.Partial: - backgroundColor = coverageColours.CoveragePartiallyTouchedArea; - break; - } - return backgroundColor; - } - } - -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/ICoverageEditorFormatDefinition.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/ICoverageEditorFormatDefinition.cs deleted file mode 100644 index 0d6bc713..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/ICoverageEditorFormatDefinition.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Windows; -using System.Windows.Media; - -namespace FineCodeCoverage.Impl -{ - internal interface ICoverageEditorFormatDefinition - { - string Identifier { get; } - CoverageType CoverageType { get; } - void SetBackgroundColor(Color backgroundColor); - ResourceDictionary CreateResourceDictionary(); - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/ICoverageMarginOptions.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/ICoverageMarginOptions.cs deleted file mode 100644 index 0819b6f6..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/ICoverageMarginOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FineCodeCoverage.Impl -{ - internal interface ICoverageMarginOptions - { - bool Show(CoverageType coverageType); - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/IEditorFormatMapCoverageColoursManager.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/IEditorFormatMapCoverageColoursManager.cs deleted file mode 100644 index 0e60f8b0..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/IEditorFormatMapCoverageColoursManager.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FineCodeCoverage.Impl -{ - internal interface IEditorFormatMapCoverageColoursManager - { - void Register(ICoverageEditorFormatDefinition coverageEditorFormatDefinition); - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/NotCoveredEditorFormatDefinition.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/NotCoveredEditorFormatDefinition.cs deleted file mode 100644 index e52d06a8..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/NotCoveredEditorFormatDefinition.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.VisualStudio.Text.Classification; -using Microsoft.VisualStudio.Utilities; -using System.ComponentModel.Composition; - -namespace FineCodeCoverage.Impl -{ - [Export(typeof(EditorFormatDefinition))] - [Name(NotCoveredEditorFormatDefinition.ResourceName)] - [UserVisible(true)] - internal class NotCoveredEditorFormatDefinition : CoverageEditorFormatDefinition - { - public const string ResourceName = "FCCNotCovered"; - - [ImportingConstructor] - public NotCoveredEditorFormatDefinition( - IEditorFormatMapCoverageColoursManager editorFormatMapCoverageColoursManager - ) : base(ResourceName, editorFormatMapCoverageColoursManager, CoverageType.NotCovered) - { - } - } -} diff --git a/SharedProject/Impl/CoverageColour/OverviewMargin/PartiallyCoveredEditorFormatDefinition.cs b/SharedProject/Impl/CoverageColour/OverviewMargin/PartiallyCoveredEditorFormatDefinition.cs deleted file mode 100644 index 2cad81ee..00000000 --- a/SharedProject/Impl/CoverageColour/OverviewMargin/PartiallyCoveredEditorFormatDefinition.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.VisualStudio.Text.Classification; -using Microsoft.VisualStudio.Utilities; -using System.ComponentModel.Composition; - -namespace FineCodeCoverage.Impl -{ - [Export(typeof(EditorFormatDefinition))] - [Name(PartiallyCoveredEditorFormatDefinition.ResourceName)] - [UserVisible(true)] - internal class PartiallyCoveredEditorFormatDefinition : CoverageEditorFormatDefinition - { - public const string ResourceName = "FCCPartial"; - [ImportingConstructor] - public PartiallyCoveredEditorFormatDefinition( - IEditorFormatMapCoverageColoursManager editorFormatMapCoverageColoursManager - ) : base(ResourceName,editorFormatMapCoverageColoursManager,CoverageType.Partial) - { - } - } -} diff --git a/SharedProject/Impl/CoverageColour/Provider/CoverageColorProvider.cs b/SharedProject/Impl/CoverageColour/Provider/CoverageColorProvider.cs deleted file mode 100644 index d0c9b2c1..00000000 --- a/SharedProject/Impl/CoverageColour/Provider/CoverageColorProvider.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.ComponentModel.Composition; -using FineCodeCoverage.Options; -using Microsoft; -using Microsoft.VisualStudio; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.Threading; -using Task = System.Threading.Tasks.Task; - -namespace FineCodeCoverage.Impl -{ - [Export(typeof(ICoverageColoursProvider))] - [Export(typeof(ICoverageColours))] - internal class CoverageColorProvider : ICoverageColoursProvider, ICoverageColours - { - private readonly ILogger logger; - private readonly uint storeFlags = (uint)(__FCSTORAGEFLAGS.FCSF_READONLY | __FCSTORAGEFLAGS.FCSF_LOADDEFAULTS | __FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS | __FCSTORAGEFLAGS.FCSF_PROPAGATECHANGES); - private readonly System.Windows.Media.Color defaultCoverageTouchedArea = System.Windows.Media.Colors.Green; - private readonly System.Windows.Media.Color defaultCoverageNotTouchedArea = System.Windows.Media.Colors.Red; - private readonly System.Windows.Media.Color defaultCoveragePartiallyTouchedArea = System.Windows.Media.Color.FromRgb(255, 165, 0); - private Guid categoryWithCoverage = Guid.Parse("ff349800-ea43-46c1-8c98-878e78f46501"); - private AsyncLazy lazyIVsFontAndColorStorage; - private bool coverageColoursFromFontsAndColours; - private bool canUseFontsAndColours = true; - public System.Windows.Media.Color CoverageTouchedArea { get; set; } - - public System.Windows.Media.Color CoverageNotTouchedArea { get; set; } - - public System.Windows.Media.Color CoveragePartiallyTouchedArea { get; set; } - - public event EventHandler ColoursChanged; - - [ImportingConstructor] - public CoverageColorProvider( - [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - IAppOptionsProvider appOptionsProvider, - ILogger logger - ) - { - lazyIVsFontAndColorStorage = new AsyncLazy(async () => - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - return (IVsFontAndColorStorage)serviceProvider.GetService(typeof(IVsFontAndColorStorage)); - }, ThreadHelper.JoinableTaskFactory); - - coverageColoursFromFontsAndColours = appOptionsProvider.Get().CoverageColoursFromFontsAndColours; - appOptionsProvider.OptionsChanged += AppOptionsProvider_OptionsChanged; - this.logger = logger; - DetermineColors(); - } - - private void AppOptionsProvider_OptionsChanged(IAppOptions appOptions) - { - coverageColoursFromFontsAndColours = appOptions.CoverageColoursFromFontsAndColours; - DetermineColors(); - } - - private void DetermineColors() - { - ThreadHelper.JoinableTaskFactory.Run(DetermineColorsAsync); - } - - private async Task DetermineColorsAsync() - { - if (coverageColoursFromFontsAndColours && canUseFontsAndColours) - { - await UpdateColoursFromFontsAndColorsAsync(); - } - else - { - UseDefaultColours(); - } - } - - private void UseDefaultColours() - { - SetColors(defaultCoverageTouchedArea, defaultCoverageNotTouchedArea, defaultCoveragePartiallyTouchedArea); - } - - public async Task PrepareAsync() - { - await DetermineColorsAsync(); - } - - private async Task UpdateColoursFromFontsAndColorsAsync() - { - var fontAndColorStorage = await lazyIVsFontAndColorStorage.GetValueAsync(); - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - var success = fontAndColorStorage.OpenCategory(ref categoryWithCoverage, storeFlags); - var usedFontsAndColors = false; - if (success == VSConstants.S_OK) - { - // https://github.com/microsoft/vs-threading/issues/993 - System.Windows.Media.Color GetColor(string displayName) - { - var touchAreaInfo = new ColorableItemInfo[1]; - var getItemSuccess = fontAndColorStorage.GetItem(displayName, touchAreaInfo); - if (getItemSuccess == VSConstants.S_OK) - { - return ParseColor(touchAreaInfo[0].crBackground); - } - throw new NotSupportedException($"{getItemSuccess}"); - } - try - { - // https://developercommunity.visualstudio.com/t/fonts-and-colors-coverage-settings-available-in-vs/1683898 - var newCoverageTouchedArea = GetColor("Coverage Touched Area"); - var newCoverageNotTouchedArea = GetColor("Coverage Not Touched Area"); - var newCoveragePartiallyTouchedArea = GetColor("Coverage Partially Touched Area"); - SetColors(newCoverageTouchedArea, newCoverageNotTouchedArea, newCoveragePartiallyTouchedArea); - usedFontsAndColors = true; - - }catch(NotSupportedException) - { - logger.Log("No coverage settings available from Fonts and Colors"); - } - } - - fontAndColorStorage.CloseCategory(); - if (!usedFontsAndColors) - { - canUseFontsAndColours = false; - UseDefaultColours(); - } - } - - private void SetColors( - System.Windows.Media.Color coverageTouchedArea, - System.Windows.Media.Color coverageNotTouchedArea, - System.Windows.Media.Color coveragePartiallyTouchedArea - ) - { - var fontsAndColorsChanged = FontsAndColorsChanged(coverageTouchedArea, coverageNotTouchedArea, coveragePartiallyTouchedArea); - if (fontsAndColorsChanged) - { - CoverageTouchedArea = coverageTouchedArea; - CoverageNotTouchedArea = coverageNotTouchedArea; - CoveragePartiallyTouchedArea = coveragePartiallyTouchedArea; - - ColoursChanged?.Invoke(this, EventArgs.Empty); - } - } - - private bool FontsAndColorsChanged( - System.Windows.Media.Color coverageTouchedArea, - System.Windows.Media.Color coverageNotTouchedArea, - System.Windows.Media.Color coveragePartiallyTouchedArea - ) - { - return !(CoverageTouchedArea == coverageTouchedArea && - CoverageNotTouchedArea == coverageNotTouchedArea && - CoveragePartiallyTouchedArea == coveragePartiallyTouchedArea); - } - - private System.Windows.Media.Color ParseColor(uint color) - { - var dcolor = System.Drawing.ColorTranslator.FromOle(Convert.ToInt32(color)); - return System.Windows.Media.Color.FromArgb(dcolor.A, dcolor.R, dcolor.G, dcolor.B); - } - - } - -} \ No newline at end of file diff --git a/SharedProject/Impl/CoverageColour/Provider/ICoverageColours.cs b/SharedProject/Impl/CoverageColour/Provider/ICoverageColours.cs deleted file mode 100644 index 66873a7d..00000000 --- a/SharedProject/Impl/CoverageColour/Provider/ICoverageColours.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace FineCodeCoverage.Impl -{ - internal interface ICoverageColours - { - event EventHandler ColoursChanged; - System.Windows.Media.Color CoverageTouchedArea { get; } - System.Windows.Media.Color CoverageNotTouchedArea { get; } - System.Windows.Media.Color CoveragePartiallyTouchedArea { get; } - } - -} \ No newline at end of file diff --git a/SharedProject/Impl/CoverageColour/Provider/ICoverageColoursProvider.cs b/SharedProject/Impl/CoverageColour/Provider/ICoverageColoursProvider.cs deleted file mode 100644 index 93fffed4..00000000 --- a/SharedProject/Impl/CoverageColour/Provider/ICoverageColoursProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -namespace FineCodeCoverage.Impl -{ - internal interface ICoverageColoursProvider - { - Task PrepareAsync(); - - } - -} \ No newline at end of file diff --git a/SharedProject/Impl/CoverageType.cs b/SharedProject/Impl/CoverageType.cs deleted file mode 100644 index 0b4937a6..00000000 --- a/SharedProject/Impl/CoverageType.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FineCodeCoverage.Engine.Model; - -namespace FineCodeCoverage.Impl -{ - internal enum CoverageType { Covered, Partial, NotCovered } - - internal static class CoverageLineExtensions - { - public static CoverageType GetCoverageType(this Engine.Cobertura.Line line) - { - var lineHitCount = line?.Hits ?? 0; - var lineConditionCoverage = line?.ConditionCoverage?.Trim(); - - var coverageType = CoverageType.NotCovered; - - if (lineHitCount > 0) - { - coverageType = CoverageType.Covered; - - if (!string.IsNullOrWhiteSpace(lineConditionCoverage) && !lineConditionCoverage.StartsWith("100")) - { - coverageType = CoverageType.Partial; - } - } - - return coverageType; - } - } -} diff --git a/SharedProject/Impl/Logger.cs b/SharedProject/Impl/Logger.cs index 62291b43..594fc1f2 100644 --- a/SharedProject/Impl/Logger.cs +++ b/SharedProject/Impl/Logger.cs @@ -55,7 +55,6 @@ private async Task SetPaneAsync() _pane = pane; } - [SuppressMessage("Usage", "VSTHRD102:Implement internal logic asynchronously")] private void LogImpl(object[] message, bool withTitle) { try diff --git a/SharedProject/Options/AppOptionsPage.cs b/SharedProject/Options/AppOptionsPage.cs index bbb9164a..38525309 100644 --- a/SharedProject/Options/AppOptionsPage.cs +++ b/SharedProject/Options/AppOptionsPage.cs @@ -7,6 +7,21 @@ namespace FineCodeCoverage.Options { + /* + + The DialogPage uses a PropertyGrid to display the options. + The PropertyGrid use TypeDescriptor to get the properties which will use the attributes + CategoryAttribute, DescriptionAttribute and DisplayNameAttribute to display the options. + The PropertyGrid by default has PropertySort.CategorizedAlphabetical. + + ` todo + When there is no DisplayNameAttribute applied the property name will be used. + Property names cannot be changed otherwise the settings will be lost. + Would like the sub categories ( which are not supported ) to appear together. + The simplest method is to apply the DisplayNameAttribute to the property but the property name is used when using xml for settings. + Could add the setting name in brackets to the DisplayNameAttribute. + This should be done when making it clear in the readme which options are only allowed in visual studio options. + */ internal class AppOptionsPage : DialogPage, IAppOptions { private const string oldRunCategory = "Run ( Coverlet / OpenCover )"; @@ -22,13 +37,18 @@ internal class AppOptionsPage : DialogPage, IAppOptions private const string commonOutputCategory = "Output ( Common )"; private const string commonReportCategory = "Report ( Common )"; private const string openCoverReportCategory = "Report ( OpenCover )"; - private const string commonUiCategory = "UI ( Common )"; + private const string toolbarCategory = "Toolbar"; + private const string editorColouringControlCategory = "Editor Colouring Control"; + private const string overviewMarginCategory = "Editor Colouring Overview Margin"; + private const string glyphMarginCategory = "Editor Colouring Glyph Margin"; + private const string lineHighlightingCategory = "Editor Colouring Line Highlighting"; private static readonly Lazy lazyAppOptionsStorageProvider = new Lazy(GetAppOptionsStorageProvider); private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() { IAppOptionsStorageProvider appOptionsStorageProvider = null; +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -38,6 +58,7 @@ private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() Assumes.Present(componentModel); appOptionsStorageProvider = componentModel.GetService(); }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously return appOptionsStorageProvider; } @@ -45,28 +66,34 @@ private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() #region common run category [Category(commonRunCategory)] [Description("Specifies whether or not coverage output is enabled")] + //[DisplayName("Enabled")] public bool Enabled { get; set; } [Category(commonRunCategory)] [Description("Set to false for VS Option Enabled=false to not disable coverage")] + //[DisplayName("Disabled No Coverage")] public bool DisabledNoCoverage { get; set; } [Category(commonRunCategory)] [Description("Specifies whether or not the ms code coverage is used (BETA). No, IfInRunSettings, Yes")] + //[DisplayName("Run Ms Code Coverage)")] public RunMsCodeCoverage RunMsCodeCoverage { get; set; } [Description("Specify false to prevent coverage when tests fail. Cannot be used in conjunction with RunInParallel")] [Category(commonRunCategory)] + //[DisplayName("Run When Tests Fail")] public bool RunWhenTestsFail { get; set; } [Description("Specify a value to only run coverage based upon the number of executing tests. Cannot be used in conjunction with RunInParallel")] [Category(commonRunCategory)] + //[DisplayName("Run When Tests Exceed")] public int RunWhenTestsExceed { get; set; } #endregion #region old run [Description("Specify true to not wait for tests to finish before running OpenCover / Coverlet coverage")] [Category(oldRunCategory)] + //[DisplayName("Run In Parallel")] public bool RunInParallel { get; set; } #endregion #endregion @@ -75,24 +102,28 @@ private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() #region common exclude include [Category(commonExcludeIncludeCategory)] [Description("Set to true to add all referenced projects to Include.")] + //[DisplayName("Include Referenced Projects")] public bool IncludeReferencedProjects { get; set; } [Category(commonExcludeIncludeCategory)] [Description( @"Specifies whether to report code coverage of the test assembly ")] + //[DisplayName("Include Test Assembly")] public bool IncludeTestAssembly { get; set; } [Category(commonExcludeIncludeCategory)] [Description( @"Provide a list of assemblies to exclude from coverage. The dll name without extension is used for matching. ")] + //[DisplayName("Exclude Assemblies")] public string[] ExcludeAssemblies { get; set; } [Category(commonExcludeIncludeCategory)] [Description( @"Provide a list of assemblies to include in coverage. The dll name without extension is used for matching. ")] + //[DisplayName("Include Assemblies")] public string[] IncludeAssemblies { get; set; } #endregion @@ -112,6 +143,7 @@ private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() Both 'Exclude' and 'Include' options can be used together but 'Exclude' takes precedence. ")] + //[DisplayName("Exclude")] public string[] Exclude { get; set; } [Category(oldExcludeIncludeCategory)] @@ -129,6 +161,7 @@ private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() Both 'Exclude' and 'Include' options can be used together but 'Exclude' takes precedence. ")] + //[DisplayName("Include")] public string[] Include { get; set; } [Category(oldExcludeIncludeCategory)] @@ -136,6 +169,7 @@ private static IAppOptionsStorageProvider GetAppOptionsStorageProvider() @"Glob patterns specifying source files to exclude (multiple) Use file path or directory path with globbing (e.g. **/Migrations/*) ")] + //[DisplayName("Exclude By File")] public string[] ExcludeByFile { get; set; } [Category(oldExcludeIncludeCategory)] @@ -147,62 +181,76 @@ You can also ignore additional attributes by adding to this list (short name or [GeneratedCode] => Present in the System.CodeDom.Compiler namespace [MyCustomExcludeFromCodeCoverage] => Any custom attribute that you may define ")] + //[DisplayName("Exclude By Attribute")] public string[] ExcludeByAttribute { get; set; } #endregion #region ms exclude include [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match assemblies specified by assembly name or file path - for exclusion")] + //[DisplayName("Module Paths Exclude")] public string[] ModulePathsExclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match assemblies specified by assembly name or file path - for inclusion")] + //[DisplayName("Module Paths Include")] public string[] ModulePathsInclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match assemblies by the Company attribute - for exclusion")] + //[DisplayName("Company Names Exclude")] public string[] CompanyNamesExclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match assemblies by the Company attribute - for inclusion")] + //[DisplayName("Company Names Include")] public string[] CompanyNamesInclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match assemblies by the public key token - for exclusion")] + //[DisplayName("Public Key Tokens Exclude")] public string[] PublicKeyTokensExclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match assemblies by the public key token - for inclusion")] + //[DisplayName("Public Key Tokens Include")] public string[] PublicKeyTokensInclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match elements by the path name of the source file in which they're defined - for exclusion")] + //[DisplayName("Sources Exclude")] public string[] SourcesExclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match elements by the path name of the source file in which they're defined - for inclusion")] + //[DisplayName("Sources Include")] public string[] SourcesInclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match elements that have the specified attribute by full name - for exclusion")] + //[DisplayName("Attributes Exclude")] public string[] AttributesExclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match elements that have the specified attribute by full name - for inclusion")] + //[DisplayName("Attributes Include")] public string[] AttributesInclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match procedures, functions, or methods by fully qualified name, including the parameter list. - for exclusion")] + //[DisplayName("Functions Exclude")] public string[] FunctionsExclude { get; set; } [Category(msExcludeIncludeCategory)] [Description("Multiple regexes that match procedures, functions, or methods by fully qualified name, including the parameter list. - for inclusion")] + //[DisplayName("Functions Include")] public string[] FunctionsInclude { get; set; } #endregion #region coverlet only [Description("Specify false for global and project options to be used for coverlet data collector configuration elements when not specified in runsettings")] [Category(coverletExcludeIncludeCategory)] + //[DisplayName("Run Settings Only")] public bool RunSettingsOnly { get; set; } #endregion #endregion @@ -211,12 +259,14 @@ You can also ignore additional attributes by adding to this list (short name or #region common output [Description("To have fcc output visible in a sub folder of your solution provide this name")] [Category(commonOutputCategory)] + //[DisplayName("FCC Solution Output Directory Name")] public string FCCSolutionOutputDirectoryName { get; set; } #endregion #region old output [Description("If your tests are dependent upon their path set this to true. OpenCover / Coverlet")] [Category(oldOutputCategory)] + //[DisplayName("Adjacent Build Output")] public bool AdjacentBuildOutput { get; set; } #endregion #endregion @@ -224,109 +274,226 @@ You can also ignore additional attributes by adding to this list (short name or #region common environment [Description("Folder to which copy tools subfolder. Must alredy exist. Requires restart of VS.")] [Category(commonEnvironmentCategory)] + //[DisplayName("Tools Directory")] public string ToolsDirectory { get; set; } #endregion - #region common ui - [Category(commonUiCategory)] - [Description("Use Environment / Fonts and Colors for editor Coverage colouring")] - public bool CoverageColoursFromFontsAndColours { get; set; } - [Category(commonUiCategory)] + #region editorColouringControlCategory + [Category(editorColouringControlCategory)] + [Description("Set to false to disable all editor coverage indicators")] + //[DisplayName("Show Editor Coverage")] + public bool ShowEditorCoverage { get; set; } + + [Category(editorColouringControlCategory)] + [Description("Set to false to use FCC Fonts And Colors items")] + public bool UseEnterpriseFontsAndColors { get; set; } + + [Category(editorColouringControlCategory)] + [Description("Set to Off, or Set to DoNotUseRoslynWhenTextChanges if there is a performance issue")] + public EditorCoverageColouringMode EditorCoverageColouringMode { get; set; } + #endregion + + #region overview margin + [Category(overviewMarginCategory)] [Description("Set to false to prevent coverage marks in the overview margin")] + //[DisplayName("Show Overview Margin Coverage")] public bool ShowCoverageInOverviewMargin { get; set; } - [Category(commonUiCategory)] + [Category(overviewMarginCategory)] [Description("Set to false to prevent covered marks in the overview margin")] + //[DisplayName("Show Overview Margin Covered")] public bool ShowCoveredInOverviewMargin { get; set; } - [Category(commonUiCategory)] + [Category(overviewMarginCategory)] [Description("Set to false to prevent uncovered marks in the overview margin")] + //[DisplayName("Show Overview Margin Uncovered")] public bool ShowUncoveredInOverviewMargin { get; set; } - [Category(commonUiCategory)] + [Category(overviewMarginCategory)] [Description("Set to false to prevent partially covered marks in the overview margin")] + //[DisplayName("Show Overview Margin Partially Covered")] public bool ShowPartiallyCoveredInOverviewMargin { get; set; } - [Category(commonUiCategory)] + [Category(overviewMarginCategory)] + [Description("Set to true to show dirty marks in the overview margin")] + public bool ShowDirtyInOverviewMargin { get; set; } + + [Category(overviewMarginCategory)] + [Description("Set to true to show new line marks in the overview margin")] + public bool ShowNewInOverviewMargin { get; set; } + + [Category(overviewMarginCategory)] + [Description("Set to true to show not included marks in the overview margin")] + public bool ShowNotIncludedInOverviewMargin { get; set; } + #endregion + #region glyph margin + [Category(glyphMarginCategory)] + [Description("Set to false to prevent coverage marks in the glyph margin")] + //[DisplayName("Show Glyph Margin Coverage")] + public bool ShowCoverageInGlyphMargin { get; set; } + + [Category(glyphMarginCategory)] + [Description("Set to false to prevent covered marks in the glyph margin")] + //[DisplayName("Show Glyph Margin Covered")] + public bool ShowCoveredInGlyphMargin { get; set; } + + [Category(glyphMarginCategory)] + [Description("Set to false to prevent uncovered marks in the glyph margin")] + //[DisplayName("Show Glyph Margin Uncovered")] + public bool ShowUncoveredInGlyphMargin { get; set; } + + [Category(glyphMarginCategory)] + [Description("Set to false to prevent partially covered marks in the glyph margin")] + //[DisplayName("Show Glyph Margin Partially Covered")] + public bool ShowPartiallyCoveredInGlyphMargin { get; set; } + + [Category(glyphMarginCategory)] + [Description("Set to true to show dirty marks in the glyph margin")] + public bool ShowDirtyInGlyphMargin { get; set; } + + [Category(glyphMarginCategory)] + [Description("Set to true to show new line marks in the glyph margin")] + public bool ShowNewInGlyphMargin { get; set; } + + [Category(glyphMarginCategory)] + [Description("Set to true to show not included marks in the glyph margin")] + public bool ShowNotIncludedInGlyphMargin { get; set; } + #endregion + #region line highlighting + [Category(lineHighlightingCategory)] + [Description("Set to true to allow coverage line highlighting")] + //[DisplayName("Show Line Highlighting Coverage")] + public bool ShowLineCoverageHighlighting { get; set; } + + [Category(lineHighlightingCategory)] + [Description("Set to false to prevent covered line highlighting")] + //[DisplayName("Show Line Highlighting Covered")] + public bool ShowLineCoveredHighlighting { get; set; } + + [Category(lineHighlightingCategory)] + [Description("Set to false to prevent uncovered line highlighting")] + //[DisplayName("Show Line Highlighting Uncovered")] + public bool ShowLineUncoveredHighlighting { get; set; } + + [Category(lineHighlightingCategory)] + [Description("Set to false to prevent partially covered line highlighting")] + //[DisplayName("Show Line Highlighting Partially Covered")] + public bool ShowLinePartiallyCoveredHighlighting { get; set; } + + [Category(lineHighlightingCategory)] + [Description("Set to true to show dirty line highlighting")] + public bool ShowLineDirtyHighlighting { get; set; } + [Category(lineHighlightingCategory)] + [Description("Set to true to show new line highlighting")] + public bool ShowLineNewHighlighting { get; set; } + + [Category(lineHighlightingCategory)] + [Description("Set to true to show not included highlighting")] + public bool ShowLineNotIncludedHighlighting { get; set; } + + #endregion + + + [Category(toolbarCategory)] [Description("Set to false to hide the toolbar on the report tool window")] + //[DisplayName("Show Tool Window Toolbar")] public bool ShowToolWindowToolbar { get; set; } - #endregion + #region common report category [Category(commonReportCategory)] [Description("When cyclomatic complexity exceeds this value for a method then the method will be present in the risk hotspots tab.")] + //[DisplayName("Threshold For Cyclomatic Complexity")] public int ThresholdForCyclomaticComplexity { get; set; } [Category(commonReportCategory)] [Description("Set to true for coverage table to have a sticky thead.")] + //[DisplayName("Sticky Coverage Table")] public bool StickyCoverageTable { get; set; } [Category(commonReportCategory)] [Description("Set to false to show types in report in short form.")] + //[DisplayName("Namespaced Classes")] public bool NamespacedClasses { get; set; } [Category(commonReportCategory)] [Description("Control qualification of types when NamespacedClasses is true.")] + //[DisplayName("Namespace Qualification")] public NamespaceQualification NamespaceQualification { get; set; } [Category(commonReportCategory)] [Description("Set to true to hide classes, namespaces and assemblies that are fully covered.")] + //[DisplayName("Hide Fully Covered")] public bool HideFullyCovered { get; set; } [Category(commonReportCategory)] [Description("Set to false to show classes, namespaces and assemblies that are not coverable.")] + //[DisplayName("Hide Not Coverable")] public bool Hide0Coverable { get; set; } [Category(commonReportCategory)] [Description("Set to true to hide classes, namespaces and assemblies that have 0% coverage.")] + //[DisplayName("Hide 0% Coverage")] public bool Hide0Coverage { get; set; } #endregion #region OpenCover report category [Category(openCoverReportCategory)] [Description("When npath complexity exceeds this value for a method then the method will be present in the risk hotspots tab. OpenCover only")] + //[DisplayName("Threshold For NPath Complexity")] public int ThresholdForNPathComplexity { get; set; } [Category(openCoverReportCategory)] [Description("When crap score exceeds this value for a method then the method will be present in the risk hotspots tab. OpenCover only")] + //[DisplayName("Threshold For Crap Score")] public int ThresholdForCrapScore { get; set; } #endregion #region coverlet tool only [Description("Specify true to use your own dotnet tools global install of coverlet console.")] [Category(coverletToolCategory)] + //[DisplayName("Coverlet Console Global")] public bool CoverletConsoleGlobal { get; set; } [Description("Specify true to use your own dotnet tools local install of coverlet console.")] [Category(coverletToolCategory)] + //[DisplayName("Coverlet Console Local")] public bool CoverletConsoleLocal { get; set; } [Description("Specify path to coverlet console exe if you need functionality that the FCC version does not provide.")] [Category(coverletToolCategory)] + //[DisplayName("Coverlet Console Custom Path")] public string CoverletConsoleCustomPath { get; set; } [Description("Specify path to directory containing coverlet collector files if you need functionality that the FCC version does not provide.")] [Category(coverletToolCategory)] + //[DisplayName("Coverlet Collector Directory Path")] public string CoverletCollectorDirectoryPath { get; set; } #endregion #region open cover tool only [Description("Specify path to open cover exe if you need functionality that the FCC version does not provide.")] [Category(openCoverToolCategory)] + //[DisplayName("OpenCover Custom Path")] public string OpenCoverCustomPath { get; set; } [Description("Change from Default if FCC determination of path32 or path64 is incorrect.")] [Category(openCoverToolCategory)] + //[DisplayName("OpenCover Register")] public OpenCoverRegister OpenCoverRegister { get; set; } [Category(openCoverToolCategory)] [Description("Supply your own target if required.")] + //[DisplayName("OpenCover Target")] public string OpenCoverTarget { get; set; } [Category(openCoverToolCategory)] [Description("If supplying your own target you can also supply additional arguments. FCC supplies the test dll path.")] + //[DisplayName("OpenCover Target Args")] public string OpenCoverTargetArgs { get; set; } + + #endregion public override void SaveSettingsToStorage() diff --git a/SharedProject/Options/AppOptionsProvider.cs b/SharedProject/Options/AppOptionsProvider.cs index 97f7f201..ae668b63 100644 --- a/SharedProject/Options/AppOptionsProvider.cs +++ b/SharedProject/Options/AppOptionsProvider.cs @@ -2,6 +2,7 @@ using System.ComponentModel.Composition; using System.Reflection; using FineCodeCoverage.Core.Utilities; +using Microsoft.VisualStudio.Settings; namespace FineCodeCoverage.Options { @@ -10,7 +11,7 @@ namespace FineCodeCoverage.Options internal class AppOptionsProvider : IAppOptionsProvider, IAppOptionsStorageProvider { private readonly ILogger logger; - private readonly IWritableSettingsStoreProvider writableSettingsStoreProvider; + private readonly IWritableUserSettingsStoreProvider writableUserSettingsStoreProvider; private readonly IJsonConvertService jsonConvertService; private readonly PropertyInfo[] appOptionsPropertyInfos; @@ -19,12 +20,12 @@ internal class AppOptionsProvider : IAppOptionsProvider, IAppOptionsStorageProvi [ImportingConstructor] public AppOptionsProvider( ILogger logger, - IWritableSettingsStoreProvider writableSettingsStoreProvider, + IWritableUserSettingsStoreProvider writableUserSettingsStoreProvider, IJsonConvertService jsonConvertService ) { this.logger = logger; - this.writableSettingsStoreProvider = writableSettingsStoreProvider; + this.writableUserSettingsStoreProvider = writableUserSettingsStoreProvider; this.jsonConvertService = jsonConvertService; appOptionsPropertyInfos =typeof(IAppOptions).GetPublicProperties(); } @@ -41,9 +42,9 @@ public IAppOptions Get() return options; } - private IWritableSettingsStore EnsureStore() + private WritableSettingsStore EnsureStore() { - var settingsStore = writableSettingsStoreProvider.Provide(); + var settingsStore = writableUserSettingsStoreProvider.Provide(); if (!settingsStore.CollectionExists(Vsix.Code)) { settingsStore.CreateCollection(Vsix.Code); @@ -65,10 +66,23 @@ private void AddDefaults(IAppOptions appOptions) appOptions.ExcludeByFile = new[] { "**/Migrations/*" }; appOptions.Enabled = true; appOptions.DisabledNoCoverage = true; + appOptions.ShowEditorCoverage = true; appOptions.ShowCoverageInOverviewMargin = true; appOptions.ShowCoveredInOverviewMargin = true; appOptions.ShowPartiallyCoveredInOverviewMargin = true; appOptions.ShowUncoveredInOverviewMargin = true; + + appOptions.ShowCoverageInGlyphMargin = true; + appOptions.ShowCoveredInGlyphMargin = true; + appOptions.ShowPartiallyCoveredInGlyphMargin = true; + appOptions.ShowUncoveredInGlyphMargin = true; + + appOptions.ShowLineCoveredHighlighting = true; + appOptions.ShowLinePartiallyCoveredHighlighting = true; + appOptions.ShowLineUncoveredHighlighting = true; + + appOptions.UseEnterpriseFontsAndColors = true; + appOptions.Hide0Coverable = true; } @@ -174,6 +188,9 @@ internal class AppOptions : IAppOptions public bool ShowUncoveredInOverviewMargin { get; set; } public bool ShowPartiallyCoveredInOverviewMargin { get; set; } + public bool ShowDirtyInOverviewMargin { get; set; } + public bool ShowNewInOverviewMargin { get; set; } + public bool ShowNotIncludedInOverviewMargin { get; set; } public bool StickyCoverageTable { get; set; } @@ -213,5 +230,24 @@ internal class AppOptions : IAppOptions public OpenCoverRegister OpenCoverRegister { get; set; } public string OpenCoverTarget { get; set; } public string OpenCoverTargetArgs { get; set; } + public bool ShowCoverageInGlyphMargin { get; set; } + public bool ShowCoveredInGlyphMargin { get; set; } + public bool ShowUncoveredInGlyphMargin { get; set; } + public bool ShowPartiallyCoveredInGlyphMargin { get; set; } + public bool ShowDirtyInGlyphMargin { get; set; } + public bool ShowNewInGlyphMargin { get; set; } + public bool ShowNotIncludedInGlyphMargin { get; set; } + public bool ShowLineCoverageHighlighting { get; set; } + public bool ShowLineCoveredHighlighting { get; set; } + public bool ShowLineUncoveredHighlighting { get; set; } + public bool ShowLinePartiallyCoveredHighlighting { get; set; } + public bool ShowLineDirtyHighlighting { get; set; } + public bool ShowLineNewHighlighting { get; set; } + public bool ShowLineNotIncludedHighlighting { get; set; } + public bool ShowEditorCoverage { get; set; } + public bool UseEnterpriseFontsAndColors { get; set; } + public EditorCoverageColouringMode EditorCoverageColouringMode { get; set; } + + } } diff --git a/SharedProject/Options/IAppOptions.cs b/SharedProject/Options/IAppOptions.cs index 01ede7cd..b0cd1d8d 100644 --- a/SharedProject/Options/IAppOptions.cs +++ b/SharedProject/Options/IAppOptions.cs @@ -1,5 +1,8 @@ namespace FineCodeCoverage.Options { + /* + Note that option properties must not be renamed + */ internal interface IFCCCommonOptions { bool Enabled { get; set; } @@ -46,7 +49,54 @@ internal interface IOpenCoverOptions string OpenCoverTarget { get; set; } string OpenCoverTargetArgs { get; set; } } - internal interface IAppOptions : IMsCodeCoverageOptions, IOpenCoverCoverletExcludeIncludeOptions, IFCCCommonOptions, IOpenCoverOptions + + interface IOverviewMarginOptions + { + bool ShowCoverageInOverviewMargin { get; set; } + bool ShowCoveredInOverviewMargin { get; set; } + bool ShowUncoveredInOverviewMargin { get; set; } + bool ShowPartiallyCoveredInOverviewMargin { get; set; } + bool ShowDirtyInOverviewMargin { get; set; } + bool ShowNewInOverviewMargin { get; set; } + bool ShowNotIncludedInOverviewMargin { get; set; } + } + + interface IGlyphMarginOptions + { + bool ShowCoverageInGlyphMargin { get; set; } + bool ShowCoveredInGlyphMargin { get; set; } + bool ShowUncoveredInGlyphMargin { get; set; } + bool ShowPartiallyCoveredInGlyphMargin { get; set; } + bool ShowDirtyInGlyphMargin { get; set; } + bool ShowNewInGlyphMargin { get; set; } + bool ShowNotIncludedInGlyphMargin { get; set; } + } + + interface IEditorLineHighlightingCoverageOptions + { + bool ShowLineCoverageHighlighting { get; set; } + bool ShowLineCoveredHighlighting { get; set; } + bool ShowLineUncoveredHighlighting { get; set; } + bool ShowLinePartiallyCoveredHighlighting { get; set; } + bool ShowLineDirtyHighlighting { get; set; } + bool ShowLineNewHighlighting { get; set; } + bool ShowLineNotIncludedHighlighting { get; set; } + } + + internal enum EditorCoverageColouringMode + { + UseRoslynWhenTextChanges, + DoNotUseRoslynWhenTextChanges, + Off + } + + interface IEditorCoverageColouringOptions : IOverviewMarginOptions, IGlyphMarginOptions,IEditorLineHighlightingCoverageOptions { + bool ShowEditorCoverage { get; set; } + bool UseEnterpriseFontsAndColors { get; set; } + EditorCoverageColouringMode EditorCoverageColouringMode { get; set; } + } + + internal interface IAppOptions : IMsCodeCoverageOptions, IOpenCoverCoverletExcludeIncludeOptions, IFCCCommonOptions, IOpenCoverOptions, IEditorCoverageColouringOptions { bool RunInParallel { get; set; } int RunWhenTestsExceed { get; set; } @@ -62,11 +112,6 @@ internal interface IAppOptions : IMsCodeCoverageOptions, IOpenCoverCoverletExclu int ThresholdForCyclomaticComplexity { get; set; } int ThresholdForNPathComplexity { get; set; } int ThresholdForCrapScore { get; set; } - bool CoverageColoursFromFontsAndColours { get; set; } - bool ShowCoverageInOverviewMargin { get; set; } - bool ShowCoveredInOverviewMargin { get; set; } - bool ShowUncoveredInOverviewMargin { get; set; } - bool ShowPartiallyCoveredInOverviewMargin { get; set; } bool StickyCoverageTable { get; set; } bool NamespacedClasses { get; set; } bool HideFullyCovered { get; set; } diff --git a/SharedProject/Options/IJsonConvertService.cs b/SharedProject/Options/IJsonConvertService.cs deleted file mode 100644 index f9847230..00000000 --- a/SharedProject/Options/IJsonConvertService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace FineCodeCoverage.Options -{ - internal interface IJsonConvertService - { - object DeserializeObject(string strValue, Type propertyType); - string SerializeObject(object objValue); - } -} diff --git a/SharedProject/Options/IReadOnlyConfigSettingsStoreProvider.cs b/SharedProject/Options/IReadOnlyConfigSettingsStoreProvider.cs new file mode 100644 index 00000000..f609a327 --- /dev/null +++ b/SharedProject/Options/IReadOnlyConfigSettingsStoreProvider.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Settings; + +namespace FineCodeCoverage.Options +{ + internal interface IReadOnlyConfigSettingsStoreProvider + { + SettingsStore Provide(); + } +} diff --git a/SharedProject/Options/IWritableSettingsStore.cs b/SharedProject/Options/IWritableSettingsStore.cs deleted file mode 100644 index 521f9669..00000000 --- a/SharedProject/Options/IWritableSettingsStore.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FineCodeCoverage.Options -{ - internal interface IWritableSettingsStore - { - bool CollectionExists(string collectionPath); - void CreateCollection(string collectionPath); - bool PropertyExists(string collectionPath, string propertyName); - string GetString(string collectionPath, string propertyName); - void SetString(string collectionPath, string propertyName, string value); - } - -} diff --git a/SharedProject/Options/IWritableSettingsStoreProvider.cs b/SharedProject/Options/IWritableSettingsStoreProvider.cs deleted file mode 100644 index 8e06bde5..00000000 --- a/SharedProject/Options/IWritableSettingsStoreProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FineCodeCoverage.Options -{ - internal interface IWritableSettingsStoreProvider - { - IWritableSettingsStore Provide(); - } -} diff --git a/SharedProject/Options/IWritableUserSettingsStoreProvider.cs b/SharedProject/Options/IWritableUserSettingsStoreProvider.cs new file mode 100644 index 00000000..ea1def6c --- /dev/null +++ b/SharedProject/Options/IWritableUserSettingsStoreProvider.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.Settings; + +namespace FineCodeCoverage.Options +{ + internal interface IWritableUserSettingsStoreProvider + { + WritableSettingsStore Provide(); + } +} diff --git a/SharedProject/Options/ReadOnlyConfigSettingsStoreProvider.cs b/SharedProject/Options/ReadOnlyConfigSettingsStoreProvider.cs new file mode 100644 index 00000000..99adb0ac --- /dev/null +++ b/SharedProject/Options/ReadOnlyConfigSettingsStoreProvider.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Settings; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; + +namespace FineCodeCoverage.Options +{ + [ExcludeFromCodeCoverage] + [Export(typeof(IReadOnlyConfigSettingsStoreProvider))] + internal class ReadOnlyConfigSettingsStoreProvider : IReadOnlyConfigSettingsStoreProvider + { + public SettingsStore Provide() + { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously + return ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider); + return settingsManager.GetReadOnlySettingsStore(SettingsScope.Configuration); + }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously + } + } +} diff --git a/SharedProject/Options/VsWritableSettingsStore.cs b/SharedProject/Options/VsWritableSettingsStore.cs deleted file mode 100644 index 3e5a3196..00000000 --- a/SharedProject/Options/VsWritableSettingsStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.VisualStudio.Settings; -using System.Diagnostics.CodeAnalysis; - -namespace FineCodeCoverage.Options -{ - [ExcludeFromCodeCoverage] - internal class VsWritableSettingsStore : IWritableSettingsStore - { - private readonly WritableSettingsStore writableSettingsStore; - - public VsWritableSettingsStore(WritableSettingsStore writableSettingsStore) - { - this.writableSettingsStore = writableSettingsStore; - } - - public bool CollectionExists(string collectionPath) - { - return writableSettingsStore.CollectionExists(collectionPath); - } - - public void CreateCollection(string collectionPath) - { - writableSettingsStore.CreateCollection(collectionPath); - } - - public string GetString(string collectionPath, string propertyName) - { - return writableSettingsStore.GetString(collectionPath, propertyName); - } - - public bool PropertyExists(string collectionPath, string propertyName) - { - return writableSettingsStore.PropertyExists(collectionPath, propertyName); - } - - public void SetString(string collectionPath, string propertyName, string value) - { - writableSettingsStore.SetString(collectionPath, propertyName, value); - } - } - -} diff --git a/SharedProject/Options/VsWritableSettingsStoreProvider.cs b/SharedProject/Options/VsWritableSettingsStoreProvider.cs deleted file mode 100644 index a98df265..00000000 --- a/SharedProject/Options/VsWritableSettingsStoreProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.VisualStudio.Settings; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Settings; -using System.ComponentModel.Composition; - -namespace FineCodeCoverage.Options -{ - [Export(typeof(IWritableSettingsStoreProvider))] - internal class VsWritableSettingsStoreProvider : IWritableSettingsStoreProvider - { - public IWritableSettingsStore Provide() - { - IWritableSettingsStore writableSettingsStore = null; - ThreadHelper.JoinableTaskFactory.Run(async () => - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider); - var vsWritableSettingsStore = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); - writableSettingsStore = new VsWritableSettingsStore(vsWritableSettingsStore); - }); - return writableSettingsStore; - } - } - -} diff --git a/SharedProject/Options/WritableUserSettingsStoreProvider.cs b/SharedProject/Options/WritableUserSettingsStoreProvider.cs new file mode 100644 index 00000000..bc2df1ee --- /dev/null +++ b/SharedProject/Options/WritableUserSettingsStoreProvider.cs @@ -0,0 +1,29 @@ +using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Settings; +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Options +{ + [Export(typeof(IWritableUserSettingsStoreProvider))] + internal class WritableUserSettingsStoreProvider : IWritableUserSettingsStoreProvider + { + private WritableSettingsStore writableSettingsStore; + public WritableSettingsStore Provide() + { + if (writableSettingsStore == null) + { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously + writableSettingsStore = ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider); + return settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); + }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously + } + return writableSettingsStore; + } + } + +} diff --git a/SharedProject/Output/OpenCoberturaCommand.cs b/SharedProject/Output/OpenCoberturaCommand.cs index 262ec05d..ff4b1382 100644 --- a/SharedProject/Output/OpenCoberturaCommand.cs +++ b/SharedProject/Output/OpenCoberturaCommand.cs @@ -27,7 +27,7 @@ internal sealed class OpenCoberturaCommand : IListener, ILis public static readonly Guid CommandSet = PackageGuids.guidOutputToolWindowPackageCmdSet; private readonly DTE2 dte; - private MenuCommand command; + private readonly MenuCommand command; private string coberturaFile; public static OpenCoberturaCommand Instance diff --git a/SharedProject/Output/OpenHotspotsCommand.cs b/SharedProject/Output/OpenHotspotsCommand.cs index 80d7b81b..01727bfb 100644 --- a/SharedProject/Output/OpenHotspotsCommand.cs +++ b/SharedProject/Output/OpenHotspotsCommand.cs @@ -27,7 +27,7 @@ internal sealed class OpenHotspotsCommand : IListener, IList public static readonly Guid CommandSet = PackageGuids.guidOutputToolWindowPackageCmdSet; private readonly DTE2 dte; - private MenuCommand command; + private readonly MenuCommand command; private string hotspotsFile; public static OpenHotspotsCommand Instance diff --git a/SharedProject/Output/OutputToolWindowPackage.cs b/SharedProject/Output/OutputToolWindowPackage.cs index 3c1e7b61..abb144aa 100644 --- a/SharedProject/Output/OutputToolWindowPackage.cs +++ b/SharedProject/Output/OutputToolWindowPackage.cs @@ -12,6 +12,8 @@ using EnvDTE80; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Core.Initialization; +using FineCodeCoverage.Impl; +using FineCodeCoverage.Editor.Management; namespace FineCodeCoverage.Output { @@ -41,7 +43,7 @@ namespace FineCodeCoverage.Output [ProvideProfile(typeof(AppOptionsPage), Vsix.Name, Vsix.Name, 101, 102, true)] [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [ProvideToolWindow(typeof(OutputToolWindow), Style = VsDockStyle.Tabbed, DockedHeight = 300, Window = EnvDTE.Constants.vsWindowKindOutput)] - public sealed class OutputToolWindowPackage : AsyncPackage + public sealed class OutputToolWindowPackage : AsyncPackage { private static Microsoft.VisualStudio.ComponentModelHost.IComponentModel componentModel; private IFCCEngine fccEngine; @@ -83,20 +85,22 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke // Do any initialization that requires the UI thread after switching to the UI thread. await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var _dte2 = (DTE2)GetGlobalService(typeof(SDTE)); + var _dte2 = (DTE2)GetGlobalService(typeof(SDTE)); var sp = new ServiceProvider(_dte2 as Microsoft.VisualStudio.OLE.Interop.IServiceProvider); + // you cannot MEF import in the constructor of the package componentModel = sp.GetService(typeof(Microsoft.VisualStudio.ComponentModelHost.SComponentModel)) as Microsoft.VisualStudio.ComponentModelHost.IComponentModel; - Assumes.Present(componentModel); - fccEngine = componentModel.GetService(); + Assumes.Present(componentModel); + fccEngine = componentModel.GetService(); var eventAggregator = componentModel.GetService(); await OpenCoberturaCommand.InitializeAsync(this, eventAggregator); await OpenHotspotsCommand.InitializeAsync(this, eventAggregator); - await ClearUICommand.InitializeAsync(this, fccEngine); - await OutputToolWindowCommand.InitializeAsync( - this, + await ClearUICommand.InitializeAsync(this, fccEngine); + await OutputToolWindowCommand.InitializeAsync( + this, componentModel.GetService(), componentModel.GetService() ); + await componentModel.GetService().InitializeAsync(cancellationToken); } diff --git a/SharedProject/SharedProject.projitems b/SharedProject/SharedProject.projitems index cb52bfc0..4f32be23 100644 --- a/SharedProject/SharedProject.projitems +++ b/SharedProject/SharedProject.projitems @@ -6,24 +6,25 @@ 9353044c-5d39-4e70-8e78-28213e05217a - SharedProject + FineCodeCoverage - - + + + + + - - - + + + - - - - - - - + + + + + @@ -79,12 +80,13 @@ - + + @@ -148,9 +150,11 @@ + + @@ -165,7 +169,10 @@ + + + @@ -178,33 +185,158 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -232,12 +364,12 @@ - - - - - - + + + + + + @@ -276,9 +408,17 @@ - + + - + + + + + + + + \ No newline at end of file diff --git a/vs-market-place-overview.md b/vs-market-place-overview.md index 9b926739..c34a71c7 100644 --- a/vs-market-place-overview.md +++ b/vs-market-place-overview.md @@ -1,92 +1,34 @@ Supports **.NET Core** projects, **.NET Framework** projects and ( probably !) **C++** projects. -Please read the [README](https://github.com/FortuneN/FineCodeCoverage) for full details. +Please read the [README](https://github.com/FortuneN/FineCodeCoverage) for configuration and full details. Feedback and ideas are welcome [click here to let me know](https://github.com/FortuneN/FineCodeCoverage/issues) -### Watch Introduction Video - ## Highlights unit test code coverage Run a(some) unit test(s) and ... #### Get highlights on the code being tested and the code doing the testing -![Code Being Tested](https://raw.githubusercontent.com/FortuneN/FineCodeCoverage/master/Art/preview-coverage.png) +[![Highlights video](https://img.youtube.com/vi/CvrySUcTi7I/0.jpg)](https://youtu.be/CvrySUcTi7I) -#### See Coverage View +#### Report Coverage View ![Coverage View](https://raw.githubusercontent.com/FortuneN/FineCodeCoverage/master/Art/Output-Coverage.png) -#### See Summary View +#### Report Summary View ![Summary View](https://raw.githubusercontent.com/FortuneN/FineCodeCoverage/master/Art/Output-Summary.png) -#### See Risk Hotspots View +#### Report Risk Hotspots View ![Risk Hotspots View](https://raw.githubusercontent.com/FortuneN/FineCodeCoverage/master/Art/Output-RiskHotspots.png) -#### Global (Shared) options -![Global Options](https://raw.githubusercontent.com/FortuneN/FineCodeCoverage/master/Art/Options-Global.png) - -#### Local (Project) options (override globals in your csproj/vbproj : OPTIONAL) -``` - - - True - - - [ThirdParty.*]* - [FourthParty]* - - - [*]* - - - **/Migrations/* - **/Hacks/*.cs - - - MyCustomExcludeFromCodeCoverage - - - True - - -``` - -#### Options -``` -Enabled Specifies whether or not coverage output is enabled -Exclude Filter expressions to exclude specific modules and types (multiple values) -Include Filter expressions to include specific modules and types (multiple values) -ExcludeByFile Glob patterns specifying source files to exclude e.g. **/Migrations/* (multiple values) -ExcludeByAttribute Attributes to exclude from code coverage (multiple values) - -Both 'Exclude' and 'Include' options can be used together but 'Exclude' takes precedence. - -You can ignore a method or an entire class from code coverage by creating and applying the [ExcludeFromCodeCoverage] attribute present in the System.Diagnostics.CodeAnalysis namespace. -You can also ignore additional attributes by adding to the 'ExcludeByAttributes' list (short name or full name supported) e.g. : -[GeneratedCode] => Present in System.CodeDom.Compiler namespace -[MyCustomExcludeFromCodeCoverage] => Any custom attribute that you may define -``` - -#### Filter Expressions -``` -Wildcards -* => matches zero or more characters - -Examples -[*]* => All types in all assemblies (nothing is instrumented) -[coverlet.*]Coverlet.Core.Coverage => The Coverage class in the Coverlet.Core namespace belonging to any assembly that matches coverlet.* (e.g coverlet.core) -[*]Coverlet.Core.Instrumentation.* => All types belonging to Coverlet.Core.Instrumentation namespace in any assembly -[coverlet.*.tests]* => All types in any assembly starting with coverlet. and ending with .tests - -Both 'Exclude' and 'Include' options can be used together but 'Exclude' takes precedence. -``` + + ## Contribute -Check out the [contribution guidelines](https://raw.githubusercontent.com/FortuneN/FineCodeCoverage/master/CONTRIBUTING.md) +Check out the [contribution guidelines](https://github.com/FortuneN/FineCodeCoverage/blob/master/CONTRIBUTING.md) if you want to contribute to this project. For cloning and building this project yourself, make sure to install the -[Extensibility Tools 2015](https://visualstudiogallery.msdn.microsoft.com/ab39a092-1343-46e2-b0f1-6a3f91155aa6) +[Extensibility Essentials](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.ExtensibilityEssentials2022) extension for Visual Studio which enables some features used by this project. @@ -104,4 +46,4 @@ used by this project. | Provider | Type | Link | |:---------|:---------:|:---------------------------------------------------------------------------------------------------------------------------------:| | Paypal | Once | [](https://paypal.me/FortuneNgwenya) | -| Librepay | Recurring | [Donate using Liberapay](https://liberapay.com/FortuneN/donate) | +| Liberapay | Recurring | [Donate using Liberapay](https://liberapay.com/FortuneN/donate) |