diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index b358d09f..085e9e90 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ -#github: FortuneN +github: FortuneN liberapay: FortuneN ko_fi: FortuneN custom: ['https://paypal.me/FortuneNgwenya', 'https://www.buymeacoffee.com/FortuneN'] diff --git a/.github/workflows/push_pull.yaml b/.github/workflows/push_pull.yaml index 32df82bb..df3558fe 100644 --- a/.github/workflows/push_pull.yaml +++ b/.github/workflows/push_pull.yaml @@ -67,20 +67,20 @@ jobs: # above are common steps for push and pull - # pull only - - name: upload vsix - if: github.event_name == 'pull_request' && !github.event.pull_request.draft - uses: actions/upload-artifact@v2 - with: - name: FineCodeCoverage ( zipped vsix) - path: ${{env.VSIX}} + # pull only - todo update to v4 + # - name: upload vsix + # if: github.event_name == 'pull_request' && !github.event.pull_request.draft + # uses: actions/upload-artifact@v2 + # with: + # name: FineCodeCoverage ( zipped vsix) + # path: ${{env.VSIX}} - - name: upload vsix 2022 - if: github.event_name == 'pull_request' && !github.event.pull_request.draft - uses: actions/upload-artifact@v2 - with: - name: FineCodeCoverage2022 ( zipped vsix) - path: ${{env.VSIX2022}} + # - name: upload vsix 2022 + # if: github.event_name == 'pull_request' && !github.event.pull_request.draft + # uses: actions/upload-artifact@v2 + # with: + # name: FineCodeCoverage2022 ( zipped vsix) + # path: ${{env.VSIX2022}} # push only - name: create release @@ -134,11 +134,12 @@ jobs: manifestPath: ${{github.workspace}}\vs-market-place-manifest-2022.json vsixPath: ${{env.VSIX2022}} - - name: comment - released and added to marketplace - if: github.event_name == 'push' - uses: tonyhallett/addCommentToPullAndIssuesFromPushAction@v1.0.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - comment: released ${{steps.next-version.outputs.version_tag}} and available on marketplace - addTo: pullandissues + # todo - actions/upload-artifact@v4 now outputs the artifact-url + # - name: comment - released and added to marketplace + # if: github.event_name == 'push' + # uses: tonyhallett/addCommentToPullAndIssuesFromPushAction@v1.0.0 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # comment: released ${{steps.next-version.outputs.version_tag}} and available on marketplace + # addTo: pullandissues diff --git a/.gitignore b/.gitignore index 0c1a1bc3..de9a4f43 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ x64/ # NCrunch *.ncrunchsolution *.ncrunchproject -_NCrunch_WebCompiler \ No newline at end of file +_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 7e3d800e..545511d6 100644 --- a/FineCodeCoverage.sln +++ b/FineCodeCoverage.sln @@ -1,18 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34408.163 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FineCodeCoverage", "FineCodeCoverage\FineCodeCoverage.csproj", "{31C104BB-D294-4942-B206-896AA7A5FCB9}" 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 CHANGELOG.md = CHANGELOG.md CONTRIBUTING.md = CONTRIBUTING.md + dev-readmes\coverage-tool-zips.md = dev-readmes\coverage-tool-zips.md ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md LICENSE = LICENSE .github\workflows\push_pull.yaml = .github\workflows\push_pull.yaml @@ -27,12 +27,12 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedProject", "SharedProj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FineCodeCoverage2022", "FineCodeCoverage2022\FineCodeCoverage2022.csproj", "{59A22196-A750-4BA4-B30E-BE1422E68B8E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BAA6EBA8-65A6-46B0-8719-CB5C7EE6AE96}" + ProjectSection(SolutionItems) = preProject + .github\FUNDING.yml = .github\FUNDING.yml + EndProjectSection +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - SharedProject\SharedProject.projitems*{31c104bb-d294-4942-b206-896aa7a5fcb9}*SharedItemsImports = 4 - SharedProject\SharedProject.projitems*{59a22196-a750-4ba4-b30e-be1422e68b8e}*SharedItemsImports = 4 - SharedProject\SharedProject.projitems*{9353044c-5d39-4e70-8e78-28213e05217a}*SharedItemsImports = 13 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -61,4 +61,9 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EE8856CF-D8F1-4ECF-A722-F677A6F107A3} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + SharedProject\SharedProject.projitems*{31c104bb-d294-4942-b206-896aa7a5fcb9}*SharedItemsImports = 4 + SharedProject\SharedProject.projitems*{59a22196-a750-4ba4-b30e-be1422e68b8e}*SharedItemsImports = 4 + SharedProject\SharedProject.projitems*{9353044c-5d39-4e70-8e78-28213e05217a}*SharedItemsImports = 13 + EndGlobalSection EndGlobal diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index 99d4a388..cc79c541 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -53,6 +53,12 @@ 4 + + OutputToolWindowPackage.cs + True + True + OutputToolWindowPackage.vsct + True @@ -84,6 +90,8 @@ OutputToolWindowPackage.vsct Menus.ctmenu + VsctGenerator + OutputToolWindowPackage.cs Resources\ClearUICommand.png @@ -93,28 +101,32 @@ Resources\OutputToolWindowCommand.png true - - ZippedTools\coverlet.collector.3.0.3.zip + + ZippedTools\microsoft.testplatform.16.9.1.zip true - - ZippedTools\coverlet.console.3.0.3.zip + + ZippedTools\reportGenerator.4.7.4.zip true - - ZippedTools\microsoft.codecoverage.17.1.0.zip + + ZippedTools\openCover.4.7.1221.zip true - - ZippedTools\microsoft.testplatform.16.9.1.zip + + ZippedTools\microsoft.codecoverage.17.13.0.zip true - - ZippedTools\openCover.4.7.922.zip + + ZippedTools\coverlet.collector.6.0.4.zip true - - ZippedTools\reportGenerator.4.7.3.zip + + ZippedTools\coverlet.console.6.0.4.zip + true + + + ZippedTools\dotnet-coverage.17.14.2.zip true + + @@ -42,21 +44,86 @@ DefaultInvisible DynamicVisibility If you do not want an image next to your command, remove the Icon node /> --> - - + + + + + + + + + + Open FCC Files + + + @@ -65,13 +132,16 @@ bitmap strip containing the bitmaps and then there are the numeric ids of the elements used inside a button definition. An important aspect of this declaration is that the element id must be the actual index (1-based) of the bitmap inside the bitmap strip. --> - - + + - + + + + @@ -79,22 +149,26 @@ - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + diff --git a/Shared Files/Resources/reportparts.xml b/Shared Files/Resources/reportparts.xml index a6b44552..d71d5ae8 100644 --- a/Shared Files/Resources/reportparts.xml +++ b/Shared Files/Resources/reportparts.xml @@ -18,7 +18,8 @@ - + + diff --git a/Shared Files/ZippedTools/coverlet.collector.3.0.3.zip b/Shared Files/ZippedTools/coverlet.collector.3.0.3.zip deleted file mode 100644 index 1deba244..00000000 Binary files a/Shared Files/ZippedTools/coverlet.collector.3.0.3.zip and /dev/null differ diff --git a/Shared Files/ZippedTools/coverlet.collector.6.0.4.zip b/Shared Files/ZippedTools/coverlet.collector.6.0.4.zip new file mode 100644 index 00000000..33bd7339 Binary files /dev/null and b/Shared Files/ZippedTools/coverlet.collector.6.0.4.zip differ diff --git a/Shared Files/ZippedTools/coverlet.console.3.0.3.zip b/Shared Files/ZippedTools/coverlet.console.3.0.3.zip deleted file mode 100644 index 9346150f..00000000 Binary files a/Shared Files/ZippedTools/coverlet.console.3.0.3.zip and /dev/null differ diff --git a/Shared Files/ZippedTools/coverlet.console.6.0.4.zip b/Shared Files/ZippedTools/coverlet.console.6.0.4.zip new file mode 100644 index 00000000..71b223d8 Binary files /dev/null and b/Shared Files/ZippedTools/coverlet.console.6.0.4.zip differ diff --git a/Shared Files/ZippedTools/dotnet-coverage.17.14.2.zip b/Shared Files/ZippedTools/dotnet-coverage.17.14.2.zip new file mode 100644 index 00000000..c8ac9bf3 Binary files /dev/null and b/Shared Files/ZippedTools/dotnet-coverage.17.14.2.zip differ diff --git a/Shared Files/ZippedTools/microsoft.codecoverage.17.1.0.zip b/Shared Files/ZippedTools/microsoft.codecoverage.17.1.0.zip deleted file mode 100644 index 52ceb7f3..00000000 Binary files a/Shared Files/ZippedTools/microsoft.codecoverage.17.1.0.zip and /dev/null differ diff --git a/Shared Files/ZippedTools/microsoft.codecoverage.17.13.0.zip b/Shared Files/ZippedTools/microsoft.codecoverage.17.13.0.zip new file mode 100644 index 00000000..b4281b33 Binary files /dev/null and b/Shared Files/ZippedTools/microsoft.codecoverage.17.13.0.zip differ diff --git a/Shared Files/ZippedTools/openCover.4.7.1221.zip b/Shared Files/ZippedTools/openCover.4.7.1221.zip new file mode 100644 index 00000000..bd157d0c Binary files /dev/null and b/Shared Files/ZippedTools/openCover.4.7.1221.zip differ diff --git a/Shared Files/ZippedTools/openCover.4.7.922.zip b/Shared Files/ZippedTools/openCover.4.7.922.zip deleted file mode 100644 index dbce8d9c..00000000 Binary files a/Shared Files/ZippedTools/openCover.4.7.922.zip and /dev/null differ diff --git a/Shared Files/ZippedTools/reportGenerator.4.7.3.zip b/Shared Files/ZippedTools/reportGenerator.4.7.3.zip deleted file mode 100644 index eba3bfa7..00000000 Binary files a/Shared Files/ZippedTools/reportGenerator.4.7.3.zip and /dev/null differ diff --git a/Shared Files/ZippedTools/reportGenerator.4.7.4.zip b/Shared Files/ZippedTools/reportGenerator.4.7.4.zip new file mode 100644 index 00000000..96930e4c Binary files /dev/null and b/Shared Files/ZippedTools/reportGenerator.4.7.4.zip differ diff --git a/SharedProject/Core/AppDataFolder.cs b/SharedProject/Core/AppDataFolder.cs index 2b06fd9b..5b56ce4c 100644 --- a/SharedProject/Core/AppDataFolder.cs +++ b/SharedProject/Core/AppDataFolder.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using FineCodeCoverage.Options; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine { 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 0425a2d1..40c06a54 100644 --- a/SharedProject/Core/Cobertura/CoberturaUtil.cs +++ b/SharedProject/Core/Cobertura/CoberturaUtil.cs @@ -1,125 +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 System.IO; +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 CoverageReport LoadReport(string xmlFile) - { - using (var reader = XmlReader.Create(xmlFile, READER_SETTINGS)) + { + private readonly ICoberturaDeserializer coberturaDeserializer; + private readonly IFileLineCoverageFactory fileLineCoverageFactory; + private CoverageReport coverageReport; + private IFileLineCoverage fileLineCoverage; + + 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; + + 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 + ) + { + fileRenameListener.ListenForFileRename((oldFile, newFile) => + { + fileLineCoverage?.UpdateRenamed(oldFile, newFile); + }); + this.coberturaDeserializer = coberturaDeserializer; + this.fileLineCoverageFactory = fileLineCoverageFactory; + } - //private void CoverageXmlFileToJsonFile(string xmlFile, string jsonFile, bool formattedJson = false) - //{ - // var xmlText = File.ReadAllText(xmlFile); - // var jsonText = CoverageXmlTextToJsonText(xmlText, formattedJson); - // File.WriteAllText(jsonFile, jsonText); - //} - - //private string CoverageXmlTextToJsonText(string xmlText, bool formattedJson = false) - //{ - // long count = 0; - - // var xmlLines = xmlText - // .Split('\r', '\n') - // .Select(x => x.Trim()) - // .Where(x => !x.StartsWith(" !x.StartsWith(" !x.StartsWith("") && !x.StartsWith("") && !x.StartsWith("")) - // .Where(x => !x.StartsWith("") && !x.StartsWith("")) - // .Where(x => !x.StartsWith("") && !x.StartsWith("")) - // .Where(x => !x.StartsWith("") && !x.StartsWith("")) - // .Where(x => !x.StartsWith("") && !x.StartsWith("")) - // .Select(x => x - // .Replace("", "") - // .Replace("", "") - // .Replace("", "") - // .Replace("", "") - // ); - - // var processedXmlText = string - // .Join(Environment.NewLine, xmlLines) - // .Replace(" ProcessCoberturaXml(string xmlFile) + + public IFileLineCoverage ProcessCoberturaXml(string xmlFile) { - var coverageLines = new List(); + fileLineCoverage = fileLineCoverageFactory.Create(); - coverageReport = LoadReport(xmlFile); + coverageReport = coberturaDeserializer.Deserialize(xmlFile); - foreach (var package in coverageReport.Packages.Package) - { - foreach (var classs in package.Classes.Class) - { - foreach (var line in classs.Lines.Line) - { - coverageLines.Add(new CoverageLine - { - Package = package, - Class = classs, - Line = line - }); - } - } - } - - return coverageLines; + 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)); - - if (package == null) - { - return new string[0]; - } - - var classes = package - .Classes.Class - .Where(x => x.Name.Equals(qualifiedClassName)); - - if (file != -1) - { - classes = new List { classes.ElementAt(file) }; - } - - var classFiles = classes - .Select(x => x.Filename) - .ToArray(); - - return classFiles; + var package = GetPackage(assemblyName); + return package == null ? new string[0] : GetSourceFilesFromPackage(package, qualifiedClassName, file); } - } + + private static string[] GetSourceFilesFromPackage(Package package, string qualifiedClassName, int file) + { + var classes = GetClasses(package, qualifiedClassName); + return GetSourceFiles(classes, file); + } + + private static IEnumerable GetClasses(Package package, string qualifiedClassName) + { + return package.Classes.Where(x => x.Name.Equals(qualifiedClassName)); + } + + 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(); + + 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..5a828332 --- /dev/null +++ b/SharedProject/Core/Cobertura/FileLineCoverage.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverage.Engine.Model +{ + + internal class UniqueCoverageLines : HashSet + { + public UniqueCoverageLines() : base(new LineComparer()) + { + } + + public void AddRange(IEnumerable lines) + { + foreach (var line in lines) + Add(line); + } + + private IEnumerable sortedLines; + public IEnumerable SortedLines => sortedLines; + + public void Sort() + { + sortedLines = this.OrderBy(l => l.Number).ToList(); + } + + class LineComparer : IEqualityComparer + { + public bool Equals(ILine x, ILine y) + { + return x.Number == y.Number; + } + + public int GetHashCode(ILine obj) + { + return obj.Number; + } + } + } + + // 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 UniqueCoverageLines(); + m_coverageLines.Add(filename, fileCoverageLines); + } + + fileCoverageLines.AddRange(lines); + } + + public void Sort() + { + foreach (var lines in m_coverageLines.Values) + lines.Sort(); + } + + public IEnumerable GetLines(string filePath) + { + if (!m_coverageLines.TryGetValue(filePath, out var lines)) + { + return Enumerable.Empty().ToList(); + } + return lines.SortedLines; + + } + + 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 ab91f313..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 { - List 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..4e9e86b0 --- /dev/null +++ b/SharedProject/Core/Cobertura/IFileLineCoverage.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace FineCodeCoverage.Engine.Model +{ + internal interface IFileLineCoverage + { + void Add(string filename, IEnumerable line); + 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/CoverageToolOutput/CoverageToolOutputManager.cs b/SharedProject/Core/CoverageToolOutput/CoverageToolOutputManager.cs index f38efa27..73399c3a 100644 --- a/SharedProject/Core/CoverageToolOutput/CoverageToolOutputManager.cs +++ b/SharedProject/Core/CoverageToolOutput/CoverageToolOutputManager.cs @@ -5,6 +5,8 @@ using System.Linq; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Output; +using SharedProject.Core.CoverageToolOutput; namespace FineCodeCoverage.Engine { @@ -12,21 +14,28 @@ namespace FineCodeCoverage.Engine internal class CoverageToolOutputManager : ICoverageToolOutputManager { private readonly ILogger logger; + private readonly IEventAggregator eventAggregator; private readonly IFileUtil fileUtil; private string outputFolderForAllProjects; private List coverageProjects; private readonly IOrderedEnumerable> outputFolderProviders; [ImportingConstructor] - public CoverageToolOutputManager(IFileUtil fileUtil, ILogger logger,[ImportMany] IEnumerable> outputFolderProviders) + public CoverageToolOutputManager( + IFileUtil fileUtil, + ILogger logger,[ImportMany] IEnumerable> outputFolderProviders, + IEventAggregator eventAggregator + ) { this.logger = logger; + this.eventAggregator = eventAggregator; this.fileUtil = fileUtil; this.outputFolderProviders = outputFolderProviders.OrderBy(p => p.Metadata.Order); } public void SetProjectCoverageOutputFolder(List coverageProjects) { + eventAggregator.SendMessage(new OutdatedOutputMessage()); this.coverageProjects = coverageProjects; DetermineOutputFolderForAllProjects(); if (outputFolderForAllProjects == null) diff --git a/SharedProject/Core/CoverageToolOutput/OutdatedOutputMessage.cs b/SharedProject/Core/CoverageToolOutput/OutdatedOutputMessage.cs new file mode 100644 index 00000000..567656cb --- /dev/null +++ b/SharedProject/Core/CoverageToolOutput/OutdatedOutputMessage.cs @@ -0,0 +1,6 @@ +namespace SharedProject.Core.CoverageToolOutput +{ + internal class OutdatedOutputMessage + { + } +} diff --git a/SharedProject/Core/CoverageToolOutput/SolutionFolderProvider.cs b/SharedProject/Core/CoverageToolOutput/SolutionFolderProvider.cs index d6fe68ac..989d3ffb 100644 --- a/SharedProject/Core/CoverageToolOutput/SolutionFolderProvider.cs +++ b/SharedProject/Core/CoverageToolOutput/SolutionFolderProvider.cs @@ -11,9 +11,9 @@ public string Provide(string projectFile) { string provided = null; var directory = new FileInfo(projectFile).Directory; - while(directory != null) + while (directory != null) { - var isSolutionDirectory = directory.EnumerateFiles().Any(f => f.Name.EndsWith(".sln")); + var isSolutionDirectory = directory.EnumerateFiles().Any(IsSolutionFile); if (isSolutionDirectory) { provided = directory.FullName; @@ -23,5 +23,10 @@ public string Provide(string projectFile) } return provided; } + + private bool IsSolutionFile(FileInfo fileInfo) + { + return fileInfo.Name.EndsWith(".sln") || fileInfo.Name.EndsWith(".slnx"); + } } } diff --git a/SharedProject/Core/CoverageUtilManager.cs b/SharedProject/Core/CoverageUtilManager.cs index 4078f38d..0dfe7ad6 100644 --- a/SharedProject/Core/CoverageUtilManager.cs +++ b/SharedProject/Core/CoverageUtilManager.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; +using System.ComponentModel.Composition; using System.Threading; using System.Threading.Tasks; using FineCodeCoverage.Engine.Coverlet; using FineCodeCoverage.Engine.Model; using FineCodeCoverage.Engine.OpenCover; -using FineCodeCoverage.Engine.ReportGenerator; namespace FineCodeCoverage.Engine { diff --git a/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsGlobalExecutor.cs b/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsGlobalExecutor.cs index 5b4d2b71..897f5bea 100644 --- a/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsGlobalExecutor.cs +++ b/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsGlobalExecutor.cs @@ -1,6 +1,7 @@ using System.ComponentModel.Composition; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine.Coverlet { diff --git a/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsLocalExecutor.cs b/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsLocalExecutor.cs index 1323dae5..0524c562 100644 --- a/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsLocalExecutor.cs +++ b/SharedProject/Core/Coverlet/Console/CoverletConsoleDotnetToolsLocalExecutor.cs @@ -1,6 +1,7 @@ using System.ComponentModel.Composition; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine.Coverlet { diff --git a/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs b/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs index 1a8f2d16..07a22397 100644 --- a/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs +++ b/SharedProject/Core/Coverlet/Console/CoverletConsoleUtil.cs @@ -6,150 +6,223 @@ using System.Threading.Tasks; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine.Coverlet { - [Export(typeof(ICoverletConsoleUtil))] - internal class CoverletConsoleUtil : ICoverletConsoleUtil + internal interface ICoverletExeArgumentsProvider { - private readonly IProcessUtil processUtil; - private readonly ILogger logger; - private readonly IFCCCoverletConsoleExecutor fccExecutor; - private readonly List executors; + List GetArguments(ICoverageProject project); + } - [ImportingConstructor] - public CoverletConsoleUtil( - IProcessUtil processUtil, - ILogger logger, - [Import(typeof(ICoverletConsoleDotnetToolsGlobalExecutor))] - ICoverletConsoleExecutor globalExecutor, - [Import(typeof(ICoverletConsoleCustomPathExecutor))] - ICoverletConsoleExecutor customPathExecutor, - [Import(typeof(ICoverletConsoleDotnetToolsLocalExecutor))] - ICoverletConsoleExecutor localExecutor, - IFCCCoverletConsoleExecutor fccExecutor - ) - { - this.processUtil = processUtil; - this.logger = logger; + [Export(typeof(ICoverletExeArgumentsProvider))] + internal class CoverletExeArgumentsProvider : ICoverletExeArgumentsProvider + { + private static IEnumerable SanitizeExcludesByAttribute(string[] excludes) + { + return (excludes ?? new string[0]) + .Where(x => x != null) + .Select(x => x.Trim(' ', '\'', '\"')) + .Where(x => !string.IsNullOrWhiteSpace(x)); + } - executors = new List + private static IEnumerable SantitizeExcludeInclude(string[] excludesOrIncludes) + { + return (excludesOrIncludes ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x)).Select(value => { - localExecutor, - customPathExecutor, - globalExecutor, - fccExecutor - }; + return value.Replace("\"", "\\\"").Trim(' ', '\''); + }); + } - this.fccExecutor = fccExecutor; + private static void AddExcludesOrIncludes(List coverletSettings, IEnumerable excludesOrIncludes, bool isInclude) + { + foreach (var value in excludesOrIncludes) + { + coverletSettings.Add($@"--{(isInclude ? "include" : "exclude")} ""{value}"""); + } } - public void Initialize(string appDataFolder, CancellationToken cancellationToken) - { - fccExecutor.Initialize(appDataFolder, cancellationToken); - } - // for now FCCCoverletConsoleExeProvider can return null for exe path + private static IEnumerable AddTestAssemblyIfNecessary( + IEnumerable projectIncludes, + IEnumerable includes, + string projectName) + { + var hasIncludes = projectIncludes.Any() || includes.Any(); + if(!hasIncludes) + { + return projectIncludes; + } + return projectIncludes.Concat(new string[] { projectName }); + } + + private static void AddProjectExcludesOrIncludes(List coverletSettings, IEnumerable excludesOrIncludes, bool isInclude) + { + AddExcludesOrIncludes(coverletSettings, excludesOrIncludes.Select(excludeOrInclude => $"[{excludeOrInclude}]*"), isInclude); + } - internal ExecuteRequest GetExecuteRequest(ICoverageProject project, string coverletSettings) + private static void AddExcludesIncludes(List coverletSettings,ICoverageProject project) { - foreach(var exeProvider in executors) + AddExcludesOrIncludes(coverletSettings, SantitizeExcludeInclude(project.Settings.Exclude), false); + AddProjectExcludesOrIncludes(coverletSettings, project.ExcludedReferencedProjects.Select(rp => rp.AssemblyName), false); + var includes = SantitizeExcludeInclude(project.Settings.Include); + AddExcludesOrIncludes(coverletSettings, includes, true); + var projectIncludes = project.IncludedReferencedProjects.Select(rp => rp.AssemblyName); + if (project.Settings.IncludeTestAssembly) { - var executeRequest = exeProvider.GetRequest(project, coverletSettings); - if(executeRequest != null) - { - return executeRequest; - } + projectIncludes = AddTestAssemblyIfNecessary(projectIncludes, includes, project.ProjectName); } - return null;//todo change to throw when using zip file + AddProjectExcludesOrIncludes(coverletSettings, projectIncludes, true); } - internal List GetCoverletSettings(ICoverageProject project) + public List GetArguments(ICoverageProject project) { - var coverletSettings = new List(); + var coverletSettings = new List(); - coverletSettings.Add($@"""{project.TestDllFile}"""); + coverletSettings.Add($@"""{project.TestDllFile}"""); - coverletSettings.Add($@"--format ""cobertura"""); + coverletSettings.Add($@"--format ""cobertura"""); - foreach (var value in (project.Settings.Exclude ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - coverletSettings.Add($@"--exclude ""{value.Replace("\"", "\\\"").Trim(' ', '\'')}"""); - } + AddExcludesIncludes(coverletSettings, project); - foreach (var referencedProjectExcludedFromCodeCoverage in project.ExcludedReferencedProjects) - { - coverletSettings.Add($@"--exclude ""[{referencedProjectExcludedFromCodeCoverage}]*"""); - } + foreach (var value in (project.Settings.ExcludeByFile ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) + { + coverletSettings.Add($@"--exclude-by-file ""{value.Replace("\"", "\\\"").Trim(' ', '\'')}"""); + } - foreach (var value in (project.Settings.Include ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - coverletSettings.Add($@"--include ""{value.Replace("\"", "\\\"").Trim(' ', '\'')}"""); - } + foreach (var value in SanitizeExcludesByAttribute(project.Settings.ExcludeByAttribute).Select(EnsureAttributeTypeUnqualified)) + { + var withoutAttributeBrackets = value.Trim('[', ']'); + coverletSettings.Add($@"--exclude-by-attribute {value}"); + } - foreach (var includedReferencedProject in project.IncludedReferencedProjects) - { - coverletSettings.Add($@"--include ""[{includedReferencedProject}]*"""); - } + if (project.Settings.IncludeTestAssembly) + { + coverletSettings.Add("--include-test-assembly"); + } - foreach (var value in (project.Settings.ExcludeByFile ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - coverletSettings.Add($@"--exclude-by-file ""{value.Replace("\"", "\\\"").Trim(' ', '\'')}"""); - } + coverletSettings.Add($@"--target ""dotnet"""); - foreach (var value in (project.Settings.ExcludeByAttribute ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - coverletSettings.Add($@"--exclude-by-attribute ""{value.Replace("\"", "\\\"").Trim(' ', '\'', '[', ']')}"""); - } + coverletSettings.Add($@"--threshold-type line"); - if (project.Settings.IncludeTestAssembly) - { - coverletSettings.Add("--include-test-assembly"); - } + coverletSettings.Add($@"--threshold-stat total"); - coverletSettings.Add($@"--target ""dotnet"""); + coverletSettings.Add($@"--threshold 0"); - coverletSettings.Add($@"--threshold-type line"); + coverletSettings.Add($@"--output ""{project.CoverageOutputFile}"""); - coverletSettings.Add($@"--threshold-stat total"); + var runSettings = !string.IsNullOrWhiteSpace(project.RunSettingsFile) ? $@"--settings """"{project.RunSettingsFile}""""" : default; + coverletSettings.Add($@"--targetargs ""test """"{project.TestDllFile}"""" --nologo --blame {runSettings} --results-directory """"{project.CoverageOutputFolder}"""" --diag """"{project.CoverageOutputFolder}/diagnostics.log"""" """); - coverletSettings.Add($@"--threshold 0"); + return coverletSettings; + } - coverletSettings.Add($@"--output ""{ project.CoverageOutputFile }"""); + private string EnsureAttributeTypeUnqualified(string attributeType) => attributeType.Split('.').Last(); + + } + + internal interface ICoverletConsoleExecuteRequestProvider + { + ExecuteRequest GetExecuteRequest(ICoverageProject project, string coverletSettings); + } + + [Export(typeof(ICoverletConsoleExecuteRequestProvider))] + internal class CoverletConsoleExecuteRequestProvider : ICoverletConsoleExecuteRequestProvider + { + private readonly List executors; + + [ImportingConstructor] + public CoverletConsoleExecuteRequestProvider( + [Import(typeof(ICoverletConsoleDotnetToolsGlobalExecutor))] + ICoverletConsoleExecutor globalExecutor, + [Import(typeof(ICoverletConsoleCustomPathExecutor))] + ICoverletConsoleExecutor customPathExecutor, + [Import(typeof(ICoverletConsoleDotnetToolsLocalExecutor))] + ICoverletConsoleExecutor localExecutor, + IFCCCoverletConsoleExecutor fccExecutor + ) + { + executors = new List + { + localExecutor, + customPathExecutor, + globalExecutor, + fccExecutor + }; + } + // for now FCCCoverletConsoleExeProvider can return null for exe path + public ExecuteRequest GetExecuteRequest(ICoverageProject project, string coverletSettings) + { + foreach (var exeProvider in executors) + { + var executeRequest = exeProvider.GetRequest(project, coverletSettings); + if (executeRequest != null) + { + return executeRequest; + } + } + return null;//todo change to throw when using zip file + } + } - var runSettings = !string.IsNullOrWhiteSpace(project.RunSettingsFile) ? $@"--settings """"{project.RunSettingsFile}""""" : default; - coverletSettings.Add($@"--targetargs ""test """"{project.TestDllFile}"""" --nologo --blame {runSettings} --results-directory """"{project.CoverageOutputFolder}"""" --diag """"{project.CoverageOutputFolder}/diagnostics.log"""" """); + [Export(typeof(ICoverletConsoleUtil))] + internal class CoverletConsoleUtil : ICoverletConsoleUtil + { + private readonly IProcessUtil processUtil; + private readonly ILogger logger; + private readonly ICoverletConsoleExecuteRequestProvider coverletConsoleExecuteRequestProvider; + private readonly IFCCCoverletConsoleExecutor fccExecutor; + private readonly ICoverletExeArgumentsProvider coverletExeArgumentsProvider; - return coverletSettings; + [ImportingConstructor] + public CoverletConsoleUtil( + IProcessUtil processUtil, + ILogger logger, + ICoverletConsoleExecuteRequestProvider coverletConsoleExecuteRequestProvider, + IFCCCoverletConsoleExecutor fccExecutor, + ICoverletExeArgumentsProvider coverletExeArgumentsProvider + ) + { + this.processUtil = processUtil; + this.logger = logger; + this.coverletConsoleExecuteRequestProvider = coverletConsoleExecuteRequestProvider; + this.fccExecutor = fccExecutor; + this.coverletExeArgumentsProvider = coverletExeArgumentsProvider; + } + public void Initialize(string appDataFolder, CancellationToken cancellationToken) + { + fccExecutor.Initialize(appDataFolder, cancellationToken); } public async Task RunAsync(ICoverageProject project, CancellationToken cancellationToken) { var title = $"Coverlet Run ({project.ProjectName})"; - var coverletSettings = GetCoverletSettings(project); + var coverletSettings = coverletExeArgumentsProvider.GetArguments(project); - logger.Log($"{title} Arguments {Environment.NewLine}{string.Join($"{Environment.NewLine}", coverletSettings)}"); + var executingLogLines = new List { $"{title} - Arguments" }; + executingLogLines.AddRange(coverletSettings); + logger.Log(executingLogLines); - var result = await processUtil - .ExecuteAsync(GetExecuteRequest(project, string.Join(" ", coverletSettings)), cancellationToken); + var result = await processUtil.ExecuteAsync( + coverletConsoleExecuteRequestProvider.GetExecuteRequest(project, string.Join(" ", coverletSettings)), + cancellationToken + ); - - - /* - 0 - Success. - 1 - If any test fails. - 2 - Coverage percentage is below threshold. - 3 - Test fails and also coverage percentage is below threshold. + /* + 0 - Success. + 1 - If any test fails. + 2 - Coverage percentage is below threshold. + 3 - Test fails and also coverage percentage is below threshold. */ - if (result.ExitCode > 3) - { - logger.Log($"{title} Error. Exit code: {result.ExitCode}"); - logger.Log($"{title} Error. Output: ", result.Output); + if (result.ExitCode > 3) + { + var errorExitCodeMessage = $"Error. Exit code: {result.ExitCode}"; + logger.Log($"{title} {errorExitCodeMessage}", result.Output); - throw new Exception(result.Output); - } + throw new Exception(errorExitCodeMessage); + } - logger.Log(title, result.Output); + logger.Log($"{title} - Output", result.Output); } } } diff --git a/SharedProject/Core/Coverlet/Console/DotNetToolListCoverlet.cs b/SharedProject/Core/Coverlet/Console/DotNetToolListCoverlet.cs index 9f4c7dbb..dd2ac6ba 100644 --- a/SharedProject/Core/Coverlet/Console/DotNetToolListCoverlet.cs +++ b/SharedProject/Core/Coverlet/Console/DotNetToolListCoverlet.cs @@ -3,6 +3,7 @@ using System.ComponentModel.Composition; using System.Linq; using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine.Coverlet { diff --git a/SharedProject/Core/Coverlet/Console/FCCCoverletConsoleExecutor.cs b/SharedProject/Core/Coverlet/Console/FCCCoverletConsoleExecutor.cs index b872dc3b..3c568750 100644 --- a/SharedProject/Core/Coverlet/Console/FCCCoverletConsoleExecutor.cs +++ b/SharedProject/Core/Coverlet/Console/FCCCoverletConsoleExecutor.cs @@ -25,18 +25,17 @@ public string FindInFolder(string folder, SearchOption searchOption) internal class FCCCoverletConsoleExecutor : IFCCCoverletConsoleExecutor { [ImportingConstructor] - public FCCCoverletConsoleExecutor(IToolFolder toolFolder, IToolZipProvider toolZipProvider) + public FCCCoverletConsoleExecutor(IToolUnzipper toolUnzipper) { - this.toolFolder = toolFolder; - this.toolZipProvider = toolZipProvider; + this.toolUnzipper = toolUnzipper; } - private readonly IToolFolder toolFolder; - private readonly IToolZipProvider toolZipProvider; private string coverletExePath; private const string zipPrefix = "coverlet.console"; private const string zipDirectoryName = "coverlet";//backwards compatibility - public ExecuteRequest GetRequest(ICoverageProject coverageProject, string coverletSettings) + private readonly IToolUnzipper toolUnzipper; + + public ExecuteRequest GetRequest(ICoverageProject coverageProject, string coverletSettings) { return new ExecuteRequest { @@ -49,7 +48,7 @@ public ExecuteRequest GetRequest(ICoverageProject coverageProject, string coverl public void Initialize(string appDataFolder, CancellationToken cancellationToken) { - var zipDestination = toolFolder.EnsureUnzipped(appDataFolder, zipDirectoryName, toolZipProvider.ProvideZip(zipPrefix), cancellationToken); + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolder, zipDirectoryName, zipPrefix, cancellationToken); coverletExePath = Directory.GetFiles(zipDestination, "coverlet.exe", SearchOption.AllDirectories).FirstOrDefault() ?? Directory.GetFiles(zipDestination, "*coverlet*.exe", SearchOption.AllDirectories).FirstOrDefault(); } 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/Coverlet/DataCollector/CoverletDataCollectorUtil.cs b/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorUtil.cs index 17732e5e..494e241a 100644 --- a/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorUtil.cs +++ b/SharedProject/Core/Coverlet/DataCollector/CoverletDataCollectorUtil.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; @@ -9,6 +10,7 @@ using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Core.Utilities.VsThreading; using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Output; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -25,8 +27,7 @@ internal class CoverletDataCollectorUtil : ICoverletDataCollectorUtil private readonly IDataCollectorSettingsBuilderFactory dataCollectorSettingsBuilderFactory; private readonly ICoverletDataCollectorGeneratedCobertura coverletDataCollectorGeneratedCobertura; private readonly IProcessResponseProcessor processResponseProcessor; - private readonly IToolZipProvider toolZipProvider; - private readonly IToolFolder toolFolder; + private readonly IToolUnzipper toolUnzipper; private readonly IVsBuildFCCSettingsProvider vsBuildFCCSettingsProvider; @@ -35,8 +36,8 @@ internal class CoverletDataCollectorUtil : ICoverletDataCollectorUtil internal ICoverageProject coverageProject; private const string LogPrefix = "Coverlet Collector Run"; internal string TestAdapterPathArg { get; set; } - internal const string zipPrefix = "coverlet.collector"; - internal const string zipDirectoryName = "coverletCollector"; + private const string zipPrefix = "coverlet.collector"; + private const string zipDirectoryName = "coverletCollector"; internal IThreadHelper ThreadHelper = new VsThreadHelper(); @@ -49,8 +50,7 @@ public CoverletDataCollectorUtil( IDataCollectorSettingsBuilderFactory dataCollectorSettingsBuilderFactory, ICoverletDataCollectorGeneratedCobertura coverletDataCollectorGeneratedCobertura, IProcessResponseProcessor processResponseProcessor, - IToolZipProvider toolZipProvider, - IToolFolder toolFolder, + IToolUnzipper toolUnzipper, IVsBuildFCCSettingsProvider vsBuildFCCSettingsProvider ) { @@ -61,8 +61,7 @@ IVsBuildFCCSettingsProvider vsBuildFCCSettingsProvider this.dataCollectorSettingsBuilderFactory = dataCollectorSettingsBuilderFactory; this.coverletDataCollectorGeneratedCobertura = coverletDataCollectorGeneratedCobertura; this.processResponseProcessor = processResponseProcessor; - this.toolZipProvider = toolZipProvider; - this.toolFolder = toolFolder; + this.toolUnzipper = toolUnzipper; this.vsBuildFCCSettingsProvider = vsBuildFCCSettingsProvider; } @@ -159,7 +158,7 @@ private string GetSettings() { var dataCollectorSettingsBuilder = dataCollectorSettingsBuilderFactory.Create(); dataCollectorSettingsBuilder - .Initialize(coverageProject.Settings, coverageProject.RunSettingsFile, Path.Combine(coverageProject.CoverageOutputFolder,"FCC.runsettings")); + .Initialize(coverageProject.Settings.RunSettingsOnly, coverageProject.RunSettingsFile, Path.Combine(coverageProject.CoverageOutputFolder,"FCC.runsettings")); // command arguments dataCollectorSettingsBuilder @@ -174,28 +173,37 @@ private string GetSettings() dataCollectorSettingsBuilder .WithResultsDirectory(coverageProject.CoverageOutputFolder); - string[] projectExcludes = coverageProject.ExcludedReferencedProjects.Select(erp => $"[{erp}]*").ToArray(); + string[] projectExcludes = coverageProject.ExcludedReferencedProjects.Select(erp => $"[{erp.AssemblyName}]*").ToArray(); if(coverageProject.Settings.Exclude != null) { - projectExcludes = projectExcludes.Concat(coverageProject.Settings.Exclude).ToArray(); + projectExcludes = projectExcludes.Concat(SanitizeExcludesOrIncludes(coverageProject.Settings.Exclude)).ToArray(); } //DataCollector Configuration dataCollectorSettingsBuilder .WithExclude(projectExcludes, runSettingsCoverletConfiguration.Exclude); dataCollectorSettingsBuilder - .WithExcludeByFile(coverageProject.Settings.ExcludeByFile, runSettingsCoverletConfiguration.ExcludeByFile); + .WithExcludeByFile( + SanitizeExcludesOrIncludes(coverageProject.Settings.ExcludeByFile), + runSettingsCoverletConfiguration.ExcludeByFile); dataCollectorSettingsBuilder - .WithExcludeByAttribute(coverageProject.Settings.ExcludeByAttribute, runSettingsCoverletConfiguration.ExcludeByAttribute); + .WithExcludeByAttribute( + SanitizeExcludesOrIncludes(coverageProject.Settings.ExcludeByAttribute), + runSettingsCoverletConfiguration.ExcludeByAttribute); - string[] projectIncludes = coverageProject.IncludedReferencedProjects.Select(irp => $"[{irp}]*").ToArray(); + var projectIncludes = coverageProject.IncludedReferencedProjects.Select(irp => $"[{irp.AssemblyName}]*"); if(coverageProject.Settings.Include != null) { - projectIncludes = projectIncludes.Concat(coverageProject.Settings.Include).ToArray(); + projectIncludes = projectIncludes.Concat(SanitizeExcludesOrIncludes(coverageProject.Settings.Include)); + } + + if (coverageProject.Settings.IncludeTestAssembly && projectIncludes.Any()) + { + projectIncludes = projectIncludes.Concat(new string[] { $"[{coverageProject.ProjectName}]*" }).ToArray(); } dataCollectorSettingsBuilder - .WithInclude(projectIncludes, runSettingsCoverletConfiguration.Include); + .WithInclude(projectIncludes.ToArray(), runSettingsCoverletConfiguration.Include); dataCollectorSettingsBuilder .WithIncludeTestAssembly(coverageProject.Settings.IncludeTestAssembly, runSettingsCoverletConfiguration.IncludeTestAssembly); @@ -213,6 +221,13 @@ private string GetSettings() } + private string[] SanitizeExcludesOrIncludes(string[] excludesOrIncludes) + { + return (excludesOrIncludes ?? new string[0]) + .Where(x => x != null) + .Select(x => x.Trim(' ', '\'', '\"')) + .Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + } private string GetTestAdapterPathArg() { if (!String.IsNullOrWhiteSpace(coverageProject.Settings.CoverletCollectorDirectoryPath)) { @@ -250,7 +265,7 @@ public async Task RunAsync(CancellationToken cancellationToken) // https://github.com/dotnet/sdk/blob/936935f18c3540ed77c97e392780a9dd82aca441/src/Cli/dotnet/commands/dotnet-test/Program.cs#L86 // test failure has exit code 1 - processResponseProcessor.Process(result, code => code == 0 || code == 1, true, GetLogTitle(), () => + processResponseProcessor.Process(result, code => code == 0 || code == 1, true, $"{GetLogTitle()} - Output", () => { coverletDataCollectorGeneratedCobertura.CorrectPath(coverageProject.CoverageOutputFolder, coverageProject.CoverageOutputFile); }); @@ -271,8 +286,8 @@ private void LogRun(string coverletSettings) public void Initialize(string appDataFolder,CancellationToken cancellationToken) { - var zipDestination = toolFolder.EnsureUnzipped(appDataFolder, zipDirectoryName,toolZipProvider.ProvideZip(zipPrefix),cancellationToken); - var testAdapterPath = Path.Combine(zipDestination, "build", "netstandard1.0"); + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolder, zipDirectoryName, zipPrefix, cancellationToken); + var testAdapterPath = Path.Combine(zipDestination, "build", "netstandard2.0"); TestAdapterPathArg = $@"""{testAdapterPath}"""; } } diff --git a/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilder.cs b/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilder.cs index 1c4611ad..a941051e 100644 --- a/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilder.cs +++ b/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilder.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.Composition; -using System.IO; using System.Linq; using System.Xml.Linq; -using FineCodeCoverage.Options; namespace FineCodeCoverage.Engine.Coverlet { @@ -13,13 +10,8 @@ internal class DataCollectorSettingsBuilder : IDataCollectorSettingsBuilder { private string generatedRunSettingsPath; private string existingRunSettings; - private IAppOptions coverageProjectSettings; - private readonly ILogger logger; + private bool runSettingsOnly; - public DataCollectorSettingsBuilder(ILogger logger) - { - this.logger = logger; - } #region Arguments internal string ProjectDll { get; set; } internal string Blame { get; set; } @@ -72,7 +64,6 @@ public string Build() private void GenerateRunSettings() { var runSettingsDocument = existingRunSettings == null ? GenerateFullRunSettings() : GenerateRunSettingsFromExisting(); - logger.Log($"Saving run settings to {generatedRunSettingsPath}"); runSettingsDocument.Save(generatedRunSettingsPath); } @@ -191,9 +182,9 @@ public void WithResultsDirectory(string resultsDirectory) ResultsDirectory = $"--results-directory {Quote(resultsDirectory)}"; } - public void Initialize(IAppOptions coverageProjectSettings, string runSettingsPath, string generatedRunSettingsPath) + public void Initialize(bool runSettingsOnly, string runSettingsPath, string generatedRunSettingsPath) { - this.coverageProjectSettings = coverageProjectSettings; + this.runSettingsOnly = runSettingsOnly; this.generatedRunSettingsPath = generatedRunSettingsPath; existingRunSettings = runSettingsPath; RunSettings = $"--settings {Quote(generatedRunSettingsPath)}"; @@ -222,7 +213,7 @@ string DelimitProject() return runSettings; } - if (!coverageProjectSettings.RunSettingsOnly) // default true + if (!runSettingsOnly) // default true { return DelimitProject(); } @@ -236,7 +227,18 @@ public void WithExclude(string[] projectExclude, string runSettingsExclude) public void WithExcludeByAttribute(string[] projectExcludeByAttribute, string runSettingsExcludeByAttribute) { - ExcludeByAttribute = RunSettingsOrProject(projectExcludeByAttribute, runSettingsExcludeByAttribute); + if(runSettingsExcludeByAttribute != null) + { + runSettingsExcludeByAttribute = string.Join(",", runSettingsExcludeByAttribute.Split(',').Select(Unqualify)); + } + ExcludeByAttribute = RunSettingsOrProject( + projectExcludeByAttribute?.Select(Unqualify).ToArray(), + runSettingsExcludeByAttribute); + + string Unqualify(string excludeByAttribute) + { + return excludeByAttribute.Split('.').Last(); + } } public void WithExcludeByFile(string[] projectExcludeByFile, string runSettingsExcludeByFile) @@ -269,7 +271,7 @@ string ProjectInclude() } else { - if (!coverageProjectSettings.RunSettingsOnly) // default true + if (!runSettingsOnly) // default true { includeTestAssembly = ProjectInclude(); } diff --git a/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilderFactory.cs b/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilderFactory.cs index 3111e53f..ab99d916 100644 --- a/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilderFactory.cs +++ b/SharedProject/Core/Coverlet/DataCollector/DataCollectorSettingsBuilderFactory.cs @@ -1,21 +1,13 @@ using System.ComponentModel.Composition; using FineCodeCoverage.Options; - namespace FineCodeCoverage.Engine.Coverlet { [Export(typeof(IDataCollectorSettingsBuilderFactory))] internal class DataCollectorSettingsBuilderFactory : IDataCollectorSettingsBuilderFactory { - private readonly ILogger logger; - - [ImportingConstructor] - public DataCollectorSettingsBuilderFactory(ILogger logger) - { - this.logger = logger; - } public IDataCollectorSettingsBuilder Create() { - return new DataCollectorSettingsBuilder(logger); + return new DataCollectorSettingsBuilder(); } } } diff --git a/SharedProject/Core/Coverlet/DataCollector/interfaces/IDataCollectorSettingsBuilder.cs b/SharedProject/Core/Coverlet/DataCollector/interfaces/IDataCollectorSettingsBuilder.cs index 82892e24..6132a644 100644 --- a/SharedProject/Core/Coverlet/DataCollector/interfaces/IDataCollectorSettingsBuilder.cs +++ b/SharedProject/Core/Coverlet/DataCollector/interfaces/IDataCollectorSettingsBuilder.cs @@ -4,7 +4,7 @@ namespace FineCodeCoverage.Engine.Coverlet { internal interface IDataCollectorSettingsBuilder { - void Initialize(IAppOptions coverageProjectSettings, string runSettingsPath, string generatedRunSettingsPath); + void Initialize(bool runSettingsOnly, string runSettingsPath, string generatedRunSettingsPath); void WithProjectDll(string projectDll); void WithExclude(string[] projectExclude, string runSettingsExclude); diff --git a/SharedProject/Core/FCCEngine.cs b/SharedProject/Core/FCCEngine.cs index c175b865..3cb3bb8a 100644 --- a/SharedProject/Core/FCCEngine.cs +++ b/SharedProject/Core/FCCEngine.cs @@ -3,6 +3,7 @@ using System.ComponentModel.Composition; using System.Linq; using System.Threading; +using FineCodeCoverage.Core.MsTestPlatform.TestingPlatform; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Cobertura; using FineCodeCoverage.Engine.Model; @@ -17,17 +18,31 @@ namespace FineCodeCoverage.Engine { internal enum ReloadCoverageStatus { Start, Done, Cancelled, Error, Initializing }; - internal class NewCoverageLinesMessage + internal sealed class NewCoverageLinesMessage { - public List CoverageLines { get; set; } + public IFileLineCoverage CoverageLines { get; set; } } - internal class DisplayCoverageResultState + internal class CoverageTaskState { public CancellationTokenSource CancellationTokenSource { get; set; } public Action CleanUp { get; set; } } + internal class ReportResult + { + public IFileLineCoverage FileLineCoverage { get; set; } + public string ProcessedReport { get; set; } + public string HotspotsFile { get; set; } + public string CoberturaFile { get; set; } + } + + class ReportFilesMessage + { + public string HotspotsFile { get; set; } + public string CoberturaFile { get; set; } + } + [Export(typeof(IFCCEngine))] internal class FCCEngine : IFCCEngine,IDisposable { @@ -46,7 +61,6 @@ internal class FCCEngine : IFCCEngine,IDisposable private readonly ILogger logger; private readonly IAppDataFolder appDataFolder; - private IInitializeStatusProvider initializeStatusProvider; private readonly ICoverageToolOutputManager coverageOutputManager; internal System.Threading.Tasks.Task reloadCoverageTask; #pragma warning disable IDE0052 // Remove unread private members @@ -54,6 +68,7 @@ internal class FCCEngine : IFCCEngine,IDisposable #pragma warning restore IDE0052 // Remove unread private members private readonly IEventAggregator eventAggregator; private readonly IDisposeAwareTaskRunner disposeAwareTaskRunner; + private readonly ITUnitCoverageRunner tUnitCoverageRunner; private bool disposed = false; [ImportingConstructor] @@ -69,13 +84,15 @@ public FCCEngine( ISolutionEvents solutionEvents, IAppOptionsProvider appOptionsProvider, IEventAggregator eventAggregator, - IDisposeAwareTaskRunner disposeAwareTaskRunner + IDisposeAwareTaskRunner disposeAwareTaskRunner, + ITUnitCoverageRunner tUnitCoverageRunner ) { this.solutionEvents = solutionEvents; this.eventAggregator = eventAggregator; this.disposeAwareTaskRunner = disposeAwareTaskRunner; - solutionEvents.AfterClosing += (s,args) => ClearOutputWindow(false); + this.tUnitCoverageRunner = tUnitCoverageRunner; + solutionEvents.AfterClosing += (s,args) => ClearUI(false); appOptionsProvider.OptionsChanged += (appOptions) => { if (!appOptions.Enabled) @@ -93,19 +110,14 @@ IDisposeAwareTaskRunner disposeAwareTaskRunner this.msCodeCoverageRunSettingsService = msCodeCoverageRunSettingsService; } - internal string GetLogReloadCoverageStatusMessage(ReloadCoverageStatus reloadCoverageStatus) - { - return $"================================== {reloadCoverageStatus.ToString().ToUpper()} =================================="; - } + private void LogReloadCoverageStatus(ReloadCoverageStatus reloadCoverageStatus) { - logger.Log(GetLogReloadCoverageStatusMessage(reloadCoverageStatus)); + logger.Log(StatusMarkerProvider.Get(reloadCoverageStatus.ToString())); } - public void Initialize(IInitializeStatusProvider initializeStatusProvider, CancellationToken cancellationToken) + public void Initialize(CancellationToken cancellationToken) { - this.initializeStatusProvider = initializeStatusProvider; - appDataFolder.Initialize(cancellationToken); AppDataFolderPath = appDataFolder.DirectoryPath; @@ -113,12 +125,13 @@ public void Initialize(IInitializeStatusProvider initializeStatusProvider, Cance msTestPlatformUtil.Initialize(AppDataFolderPath, cancellationToken); coverageUtilManager.Initialize(AppDataFolderPath, cancellationToken); msCodeCoverageRunSettingsService.Initialize(AppDataFolderPath, this,cancellationToken); + tUnitCoverageRunner.Initialize(AppDataFolderPath, cancellationToken); } - public void ClearUI() + public void ClearUI(bool clearOutputWindowHistory = true) { ClearCoverageLines(); - ClearOutputWindow(true); + ClearOutputWindow(clearOutputWindowHistory); } private void ClearOutputWindow(bool withHistory) @@ -163,7 +176,6 @@ await coverageProject.StepAsync("Run Coverage Tool", async (project) => var coverageTool = coverageUtilManager.CoverageToolName(project); var runCoverToolMessage = $"Run {coverageTool} ({project.ProjectName})"; - logger.Log(runCoverToolMessage); reportGeneratorUtil.LogCoverageProcess(runCoverToolMessage); await coverageUtilManager.RunCoverageAsync(project, vsShutdownLinkedCancellationToken); @@ -202,12 +214,12 @@ private void ClearCoverageLines() RaiseCoverageLines(null); } - private void RaiseCoverageLines(List coverageLines) + private void RaiseCoverageLines(IFileLineCoverage coverageLines) { eventAggregator.SendMessage(new NewCoverageLinesMessage { CoverageLines = coverageLines}); } - private void UpdateUI(List coverageLines, string reportHtml) + private void UpdateUI(IFileLineCoverage coverageLines, string reportHtml) { RaiseCoverageLines(coverageLines); if (reportHtml == null) @@ -217,12 +229,12 @@ private void UpdateUI(List coverageLines, string reportHtml) RaiseUpdateOutputWindow(reportHtml); } - private async System.Threading.Tasks.Task<(List coverageLines,string reportFilePath)> RunAndProcessReportAsync(string[] coverOutputFiles, CancellationToken vsShutdownLinkedCancellationToken) + private async System.Threading.Tasks.Task RunAndProcessReportAsync(string[] coverOutputFiles, CancellationToken vsShutdownLinkedCancellationToken) { var reportOutputFolder = coverageOutputManager.GetReportOutputFolder(); vsShutdownLinkedCancellationToken.ThrowIfCancellationRequested(); var result = await reportGeneratorUtil.GenerateAsync(coverOutputFiles,reportOutputFolder,vsShutdownLinkedCancellationToken); - + vsShutdownLinkedCancellationToken.ThrowIfCancellationRequested(); logger.Log("Processing cobertura"); var coverageLines = coberturaUtil.ProcessCoberturaXml(result.UnifiedXmlFile); @@ -230,7 +242,13 @@ private void UpdateUI(List coverageLines, string reportHtml) vsShutdownLinkedCancellationToken.ThrowIfCancellationRequested(); logger.Log("Processing report"); string processedReport = reportGeneratorUtil.ProcessUnifiedHtml(result.UnifiedHtml, reportOutputFolder); - return (coverageLines, processedReport); + return new ReportResult + { + FileLineCoverage = coverageLines, + HotspotsFile = result.HotspotsFile, + CoberturaFile = result.UnifiedXmlFile, + ProcessedReport = processedReport + }; } private async System.Threading.Tasks.Task PrepareCoverageProjectsAsync(List coverageProjects, CancellationToken cancellationToken) @@ -253,6 +271,7 @@ private async System.Threading.Tasks.Task PrepareCoverageProjectsAsync(List coverageLines, string reportHtml)> t, object state) + private void CoverageTaskCompletion(System.Threading.Tasks.Task t, object state) { - var displayCoverageResultState = (DisplayCoverageResultState)state; + var displayCoverageResultState = (CoverageTaskState)state; if (!IsVsShutdown) { switch (t.Status) @@ -276,16 +295,19 @@ private void DisplayCoverageResult(System.Threading.Tasks.Task<(List { - List coverageLines = null; - string reportHtml = null; + ReportResult reportResult = new ReportResult(); if (coberturaFiles.Any()) { - (coverageLines, reportHtml) = await RunAndProcessReportAsync(coberturaFiles, vsShutdownLinkedCancellationToken); + reportResult = await RunAndProcessReportAsync(coberturaFiles, vsShutdownLinkedCancellationToken); } - return (coverageLines, reportHtml); + return reportResult; }, cleanUp); } private void RunCancellableCoverageTask( - Func, string)>> taskCreator, Action cleanUp) + Func> taskCreator, Action cleanUp) { var vsLinkedCancellationTokenSource = Reset(); var vsShutdownLinkedCancellationToken = vsLinkedCancellationTokenSource.Token; - disposeAwareTaskRunner.RunAsync(() => + disposeAwareTaskRunner.RunAsyncFunc(() => { reloadCoverageTask = System.Threading.Tasks.Task.Run(async () => { - await PollInitializedStatusAsync(vsShutdownLinkedCancellationToken); var result = await taskCreator(vsShutdownLinkedCancellationToken); return result; }, vsShutdownLinkedCancellationToken) - .ContinueWith(DisplayCoverageResult, new DisplayCoverageResultState { CancellationTokenSource = vsLinkedCancellationTokenSource, CleanUp = cleanUp}, System.Threading.Tasks.TaskScheduler.Default); + .ContinueWith(CoverageTaskCompletion, new CoverageTaskState { CancellationTokenSource = vsLinkedCancellationTokenSource, CleanUp = cleanUp}, System.Threading.Tasks.TaskScheduler.Default); return reloadCoverageTask; }); } @@ -355,13 +361,7 @@ public void ReloadCoverage(Func { - List coverageLines = null; - string reportHtml = null; - - await PollInitializedStatusAsync(vsShutdownLinkedCancellationToken); - - reportGeneratorUtil.LogCoverageProcess("Starting coverage - full details in FCC Output Pane"); - LogReloadCoverageStatus(ReloadCoverageStatus.Start); + ReportResult reportResult = new ReportResult(); var coverageProjects = await coverageRequestCallback(); vsShutdownLinkedCancellationToken.ThrowIfCancellationRequested(); @@ -372,10 +372,10 @@ public 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/FirstTimeToolWindowOpener.cs b/SharedProject/Core/Initialization/FirstTimeToolWindowOpener.cs new file mode 100644 index 00000000..d7bb6c3e --- /dev/null +++ b/SharedProject/Core/Initialization/FirstTimeToolWindowOpener.cs @@ -0,0 +1,39 @@ +using FineCodeCoverage.Core.Utilities; +using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.Initialization +{ + [Export(typeof(IFirstTimeToolWindowOpener))] + internal class FirstTimeToolWindowOpener : IFirstTimeToolWindowOpener + { + private readonly IInitializedFromTestContainerDiscoverer initializedFromTestContainerDiscoverer; + private readonly IShownToolWindowHistory shownToolWindowHistory; + private readonly IToolWindowOpener toolWindowOpener; + + [ImportingConstructor] + public FirstTimeToolWindowOpener( + IInitializedFromTestContainerDiscoverer initializedFromTestContainerDiscoverer, + IShownToolWindowHistory shownToolWindowHistory, + IToolWindowOpener toolWindowOpener + ) + { + this.initializedFromTestContainerDiscoverer = initializedFromTestContainerDiscoverer; + this.shownToolWindowHistory = shownToolWindowHistory; + this.toolWindowOpener = toolWindowOpener; + } + + public async Task OpenIfFirstTimeAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if ( + initializedFromTestContainerDiscoverer.InitializedFromTestContainerDiscoverer && + !shownToolWindowHistory.HasShownToolWindow + ) + { + await toolWindowOpener.OpenToolWindowAsync(); + } + } + } +} diff --git a/SharedProject/Core/Initialization/IFirstTimeToolWindowOpener.cs b/SharedProject/Core/Initialization/IFirstTimeToolWindowOpener.cs new file mode 100644 index 00000000..bfef9922 --- /dev/null +++ b/SharedProject/Core/Initialization/IFirstTimeToolWindowOpener.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.Initialization +{ + internal interface IFirstTimeToolWindowOpener + { + Task OpenIfFirstTimeAsync(CancellationToken cancellationToken); + } +} diff --git a/SharedProject/Impl/TestContainerDiscovery/IInitializeStatusProvider.cs b/SharedProject/Core/Initialization/IInitializeStatusProvider.cs similarity index 78% rename from SharedProject/Impl/TestContainerDiscovery/IInitializeStatusProvider.cs rename to SharedProject/Core/Initialization/IInitializeStatusProvider.cs index 6cc153de..a6d80413 100644 --- a/SharedProject/Impl/TestContainerDiscovery/IInitializeStatusProvider.cs +++ b/SharedProject/Core/Initialization/IInitializeStatusProvider.cs @@ -1,4 +1,4 @@ -namespace FineCodeCoverage.Impl +namespace FineCodeCoverage.Core.Initialization { internal interface IInitializeStatusProvider { diff --git a/SharedProject/Core/Initialization/IInitializedFromTestContainerDiscoverer.cs b/SharedProject/Core/Initialization/IInitializedFromTestContainerDiscoverer.cs new file mode 100644 index 00000000..6241a56a --- /dev/null +++ b/SharedProject/Core/Initialization/IInitializedFromTestContainerDiscoverer.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Core.Initialization +{ + internal interface IInitializedFromTestContainerDiscoverer + { + bool InitializedFromTestContainerDiscoverer { get; } + } +} diff --git a/SharedProject/Impl/TestContainerDiscovery/IInitializer.cs b/SharedProject/Core/Initialization/IInitializer.cs similarity index 81% rename from SharedProject/Impl/TestContainerDiscovery/IInitializer.cs rename to SharedProject/Core/Initialization/IInitializer.cs index 3ed5fcab..67e55b5e 100644 --- a/SharedProject/Impl/TestContainerDiscovery/IInitializer.cs +++ b/SharedProject/Core/Initialization/IInitializer.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace FineCodeCoverage.Impl +namespace FineCodeCoverage.Core.Initialization { internal interface IInitializer : IInitializeStatusProvider { diff --git a/SharedProject/Core/Initialization/IPackageLoader.cs b/SharedProject/Core/Initialization/IPackageLoader.cs new file mode 100644 index 00000000..d0c7e024 --- /dev/null +++ b/SharedProject/Core/Initialization/IPackageLoader.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.Initialization +{ + internal interface IPackageLoader + { + Task LoadPackageAsync(System.Threading.CancellationToken cancellationToken); + } + +} + diff --git a/SharedProject/Impl/TestContainerDiscovery/InitializeStatus.cs b/SharedProject/Core/Initialization/InitializeStatus.cs similarity index 59% rename from SharedProject/Impl/TestContainerDiscovery/InitializeStatus.cs rename to SharedProject/Core/Initialization/InitializeStatus.cs index 5df01278..bce17166 100644 --- a/SharedProject/Impl/TestContainerDiscovery/InitializeStatus.cs +++ b/SharedProject/Core/Initialization/InitializeStatus.cs @@ -1,6 +1,5 @@ -namespace FineCodeCoverage.Impl +namespace FineCodeCoverage.Core.Initialization { internal enum InitializeStatus { Initializing, Initialized, Error }; -} - +} \ No newline at end of file diff --git a/SharedProject/Impl/TestContainerDiscovery/Initializer.cs b/SharedProject/Core/Initialization/Initializer.cs similarity index 72% rename from SharedProject/Impl/TestContainerDiscovery/Initializer.cs rename to SharedProject/Core/Initialization/Initializer.cs index 4a76010a..82ef4d9f 100644 --- a/SharedProject/Impl/TestContainerDiscovery/Initializer.cs +++ b/SharedProject/Core/Initialization/Initializer.cs @@ -3,17 +3,19 @@ using System.Threading; using System.Threading.Tasks; using FineCodeCoverage.Engine; -using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Output; -namespace FineCodeCoverage.Impl +namespace FineCodeCoverage.Core.Initialization { + interface IInitializable { } + [Export(typeof(IInitializer))] + [Export(typeof(IInitializeStatusProvider))] internal class Initializer : IInitializer { private readonly IFCCEngine fccEngine; private readonly ILogger logger; - private readonly ICoverageProjectFactory coverageProjectFactory; - private readonly IPackageInitializer packageInitializer; + private readonly IFirstTimeToolWindowOpener firstTimeToolWindowOpener; public InitializeStatus InitializeStatus { get; set; } = InitializeStatus.Initializing; public string InitializeExceptionMessage { get; set; } @@ -22,14 +24,14 @@ internal class Initializer : IInitializer public Initializer( IFCCEngine fccEngine, ILogger logger, - ICoverageProjectFactory coverageProjectFactory, - IPackageInitializer packageInitializer + IFirstTimeToolWindowOpener firstTimeToolWindowOpener, + [ImportMany] + IInitializable[] initializables ) { this.fccEngine = fccEngine; this.logger = logger; - this.coverageProjectFactory = coverageProjectFactory; - this.packageInitializer = packageInitializer; + this.firstTimeToolWindowOpener = firstTimeToolWindowOpener; } public async Task InitializeAsync(CancellationToken cancellationToken) { @@ -39,13 +41,14 @@ public async Task InitializeAsync(CancellationToken cancellationToken) logger.Log($"Initializing"); cancellationToken.ThrowIfCancellationRequested(); - coverageProjectFactory.Initialize(); - fccEngine.Initialize(this, cancellationToken); - await packageInitializer.InitializeAsync(cancellationToken); + fccEngine.Initialize(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); logger.Log($"Initialized"); + + + await firstTimeToolWindowOpener.OpenIfFirstTimeAsync(cancellationToken); } catch (Exception exception) { diff --git a/SharedProject/Core/Initialization/PackageLoader.cs b/SharedProject/Core/Initialization/PackageLoader.cs new file mode 100644 index 00000000..c6d0d3f6 --- /dev/null +++ b/SharedProject/Core/Initialization/PackageLoader.cs @@ -0,0 +1,68 @@ +using System; +using System.ComponentModel.Composition; +using System.Threading; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Task = System.Threading.Tasks.Task; + +namespace FineCodeCoverage.Core.Initialization +{ + internal interface IShellPackageLoader + { + Task LoadPackageAsync(); + } + + [Export(typeof(IShellPackageLoader))] + internal class ShellPackageLoader : IShellPackageLoader + { + private readonly IServiceProvider serviceProvider; + + [ImportingConstructor] + public ShellPackageLoader( + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider + ) + { + this.serviceProvider = serviceProvider; + } + public async Task LoadPackageAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (serviceProvider.GetService(typeof(SVsShell)) is IVsShell shell) + { + var packageToBeLoadedGuid = PackageGuids.guidOutputToolWindowPackage; + shell.LoadPackage(ref packageToBeLoadedGuid, out var _); + } + } + + } + + [Export(typeof(IPackageLoader))] + [Export(typeof(IInitializedFromTestContainerDiscoverer))] + internal class PackageLoader : IPackageLoader, IInitializedFromTestContainerDiscoverer + { + private readonly IShellPackageLoader shellPackageLoader; + + public bool InitializedFromTestContainerDiscoverer { get; private set; } + + [ImportingConstructor] + public PackageLoader( + IShellPackageLoader shellPackageLoader + ) + { + this.shellPackageLoader = shellPackageLoader; + } + + public async Task LoadPackageAsync(CancellationToken cancellationToken) + { + InitializedFromTestContainerDiscoverer = true; + cancellationToken.ThrowIfCancellationRequested(); + await shellPackageLoader.LoadPackageAsync(); + + } + } +} + + + diff --git a/SharedProject/Core/Model/CoverageLine.cs b/SharedProject/Core/Model/CoverageLine.cs deleted file mode 100644 index 3df41b74..00000000 --- a/SharedProject/Core/Model/CoverageLine.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FineCodeCoverage.Engine.Cobertura; - -namespace FineCodeCoverage.Engine.Model -{ - internal class CoverageLine - { - public Package Package { get; internal set; } - public Class Class { get; internal set; } - public Line Line { get; internal set; } - } -} diff --git a/SharedProject/Core/Model/CoverageProject.cs b/SharedProject/Core/Model/CoverageProject.cs index 9ca483da..cdc025b3 100644 --- a/SharedProject/Core/Model/CoverageProject.cs +++ b/SharedProject/Core/Model/CoverageProject.cs @@ -5,15 +5,10 @@ using System.Threading.Tasks; using Task = System.Threading.Tasks.Task; using System.Xml.Linq; -using System.Xml.XPath; -using EnvDTE; -using FineCodeCoverage.Core.Model; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.FileSynchronization; using FineCodeCoverage.Options; using Microsoft.VisualStudio.Shell; -using Microsoft.CodeAnalysis.MSBuild; -using EnvDTE80; using System.Threading; namespace FineCodeCoverage.Engine.Model @@ -22,16 +17,15 @@ internal class CoverageProject : ICoverageProject { private readonly IAppOptionsProvider appOptionsProvider; private readonly IFileSynchronizationUtil fileSynchronizationUtil; - private readonly ILogger logger; - private readonly DTE2 dte; private readonly ICoverageProjectSettingsManager settingsManager; - private readonly bool canUseMsBuildWorkspace; + private readonly IReferencedProjectsHelper referencedProjectsHelper; private XElement projectFileXElement; private IAppOptions settings; private string targetFramework; private readonly string fccFolderName = "fine-code-coverage"; private readonly string buildOutputFolderName = "build-output"; private string buildOutputPath; + private bool? isDotNetSdkStyle; private string BuildOutputPath { get @@ -62,100 +56,62 @@ private string BuildOutputPath public CoverageProject( IAppOptionsProvider appOptionsProvider, IFileSynchronizationUtil fileSynchronizationUtil, - ILogger logger, - DTE2 dte, ICoverageProjectSettingsManager settingsManager, - bool canUseMsBuildWorkspace) + IReferencedProjectsHelper referencedProjectsHelper) { this.appOptionsProvider = appOptionsProvider; this.fileSynchronizationUtil = fileSynchronizationUtil; - this.logger = logger; - this.dte = dte; this.settingsManager = settingsManager; - this.canUseMsBuildWorkspace = canUseMsBuildWorkspace; + this.referencedProjectsHelper = referencedProjectsHelper; } public string FCCOutputFolder => Path.Combine(ProjectOutputFolder, fccFolderName); public bool IsDotNetSdkStyle() { - return ProjectFileXElement + if (isDotNetSdkStyle.HasValue) + { + return isDotNetSdkStyle.Value; + } + + isDotNetSdkStyle = ProjectFileXElement .DescendantsAndSelf() .Where(x => { //https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk?view=vs-2019 + return IsRootProjectElementWithSdkAttribute(x) || + IsRootProjectElementSdkElementChild(x) || + IsRootImportElementWithSdkAttribute(x); + }) + .Any(); - /* - - ... - - - ... - - */ - if - ( - x?.Name?.LocalName?.Equals("Project", StringComparison.OrdinalIgnoreCase) == true && - x?.Parent == null - ) - { - var sdkAttr = x?.Attributes()?.FirstOrDefault(attr => attr?.Name?.LocalName?.Equals("Sdk", StringComparison.OrdinalIgnoreCase) == true); - - if (sdkAttr?.Value?.Trim()?.StartsWith("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase) == true) - { - return true; - } - } + return isDotNetSdkStyle.Value; - /* - - - ... - - */ - if - ( - x?.Name?.LocalName?.Equals("Sdk", StringComparison.OrdinalIgnoreCase) == true && - x?.Parent?.Name?.LocalName?.Equals("Project", StringComparison.OrdinalIgnoreCase) == true && - x?.Parent?.Parent == null - ) - { - var nameAttr = x?.Attributes()?.FirstOrDefault(attr => attr?.Name?.LocalName?.Equals("Name", StringComparison.OrdinalIgnoreCase) == true); + bool HasSdkAttribute(XElement x) + { + return x?.Attributes()?.FirstOrDefault(attr => attr?.Name?.LocalName?.Equals("Sdk", StringComparison.OrdinalIgnoreCase) == true) != null; + } - if (nameAttr?.Value?.Trim()?.StartsWith("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase) == true) - { - return true; - } - } + bool IsRootProjectElementWithSdkAttribute(XElement x) + { + return x?.Name?.LocalName?.Equals("Project", StringComparison.OrdinalIgnoreCase) == true && + x?.Parent == null && HasSdkAttribute(x); + } - /* - - - Value - - - ... - - - */ - if - ( - x?.Name?.LocalName?.Equals("Import", StringComparison.OrdinalIgnoreCase) == true && + bool IsRootProjectElementSdkElementChild(XElement x) + { + return x?.Name?.LocalName?.Equals("Sdk", StringComparison.OrdinalIgnoreCase) == true && x?.Parent?.Name?.LocalName?.Equals("Project", StringComparison.OrdinalIgnoreCase) == true && - x?.Parent?.Parent == null - ) - { - var sdkAttr = x?.Attributes()?.FirstOrDefault(attr => attr?.Name?.LocalName?.Equals("Sdk", StringComparison.OrdinalIgnoreCase) == true); - - if (sdkAttr?.Value?.Trim()?.StartsWith("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase) == true) - { - return true; - } - } + x?.Parent?.Parent == null; + } - return false; - }) - .Any(); + bool IsRootImportElementWithSdkAttribute(XElement x) + { + return x?.Name?.LocalName?.Equals("Import", StringComparison.OrdinalIgnoreCase) == true && + x?.Parent?.Name?.LocalName?.Equals("Project", StringComparison.OrdinalIgnoreCase) == true && + x?.Parent?.Parent == null && HasSdkAttribute(x); + } } + public string TestDllFile { get; set; } public string ProjectOutputFolder => Path.GetDirectoryName(TestDllFile); public string FailureDescription { get; set; } @@ -172,10 +128,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; } @@ -195,8 +153,8 @@ public XElement ProjectFileXElement } } - public List ExcludedReferencedProjects { get; } = new List(); - public List IncludedReferencedProjects { get; set; } = new List(); + public List ExcludedReferencedProjects { get; } = new List(); + public List IncludedReferencedProjects { get; set; } = new List(); public bool Is64Bit { get; set; } public string RunSettingsFile { get; set; } public bool IsDotNetFramework { get; private set; } @@ -265,156 +223,30 @@ public async Task PrepareForCoverageA private async Task SetIncludedExcludedReferencedProjectsAsync() { - List referencedProjects = await GetReferencedProjectsAsync(); + var referencedProjects = await referencedProjectsHelper.GetReferencedProjectsAsync(ProjectFile,() => ProjectFileXElement); SetExcludedReferencedProjects(referencedProjects); SetIncludedReferencedProjects(referencedProjects); } - private void SetIncludedReferencedProjects(List referencedProjects) + private void SetIncludedReferencedProjects(List referencedProjects) { if (Settings.IncludeReferencedProjects) { - IncludedReferencedProjects = referencedProjects.Select(referencedProject => referencedProject.AssemblyName).ToList(); + IncludedReferencedProjects = new List(referencedProjects); } } - private void SetExcludedReferencedProjects(List referencedProjects) + private void SetExcludedReferencedProjects(List referencedProjects) { foreach (var referencedProject in referencedProjects) { if (referencedProject.ExcludeFromCodeCoverage) { - ExcludedReferencedProjects.Add(referencedProject.AssemblyName); + ExcludedReferencedProjects.Add(referencedProject); } } } - private async Task> GetReferencedProjectsAsync() - { - List referencedProjects = await SafeGetReferencedProjectsFromDteAsync(); - - if (referencedProjects == null) - { - referencedProjects = await GetReferencedProjectsFromProjectFileAsync(); - } - return referencedProjects; - } - - private async Task> SafeGetReferencedProjectsFromDteAsync() - { - try - { - return await GetReferencedProjectsFromDteAsync(); - } - catch (Exception) { } - return null; - } - - private async Task> GetReferencedProjectsFromDteAsync() - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - var project = dte.Solution.Projects.Cast().FirstOrDefault(p => - { - ThreadHelper.ThrowIfNotOnUIThread(); - //have to try here as unloaded projects will throw - var projectFullName = ""; - try - { - projectFullName = p.FullName; - } - catch { } - return projectFullName == ProjectFile; - }); - - if (project == null) - { - return null; - } - - var vsproject = project.Object as VSLangProj.VSProject; - return vsproject.References.Cast().Where(r => r.SourceProject != null).Select(r => - { - ThreadHelper.ThrowIfNotOnUIThread(); - var assemblyName = Path.GetFileNameWithoutExtension(r.Path); - return new ReferencedProject(r.SourceProject.FullName, assemblyName); - }).ToList(); - - } - - private async Task> SafeGetReferencedProjectsWithDesignTimeBuildAsync() - { - try - { - return await GetReferencedProjectsWithDesignTimeBuildWorkerAsync(); - } - catch (Exception exception) - { - logger.Log("Unable to get referenced projects with design time build", exception); - } - return new List(); - } - - private async Task> GetReferencedProjectsWithDesignTimeBuildWorkerAsync() - { - var msBuildWorkspace = MSBuildWorkspace.Create(); - var project = await msBuildWorkspace.OpenProjectAsync(ProjectFile); - var solution = msBuildWorkspace.CurrentSolution; - return project.ProjectReferences.Select( - pr => solution.Projects.First(p => p.Id == pr.ProjectId).FilePath) - .Where(path => path != null) - .Select(path => new ReferencedProject(path)).ToList(); - } - - private async Task> GetReferencedProjectsFromProjectFileAsync() - { - /* - - - - - */ - - - var xprojectReferences = ProjectFileXElement.XPathSelectElements($"/ItemGroup/ProjectReference[@Include]"); - var requiresDesignTimeBuild = false; - List referencedProjectFiles = new List(); - foreach (var xprojectReference in xprojectReferences) - { - var referencedProjectProjectFile = xprojectReference.Attribute("Include").Value; - if (referencedProjectProjectFile.Contains("$(")) - { - if (canUseMsBuildWorkspace) - { - requiresDesignTimeBuild = true; - break; - } - else - { - logger.Log($"Cannot exclude referenced project {referencedProjectProjectFile} of {ProjectFile} with {ReferencedProject.excludeFromCodeCoveragePropertyName}. Cannot use MSBuildWorkspace"); - } - - } - else - { - if (!Path.IsPathRooted(referencedProjectProjectFile)) - { - referencedProjectProjectFile = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(ProjectFile), referencedProjectProjectFile)); - } - referencedProjectFiles.Add(referencedProjectProjectFile); - } - - } - - if (requiresDesignTimeBuild) - { - var referencedProjects = await SafeGetReferencedProjectsWithDesignTimeBuildAsync(); - return referencedProjects; - - } - - return referencedProjectFiles.Select(referencedProjectProjectFile => new ReferencedProject(referencedProjectProjectFile)).ToList(); - } - private void EnsureDirectories() { EnsureFccDirectory(); diff --git a/SharedProject/Core/Model/CoverageProjectFactory.cs b/SharedProject/Core/Model/CoverageProjectFactory.cs index 6d958cdd..f0b1eb77 100644 --- a/SharedProject/Core/Model/CoverageProjectFactory.cs +++ b/SharedProject/Core/Model/CoverageProjectFactory.cs @@ -1,13 +1,6 @@ -using System; -using System.ComponentModel.Composition; -using System.Threading.Tasks; -using EnvDTE; -using EnvDTE80; +using System.ComponentModel.Composition; using FineCodeCoverage.Engine.FileSynchronization; using FineCodeCoverage.Options; -using Microsoft.Build.Locator; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Threading; namespace FineCodeCoverage.Engine.Model { @@ -16,54 +9,30 @@ internal class CoverageProjectFactory : ICoverageProjectFactory { private readonly IAppOptionsProvider appOptionsProvider; private readonly IFileSynchronizationUtil fileSynchronizationUtil; - private readonly ILogger logger; private readonly ICoverageProjectSettingsManager coverageProjectSettingsManager; - private bool canUseMsBuildWorkspace = true; - private readonly AsyncLazy lazyDTE2; + private readonly IReferencedProjectsHelper referencedProjectsHelper; [ImportingConstructor] public CoverageProjectFactory( IAppOptionsProvider appOptionsProvider, IFileSynchronizationUtil fileSynchronizationUtil, - ILogger logger, ICoverageProjectSettingsManager coverageProjectSettingsManager, - [Import(typeof(SVsServiceProvider))] - IServiceProvider serviceProvider) + IReferencedProjectsHelper referencedProjectsHelper + ) { this.appOptionsProvider = appOptionsProvider; this.fileSynchronizationUtil = fileSynchronizationUtil; - this.logger = logger; this.coverageProjectSettingsManager = coverageProjectSettingsManager; - - lazyDTE2 = new AsyncLazy(async () => - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - return (DTE2)serviceProvider.GetService(typeof(DTE)); - },ThreadHelper.JoinableTaskFactory); + this.referencedProjectsHelper = referencedProjectsHelper; } - public void Initialize() - { - try - { - MSBuildLocator.RegisterDefaults(); - } - catch - { - canUseMsBuildWorkspace = false; - } - } - public async Task CreateAsync() + public ICoverageProject Create() { - var dte2 = await lazyDTE2.GetValueAsync(); - return new CoverageProject( appOptionsProvider, fileSynchronizationUtil, - logger, - dte2, coverageProjectSettingsManager, - canUseMsBuildWorkspace); + referencedProjectsHelper); } } } diff --git a/SharedProject/Core/Model/CoverageProjectSettingsManager.cs b/SharedProject/Core/Model/CoverageProjectSettingsManager.cs index e744f7a4..cb7dff8c 100644 --- a/SharedProject/Core/Model/CoverageProjectSettingsManager.cs +++ b/SharedProject/Core/Model/CoverageProjectSettingsManager.cs @@ -1,6 +1,9 @@ using FineCodeCoverage.Options; +using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace FineCodeCoverage.Engine.Model @@ -32,7 +35,45 @@ public async Task GetSettingsAsync(ICoverageProject coverageProject var projectDirectory = Path.GetDirectoryName(coverageProject.ProjectFile); var settingsFilesElements = fccSettingsFilesProvider.Provide(projectDirectory); var projectSettingsElement = await coverageProjectSettingsProvider.ProvideAsync(coverageProject); - return settingsMerger.Merge(appOptionsProvider.Get(), settingsFilesElements, projectSettingsElement); + var merged = settingsMerger.Merge(appOptionsProvider.Get(), settingsFilesElements, projectSettingsElement); + AddCommonAssemblyExcludesIncludes(merged); + return merged; + } + + private void AddCommonAssemblyExcludesIncludes(IAppOptions appOptions) + { + var (newOldStyleExclude,newMsExclude) = AddCommon(appOptions.Exclude, appOptions.ModulePathsExclude, appOptions.ExcludeAssemblies); + var (newOldStyleInclude,newMsInclude) = AddCommon(appOptions.Include, appOptions.ModulePathsInclude, appOptions.IncludeAssemblies); + appOptions.Exclude = newOldStyleExclude; + appOptions.Include = newOldStyleInclude; + appOptions.ModulePathsExclude = newMsExclude; + appOptions.ModulePathsInclude = newMsInclude; + } + + private (string[] newOldStyle,string[] newMs) AddCommon(string[] oldStyle,string[] ms, string[] common ) + { + if(common == null) + { + return(oldStyle,ms); + } + var newMs = ListFromExisting(ms); + var newOldStyle = ListFromExisting(oldStyle); + + var nonWhitespaceCommon = common.Where(c => !string.IsNullOrWhiteSpace(c)); + foreach(var assemblyFileName in nonWhitespaceCommon) + { + var msModulePath = $".*\\{assemblyFileName}.dll$"; + newMs.Add(msModulePath); + var old = $"[{assemblyFileName}]*"; + newOldStyle.Add(old); + } + + return (newOldStyle.ToArray(), newMs.ToArray()); + } + + private List ListFromExisting(string[] existing) + { + return new List(existing ?? new string[0]); } } 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/ICoverageProject.cs b/SharedProject/Core/Model/ICoverageProject.cs index 67bad4cc..01821f3d 100644 --- a/SharedProject/Core/Model/ICoverageProject.cs +++ b/SharedProject/Core/Model/ICoverageProject.cs @@ -12,8 +12,8 @@ internal interface ICoverageProject string CoverageOutputFile { get; } string CoverageOutputFolder { get; set; } string DefaultCoverageOutputFolder { get; } - List ExcludedReferencedProjects { get; } - List IncludedReferencedProjects { get; } + List ExcludedReferencedProjects { get; } + List IncludedReferencedProjects { get; } string FailureDescription { get; set; } string FailureStage { get; set; } bool HasFailed { get; } diff --git a/SharedProject/Core/Model/ICoverageProjectFactory.cs b/SharedProject/Core/Model/ICoverageProjectFactory.cs index ef54b62c..a9328ccb 100644 --- a/SharedProject/Core/Model/ICoverageProjectFactory.cs +++ b/SharedProject/Core/Model/ICoverageProjectFactory.cs @@ -1,10 +1,7 @@ -using System.Threading.Tasks; - -namespace FineCodeCoverage.Engine.Model +namespace FineCodeCoverage.Engine.Model { internal interface ICoverageProjectFactory { - Task CreateAsync(); - void Initialize(); + ICoverageProject Create(); } } diff --git a/SharedProject/Core/Model/ReferencedProjects/CPPReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/CPPReferencedProjectsHelper.cs new file mode 100644 index 00000000..35ea4f7d --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/CPPReferencedProjectsHelper.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.VCProjectEngine; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Engine.Model +{ + [Export(typeof(ICPPReferencedProjectsHelper))] + internal class CPPReferencedProjectsHelper : ICPPReferencedProjectsHelper + { + private VCProject GetReferencedVCProject(VCProjectReference projectReference) + { + ThreadHelper.ThrowIfNotOnUIThread(); + return projectReference.ReferencedProject as VCProject + ?? (projectReference.ReferencedProject as EnvDTE.Project)?.Object as VCProject; + } + + private bool? IsDll(VCProject vcProject) + { + if (!(vcProject.Configurations is IEnumerable configurations)) + return null; + + var configuration = configurations.Cast().FirstOrDefault(); + if (configuration == null) + return null; + + bool isDll = configuration.ConfigurationType == ConfigurationTypes.typeDynamicLibrary; + bool isApplication = configuration.ConfigurationType == ConfigurationTypes.typeApplication; + if (!isDll && !isApplication) + return null; + return isDll; + } + + private string GetCPPProjectReferenceProjectFilePath(VCProjectReference reference) + { + ThreadHelper.ThrowIfNotOnUIThread(); + var vsReference = reference.Reference as VSLangProj.Reference; + var sourceProject = vsReference.SourceProject; + return sourceProject.FileName; + } + + public async Task> GetInstrumentableReferencedProjectsAsync(VCProject cppProject) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (!(cppProject.VCReferences is IEnumerable vcReferences)) + return null; + + return vcReferences + .OfType() + .Select(reference => + { + var referencedProject = GetReferencedVCProject(reference); + + var isDll = IsDll(referencedProject); + return isDll.HasValue ?(IExcludableReferencedProject) new ReferencedProject( + GetCPPProjectReferenceProjectFilePath(reference), + Path.GetFileNameWithoutExtension(reference.FullPath), + isDll.Value + ) + : null; + }) + .Where(p => p != null) + .ToList(); + } + + } + +} diff --git a/SharedProject/Core/Model/ReferencedProjects/DotNetReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/DotNetReferencedProjectsHelper.cs new file mode 100644 index 00000000..18933374 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/DotNetReferencedProjectsHelper.cs @@ -0,0 +1,52 @@ +using EnvDTE; +using Microsoft.VisualStudio.Shell; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading.Tasks; +using VSLangProj; +using VSLangProj80; + +namespace FineCodeCoverage.Engine.Model +{ + [Export(typeof(IDotNetReferencedProjectsHelper))] + internal class DotNetReferencedProjectsHelper : IDotNetReferencedProjectsHelper + { + public async Task> GetReferencedProjectsAsync(VSProject vsProject) + { + var referencedProjects = (await System.Threading.Tasks.Task.WhenAll(GetReferencedSourceProjects(vsProject).Select(GetReferencedProjectAsync))).ToList(); + return new List(referencedProjects); + } + + private IEnumerable GetReferencedSourceProjects(VSProject vsproject) + { + return vsproject.References.Cast().Where(r => r.SourceProject != null) + .Select(r => r.SourceProject); + } + + private async Task GetReferencedProjectAsync(Project project) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var (assemblyName, isDll) = await GetAssemblyNameIsDllAsync(project); + return new ReferencedProject(project.FullName, assemblyName, isDll); + } + + private async Task<(string, bool)> GetAssemblyNameIsDllAsync(Project project) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var assemblyNameProperty = project.Properties.Item(nameof(ProjectProperties3.AssemblyName)); + var assemblyName = assemblyNameProperty?.Value.ToString() ?? project.Name; + var outputTypeProperty = project.Properties.Item(nameof(ProjectProperties3.OutputType)); + var isDll = true; + if (outputTypeProperty != null) + { + prjOutputType po = (prjOutputType)Enum.Parse(typeof(prjOutputType), outputTypeProperty.Value.ToString()); + isDll = po == prjOutputType.prjOutputTypeLibrary; + } + + return (assemblyName, isDll); + } + } + +} diff --git a/SharedProject/Core/Model/ReferencedProjects/ICPPReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/ICPPReferencedProjectsHelper.cs new file mode 100644 index 00000000..d2020811 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/ICPPReferencedProjectsHelper.cs @@ -0,0 +1,12 @@ +using Microsoft.VisualStudio.VCProjectEngine; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Engine.Model +{ + internal interface ICPPReferencedProjectsHelper + { + Task> GetInstrumentableReferencedProjectsAsync(VCProject cppProject); + } + +} diff --git a/SharedProject/Core/Model/ReferencedProjects/IDotNetReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/IDotNetReferencedProjectsHelper.cs new file mode 100644 index 00000000..fe7e57af --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/IDotNetReferencedProjectsHelper.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VSLangProj; + +namespace FineCodeCoverage.Engine.Model +{ + internal interface IDotNetReferencedProjectsHelper + { + Task> GetReferencedProjectsAsync(VSProject vsProject); + } +} diff --git a/SharedProject/Core/Model/ReferencedProjects/IExcludableReferencedProject.cs b/SharedProject/Core/Model/ReferencedProjects/IExcludableReferencedProject.cs new file mode 100644 index 00000000..6bd33618 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/IExcludableReferencedProject.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Engine.Model +{ + interface IExcludableReferencedProject : IReferencedProject + { + bool ExcludeFromCodeCoverage { get; } + } +} diff --git a/SharedProject/Core/Model/ReferencedProjects/IProjectFileReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/IProjectFileReferencedProjectsHelper.cs new file mode 100644 index 00000000..068d4c84 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/IProjectFileReferencedProjectsHelper.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace FineCodeCoverage.Engine.Model +{ + interface IProjectFileReferencedProjectsHelper + { + List GetReferencedProjects(string projectFile, XElement projectFileXElement); + } +} diff --git a/SharedProject/Core/Model/ReferencedProjects/IReferencedProject.cs b/SharedProject/Core/Model/ReferencedProjects/IReferencedProject.cs new file mode 100644 index 00000000..42172682 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/IReferencedProject.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Engine.Model +{ + internal interface IReferencedProject + { + string AssemblyName { get; } + bool IsDll { get; } + } +} diff --git a/SharedProject/Core/Model/ReferencedProjects/IReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/IReferencedProjectsHelper.cs new file mode 100644 index 00000000..446b6902 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/IReferencedProjectsHelper.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace FineCodeCoverage.Engine.Model +{ + internal interface IReferencedProjectsHelper + { + + // todo - should not need ms build workspaces or parsing the project file + Task> GetReferencedProjectsAsync( + string projectFile, Func projectFileXElementProvider); + } +} diff --git a/SharedProject/Core/Model/ReferencedProjects/IVsApiReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/IVsApiReferencedProjectsHelper.cs new file mode 100644 index 00000000..5f894195 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/IVsApiReferencedProjectsHelper.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Engine.Model +{ + internal interface IVsApiReferencedProjectsHelper + { + Task> GetReferencedProjectsAsync(string projectFile); + } +} diff --git a/SharedProject/Core/Model/ReferencedProjects/ProjectFileReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/ProjectFileReferencedProjectsHelper.cs new file mode 100644 index 00000000..281bfa40 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/ProjectFileReferencedProjectsHelper.cs @@ -0,0 +1,65 @@ +using FineCodeCoverage.Output; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace FineCodeCoverage.Engine.Model +{ + // todo - remove this ? Should not be necessary + [Export(typeof(IProjectFileReferencedProjectsHelper))] + internal class ProjectFileReferencedProjectsHelper : IProjectFileReferencedProjectsHelper + + { + private readonly ILogger logger; + + [ImportingConstructor] + public ProjectFileReferencedProjectsHelper(ILogger logger) + { + this.logger = logger; + } + + public List GetReferencedProjects( + string projectFile, XElement projectFileXElement + ) + { + /* + + + + + */ + + var xprojectReferences = projectFileXElement.XPathSelectElements($"/ItemGroup/ProjectReference[@Include]"); + var requiresDesignTimeBuild = false; + List referencedProjectFiles = new List(); + foreach (var xprojectReference in xprojectReferences) + { + var referencedProjectProjectFile = xprojectReference.Attribute("Include").Value; + if (referencedProjectProjectFile.Contains("$(")) + { + logger.Log($"Cannot exclude referenced project {referencedProjectProjectFile} of {projectFile} with {ReferencedProject.excludeFromCodeCoveragePropertyName}. Cannot use MSBuildWorkspace"); + } + else + { + if (!Path.IsPathRooted(referencedProjectProjectFile)) + { + referencedProjectProjectFile = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectFile), referencedProjectProjectFile)); + } + referencedProjectFiles.Add(referencedProjectProjectFile); + } + + } + + if (requiresDesignTimeBuild) + { + return new List(); + + } + + return referencedProjectFiles.Select(referencedProjectProjectFile => (IExcludableReferencedProject)new ReferencedProject(referencedProjectProjectFile)).ToList(); + } + } +} diff --git a/SharedProject/Core/Model/ReferencedProject.cs b/SharedProject/Core/Model/ReferencedProjects/ReferencedProject.cs similarity index 68% rename from SharedProject/Core/Model/ReferencedProject.cs rename to SharedProject/Core/Model/ReferencedProjects/ReferencedProject.cs index 882e3010..32eaeb74 100644 --- a/SharedProject/Core/Model/ReferencedProject.cs +++ b/SharedProject/Core/Model/ReferencedProjects/ReferencedProject.cs @@ -3,18 +3,21 @@ using System.Xml.XPath; using FineCodeCoverage.Core.Utilities; -namespace FineCodeCoverage.Core.Model +namespace FineCodeCoverage.Engine.Model { - internal class ReferencedProject - { + internal class ReferencedProject : IExcludableReferencedProject + { internal const string excludeFromCodeCoveragePropertyName = "FCCExcludeFromCodeCoverage"; private readonly string projectPath; + - public ReferencedProject(string projectPath,string assemblyName) + public ReferencedProject(string projectPath,string assemblyName,bool isDll) { - AssemblyName = assemblyName; this.projectPath = projectPath; + AssemblyName = assemblyName; + IsDll = isDll; } + public ReferencedProject(string projectPath) { this.projectPath = projectPath; @@ -43,8 +46,17 @@ private string GetAssemblyName(XElement projectFileXElement, string fallbackName return result; } - public string AssemblyName { get; private set; } - public bool ExcludeFromCodeCoverage + public string AssemblyName { get; } + + public bool IsDll { get; } = true; + + /* + Annoyingly by allowing and not true + it is not possible to use IVsBuildPropertyStorage. + Todo - consider breaking change to true + Given that purpose is for dotnet framework..... + */ + public bool ExcludeFromCodeCoverage { get { diff --git a/SharedProject/Core/Model/ReferencedProjects/ReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/ReferencedProjectsHelper.cs new file mode 100644 index 00000000..134fa97d --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/ReferencedProjectsHelper.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace FineCodeCoverage.Engine.Model +{ + [Export(typeof(IReferencedProjectsHelper))] + internal class ReferencedProjectsHelper : IReferencedProjectsHelper + { + private readonly IVsApiReferencedProjectsHelper vsApiReferencedProjectsHelper; + private readonly IProjectFileReferencedProjectsHelper projectFileReferencedProjectsHelper; + private string projectFile { get; set; } + private Func projectFileXElementProvider; + + [ImportingConstructor] + public ReferencedProjectsHelper( + IVsApiReferencedProjectsHelper vsApiReferencedProjectsHelper, + IProjectFileReferencedProjectsHelper projectFileReferencedProjectsHelper + ) + { + this.vsApiReferencedProjectsHelper = vsApiReferencedProjectsHelper; + this.projectFileReferencedProjectsHelper = projectFileReferencedProjectsHelper; + } + + public async Task> GetReferencedProjectsAsync(string projectFile, Func projectFileXElementProvider) + { + this.projectFileXElementProvider = projectFileXElementProvider; + this.projectFile = projectFile; + var referencedProjects = await GetReferencedProjectsAsync(); + return new List(referencedProjects); + } + + private async Task> GetReferencedProjectsAsync() + { + return await SafeGetReferencedProjectsFromVSApiAsync() ?? projectFileReferencedProjectsHelper.GetReferencedProjects(projectFile, projectFileXElementProvider()); + } + + private async Task> SafeGetReferencedProjectsFromVSApiAsync() + { + try + { + return await vsApiReferencedProjectsHelper.GetReferencedProjectsAsync(projectFile); + } + catch (Exception) { } + return null; + } + } + +} diff --git a/SharedProject/Core/Model/ReferencedProjects/VsApiReferencedProjectsHelper.cs b/SharedProject/Core/Model/ReferencedProjects/VsApiReferencedProjectsHelper.cs new file mode 100644 index 00000000..f0991049 --- /dev/null +++ b/SharedProject/Core/Model/ReferencedProjects/VsApiReferencedProjectsHelper.cs @@ -0,0 +1,84 @@ +using EnvDTE; +using EnvDTE80; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.VCProjectEngine; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading.Tasks; +using VSLangProj; + +namespace FineCodeCoverage.Engine.Model +{ + + [Export(typeof(IVsApiReferencedProjectsHelper))] + internal class VsApiReferencedProjectsHelper : IVsApiReferencedProjectsHelper + { + private readonly ICPPReferencedProjectsHelper cppReferencedProjectsHelper; + private readonly IDotNetReferencedProjectsHelper dotNetReferencedProjectsHelper; + private AsyncLazy lazyDTE2; + + [ImportingConstructor] + public VsApiReferencedProjectsHelper( + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider, + ICPPReferencedProjectsHelper cppReferencedProjectsHelper, + IDotNetReferencedProjectsHelper dotNetReferencedProjectsHelper + ) + { + lazyDTE2 = new AsyncLazy(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + return (DTE2)serviceProvider.GetService(typeof(DTE)); + }, ThreadHelper.JoinableTaskFactory); + this.cppReferencedProjectsHelper = cppReferencedProjectsHelper; + this.dotNetReferencedProjectsHelper = dotNetReferencedProjectsHelper; + } + public async Task> GetReferencedProjectsAsync(string projectFile) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var project = await GetProjectAsync(projectFile); + + if (project == null) + { + return null; + } + + var cppProject = project.Object as VCProject; + if (cppProject != null) + { + return await cppReferencedProjectsHelper.GetInstrumentableReferencedProjectsAsync(cppProject); + } + + var vsProject = project.Object as VSProject; + if (vsProject != null) + { + return await dotNetReferencedProjectsHelper.GetReferencedProjectsAsync(vsProject); + } + + return null; + } + + private async Task GetProjectAsync(string projectFile) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte2 = await lazyDTE2.GetValueAsync(); + // note that cannot do dte.Solution.Projects.Item(ProjectFile) - fails when dots in path + return dte2.Solution.Projects.Cast().FirstOrDefault(p => + { + ThreadHelper.ThrowIfNotOnUIThread(); + //have to try here as unloaded projects will throw + var projectFullName = ""; + try + { + projectFullName = p.FullName; + } + catch { } + return projectFullName == projectFile; + }); + } + } + +} diff --git a/SharedProject/Core/Model/SettingsMerger.cs b/SharedProject/Core/Model/SettingsMerger.cs index 0fd6c570..e4a6a1cc 100644 --- a/SharedProject/Core/Model/SettingsMerger.cs +++ b/SharedProject/Core/Model/SettingsMerger.cs @@ -1,5 +1,6 @@ using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Options; +using FineCodeCoverage.Output; using System; using System.Collections.Generic; using System.ComponentModel.Composition; @@ -75,6 +76,7 @@ private class SettingsElementDefaultMerge { public XElement SettingsElement { get; set; } public bool DefaultMerge { get; set; } + public bool FromProjectSettings { get; internal set; } } private readonly PropertyInfo[] settingsPropertyInfos; @@ -90,13 +92,27 @@ ILogger logger } - public IAppOptions Merge(IAppOptions globalOptions, List settingsFileElements, XElement projectSettingsElement) + public IAppOptions Merge( + IAppOptions globalOptions, + List settingsFileElements, + XElement projectSettingsElement) { var settingsElementsWithDefaultMergeStrategy = - settingsFileElements.Select(e => new SettingsElementDefaultMerge { SettingsElement = e, DefaultMerge = settingsFileDefaultMerge }).ToList(); + settingsFileElements.Select(e => new SettingsElementDefaultMerge { + SettingsElement = e, + DefaultMerge = settingsFileDefaultMerge, + FromProjectSettings = false + }).ToList(); + if (projectSettingsElement != null) { - settingsElementsWithDefaultMergeStrategy.Add(new SettingsElementDefaultMerge { SettingsElement = projectSettingsElement, DefaultMerge = projectSettingsDefaultMerge }); + settingsElementsWithDefaultMergeStrategy.Add( + new SettingsElementDefaultMerge { + SettingsElement = projectSettingsElement, + DefaultMerge = projectSettingsDefaultMerge, + FromProjectSettings = true + } + ); } if (settingsElementsWithDefaultMergeStrategy.Count != 0) @@ -107,7 +123,11 @@ public IAppOptions Merge(IAppOptions globalOptions, List settingsFileE return globalOptions; } - private void Merge(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, List settingsElementsWithDefaultMergeStrategy) + private void Merge( + IAppOptions globalOptions, + PropertyInfo settingPropertyInfo, + List settingsElementsWithDefaultMergeStrategy + ) { var canMerge = settingsMergeLogic.CanMerge(settingPropertyInfo.PropertyType); if (canMerge) @@ -119,14 +139,15 @@ private void Merge(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, var propertyElement = GetPropertyElement(settingsElement, settingPropertyInfo.Name); if (propertyElement != null) { + var fromProjectSettings = settingsElementWithDefaultMerge.FromProjectSettings; var merge = GetMerge(defaultMerge, propertyElement); if (merge) { - Merge(globalOptions, settingPropertyInfo, settingsElement); + Merge(globalOptions, settingPropertyInfo, settingsElement,fromProjectSettings); } else { - Overwrite(globalOptions, settingPropertyInfo, settingsElement); + Overwrite(globalOptions, settingPropertyInfo, settingsElement,fromProjectSettings); } } @@ -134,14 +155,13 @@ private void Merge(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, } else { - var settingsElements = settingsElementsWithDefaultMergeStrategy.Select(x => x.SettingsElement); - Overwrite(globalOptions, settingPropertyInfo, settingsElements); + Overwrite(globalOptions, settingPropertyInfo, settingsElementsWithDefaultMergeStrategy); } } - private void Merge(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, XElement settingsElement) + private void Merge(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, XElement settingsElement,bool fromProjectSettings) { - var value = TryGetValueFromXml(settingsElement, settingPropertyInfo); + var value = TryGetValueFromXml(settingsElement, settingPropertyInfo,fromProjectSettings); if (value != null) { var currentValue = settingPropertyInfo.GetValue(globalOptions); @@ -179,17 +199,17 @@ private bool GetDefaultMerge(bool defaultDefaultMerge, XElement root) return defaultMergeAttribute.Value.ToLower() == "true"; } - private void Overwrite(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, IEnumerable settingsElements) + private void Overwrite(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, IEnumerable settingsElementsDefaultMerge) { - foreach (var settingsElement in settingsElements) + foreach (var settingsElementDefaultMerge in settingsElementsDefaultMerge) { - Overwrite(globalOptions, settingPropertyInfo, settingsElement); + Overwrite(globalOptions, settingPropertyInfo, settingsElementDefaultMerge.SettingsElement,settingsElementDefaultMerge.FromProjectSettings); } } - private void Overwrite(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, XElement settingsElement) + private void Overwrite(IAppOptions globalOptions, PropertyInfo settingPropertyInfo, XElement settingsElement,bool fromProjectSettings) { - var value = TryGetValueFromXml(settingsElement, settingPropertyInfo); + var value = TryGetValueFromXml(settingsElement, settingPropertyInfo,fromProjectSettings); if (value != null) { settingPropertyInfo.SetValue(globalOptions, value); @@ -201,7 +221,7 @@ private XElement GetPropertyElement(XElement settingsElement, string propertyNam return settingsElement.Descendants().FirstOrDefault(x => x.Name.LocalName.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); } - private object TryGetValueFromXml(XElement settingsElement, PropertyInfo property) + private object TryGetValueFromXml(XElement settingsElement, PropertyInfo property,bool fromProjectSettings) { try { @@ -209,7 +229,8 @@ private object TryGetValueFromXml(XElement settingsElement, PropertyInfo propert } catch (Exception exception) { - logger.Log($"Failed to override '{property.Name}' setting", exception); + var from = fromProjectSettings ? "project settings" : "settings file"; + logger.Log($"Failed to get '{property.Name}' setting from {from}", exception); } return null; } @@ -372,7 +393,7 @@ internal object GetValueFromXml(XElement settingsElement, PropertyInfo property) else { - throw new Exception($"Cannot handle '{property.PropertyType.Name}' yet"); + throw new Exception($"Unexpected settings type '{property.PropertyType.Name}' for setting {property.Name} in settings merger GetValueFromXml"); } return null; diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplate.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplate.cs index 5108110d..9e591748 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplate.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplate.cs @@ -16,7 +16,11 @@ ITemplateReplacementResult ReplaceTemplate( IRunSettingsTemplateReplacements replacements, bool isNetFrameworkProject); string Replace(string templatedXml, IRunSettingsTemplateReplacements replacements); - + + // returns a string representation of the runsettings xml containing markers for string replacement + string Get(); + + // returns a string representation of the runsettings xml containing markers for string replacement string ConfigureCustom(string runSettingsTemplate); string DataCollectionRunSettingsElement { get; } diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplateReplacements.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplateReplacements.cs index 9298e674..94efa0c8 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplateReplacements.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/IRunSettingsTemplateReplacements.cs @@ -1,10 +1,14 @@ namespace FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage { + // string values for string.Replace of templated values - e.g %fcc_modulepaths_exclude% internal interface IRunSettingsTemplateReplacements { string Enabled { get; } string ResultsDirectory { get; } string TestAdapter { get; } + + // the following are xml fragments as strings + // e.g path1path2 string ModulePathsExclude { get; } string ModulePathsInclude { get; } string FunctionsExclude { get; } diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/ITemplatedRunSettingsService.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/ITemplatedRunSettingsService.cs index 5eda4579..21943276 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/ITemplatedRunSettingsService.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/ITemplatedRunSettingsService.cs @@ -17,9 +17,23 @@ internal interface IProjectRunSettingsFromTemplateResult List CoverageProjectsWithFCCMsTestAdapter { get; } } + internal class TemplatedCoverageProjectRunSettingsResult : ICoverageProjectRunSettings + { + public ICoverageProject CoverageProject { get; set; } + public string RunSettings { get; set; } + public string CustomTemplatePath { get; internal set; } + public bool ReplacedTestAdapter { get; internal set; } + } + internal interface ITemplatedRunSettingsService { Task GenerateAsync(IEnumerable coverageProjectsWithoutRunSettings, string solutionDirectory, string fccMsTestAdapterPath); Task CleanUpAsync(List coverageProjects); + List CreateProjectsRunSettings( + IEnumerable coverageProjects, + string solutionDirectory, + string fccMsTestAdapterPath + ); + } } diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/IUserRunSettingsProjectDetails.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/IUserRunSettingsProjectDetails.cs index a4df082e..a39925b1 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/IUserRunSettingsProjectDetails.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/IUserRunSettingsProjectDetails.cs @@ -1,12 +1,13 @@ -using FineCodeCoverage.Options; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Options; using System.Collections.Generic; namespace FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage { internal interface IUserRunSettingsProjectDetails { - List ExcludedReferencedProjects { get; set; } - List IncludedReferencedProjects { get; set; } + List ExcludedReferencedProjects { get; set; } + List IncludedReferencedProjects { get; set; } string CoverageOutputFolder { get; set; } IMsCodeCoverageOptions Settings { get; set; } string TestDllFile { get; set; } diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRegex.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRegex.cs index 2301c13c..41c44ae0 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRegex.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRegex.cs @@ -7,9 +7,15 @@ public static string RegexEscapePath(string path) return path.Replace(@"\", @"\\"); } - public static string RegexModuleName(string moduleName) + public static string RegexModuleName(string moduleName, bool isDll) + { + var extensionMatch = isDll ? "dll" : "(dll|exe)"; + return $".*\\\\{EscapeDots(moduleName)}\\.{extensionMatch}$"; + } + + private static string EscapeDots(string moduleName) { - return $".*\\\\{moduleName}.dll$"; + return moduleName.Replace(".", @"\."); } } diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRunSettingsService.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRunSettingsService.cs index 55a3b799..a2afed0d 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRunSettingsService.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsCodeCoverageRunSettingsService.cs @@ -29,8 +29,8 @@ private class UserRunSettingsProjectDetails : IUserRunSettingsProjectDetails public IMsCodeCoverageOptions Settings { get; set; } public string CoverageOutputFolder { get; set; } public string TestDllFile { get; set; } - public List ExcludedReferencedProjects { get; set; } - public List IncludedReferencedProjects { get; set; } + public List ExcludedReferencedProjects { get; set; } + public List IncludedReferencedProjects { get; set; } } private class CoverageProjectsByType { @@ -57,12 +57,11 @@ public static async Task CreateAsync(ITestOperation test } } - private readonly IToolFolder toolFolder; - private readonly IToolZipProvider toolZipProvider; + private readonly IToolUnzipper toolUnzipper; private readonly IAppOptionsProvider appOptionsProvider; private readonly ICoverageToolOutputManager coverageOutputManager; private readonly IShimCopier shimCopier; - private readonly ILogger logger; + private readonly Output.ILogger logger; private readonly IReportGeneratorUtil reportGeneratorUtil; private IFCCEngine fccEngine; @@ -89,19 +88,17 @@ public static async Task CreateAsync(ITestOperation test [ImportingConstructor] public MsCodeCoverageRunSettingsService( - IToolFolder toolFolder, - IToolZipProvider toolZipProvider, + IToolUnzipper toolUnzipper, IAppOptionsProvider appOptionsProvider, ICoverageToolOutputManager coverageOutputManager, IUserRunSettingsService userRunSettingsService, ITemplatedRunSettingsService templatedRunSettingsService, IShimCopier shimCopier, - ILogger logger, + Output.ILogger logger, IReportGeneratorUtil reportGeneratorUtil ) { - this.toolFolder = toolFolder; - this.toolZipProvider = toolZipProvider; + this.toolUnzipper = toolUnzipper; this.appOptionsProvider = appOptionsProvider; this.coverageOutputManager = coverageOutputManager; this.shimCopier = shimCopier; @@ -114,9 +111,9 @@ IReportGeneratorUtil reportGeneratorUtil public void Initialize(string appDataFolder, IFCCEngine fccEngine, CancellationToken cancellationToken) { this.fccEngine = fccEngine; - var zipDestination = toolFolder.EnsureUnzipped(appDataFolder, zipDirectoryName, toolZipProvider.ProvideZip(zipPrefix), cancellationToken); - fccMsTestAdapterPath = Path.Combine(zipDestination, "build", "netstandard1.0"); - shimPath = Path.Combine(zipDestination, "build", "netstandard1.0", "CodeCoverage", "coreclr", "Microsoft.VisualStudio.CodeCoverage.Shim.dll"); + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolder, zipDirectoryName,zipPrefix, cancellationToken); + fccMsTestAdapterPath = Path.Combine(zipDestination, "build", "netstandard2.0"); + shimPath = Path.Combine(zipDestination, "build", "netstandard2.0", "CodeCoverage", "coreclr", "Microsoft.VisualStudio.CodeCoverage.Shim.dll"); } #region set up for collection @@ -124,12 +121,7 @@ public void Initialize(string appDataFolder, IFCCEngine fccEngine, CancellationT public async Task IsCollectingAsync(ITestOperation testOperation) { await InitializeIsCollectingAsync(testOperation); - if( runMsCodeCoverage == RunMsCodeCoverage.No) - { - logger.Log($"See option {nameof(IAppOptions.RunMsCodeCoverage)} for a better ( Beta ) experience. {FCCGithub.Readme}"); - reportGeneratorUtil.LogCoverageProcess($"See option {nameof(IAppOptions.RunMsCodeCoverage)} for a better ( Beta ) experience. View readme."); - } - else + if( runMsCodeCoverage != RunMsCodeCoverage.No) { await TrySetUpForCollectionAsync(testOperation.SolutionDirectory); } @@ -178,7 +170,6 @@ private void ReportEndOfCoverageRunIfError() private Task InitializeIsCollectingAsync(ITestOperation testOperation) { - collectionStatus = MsCodeCoverageCollectionStatus.NotCollecting; runMsCodeCoverage = appOptionsProvider.Get().RunMsCodeCoverage; useMsCodeCoverage = runMsCodeCoverage == RunMsCodeCoverage.Yes; userRunSettingsProjectDetailsLookup = null; @@ -270,13 +261,11 @@ private async Task ProcessTemplateGenerationResultAsync(IProjectRunSettingsFromT private async Task CollectingWithTemplateAsync(IProjectRunSettingsFromTemplateResult generationResult, List coverageProjectsForShim) { coverageProjectsForShim.AddRange(generationResult.CoverageProjectsWithFCCMsTestAdapter); - await CombinedLogAsync(() => - { - var leadingMessage = generationResult.CustomTemplatePaths.Any() ? $"{msCodeCoverageMessage} - custom template paths" : msCodeCoverageMessage; - var loggerMessages = new List { leadingMessage }.Concat(generationResult.CustomTemplatePaths.Distinct()); - logger.Log(loggerMessages); - reportGeneratorUtil.LogCoverageProcess(msCodeCoverageMessage); - }); + await threadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var leadingMessage = generationResult.CustomTemplatePaths.Any() ? $"{msCodeCoverageMessage} - custom template paths" : msCodeCoverageMessage; + var loggerMessages = new List { leadingMessage }.Concat(generationResult.CustomTemplatePaths.Distinct()); + logger.Log(loggerMessages); + reportGeneratorUtil.LogCoverageProcess(msCodeCoverageMessage); collectionStatus = MsCodeCoverageCollectionStatus.Collecting; } @@ -373,27 +362,17 @@ public void StopCoverage() #region Logging private async Task CombinedLogAsync(string message) - { - await CombinedLogAsync(() => - { - logger.Log(message); - reportGeneratorUtil.LogCoverageProcess(message); - }); - } - - private async Task CombinedLogAsync(Action action) { await threadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - action(); + logger.Log(message); + reportGeneratorUtil.LogCoverageProcess(message); } - private Task CombinedLogExceptionAsync(Exception ex, string reason) + private async Task CombinedLogExceptionAsync(Exception ex, string reason) { - return CombinedLogAsync(() => - { - logger.Log(reason, ex.ToString()); - reportGeneratorUtil.LogCoverageProcess(reason); - }); + await threadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + logger.Log(reason, ex.ToString()); + reportGeneratorUtil.LogCoverageProcess(reason); } #endregion @@ -407,6 +386,7 @@ private async Task CleanUpAsync(ITestOperation testOperation) { coverageProjectsByType = await CoverageProjectsByType.CreateAsync(testOperation); await templatedRunSettingsService.CleanUpAsync(coverageProjectsByType.RunSettings); + collectionStatus = MsCodeCoverageCollectionStatus.NotCollecting; } } @@ -415,7 +395,7 @@ public static bool IsTestExecution(this IRunSettingsConfigurationInfo configurat { return configurationInfo.RequestState == RunSettingConfigurationInfoState.Execution; } - + } internal static class UserRunSettingsAnalysisResultExtensions diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs new file mode 100644 index 00000000..468676ad --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/MsTemplateReplacementException.cs @@ -0,0 +1,25 @@ +using System; +using System.Xml; + +namespace FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage +{ + public class MsTemplateReplacementException : Exception + { + private readonly XmlException innerException; + private readonly string replacedRunSettingsTemplate; + public MsTemplateReplacementException(XmlException innerException, string replacedRunSettingsTemplate) + { + this.innerException = innerException; + this.replacedRunSettingsTemplate = replacedRunSettingsTemplate; + } + + public override string ToString() + { + return $@"${innerException} +Replaced template : +${replacedRunSettingsTemplate} +"; + } + + } +} diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/ProjectRunSettingsGenerator.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/ProjectRunSettingsGenerator.cs index edfe7e58..c379d6a3 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/ProjectRunSettingsGenerator.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/ProjectRunSettingsGenerator.cs @@ -7,6 +7,7 @@ using System.Linq; using FineCodeCoverage.Core.Utilities; using System.Xml.Linq; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage { @@ -15,16 +16,19 @@ internal class ProjectRunSettingsGenerator : IProjectRunSettingsGenerator { private readonly IFileUtil fileUtil; private readonly IVsRunSettingsWriter vsRunSettingsWriter; + private readonly ILogger logger; private const string fccGeneratedRunSettingsSuffix = "fcc-mscodecoverage-generated"; [ImportingConstructor] public ProjectRunSettingsGenerator( IFileUtil fileUtil, - IVsRunSettingsWriter vsRunSettingsWriter + IVsRunSettingsWriter vsRunSettingsWriter, + ILogger logger ) { this.fileUtil = fileUtil; this.vsRunSettingsWriter = vsRunSettingsWriter; + this.logger = logger; } public Task RemoveGeneratedProjectSettingsAsync(IEnumerable coverageProjects) @@ -43,7 +47,7 @@ public Task WriteProjectsRunSettingsAsync(IEnumerable recommendedYouDoNotChangeElementsNetCore = new List<(string elementName, string value)> { @@ -163,10 +164,7 @@ public RunSettingsTemplate() "; } - public override string ToString() - { - return template; - } + public string Get() => template; public ITemplateReplacementResult ReplaceTemplate( string runSettingsTemplate, @@ -186,7 +184,14 @@ bool isNetFramework private string AddRecommendedYouDoNotChangeElementsIfNotProvided(string replacedRunSettingsTemplate, bool isNetFramework) { - var templateDocument = XDocument.Parse(replacedRunSettingsTemplate); + XDocument templateDocument; + try + { + templateDocument = XDocument.Parse(replacedRunSettingsTemplate); + }catch(XmlException exc) + { + throw new MsTemplateReplacementException(exc, replacedRunSettingsTemplate); + } var msDataCollectorCodeCoverageElement = GetMsDataCollectorCodeCoverageElement(templateDocument); if (msDataCollectorCodeCoverageElement != null) { @@ -212,11 +217,21 @@ private XElement GetMsDataCollectorCodeCoverageElement(XDocument templateDocumen return msDataCollector.GetStrictDescendant("Configuration/CodeCoverage"); } + // hacky. due to tests + private string SafeFilePathEscape(string path) + { + if (path == null) + { + return null; + } + return XmlFileEscaper.Escape(path); + } + public string Replace(string templatedXml, IRunSettingsTemplateReplacements replacements) { return templatedXml - .Replace(replacementLookups.ResultsDirectory, replacements.ResultsDirectory) - .Replace(replacementLookups.TestAdapter, replacements.TestAdapter) + .Replace(replacementLookups.ResultsDirectory, SafeFilePathEscape(replacements.ResultsDirectory)) + .Replace(replacementLookups.TestAdapter, SafeFilePathEscape(replacements.TestAdapter)) .Replace(replacementLookups.Enabled, replacements.Enabled) .Replace(replacementLookups.ModulePathsExclude, replacements.ModulePathsExclude) .Replace(replacementLookups.ModulePathsInclude, replacements.ModulePathsInclude) diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplateReplacementsFactory.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplateReplacementsFactory.cs index e9569a37..96b4f4b8 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplateReplacementsFactory.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/RunSettingsTemplateReplacementsFactory.cs @@ -146,37 +146,81 @@ public IRunSettingsTemplateReplacements Create( var allProjectDetails = testContainers.Select(tc => userRunSettingsProjectDetailsLookup[tc.Source]).ToList(); var resultsDirectory = allProjectDetails[0].CoverageOutputFolder; var allSettings = allProjectDetails.Select(pd => pd.Settings); + var allProjectsDisabled = allSettings.All(s => !s.Enabled); var mergedSettings = new MergedIncludesExcludesOptions(allSettings); var additionalModulePathsExclude = allProjectDetails.SelectMany(pd => - { - var additional = pd.ExcludedReferencedProjects.Select(rp => MsCodeCoverageRegex.RegexModuleName(rp)).ToList(); - if (!pd.Settings.IncludeTestAssembly) - { - additional.Add(MsCodeCoverageRegex.RegexEscapePath(pd.TestDllFile)); - } - return additional; + GetAdditionalModulePathsExclude(pd.ExcludedReferencedProjects, pd.TestDllFile, pd.Settings.IncludeTestAssembly)); - }); + var hasIncludes = allProjectDetails.Any(pd => HasIncludes(pd.Settings.ModulePathsInclude, pd.IncludedReferencedProjects)); - var additionalModulePathsInclude = allProjectDetails.SelectMany(projectDetails => projectDetails.IncludedReferencedProjects.Select(rp => MsCodeCoverageRegex.RegexModuleName(rp))); + var additionalModulePathsInclude = allProjectDetails.SelectMany(pd => + GetAdditionalModulePathsInclude(hasIncludes, pd.IncludedReferencedProjects, pd.TestDllFile, pd.Settings.IncludeTestAssembly)); var settings = new CombinedIncludesExcludesOptions(mergedSettings, additionalModulePathsInclude, additionalModulePathsExclude); - return new RunSettingsTemplateReplacements(settings, resultsDirectory, "true", testAdapter); + return new RunSettingsTemplateReplacements(settings, resultsDirectory, (!allProjectsDisabled).ToString().ToLower(), testAdapter); + } + + private static IEnumerable GetAdditionalModulePaths( + IEnumerable referencedProjects, + string testDllFile, + bool includeTestAssembly, + bool isInclude + ) + { + var additionalReferenced = referencedProjects.Select( + rp => MsCodeCoverageRegex.RegexModuleName(rp.AssemblyName,rp.IsDll)); + if(includeTestAssembly == isInclude) + { + additionalReferenced = additionalReferenced.Append(MsCodeCoverageRegex.RegexEscapePath(testDllFile)); + } + return additionalReferenced; + + } + + private static IEnumerable GetAdditionalModulePathsExclude( + IEnumerable referencedProjects, string testDllFile, bool includeTestAssembly) + { + return GetAdditionalModulePaths(referencedProjects, testDllFile, includeTestAssembly, false); + } + + private static bool HasIncludes( + string[] modulePathsInclude, + List includedReferencedProjects) + { + return modulePathsInclude?.Any() == true || includedReferencedProjects.Any(); + } + + private static IEnumerable GetAdditionalModulePathsInclude( + bool hasIncludes, + List includedReferencedProjects, + string testDllFile, + bool includeTestAssembly) + + { + includeTestAssembly = includeTestAssembly && hasIncludes; + return GetAdditionalModulePaths( + includedReferencedProjects, + testDllFile, + includeTestAssembly, + true); + } public IRunSettingsTemplateReplacements Create(ICoverageProject coverageProject, string testAdapter) { var projectSettings = coverageProject.Settings; - var additionalModulePathsExclude = coverageProject.ExcludedReferencedProjects.Select( - rp => MsCodeCoverageRegex.RegexModuleName(rp)).ToList(); + var additionalModulePathsExclude = GetAdditionalModulePathsExclude( + coverageProject.ExcludedReferencedProjects, + coverageProject.TestDllFile, + projectSettings.IncludeTestAssembly); - if (!projectSettings.IncludeTestAssembly) - { - additionalModulePathsExclude.Add(MsCodeCoverageRegex.RegexEscapePath(coverageProject.TestDllFile)); - } + var additionalModulePathsInclude = GetAdditionalModulePathsInclude( + HasIncludes(coverageProject.Settings.ModulePathsInclude, coverageProject.IncludedReferencedProjects), + coverageProject.IncludedReferencedProjects, + coverageProject.TestDllFile, + projectSettings.IncludeTestAssembly); - var additionalModulePathsInclude = coverageProject.IncludedReferencedProjects.Select(rp => MsCodeCoverageRegex.RegexModuleName(rp)).ToList(); var settings = new CombinedIncludesExcludesOptions(projectSettings, additionalModulePathsInclude, additionalModulePathsExclude); return new RunSettingsTemplateReplacements(settings, coverageProject.CoverageOutputFolder, projectSettings.Enabled.ToString(), testAdapter); } diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/TemplatedRunSettingsService.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/TemplatedRunSettingsService.cs index 724b8362..2521aaca 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/TemplatedRunSettingsService.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/TemplatedRunSettingsService.cs @@ -17,14 +17,6 @@ internal class TemplatedRunSettingsService : ITemplatedRunSettingsService private readonly IRunSettingsTemplateReplacementsFactory runSettingsTemplateReplacementsFactory; private readonly IProjectRunSettingsGenerator projectRunSettingsGenerator; - private class TemplatedCoverageProjectRunSettingsResult : ICoverageProjectRunSettings - { - public ICoverageProject CoverageProject { get; set; } - public string RunSettings { get; set; } - public string CustomTemplatePath { get; internal set; } - public bool ReplacedTestAdapter { get; internal set; } - } - private class ProjectRunSettingsFromTemplateResult : IProjectRunSettingsFromTemplateResult { private class ExceptionReasonImpl : IExceptionReason @@ -120,7 +112,7 @@ private IProjectRunSettingsFromTemplateResult CreateSuccessResult(IEnumerable CreateProjectsRunSettings( + public List CreateProjectsRunSettings( IEnumerable coverageProjects, string solutionDirectory, string fccMsTestAdapterPath @@ -129,8 +121,8 @@ string fccMsTestAdapterPath return coverageProjects.Select(coverageProject => { var projectDirectory = Path.GetDirectoryName(coverageProject.ProjectFile); - var (runSettingsTemplate, customTemplatePath) = GetRunSettingsTemplate(projectDirectory, solutionDirectory); - var templateReplaceResult = ReplaceTemplate(coverageProject, runSettingsTemplate, fccMsTestAdapterPath); + var (replaceableTemplate, customTemplatePath) = GetRunSettingsTemplate(projectDirectory, solutionDirectory); + var templateReplaceResult = ReplaceTemplate(coverageProject, replaceableTemplate, fccMsTestAdapterPath); return new TemplatedCoverageProjectRunSettingsResult { @@ -143,28 +135,28 @@ string fccMsTestAdapterPath }).ToList(); } - private (string Template, string CustomPath) GetRunSettingsTemplate(string projectDirectory, string solutionDirectory) + private (string ReplaceableTemplate, string CustomPath) GetRunSettingsTemplate(string projectDirectory, string solutionDirectory) { string customPath = null; - string template; + string replaceableTemplate; var customRunSettingsTemplateDetails = customRunSettingsTemplateProvider.Provide(projectDirectory, solutionDirectory); if (customRunSettingsTemplateDetails != null) { customPath = customRunSettingsTemplateDetails.Path; - template = runSettingsTemplate.ConfigureCustom(customRunSettingsTemplateDetails.Template); + replaceableTemplate = runSettingsTemplate.ConfigureCustom(customRunSettingsTemplateDetails.Template); } else { - template = runSettingsTemplate.ToString(); + replaceableTemplate = runSettingsTemplate.Get(); } - return (template, customPath); + return (replaceableTemplate, customPath); } - private ITemplateReplacementResult ReplaceTemplate(ICoverageProject coverageProject, string runSettingsTemplate, string fccMsTestAdapterPath) + private ITemplateReplacementResult ReplaceTemplate(ICoverageProject coverageProject, string replaceableTemplate, string fccMsTestAdapterPath) { var replacements = runSettingsTemplateReplacementsFactory.Create(coverageProject, fccMsTestAdapterPath); - return this.runSettingsTemplate.ReplaceTemplate(runSettingsTemplate, replacements, coverageProject.IsDotNetFramework); + return this.runSettingsTemplate.ReplaceTemplate(replaceableTemplate, replacements, coverageProject.IsDotNetFramework); } public Task CleanUpAsync(List coverageProjects) diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/UserRunSettingsService.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/UserRunSettingsService.cs index e7b02e83..d77cb701 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/UserRunSettingsService.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/UserRunSettingsService.cs @@ -145,13 +145,14 @@ public IXPathNavigable AddFCCRunSettings(IXPathNavigable inputRunSettingDocument private IXPathNavigable AddFCCRunSettingsActual(IXPathNavigable inputRunSettingDocument, IRunSettingsConfigurationInfo configurationInfo, Dictionary userRunSettingsProjectDetailsLookup, string fccMsTestAdapterPath) { var navigator = inputRunSettingDocument.CreateNavigator(); - navigator.MoveToFirstChild(); + navigator.MoveToChild("RunSettings",""); var clonedNavigator = navigator.Clone(); var replacements = runSettingsTemplateReplacementsFactory.Create( configurationInfo.TestContainers, userRunSettingsProjectDetailsLookup, fccMsTestAdapterPath ); + EnsureTestAdaptersPathsAndReplace(navigator, replacements); EnsureCorrectMsDataCollectorAndReplace(clonedNavigator, replacements); return navigator; @@ -212,14 +213,31 @@ private void EnsureCorrectMsDataCollectorAndReplace(XPathNavigator xpathNavigato xpathNavigator.AppendChild(runSettingsTemplate.DataCollectionRunSettingsElement); } - if (addedMsDataCollector) + // todo - improve this + var disableMsDataCollector = replacements.Enabled == "false"; + if (addedMsDataCollector || disableMsDataCollector) { xpathNavigator.MoveToRoot(); var dataCollectorsNavigator = xpathNavigator.SelectSingleNode("/RunSettings/DataCollectionRunSettings/DataCollectors"); var msDataCollectorNavigator = MoveToMsDataCollectorFromDataCollectors(dataCollectorsNavigator); - ReplaceExcludesIncludes(msDataCollectorNavigator, replacements); + + if (disableMsDataCollector) + { + DisableMsDataCollector(msDataCollectorNavigator); + } + else + { + ReplaceExcludesIncludes(msDataCollectorNavigator, replacements); // no need to replace if we are disabling + } } + } + + private void DisableMsDataCollector(XPathNavigator msDataCollectorNavigator) + { + var element = XElement.Parse(msDataCollectorNavigator.OuterXml); + element.SetAttributeValue("enabled", "false"); + msDataCollectorNavigator.OuterXml = element.ToString(); } private XPathNavigator MoveToMsDataCollectorFromDataCollectors(XPathNavigator navigator) diff --git a/SharedProject/Core/MsTestPlatform/CodeCoverage/VsRunSettingsWriter.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/VsRunSettingsWriter.cs index 8f9f7519..0bd9d17f 100644 --- a/SharedProject/Core/MsTestPlatform/CodeCoverage/VsRunSettingsWriter.cs +++ b/SharedProject/Core/MsTestPlatform/CodeCoverage/VsRunSettingsWriter.cs @@ -4,24 +4,31 @@ using Microsoft.VisualStudio.Shell.Interop; using System; using System.ComponentModel.Composition; -using Task = System.Threading.Tasks.Task; using System.Threading.Tasks; +using FineCodeCoverage.Core.MsTestPlatform.CodeCoverage; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage { [Export(typeof(IVsRunSettingsWriter))] internal class VsRunSettingsWriter : IVsRunSettingsWriter { - private const string projectRunSettingsFilePathElementName = "RunSettingsFilePath"; + private const string projectRunSettingsFilePathElementName = "RunSettingsFilePath"; private readonly IServiceProvider serviceProvider; + private readonly IProjectSaver projectSaver; + private readonly IProjectFilePropertyWriter projectFilePropertyWriter; [ImportingConstructor] public VsRunSettingsWriter( [Import(typeof(SVsServiceProvider))] - IServiceProvider serviceProvider + IServiceProvider serviceProvider, + IProjectSaver projectSaver, + IProjectFilePropertyWriter projectFilePropertyWriter ) { this.serviceProvider = serviceProvider; + this.projectSaver = projectSaver; + this.projectFilePropertyWriter = projectFilePropertyWriter; } public async Task WriteRunSettingsFilePathAsync(Guid projectGuid, string projectRunSettingsFilePath) @@ -32,11 +39,7 @@ public async Task WriteRunSettingsFilePathAsync(Guid projectGuid, string p Assumes.Present(vsSolution); if (vsSolution.GetProjectOfGuid(ref projectGuid, out var vsHierarchy) == VSConstants.S_OK) { - if (vsHierarchy is IVsBuildPropertyStorage vsBuildPropertyStorage) - { - // care not to use 2 ! - success = vsBuildPropertyStorage.SetPropertyValue(projectRunSettingsFilePathElementName, null, 1, projectRunSettingsFilePath) == VSConstants.S_OK; - } + success = await projectFilePropertyWriter.WritePropertyAsync(vsHierarchy, projectRunSettingsFilePathElementName, projectRunSettingsFilePath); } return success; } @@ -49,9 +52,10 @@ public async Task RemoveRunSettingsFilePathAsync(Guid projectGuid) Assumes.Present(vsSolution); if (vsSolution.GetProjectOfGuid(ref projectGuid, out var vsHierarchy) == VSConstants.S_OK) { - if (vsHierarchy is IVsBuildPropertyStorage vsBuildPropertyStorage) + ok = await projectFilePropertyWriter.RemovePropertyAsync(vsHierarchy, projectRunSettingsFilePathElementName); + if (ok) { - ok = vsBuildPropertyStorage.RemoveProperty(projectRunSettingsFilePathElementName, null, 1) == VSConstants.S_OK; + await this.projectSaver.SaveProjectAsync(vsHierarchy); } } return ok; diff --git a/SharedProject/Core/MsTestPlatform/MsTestPlatformUtil.cs b/SharedProject/Core/MsTestPlatform/MsTestPlatformUtil.cs index 0ad0ffff..09a4c7ca 100644 --- a/SharedProject/Core/MsTestPlatform/MsTestPlatformUtil.cs +++ b/SharedProject/Core/MsTestPlatform/MsTestPlatformUtil.cs @@ -10,20 +10,18 @@ namespace FineCodeCoverage.Engine.MsTestPlatform internal class MsTestPlatformUtil:IMsTestPlatformUtil { public string MsTestPlatformExePath { get; private set; } - private readonly IToolFolder toolFolder; - private readonly IToolZipProvider toolZipProvider; - private const string zipPrefix = "microsoft.testplatform"; + private readonly IToolUnzipper toolUnzipper; + private const string zipPrefix = "microsoft.testplatform"; private const string zipDirectoryName = "msTestPlatform"; [ImportingConstructor] - public MsTestPlatformUtil(IToolFolder toolFolder, IToolZipProvider toolZipProvider) + public MsTestPlatformUtil(IToolUnzipper toolUnzipper) { - this.toolFolder = toolFolder; - this.toolZipProvider = toolZipProvider; + this.toolUnzipper = toolUnzipper; } public void Initialize(string appDataFolder, CancellationToken cancellationToken) { - var zipDestination = toolFolder.EnsureUnzipped(appDataFolder, zipDirectoryName, toolZipProvider.ProvideZip(zipPrefix), cancellationToken); + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolder, zipDirectoryName, zipPrefix, cancellationToken); MsTestPlatformExePath = Directory .GetFiles(zipDestination, "vstest.console.exe", SearchOption.AllDirectories) .FirstOrDefault(); diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs new file mode 100644 index 00000000..cee56e68 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs @@ -0,0 +1,127 @@ +using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Options; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Build; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + // https://github.com/dotnet/project-system + // https://github.com/microsoft/VSProjectSystem/blob/master/doc/extensibility/IProjectGlobalPropertiesProvider.md + [Export(typeof(IProjectGlobalPropertiesProvider))] + // https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/buildMultiTargeting/Microsoft.Testing.Platform.props + /* + https://github.com/microsoft/VSProjectSystem/blob/master/doc/overview/about_project_capabilities.md + Classes exported via MEF can declare the project capabilities under which they apply. + + See https://learn.microsoft.com/en-gb/dotnet/api/microsoft.visualstudio.shell.interop.vsprojectcapabilityexpressionmatcher?view=visualstudiosdk-2022 + For expression syntax + */ + [AppliesTo("TestContainer")] + internal class DisableTestingPlatformServerCapabilityGlobalPropertiesProvider : StaticGlobalPropertiesProviderBase + { + private readonly UnconfiguredProject unconfiguredProject; + private readonly IAppOptionsProvider appOptionsProvider; + private readonly ICoverageProjectSettingsManager coverageProjectSettingsManager; + + [ImportingConstructor] + public DisableTestingPlatformServerCapabilityGlobalPropertiesProvider( + IProjectService projectService, + UnconfiguredProject unconfiguredProject, + IAppOptionsProvider appOptionsProvider, + ICoverageProjectSettingsManager coverageProjectSettingsManager + ) + : base((IProjectCommonServices)projectService.Services) + { + this.unconfiguredProject = unconfiguredProject; + this.appOptionsProvider = appOptionsProvider; + this.coverageProjectSettingsManager = coverageProjectSettingsManager; + } + + private bool AllProjectsDisabled() + { + var appOptions = appOptionsProvider.Get(); + return !appOptions.Enabled && appOptions.DisabledNoCoverage; + } + + private async Task IsTestProjectAsync(ConfiguredProject configuredProject) + { + var commonProperties = configuredProject.Services.ProjectPropertiesProvider.GetCommonProperties(); + var isTestProjectPropertValue = await commonProperties.GetEvaluatedPropertyValueAsync("IsTestProject"); + if (String.IsNullOrEmpty(isTestProjectPropertValue)) { return false; } + if (bool.TryParse(isTestProjectPropertValue, out var isTestProject)){ + return isTestProject; + } + return false; + } + + private async Task IsApplicableAsync() + { + try + { + var configuredProject = await unconfiguredProject.GetSuggestedConfiguredProjectAsync(); + var isTestProject = await IsTestProjectAsync(configuredProject); + if (!isTestProject) return false; + var references = await configuredProject.Services.PackageReferences.GetUnresolvedReferencesAsync(); + return !references.Any(r => r.UnevaluatedInclude == TUnitConstants.TUnitPackageId); + } + catch { } + return false; + } + + private async Task ProjectEnabledAsync() + { + var projectGuid = await GetProjectGuidAsync(); + if (!projectGuid.HasValue) return false; + + var coverageProject = GetCoverageProject(projectGuid.Value); + var projectSettings = await coverageProjectSettingsManager.GetSettingsAsync(coverageProject); + return projectSettings.Enabled; + } + + private async Task GetProjectGuidAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var hostObject = unconfiguredProject.Services.HostObject; + + var vsHierarchy = (IVsHierarchy)hostObject; + if (vsHierarchy != null) + { + var success = vsHierarchy.GetGuidProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID.VSHPROPID_ProjectIDGuid, out Guid projectGuid) == VSConstants.S_OK; + + if (success) + { + return projectGuid; + } + } + return null; + } + + private CoverageProject GetCoverageProject(Guid projectGuid) + { + return new CoverageProject(appOptionsProvider, null, coverageProjectSettingsManager, null) + { + Id = projectGuid, + ProjectFile = unconfiguredProject.FullPath + }; + } + + public override async Task> GetGlobalPropertiesAsync(CancellationToken cancellationToken) + { + if (await IsApplicableAsync() && !AllProjectsDisabled() && await ProjectEnabledAsync()) + { + return Empty.PropertiesMap.Add("DisableTestingPlatformServerCapability", "true"); + } + return Empty.PropertiesMap; + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/BuildHelper.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/BuildHelper.cs new file mode 100644 index 00000000..c07c3138 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/BuildHelper.cs @@ -0,0 +1,147 @@ +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.ComponentModel.Composition; +using System.Threading; +using Microsoft; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal class BuildStartEnd : IVsUpdateSolutionEvents { + public event EventHandler BuildEvent; + + public int UpdateSolution_Begin(ref int pfCancelUpdate) + { + return VSConstants.S_OK; + } + + public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand) + { + BuildEvent?.Invoke(this, new BuildStartEndArgs(false)); + return VSConstants.S_OK; + } + + public int UpdateSolution_StartUpdate(ref int pfCancelUpdate) + { + BuildEvent?.Invoke(this, new BuildStartEndArgs(true)); + return VSConstants.S_OK; + } + + public int UpdateSolution_Cancel() + { + return VSConstants.S_OK; + } + + public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy) + { + return VSConstants.S_OK; + } + } + + internal class BuildCompletionHandler : IVsUpdateSolutionEvents, IDisposable + { + private readonly TaskCompletionSource _tcs = new TaskCompletionSource(); + private readonly CancellationTokenRegistration registration; + public BuildCompletionHandler(CancellationToken cancellationToken) + { + registration = cancellationToken.Register(() => _tcs.TrySetCanceled()); + } + + /// + /// Task that completes when the build finishes. + /// + public Task BuildCompleted => _tcs.Task; + + public int UpdateSolution_Begin(ref int pfCancelUpdate) => VSConstants.S_OK; + + public int UpdateSolution_Cancel() => VSConstants.S_OK; + + public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand) + { + var cancelled = fCancelCommand == 1; + var nonFailed = fSucceeded == 1; + var anySucceeded = fModified == 1; + + // Signal the task completion. + _tcs.TrySetResult(fSucceeded != 0); + return VSConstants.S_OK; + } + + public int UpdateSolution_StartUpdate(ref int pfCancelUpdate) => VSConstants.S_OK; + + public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy) => VSConstants.S_OK; + + public void Dispose() + { + registration.Dispose(); + } + } + + [Export(typeof(IBuildHelper))] + internal class BuildHelper : IBuildHelper + { + private IVsSolutionBuildManager2 solutionBuildManager2; + private BuildStartEnd buildStartEnd; + private bool building; + public event EventHandler ExternalBuildEvent; + + [ImportingConstructor] + public BuildHelper( + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider + ) + { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously + ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + this.solutionBuildManager2 = serviceProvider.GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager2; + Assumes.Present(this.solutionBuildManager2); + buildStartEnd = new BuildStartEnd(); + this.solutionBuildManager2.AdviseUpdateSolutionEvents(buildStartEnd, out var cookie); + buildStartEnd.BuildEvent += BuildStartEnd_BuildEvent; + }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously + } + + private void BuildStartEnd_BuildEvent(object sender, BuildStartEndArgs e) + { + if (!building) + { + ExternalBuildEvent?.Invoke(this, e); + } + } + + public async Task BuildAsync(List projects,CancellationToken cancellationToken) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var buildHandler = new BuildCompletionHandler(cancellationToken); + int hr = solutionBuildManager2.AdviseUpdateSolutionEvents(buildHandler, out uint cookie); + ErrorHandler.ThrowOnFailure(hr); + var succeeded = false; + try + { + building = true; + var result = solutionBuildManager2.StartSimpleUpdateSolutionConfiguration( + (uint)VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_BUILD, 0, 0); + ErrorHandler.ThrowOnFailure(result); + succeeded = await buildHandler.BuildCompleted; + } + catch (OperationCanceledException) + { + solutionBuildManager2.CancelUpdateSolutionConfiguration(); + throw; + } + finally + { + building = false; + buildHandler.Dispose(); + solutionBuildManager2.UnadviseUpdateSolutionEvents(cookie); + } + return succeeded; + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CPSTestProjectService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CPSTestProjectService.cs new file mode 100644 index 00000000..756e70fc --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CPSTestProjectService.cs @@ -0,0 +1,23 @@ +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ICPSTestProjectService))] + internal class CPSTestProjectService : ICPSTestProjectService + { + public async Task GetProjectAsync(IVsHierarchy hierarchy) + { + if (!hierarchy.IsCapabilityMatch("TestContainer")) + { + return null; + } + var unconfiguredProject = await hierarchy.AsUnconfiguredProjectAsync(); + if (unconfiguredProject == null) return null; + return await unconfiguredProject.GetSuggestedConfiguredProjectAsync(); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseOption.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseOption.cs new file mode 100644 index 00000000..37daea1a --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseOption.cs @@ -0,0 +1,24 @@ +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + /* + adjusted from + https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/CommandLine/OptionRecord.cs + */ + internal sealed class CommandLineParseOption + { + public CommandLineParseOption(string name, string[] arguments) + { + Name = name; + Arguments = arguments; + } + /// + /// Gets the name of the option. + /// + public string Name { get; } + + /// + /// Gets the arguments of the option. + /// + public string[] Arguments { get; } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseResult.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseResult.cs new file mode 100644 index 00000000..0c2f8936 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseResult.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + /* + adjusted from + https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/CommandLine/ParseResult.cs + */ + internal class CommandLineParseResult + { + public CommandLineParseResult( + IReadOnlyList options, + IReadOnlyList errors) + { + Options = options; + Errors = errors; + } + + public static CommandLineParseResult Empty { get; } = new CommandLineParseResult(Enumerable.Empty().ToList(),Enumerable.Empty().ToList()); + + public const char OptionPrefix = '-'; + + public IReadOnlyList Options { get; } + public IReadOnlyList Errors { get; } + + public bool HasError => Errors.Count > 0; + + public bool IsOptionSet(string optionName) + => Options.Any(o => o.Name.Equals(optionName.Trim(OptionPrefix), StringComparison.OrdinalIgnoreCase)); + + public bool TryGetOptionArgumentList(string optionName, out string[] arguments) + { + optionName = optionName.Trim(OptionPrefix); + IEnumerable result = Options.Where(x => x.Name == optionName); + if (result.Any()) + { + arguments = result.SelectMany(x => x.Arguments).ToArray(); + return true; + } + + arguments = null; + return false; + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParser.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParser.cs new file mode 100644 index 00000000..b1925e88 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParser.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.CommandLine.Parsing; +using System.ComponentModel.Composition; +using System.Linq; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ICommandLineParser + { + CommandLineParseResult Parse(string argumentsString); + } + + /* + Adjusted from + https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs + without escaping + */ + [Export(typeof(ICommandLineParser))] + internal class CommandLineParser : ICommandLineParser + { + public CommandLineParseResult Parse(string argumentsString) + { + var args = CommandLineStringSplitter.Instance.Split(argumentsString).ToList(); + if (!args.Any()) + { + return CommandLineParseResult.Empty; + } + return Parse(args); + } + + public CommandLineParseResult Parse(string[] args) + => Parse(args.ToList()); + + private CommandLineParseResult Parse(List args) + { + List options = new List(); + List errors = new List(); + + string currentOption = null; + string currentArg = null; + string toolName = null; + List currentOptionArguments = new List(); + for (int i = 0; i < args.Count; i++) + { + if (args[i].StartsWith("@") && ResponseFileHelper.TryReadResponseFile(args[i].Substring(1), errors, out string[] newArguments)) + { + args.InsertRange(i + 1, newArguments); + continue; + } + bool argumentHandled = false; + currentArg = args[i]; + + while (!argumentHandled) + { + if (currentArg is null) + { + errors.Add($"UnexpectedNullArgument {i}"); + break; + } + + // we accept as start for options -- and - all the rest are arguments to the previous option + if ((args[i].Length > 1 && currentArg[0].Equals('-') && !currentArg[1].Equals('-')) || + (args[i].Length > 2 && currentArg[0].Equals('-') && currentArg[1].Equals('-') && !currentArg[2].Equals('-'))) + { + if (currentOption is null) + { + ParseOptionAndSeparators(args[i], out currentOption, out currentArg); + argumentHandled = currentArg is null; + } + else + { + options.Add(new CommandLineParseOption(currentOption, currentOptionArguments.ToArray())); + currentOptionArguments.Clear(); + ParseOptionAndSeparators(args[i], out currentOption, out currentArg); + argumentHandled = true; + } + } + else + { + // If it's the first argument and it doesn't start with - then it's the tool name + if (i == 0 && !args[0][0].Equals('-')) + { + toolName = currentArg; + } + else if (currentOption is null) + { + errors.Add($"UnexpectedArgument {args[i]}"); + } + else + { + currentOptionArguments.Add(currentArg.Trim()); + currentArg = null; + } + + argumentHandled = true; + } + } + } + + if (currentOption != null) + { + if (currentArg != null) + { + currentOptionArguments.Add(currentArg.Trim()); + } + + options.Add(new CommandLineParseOption(currentOption, currentOptionArguments.ToArray())); + } + + return new CommandLineParseResult(options, errors); + } + + private static void ParseOptionAndSeparators(string arg, out string currentOption, out string currentArg) + { + var delimiterIndex = arg.IndexOfAny(new char[] { ':', '=', ' ' }); + if (delimiterIndex == -1) + { + currentOption = arg; + currentArg = null; + } + else + { + currentOption = arg.Substring(0, delimiterIndex); + currentArg = arg.Substring(++delimiterIndex); + } + currentOption = currentOption.TrimStart('-'); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IBuildHelper.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IBuildHelper.cs new file mode 100644 index 00000000..c67957d3 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IBuildHelper.cs @@ -0,0 +1,22 @@ +using Microsoft.VisualStudio.Shell.Interop; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal class BuildStartEndArgs + { + public BuildStartEndArgs(bool isStart) + { + IsStart = isStart; + } + + public bool IsStart { get; } + } + internal interface IBuildHelper + { + event EventHandler ExternalBuildEvent; + Task BuildAsync(List projects, System.Threading.CancellationToken cancellationToken); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ICPSTestProjectService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ICPSTestProjectService.cs new file mode 100644 index 00000000..b01fbfe8 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ICPSTestProjectService.cs @@ -0,0 +1,11 @@ +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell.Interop; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ICPSTestProjectService + { + Task GetProjectAsync(IVsHierarchy hierarchy); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/INugetProjectServiceProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/INugetProjectServiceProvider.cs new file mode 100644 index 00000000..65d0b471 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/INugetProjectServiceProvider.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.Threading; +using NuGet.VisualStudio.Contracts; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface INugetProjectServiceProvider + { + AsyncLazy LazyNugetProjectService { get; } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IRunSettingsToConfiguration.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IRunSettingsToConfiguration.cs new file mode 100644 index 00000000..d1f89b05 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IRunSettingsToConfiguration.cs @@ -0,0 +1,9 @@ +using System.Xml.Linq; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface IRunSettingsToConfiguration + { + XElement ConvertToConfiguration(XElement runSettingsElement); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ISolutionProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ISolutionProjectsProvider.cs new file mode 100644 index 00000000..500cb448 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ISolutionProjectsProvider.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.Shell.Interop; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ISolutionProjectsProvider + { + Task IsSolutionOpenAsync(); + Task> GetLoadedProjectsAsync(CancellationToken cancellationToken ); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitChangeNotifier.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitChangeNotifier.cs new file mode 100644 index 00000000..25dbdd5d --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitChangeNotifier.cs @@ -0,0 +1,23 @@ +using Microsoft.VisualStudio.Shell.Interop; +using System; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal class ProjectAddedRemoved + { + public ProjectAddedRemoved(bool added, IVsHierarchy project) + { + Added = added; + Project = project; + } + public bool Added { get; } + public IVsHierarchy Project { get; } + } + + internal interface ITUnitChangeNotifier + { + event EventHandler ProjectAddedRemovedEvent; + event EventHandler SolutionClosedEvent; + event EventHandler SolutionOpenedEvent; + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverage.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverage.cs new file mode 100644 index 00000000..7d8ca2b6 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverage.cs @@ -0,0 +1,14 @@ +using System; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + interface ITUnitCoverage + { + bool Ready { get; } + + event EventHandler CollectingChangedEvent; + event EventHandler ReadyEvent; + void CollectCoverage(); + void Cancel(); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageProjectFactory.cs new file mode 100644 index 00000000..3b0e50ba --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageProjectFactory.cs @@ -0,0 +1,22 @@ +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Shell.Interop; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ITUnitCoverageProject + { + string ExePath { get; } + Task GetConfigurationAsync(CancellationToken cancellationToken); + ICoverageProject CoverageProject { get; } + IVsHierarchy VsHierarchy { get; } + bool HasCoverageExtension { get; } + CommandLineParseResult CommandLineParseResult { get; } + } + internal interface ITUnitCoverageProjectFactory + { + Task CreateTUnitCoverageProjectAsync( + ITUnitProject tUnitProject,CancellationToken cancellationToken); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageRunner.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageRunner.cs new file mode 100644 index 00000000..48b109c1 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageRunner.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal class TUnitSettings + { + public TUnitSettings( + string exePath, + string settingsPath, + string outputPath, + string additionalArgs + ) + { + ExePath = exePath; + SettingsPath = settingsPath; + OutputPath = outputPath; + AdditionalArgs = additionalArgs; + } + + public string ExePath { get; } + public string SettingsPath { get; } + public string OutputPath { get; } + public string AdditionalArgs { get; } + } + + internal interface ITUnitCoverageRunner + { + event EventHandler ReadyEvent; + void Initialize(string appDataFolderPath, CancellationToken cancellationToken); + Task RunAsync( + TUnitSettings tUnitSettings, + bool hasCoverageExtension, + bool showWindow = false, + CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitInstalledPackagesService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitInstalledPackagesService.cs new file mode 100644 index 00000000..f3a9c37c --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitInstalledPackagesService.cs @@ -0,0 +1,28 @@ +using NuGet.VisualStudio.Contracts; +using System.Threading.Tasks; +using System.Threading; +using System; +using System.Collections.Immutable; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal class TUnitInstalledPackageResult + { + public TUnitInstalledPackageResult(InstalledPackageResultStatus status, bool hasCoverageExtension, bool hasTunit) + { + Status = status; + HasCoverageExtension = hasCoverageExtension; + HasTUnit = hasTunit; + } + + public bool HasTUnit { get; } + public bool HasCoverageExtension { get; } + public InstalledPackageResultStatus Status { get; } + } + + interface ITUnitInstalledPackagesService + { + TUnitInstalledPackageResult GetTUnitInstalledPackages(IImmutableDictionary> packageReferenceItems); + Task GetTUnitInstalledPackagesAsync(Guid projectGuid, CancellationToken cancellationToken); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectCache.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectCache.cs new file mode 100644 index 00000000..8ab9025b --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectCache.cs @@ -0,0 +1,16 @@ +using Microsoft.VisualStudio.Shell.Interop; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ITUnitProjectCache + { + void Initialize(List tUnitProjects); + Task> GetTUnitProjectsAsync(CancellationToken cancellationToken); + void Remove(IVsHierarchy project); + void Add(ITUnitProject iTUnitProject); + void Clear(); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectFactory.cs new file mode 100644 index 00000000..0f1377b4 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectFactory.cs @@ -0,0 +1,22 @@ +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell.Interop; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ITUnitProject : IDisposable + { + bool IsTUnit { get;} // probably will not change + bool HasCoverageExtension { get;} // could change + IVsHierarchy Hierarchy { get; } + Task UpdateStateAsync(CancellationToken cancellationToken); + CommandLineParseResult CommandLineParseResult { get; } + } + + internal interface ITUnitProjectFactory + { + ITUnitProject Create(IVsHierarchy project, ConfiguredProject configuredProject); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectsProvider.cs new file mode 100644 index 00000000..d0cefd4a --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectsProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ITUnitProjectsProvider + { + bool Ready { get; } + + event EventHandler ReadyEvent; + Task> GetTUnitProjectsAsync(CancellationToken cancellationToken); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitSettingsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitSettingsProvider.cs new file mode 100644 index 00000000..082a86ad --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitSettingsProvider.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using System.Threading; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal interface ITUnitSettingsProvider + { + Task ProvideAsync(ITUnitCoverageProject tUnitCoverageProject, CancellationToken cancellationToken); + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IVsHierarchyExtensions.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IVsHierarchyExtensions.cs new file mode 100644 index 00000000..698c7c98 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IVsHierarchyExtensions.cs @@ -0,0 +1,69 @@ +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio; +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.ProjectSystem; +using EnvDTE; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal static class IVsHierarchyExtensions + { + // https://github.com/microsoft/VSProjectSystem/blob/master/doc/automation/finding_CPS_in_a_VS_project.md + public async static Task AsUnconfiguredProjectAsync(this IVsHierarchy hier) + { + if (hier is IVsBrowseObjectContext context) + { + return context.UnconfiguredProject; + } + else + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + object pvar; + if (ErrorHandler.Succeeded(hier.GetProperty(4294967294U, -2027, out pvar)) && pvar is Project project) + { + context = project.Object as IVsBrowseObjectContext; + return context.UnconfiguredProject; + } + } + + return null; + } + + public static Project ToProject(this IVsHierarchy hierarchy) + { + ThreadHelper.ThrowIfNotOnUIThread(); + // Retrieve the automation object from the root of the hierarchy. + int hr = hierarchy.GetProperty( + VSConstants.VSITEMID_ROOT, + (int)__VSHPROPID.VSHPROPID_ExtObject, + out object extObject); + + if (ErrorHandler.Succeeded(hr) && extObject is Project project) + { + return project; + } + + return null; + } + + public static Guid GetGuid(this IVsHierarchy hierarchy) + { + ThreadHelper.ThrowIfNotOnUIThread(); + int hr = hierarchy.GetGuidProperty( + VSConstants.VSITEMID_ROOT, + (int)__VSHPROPID.VSHPROPID_ProjectIDGuid, + out Guid projectGuid); + + return projectGuid; + } + + public async static Task GetGuidAsync(this IVsHierarchy hierarchy) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + return GetGuid(hierarchy); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/NugetProjectServiceProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/NugetProjectServiceProvider.cs new file mode 100644 index 00000000..4de28763 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/NugetProjectServiceProvider.cs @@ -0,0 +1,34 @@ +using FineCodeCoverage.Core.Utilities; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Microsoft.VisualStudio.Threading; +using NuGet.VisualStudio.Contracts; +using System; +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(INugetProjectServiceProvider))] + internal class NugetProjectServiceProvider : INugetProjectServiceProvider + { + public AsyncLazy LazyNugetProjectService { get; } + + [ImportingConstructor] + public NugetProjectServiceProvider( + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider + ) + { + LazyNugetProjectService = new AsyncLazy(async () => + { + var brokeredServiceContainer = serviceProvider.GetService(); + IServiceBroker serviceBroker = brokeredServiceContainer.GetFullAccessServiceBroker(); +#pragma warning disable ISB001 // Dispose of proxies + INuGetProjectService nugetProjectService = await serviceBroker.GetProxyAsync(NuGetServices.NuGetProjectServiceV1); +#pragma warning restore ISB001 // Dispose of proxies + return nugetProjectService; + }, ThreadHelper.JoinableTaskFactory); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ResponseFileHelper.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ResponseFileHelper.cs new file mode 100644 index 00000000..1ed67900 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ResponseFileHelper.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal static class ResponseFileHelper + { + private enum Boundary + { + TokenStart, + WordEnd, + QuoteStart, + QuoteEnd, + } + + internal static bool TryReadResponseFile(string rspFilePath, ICollection errors, out string[] newArguments) + { + try + { + newArguments = ExpandResponseFile(rspFilePath).ToArray(); + return true; + } + catch (FileNotFoundException) + { + errors.Add($"ResponseFileNotFound {rspFilePath}"); + } + catch (IOException e) + { + errors.Add($"FailedToReadResponseFile {rspFilePath} {e.ToString()}"); + } + + newArguments = null; + return false; + + // Local functions + IEnumerable ExpandResponseFile(string filePath) + { + string[] lines = File.ReadAllLines(filePath); + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + + foreach (string p in SplitLine(line)) + { + yield return p; + } + } + } + + IEnumerable SplitLine(string line) + { + string arg = line.Trim(); + + if (arg.Length == 0 || arg[0] == '#') + { + yield break; + } + + foreach (string word in SplitCommandLine(arg)) + { + yield return word; + } + } + } + + public static IEnumerable SplitCommandLine(string commandLine) + { + int startTokenIndex = 0; + + int pos = 0; + + Boundary seeking = Boundary.TokenStart; + Boundary seekingQuote = Boundary.QuoteStart; + + while (pos < commandLine.Length) + { + char c = commandLine[pos]; + + if (char.IsWhiteSpace(c)) + { + if (seekingQuote == Boundary.QuoteStart) + { + switch (seeking) + { + case Boundary.WordEnd: + yield return CurrentToken(); + startTokenIndex = pos; + seeking = Boundary.TokenStart; + break; + + case Boundary.TokenStart: + startTokenIndex = pos; + break; + } + } + } + else if (c == '\"') + { + if (seeking == Boundary.TokenStart) + { + switch (seekingQuote) + { + case Boundary.QuoteEnd: + yield return CurrentToken(); + startTokenIndex = pos; + seekingQuote = Boundary.QuoteStart; + break; + + case Boundary.QuoteStart: + startTokenIndex = pos + 1; + seekingQuote = Boundary.QuoteEnd; + break; + } + } + else + { + switch (seekingQuote) + { + case Boundary.QuoteEnd: + seekingQuote = Boundary.QuoteStart; + break; + + case Boundary.QuoteStart: + seekingQuote = Boundary.QuoteEnd; + break; + } + } + } + else if (seeking == Boundary.TokenStart && seekingQuote == Boundary.QuoteStart) + { + seeking = Boundary.WordEnd; + startTokenIndex = pos; + } + + Advance(); + + if (IsAtEndOfInput()) + { + switch (seeking) + { + case Boundary.TokenStart: + break; + default: + yield return CurrentToken(); + break; + } + } + } + + void Advance() => pos++; + + string CurrentToken() => commandLine.Substring(startTokenIndex, IndexOfEndOfToken()).Replace("\"", string.Empty); + + int IndexOfEndOfToken() => pos - startTokenIndex; + + bool IsAtEndOfInput() => pos == commandLine.Length; + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/RunSettingsToConfiguration.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/RunSettingsToConfiguration.cs new file mode 100644 index 00000000..e4eff207 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/RunSettingsToConfiguration.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.Composition; +using System.Linq; +using System.Xml.Linq; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(IRunSettingsToConfiguration))] + internal class RunSettingsToConfiguration : IRunSettingsToConfiguration + { + public XElement ConvertToConfiguration(XElement runSettingsElement) + { + var dataCollectorsElement = runSettingsElement.Element("DataCollectionRunSettings").Element("DataCollectors"); + var codeCoverageDataCollectorElement = dataCollectorsElement.Elements().FirstOrDefault(dataCollectorElement => + { + var friendlyName = dataCollectorElement.Attribute((XName)"friendlyName")?.Value ?? string.Empty; + return friendlyName.Equals("Code Coverage", StringComparison.OrdinalIgnoreCase); + }); + return codeCoverageDataCollectorElement?.Element("Configuration"); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/SolutionProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/SolutionProjectsProvider.cs new file mode 100644 index 00000000..7930ea5c --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/SolutionProjectsProvider.cs @@ -0,0 +1,71 @@ +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.ComponentModel.Composition; +using System.Threading; +using Microsoft; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + + [Export(typeof(ISolutionProjectsProvider))] + internal class SolutionProjectsProvider : ISolutionProjectsProvider + { + private readonly IServiceProvider serviceProvider; + + [ImportingConstructor] + public SolutionProjectsProvider( + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider + ) + { + this.serviceProvider = serviceProvider; + } + + public async Task> GetLoadedProjectsAsync(CancellationToken cancellationToken) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution; + return GetProjects(vsSolution, __VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION); + } + + public async Task IsSolutionOpenAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution; + Assumes.Present(vsSolution); + vsSolution.GetProperty((int)__VSPROPID.VSPROPID_IsSolutionOpen, out var isSolutionOpen); + return (bool)isSolutionOpen; + } + + private List GetProjects(IVsSolution vsSolution, __VSENUMPROJFLAGS flags) + { + ThreadHelper.ThrowIfNotOnUIThread(); + var projects = new List(); + var result = vsSolution.GetProjectEnum((uint)flags, Guid.Empty, out var enumHierarchies); + if (result == VSConstants.S_OK) + { + IVsHierarchy[] rgelt = new IVsHierarchy[1]; + uint fetched = 0; + while (enumHierarchies.Next(1, rgelt, out fetched) == VSConstants.S_OK && fetched > 0) + { + int hr = rgelt[0].GetGuidProperty( + VSConstants.VSITEMID_ROOT, + (int)__VSHPROPID.VSHPROPID_TypeGuid, + out var typeGuid + ); + + if (typeGuid != VSConstants.GUID_ItemType_VirtualFolder) + { + projects.Add(rgelt[0]); + } + } + } + return projects; + } + } + +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitChangeNotifier.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitChangeNotifier.cs new file mode 100644 index 00000000..f09facdc --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitChangeNotifier.cs @@ -0,0 +1,100 @@ +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio; +using System; +using System.ComponentModel.Composition; +using Microsoft; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitChangeNotifier))] + internal class TUnitChangeNotifier : ITUnitChangeNotifier, IVsSolutionEvents + { + public event EventHandler ProjectAddedRemovedEvent; + public event EventHandler SolutionClosedEvent; + public event EventHandler SolutionOpenedEvent; + + [ImportingConstructor] + public TUnitChangeNotifier( + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider + ) + { +#pragma warning disable VSTHRD102 // Implement internal logic asynchronously + ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution; + Assumes.Present(vsSolution); + vsSolution.AdviseSolutionEvents(this, out uint _); + }); +#pragma warning restore VSTHRD102 // Implement internal logic asynchronously + } + + + #region solution events + public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) + { + if (fAdded == 1) + { + ProjectAddedRemovedEvent?.Invoke(this, new ProjectAddedRemoved(true, pHierarchy)); + } + return VSConstants.S_OK; + } + + public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) + { + if (fRemoved == 1) + { + ProjectAddedRemovedEvent?.Invoke(this, new ProjectAddedRemoved(false, pHierarchy)); + } + return VSConstants.S_OK; + } + + public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) + { + return VSConstants.S_OK; + } + + public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) + { + return VSConstants.S_OK; + } + + public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) + { + return VSConstants.S_OK; + } + + public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) + { + return VSConstants.S_OK; + } + + public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) + { + SolutionOpenedEvent?.Invoke(this, EventArgs.Empty); + return VSConstants.S_OK; + } + + public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) + { + return VSConstants.S_OK; + } + + public int OnBeforeCloseSolution(object pUnkReserved) + { + SolutionClosedEvent?.Invoke(this, EventArgs.Empty); + return VSConstants.S_OK; + } + + public int OnAfterCloseSolution(object pUnkReserved) + { + return VSConstants.S_OK; + } + #endregion + + + } + +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitConstants.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitConstants.cs new file mode 100644 index 00000000..aaea4f36 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitConstants.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + internal abstract class TUnitConstants + { + public const string TUnitPackageId = "TUnit"; + public const string CodeCoveragePackageId = "Microsoft.Testing.Extensions.CodeCoverage"; + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverage.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverage.cs new file mode 100644 index 00000000..cecb9e84 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverage.cs @@ -0,0 +1,232 @@ +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Engine.ReportGenerator; +using FineCodeCoverage.Impl; +using FineCodeCoverage.Impl.TestContainerDiscovery; +using FineCodeCoverage.Output; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Task = System.Threading.Tasks.Task; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitCoverage))] + [Export(typeof(ICoverageCollectableFromTestExplorer))] + internal class TUnitCoverage : ITUnitCoverage, ICoverageCollectableFromTestExplorer + { + private readonly ITUnitProjectsProvider tUnitProjectsProvider; + private readonly IBuildHelper buildHelper; + private readonly ITUnitCoverageProjectFactory tUnitCoverageProjectFactory; + private readonly ITUnitCoverageRunner tUnitCoverageRunner; + private readonly ICoverageToolOutputManager coverageToolOutputManager; + private readonly IFCCEngine fccEngine; + private readonly ITUnitSettingsProvider tUnitSettingsProvider; + private readonly IDisposeAwareTaskRunner disposeAwareTaskRunner; + private readonly IEventAggregator eventAggregator; + private readonly ILogger logger; + private readonly IReportGeneratorUtil reportGeneratorUtil; + private int coverageRunNumber = 1; + private ICancellationTokenSource cancellationTokenSource; + private bool runnerReady; + private bool projectsProviderReady; + private bool externalBuildInProgress; + + public event EventHandler ReadyEvent; + public event EventHandler CollectingChangedEvent; + + [ImportingConstructor] + public TUnitCoverage( + ITUnitProjectsProvider tUnitProjectsProvider, + IBuildHelper buildHelper, + ITUnitCoverageProjectFactory tUnitCoverageProjectFactory, + ITUnitCoverageRunner tUnitCoverageRunner, + ICoverageToolOutputManager coverageToolOutputManager, + IFCCEngine fccEngine, + ITUnitSettingsProvider tUnitSettingsProvider, + IDisposeAwareTaskRunner disposeAwareTaskRunner, + IEventAggregator eventAggregator, + ILogger logger, + IReportGeneratorUtil reportGeneratorUtil + ) + { + buildHelper.ExternalBuildEvent += BuildHelper_ExternalBuildEvent; + tUnitProjectsProvider.ReadyEvent += TUnitProjectsProvider_ReadyEvent; + tUnitCoverageRunner.ReadyEvent += TUnitRunner_ReadyEvent; + projectsProviderReady = tUnitProjectsProvider.Ready; + this.tUnitProjectsProvider = tUnitProjectsProvider; + this.buildHelper = buildHelper; + this.tUnitCoverageProjectFactory = tUnitCoverageProjectFactory; + this.tUnitCoverageRunner = tUnitCoverageRunner; + this.coverageToolOutputManager = coverageToolOutputManager; + this.fccEngine = fccEngine; + this.tUnitSettingsProvider = tUnitSettingsProvider; + this.disposeAwareTaskRunner = disposeAwareTaskRunner; + this.eventAggregator = eventAggregator; + this.logger = logger; + this.reportGeneratorUtil = reportGeneratorUtil; + } + + private void BuildHelper_ExternalBuildEvent(object sender, BuildStartEndArgs e) + { + externalBuildInProgress = e.IsStart; + OnReady(); + } + + private void TUnitRunner_ReadyEvent(object sender, EventArgs e) + { + runnerReady = true; + OnReady(); + } + + private void TUnitProjectsProvider_ReadyEvent(object sender, EventArgs e) + { + projectsProviderReady = tUnitProjectsProvider.Ready; + OnReady(); + } + + public bool Ready => runnerReady && projectsProviderReady && !externalBuildInProgress; + private void OnReady() + { + ReadyEvent?.Invoke(this, EventArgs.Empty); + } + + protected void OnCollectingChanged(bool collecting) + { + CollectingChangedEvent?.Invoke(this, collecting); + } + + public void Cancel() + { + try + { + if(cancellationTokenSource?.IsCancellationRequested == false) + { + cancellationTokenSource.Cancel(); + } + } + catch (ObjectDisposedException) { } + } + + public void CollectCoverage() + { + Cancel(); + cancellationTokenSource = disposeAwareTaskRunner.CreateLinkedTokenSource(); + disposeAwareTaskRunner.RunAsyncFunc(CollectCoverageAsync); + } + + private void LogCoverageStarting() + { + reportGeneratorUtil.LogCoverageProcess($"Coverage Starting - {coverageRunNumber++}"); + logger.Log(StatusMarkerProvider.Get($"Coverage Starting - {coverageRunNumber++}")); + } + + private async Task> GetEnabledTUnitProjectsAsync(CancellationToken cancellationToken) + { + var tUnitProjects = await tUnitProjectsProvider.GetTUnitProjectsAsync(cancellationToken); + var tUnitCoverageProjects = await Task.WhenAll(tUnitProjects.Select(tUnitProject => tUnitCoverageProjectFactory.CreateTUnitCoverageProjectAsync(tUnitProject, cancellationToken))); + return tUnitCoverageProjects.Where(tp => tp.CoverageProject.Settings.Enabled).ToList(); + } + + private async Task CollectCoverageAsync() + { + var cancellationToken = cancellationTokenSource.Token; + LogCoverageStarting(); + this.eventAggregator.SendMessage(new TestExecutionStartingMessage()); + + OnCollectingChanged(true);//order important + + var raiseCoverageEndedMessage = true; + try + { + var tUnitCoverageProjects = await GetEnabledTUnitProjectsAsync(cancellationToken); + if (tUnitCoverageProjects.Any()) + { + var success = await BuildAndCollectAsync(tUnitCoverageProjects, cancellationToken); + raiseCoverageEndedMessage = !success; + } + else + { + logger.Log("No enabled Tunit test projects."); + } + } + catch (OperationCanceledException) + { + logger.Log("Coverage collection cancelled"); + } + catch(Exception exc) + { + logger.Log(exc.ToString()); + } + if (raiseCoverageEndedMessage) + { + reportGeneratorUtil.EndOfCoverageRun(); + } + cancellationTokenSource.Dispose(); + cancellationTokenSource = null; + OnCollectingChanged(false); + } + + private async Task BuildAndCollectAsync(List tUnitCoverageProjects, CancellationToken cancellationToken) + { + var success = false; + logger.Log("Starting build"); + var buildSuccess = await buildHelper.BuildAsync(tUnitCoverageProjects.ConvertAll(tp => tp.VsHierarchy), cancellationToken); + if (buildSuccess) + { + success = await CollectCoverageAsync(tUnitCoverageProjects, cancellationToken); + } + else + { + logger.Log("Unsuccessful build. Not collecting coverage"); + } + return success; + } + + private async Task CollectCoverageAsync(List tUnitCoverageProjects, CancellationToken cancellationToken) + { + logger.Log($"Collecting coverage for {tUnitCoverageProjects.Count} enabled TUnit test projects with coverage extension"); + + var coverageProjects = tUnitCoverageProjects.ConvertAll(tUnitCoverageProject => tUnitCoverageProject.CoverageProject); + cancellationToken.ThrowIfCancellationRequested(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + + var runAllProjects = true; + List coberturaFiles = new List(); + foreach (var tUnitCoverageProject in tUnitCoverageProjects) + { + var tUnitSettings = await tUnitSettingsProvider.ProvideAsync(tUnitCoverageProject, cancellationToken); + var success = await tUnitCoverageRunner.RunAsync(tUnitSettings, tUnitCoverageProject.HasCoverageExtension, false, cancellationToken); + if (success) + { + coberturaFiles.Add(tUnitSettings.OutputPath); + } + else + { + runAllProjects = false; + break; + } + } + + if (runAllProjects) + { + fccEngine.RunAndProcessReport(coberturaFiles.ToArray(), null); + } + else + { + logger.Log("Not collecting coverage due to unsuccessful test"); + } + return runAllProjects; + } + + async Task ICoverageCollectableFromTestExplorer.IsCollectableAsync() + { + var tunitProjects = await tUnitProjectsProvider.GetTUnitProjectsAsync(CancellationToken.None); + return !tunitProjects.Any(); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageProjectFactory.cs new file mode 100644 index 00000000..5d55f739 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageProjectFactory.cs @@ -0,0 +1,147 @@ +using FineCodeCoverage.Engine.Model; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio; +using System.Threading.Tasks; +using System.ComponentModel.Composition; +using System.IO; +using System.Threading; +using FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage; +using System.Xml.Linq; +using System; +using FineCodeCoverage.Core.Utilities; +using Microsoft; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitCoverageProjectFactory))] + internal class TUnitCoverageProjectFactory : ITUnitCoverageProjectFactory + { + private readonly ICoverageProjectFactory coverageProjectFactory; + private readonly ITemplatedRunSettingsService templatedRunSettingsService; + private readonly IServiceProvider serviceProvider; + private readonly IXmlUtils xmlUtils; + private readonly IRunSettingsToConfiguration runSettingsToConfiguration; + + class TUnitCoverageProject : ITUnitCoverageProject + { + private readonly Func> configurationProvider; + + public TUnitCoverageProject( + string exePath, + ICoverageProject coverageProject, + IVsHierarchy vsHierarchy, + CommandLineParseResult commandLineParseResult, + Func> configurationProvider, + bool hasCoverageExtension + ) + { + ExePath = exePath; + CoverageProject = coverageProject; + VsHierarchy = vsHierarchy; + CommandLineParseResult = commandLineParseResult; + this.configurationProvider = configurationProvider; + HasCoverageExtension = hasCoverageExtension; + } + public string ExePath { get; } + public Task GetConfigurationAsync(CancellationToken cancellationToken) + { + return configurationProvider(cancellationToken); + } + public ICoverageProject CoverageProject { get; } + public IVsHierarchy VsHierarchy { get; } + public CommandLineParseResult CommandLineParseResult { get; } + public bool HasCoverageExtension { get; } + } + + [ImportingConstructor] + public TUnitCoverageProjectFactory( + ICoverageProjectFactory coverageProjectFactory, + ITemplatedRunSettingsService templatedRunSettingsService, + [Import(typeof(SVsServiceProvider))] + IServiceProvider serviceProvider, + IXmlUtils xmlUtils, + IRunSettingsToConfiguration runSettingsToConfiguration + ) + { + this.coverageProjectFactory = coverageProjectFactory; + this.templatedRunSettingsService = templatedRunSettingsService; + this.serviceProvider = serviceProvider; + this.xmlUtils = xmlUtils; + this.runSettingsToConfiguration = runSettingsToConfiguration; + } + + private async Task CreateCoverageProjectAsync( + IVsHierarchy project, + CancellationToken cancellationToken) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var coverageProject = coverageProjectFactory.Create(); + project.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_Name, out var projectName); + coverageProject.ProjectName = projectName.ToString(); + project.GetGuidProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_CmdUIGuid, out var projectGuid); + coverageProject.Id = projectGuid; + project.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID4.VSHPROPID_TargetFrameworkMoniker, out var targetFrameworkMoniker); + cancellationToken.ThrowIfCancellationRequested(); + if (project is IVsBuildPropertyStorage buildPropertyStorage) + { + //todo configuration parameter for Debug + int hr = buildPropertyStorage.GetPropertyValue("TargetPath", null, 1, out var outputFile); + ErrorHandler.ThrowOnFailure(hr); + coverageProject.TestDllFile = outputFile; + }//todo throw if not + cancellationToken.ThrowIfCancellationRequested(); + if (project is IVsProject vsProject) + { + int hr = vsProject.GetMkDocument(VSConstants.VSITEMID_ROOT, out var projectFilePath); + ErrorHandler.ThrowOnFailure(hr); + coverageProject.ProjectFile = projectFilePath; + }//todo throw if not + + return coverageProject; + } + + private async Task GetSolutionDirectoryAsync(CancellationToken cancellationToken) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution; + Assumes.Present(vsSolution); + vsSolution.GetSolutionInfo(out string solutionDirectory, out var _, out var __); + return solutionDirectory; + } + + private async Task GetConfigurationElementAsync(ICoverageProject coverageProject, CancellationToken ct) + { + var solutionDirectory = await GetSolutionDirectoryAsync(ct); + var runSettings = templatedRunSettingsService.CreateProjectsRunSettings(new ICoverageProject[] { coverageProject }, solutionDirectory, "")[0].RunSettings; + return runSettingsToConfiguration.ConvertToConfiguration(XElement.Parse(runSettings)); + } + + public async Task CreateTUnitCoverageProjectAsync( + ITUnitProject tUnitProject, + CancellationToken cancellationToken) + { + var coverageProject = await CreateCoverageProjectAsync(tUnitProject.Hierarchy, cancellationToken); + var exePath = Path.ChangeExtension(coverageProject.TestDllFile, ".exe"); + + Func> configurationProvider = async (ct) => + { + var configurationElement = await GetConfigurationElementAsync(coverageProject, ct); + if (coverageProject.Settings.IncludeTestAssembly) + { + configurationElement.Add(new XElement("IncludeTestAssembly", true)); + } + return xmlUtils.Serialize(configurationElement); + }; + + return new TUnitCoverageProject( + exePath, + coverageProject, + tUnitProject.Hierarchy, + tUnitProject.CommandLineParseResult, + configurationProvider, + tUnitProject.HasCoverageExtension); + } + } + +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageRunner.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageRunner.cs new file mode 100644 index 00000000..e90db0b9 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageRunner.cs @@ -0,0 +1,148 @@ +using FineCodeCoverage.Core.Initialization; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Output; +using Microsoft.VisualStudio.Threading; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitCoverageRunner))] + internal class TUnitCoverageRunner : ITUnitCoverageRunner + { + private const string zipDirectoryName = "dotnet-coverage"; + private const string zipPrefix = "dotnet-coverage"; + private readonly ILogger logger; + private readonly IToolUnzipper toolUnzipper; + private const int successExitCode = 0; + private readonly Dictionary nonSuccessExitCodeMessages = new Dictionary + { + { 2, "At least one test failure." }, + { 3, "Test session was aborted." }, + { 4, "Setup of used extension is invalid."}, + { 5, "Command line arguments are invalid."}, + { 6, "Test session is using a non-implemented feature." }, + { 7, "Test session was unable to complete successfully, and likely crashed. It's possible that this was caused by a test session that was run via a test controller's extension point."}, + // todo check the source for this one as may be the minimum expected tests setting + { 8, "Test session ran 0 tests." }, + { 9, "Minimum execution policy for the executed tests was violated." }, + { 10, "The test adapter failed to run tests for an infrastructure reason unrelated to the test's self. An example is failing to create a fixture needed by tests." }, + { 11, "The test process will exit if dependent process exits" }, + { 12, "Test session was unable to run because the client does not support any of the supported protocol versions." }, + { 13, "Test session was stopped due to reaching the specified number of maximum failed tests using --maximum-failed-tests command-line option." } + }; + + public event EventHandler ReadyEvent; + + [ImportingConstructor] + public TUnitCoverageRunner( + ILogger logger, + IToolUnzipper toolUnzipper + ) + { + this.logger = logger; + this.toolUnzipper = toolUnzipper; + } + + private (string,string) GetExeAndArgs( + TUnitSettings tUnitSettings, + bool hasCoverageExtension + ) + { + var path = hasCoverageExtension ? tUnitSettings.ExePath : dotnetCoverageExePath; + var args = hasCoverageExtension ? $"--disable-logo --coverage --coverage-output-format cobertura --coverage-settings \"{tUnitSettings.SettingsPath}\" --coverage-output \"{tUnitSettings.OutputPath}\"" : + $"collect \"{tUnitSettings.ExePath}\" --disable-logo -f cobertura -o \"{tUnitSettings.OutputPath}\" -s \"{tUnitSettings.SettingsPath}\" --nologo"; + args = $"{args} {tUnitSettings.AdditionalArgs}"; + return (path, args); + } + + private CancellationToken cancellationToken; + private string dotnetCoverageExePath; + + public async Task RunAsync( + TUnitSettings tUnitSettings, + bool hasCoverageExtension, + bool showWindow = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + this.cancellationToken = cancellationToken; + var (path,args) = GetExeAndArgs(tUnitSettings, hasCoverageExtension); + // could have FCC option - hide-test-output or just allow them to supply their own + logger.Log("Executing TUnit", path, "Arguments", args); + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo + { + FileName = path, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = !showWindow, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + process.OutputDataReceived += Process_OutputDataReceived; + process.ErrorDataReceived += Process_ErrorDataReceived; + cancellationToken.ThrowIfCancellationRequested(); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(cancellationToken); + process.WaitForExit(1000); // Ensures all output is handled + + /* + from https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro?tabs=dotnetcli#run-and-debug-tests + The app exits with a nonzero exit code if there's an error, which is typical for most executables. For more information on the known exit codes, see Microsoft.Testing.Platform exit codes. + Tip + You can ignore a specific exit code using the --ignore-exit-code command line option. + + */ + LogNonSuccessExitCode(process.ExitCode); + logger.Log("-----------"); + return process.ExitCode == successExitCode; + } + } + + private void LogNonSuccessExitCode(int exitCode) + { + if(exitCode != successExitCode) + { + string message = $"Non success exit code : {exitCode}."; + if(nonSuccessExitCodeMessages.TryGetValue(exitCode, out var msg)) + { + message = $"{message} {msg}"; + } + logger.Log(message); + } + } + + private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + { + logger.Log($"Error: {e.Data}"); + } + } + + private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (!cancellationToken.IsCancellationRequested) + { + logger.Log(e.Data); + } + } + + public void Initialize(string appDataFolderPath, CancellationToken cancellationToken) + { + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolderPath, zipDirectoryName, zipPrefix, cancellationToken); + dotnetCoverageExePath = Directory.GetFiles(zipDestination, "dotnet-coverage.exe", SearchOption.AllDirectories).First(); + ReadyEvent?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitInstalledPackagesService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitInstalledPackagesService.cs new file mode 100644 index 00000000..d839d410 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitInstalledPackagesService.cs @@ -0,0 +1,86 @@ +using Microsoft.VisualStudio.Threading; +using NuGet.VisualStudio.Contracts; +using System; +using System.Threading.Tasks; +using System.Threading; +using System.ComponentModel.Composition; +using System.Collections.Immutable; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + + [Export(typeof(ITUnitInstalledPackagesService))] + internal class TUnitInstalledPackagesService : ITUnitInstalledPackagesService + { + private readonly AsyncLazy lazyNugetProjectService; + + [ImportingConstructor] + public TUnitInstalledPackagesService( + INugetProjectServiceProvider nugetProjectServiceProvider + ) + { + this.lazyNugetProjectService = nugetProjectServiceProvider.LazyNugetProjectService; + } + + public TUnitInstalledPackageResult GetTUnitInstalledPackages(IImmutableDictionary> packageReferenceItems) + { + if(packageReferenceItems == null) + { + return new TUnitInstalledPackageResult(InstalledPackageResultStatus.Unknown, false, false); + } + + var hasTUnit = false; + var hasCoverageExtension = false; + foreach (var packageReference in packageReferenceItems) + { + var id = packageReference.Key; + if (id == TUnitConstants.TUnitPackageId) + { + hasTUnit = true; + continue; + } + if (id == TUnitConstants.CodeCoveragePackageId) + { + hasCoverageExtension = true; + } + if (hasTUnit && hasCoverageExtension) + { + break; + } + } + return new TUnitInstalledPackageResult(InstalledPackageResultStatus.Successful, hasCoverageExtension, hasTUnit); + } + + public async Task GetTUnitInstalledPackagesAsync(Guid projectGuid, CancellationToken cancellationToken) + { + var nugetProjectService = await lazyNugetProjectService.GetValueAsync(); + var result = await nugetProjectService.GetInstalledPackagesAsync(projectGuid, cancellationToken); + if (result.Status == InstalledPackageResultStatus.Successful) + { + var hasTUnit = false; + var hasCoverageExtension = false; + foreach (var package in result.Packages) + { + var id = package.Id; + if (id == TUnitConstants.TUnitPackageId) + { + hasTUnit = true; + continue; + } + if (id == TUnitConstants.CodeCoveragePackageId) + { + hasCoverageExtension = true; + } + if (hasTUnit && hasCoverageExtension) + { + break; + } + } + return new TUnitInstalledPackageResult(result.Status, hasCoverageExtension, hasTUnit); + } + return new TUnitInstalledPackageResult(result.Status, false, false); + } + } + + +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectCache.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectCache.cs new file mode 100644 index 00000000..a3a1198b --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectCache.cs @@ -0,0 +1,54 @@ +using Microsoft.VisualStudio.Shell.Interop; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitProjectCache))] + internal class TUnitProjectCache : ITUnitProjectCache + { + private Dictionary projectLookup; + public void Add(ITUnitProject tUnitProject) + { + projectLookup.Add(tUnitProject.Hierarchy, tUnitProject); + } + + public void Clear() + { + foreach(var tUnitproject in projectLookup.Values) + { + tUnitproject.Dispose(); + } + projectLookup = null; + } + + public async Task> GetTUnitProjectsAsync(CancellationToken cancellationToken) + { + var tUnitProjects = new List(); + foreach (var project in projectLookup.Values) + { + await project.UpdateStateAsync(cancellationToken); + if (project.IsTUnit) + { + tUnitProjects.Add(project); + } + } + return tUnitProjects; + + } + + public void Initialize(List tUnitProjects) + { + projectLookup = tUnitProjects.ToDictionary(p => p.Hierarchy); + } + + public void Remove(IVsHierarchy project) + { + projectLookup[project].Dispose(); + projectLookup.Remove(project); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectFactory.cs new file mode 100644 index 00000000..afacebd0 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectFactory.cs @@ -0,0 +1,217 @@ +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.Shell.Interop; +using NuGet.VisualStudio.Contracts; +using System; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitProjectFactory))] + internal class TUnitProjectFactory : ITUnitProjectFactory + { + private readonly ITUnitInstalledPackagesService tUnitInstalledPackagesService; + private readonly ICommandLineParser commandLineParser; + + class TUnitProject : ITUnitProject, IDisposable + { + private readonly ITUnitInstalledPackagesService tUnitInstalledPackagesService; + private readonly ICommandLineParser commandLineParser; + private IImmutableDictionary> packageReferenceItems; + private bool requiresUpdate = true; + private bool disposedValue; + private readonly IProjectProperties commonProperties; + private readonly IDisposable packageChangeSubscription; + private const string FCCTestingPlatformCommandLineArgumentsPropertyName = "FCCTestingPlatformCommandLineArguments"; + private const string TestingPlatformCommandLineArgumentsPropertyName = "TestingPlatformCommandLineArguments"; + + + /* + in VS2022 there is also + https://learn.microsoft.com/en-us/visualstudio/extensibility/visualstudio.extensibility/project/project?view=vs-2022 + https://learn.microsoft.com/en-us/visualstudio/extensibility/project-visual-studio-sdk?view=vs-2022 o + */ + + public TUnitProject( + ITUnitInstalledPackagesService tUnitInstalledPackagesService, + ICommandLineParser commandLineParser, + ConfiguredProject configuredProject, + IVsHierarchy hierarchy + ) + { + commonProperties = configuredProject.Services.ProjectPropertiesProvider.GetCommonProperties(); + this.Hierarchy = hierarchy; + this.tUnitInstalledPackagesService = tUnitInstalledPackagesService; + this.commandLineParser = commandLineParser; + this.packageChangeSubscription = this.SubscribeToPackageReferenceChanges(configuredProject); + } + + /* + cannot use GetEvaluatedPropertyValueAsync as absence returns empty string + */ + private async Task UseFCCTestingPlatformCommandLineArgumentsPropertyNameAsync() + { + var propertyNames = await commonProperties.GetPropertyNamesAsync(); + var hasTestingPlatformCommandLineArgumentsPropertyName = false; + foreach (var propertyName in propertyNames) + { + if(propertyName == FCCTestingPlatformCommandLineArgumentsPropertyName) + { + return true; + } + if(propertyName == TestingPlatformCommandLineArgumentsPropertyName) + { + hasTestingPlatformCommandLineArgumentsPropertyName = true; + } + } + if (hasTestingPlatformCommandLineArgumentsPropertyName) + { + return false; + } + return null; + } + + private async Task ParseTestingPlatformCommandLineArgumentsAsync() + { + var useFCCTestingPlatformCommandLineArgumentsPropertyName = await UseFCCTestingPlatformCommandLineArgumentsPropertyNameAsync(); + if (!useFCCTestingPlatformCommandLineArgumentsPropertyName.HasValue) + { + CommandLineParseResult = CommandLineParseResult.Empty; + } + else + { + var propertyName = useFCCTestingPlatformCommandLineArgumentsPropertyName.Value ? FCCTestingPlatformCommandLineArgumentsPropertyName : TestingPlatformCommandLineArgumentsPropertyName; + var testingPlatformCommandLineArguments = await commonProperties.GetEvaluatedPropertyValueAsync(propertyName); + + CommandLineParseResult = commandLineParser.Parse(testingPlatformCommandLineArguments); + } + } + + private IDisposable SubscribeToPackageReferenceChanges(ConfiguredProject configuredProject) + { + // there is ActiveConfiguredProjectSubscription but not available in 2019 + var subscriptionService = configuredProject.Services.ProjectSubscription; + var receivingBlock = new ActionBlock>(ProjectUpdateAsync); + return subscriptionService.JointRuleSource.SourceBlock.LinkTo(receivingBlock, ruleNames: new string[] { "PackageReference" }); + } + + /* + Idea was to use Nuget api, but + IVsPackageInstallerEvents + These events are only raised for packages.config projects. + To get updates for both packages.config and PackageReference use IVsNuGetProjectUpdateEvents instead. + + But IVsNuGetProjectUpdateEvents shipped in version 6.2 - Visual Studio 2022 + + -- + Also note that IVSProject4 has PackageReferences but the project is IVSProject ! + and cannot get change event from VSProjectEvents.ReferencesEvents + */ + + /* + if did not want real-time changes then could have used configuredProject.Services.PackageReferences + public interface IPackageReference : IReference + { + } + */ + + private Task ProjectUpdateAsync(IProjectVersionedValue update) + { + // if need to switch to the main thread will need CPS IThreadHandling + // This runs on a background thread. + packageReferenceItems = update.Value.CurrentState["PackageReference"].Items; + requiresUpdate = true; + return Task.CompletedTask; + } + public bool IsTUnit { get; private set; } + public bool HasCoverageExtension { get; private set; } + public IVsHierarchy Hierarchy { get; } + + public CommandLineParseResult CommandLineParseResult { get; private set; } = CommandLineParseResult.Empty; + public async Task UpdateStateAsync(CancellationToken cancellationToken) + { + if (requiresUpdate) + { + var installedPackagesResult = await tUnitInstalledPackagesService.GetTUnitInstalledPackagesAsync(await Hierarchy.GetGuidAsync(), cancellationToken); + if (installedPackagesResult.Status != InstalledPackageResultStatus.Successful) + { + // fallback but not transitive + // the data flow block should get data immediately + installedPackagesResult = tUnitInstalledPackagesService.GetTUnitInstalledPackages(packageReferenceItems); + } + + IsTUnit = installedPackagesResult.HasTUnit; + HasCoverageExtension = installedPackagesResult.HasCoverageExtension; + + requiresUpdate = false; + } + + if (IsTUnit) + { + /* + alternative is + var projectSnapshotService = configuredProject.Services.ProjectSnapshotService; + var receivingBlock = new ActionBlock>((pvv) => + { + var projectInstance = pvv.Value.ProjectInstance; + var argsProperty = projectInstance.GetProperty(FCCTestingPlatformCommandLineArgumentsPropertyName); + if (argsProperty == null) + { + argsProperty = projectInstance.GetProperty(TestingPlatformCommandLineArgumentsPropertyName); + } + if(argsProperty != null) + { + var value = argsProperty.EvaluatedValue; + } + + }); + return projectSnapshotService.SourceBlock.LinkTo(receivingBlock); + + */ + await ParseTestingPlatformCommandLineArgumentsAsync(); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + packageChangeSubscription.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + + [ImportingConstructor] + public TUnitProjectFactory( + ITUnitInstalledPackagesService tUnitInstalledPackagesService, + ICommandLineParser commandLineParser + ) + { + this.tUnitInstalledPackagesService = tUnitInstalledPackagesService; + this.commandLineParser = commandLineParser; + } + public ITUnitProject Create(IVsHierarchy hierarchy,ConfiguredProject configuredProject) + { + return new TUnitProject(tUnitInstalledPackagesService, commandLineParser, configuredProject, hierarchy); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectsProvider.cs new file mode 100644 index 00000000..1e943792 --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectsProvider.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.ComponentModel.Composition; +using System; +using System.Threading; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitProjectsProvider))] + internal class TUnitProjectsProvider : ITUnitProjectsProvider + { + private readonly ISolutionProjectsProvider solutionProjectsProvider; + private readonly ICPSTestProjectService cpsTestProjectService; + private readonly ITUnitChangeNotifier tUnitChangeNotifier; + private readonly ITUnitProjectFactory tUnitProjectFactory; + private readonly ITUnitProjectCache tUnitProjectCache; + private bool initializedCache; + private readonly List addedProjects = new List(); + + public event EventHandler ReadyEvent; + + [ImportingConstructor] + public TUnitProjectsProvider( + ISolutionProjectsProvider solutionProjectsProvider, + ICPSTestProjectService cpsTestProjectService, + ITUnitChangeNotifier tUnitChangeNotifier, + ITUnitProjectFactory tUnitProjectFactory, + ITUnitProjectCache tUnitProjectCache + ) + { + tUnitChangeNotifier.ProjectAddedRemovedEvent += TUnitChangeNotifier_ProjectAddedRemovedEvent; + tUnitChangeNotifier.SolutionClosedEvent += TUnitChangeNotifier_SolutionClosedEvent; + tUnitChangeNotifier.SolutionOpenedEvent += TUnitChangeNotifier_SolutionOpenedEvent; + this.solutionProjectsProvider = solutionProjectsProvider; + this.cpsTestProjectService = cpsTestProjectService; + this.tUnitChangeNotifier = tUnitChangeNotifier; + this.tUnitProjectFactory = tUnitProjectFactory; + this.tUnitProjectCache = tUnitProjectCache; + ThreadHelper.JoinableTaskFactory.Run(async () => + { + var solutionOpen = await solutionProjectsProvider.IsSolutionOpenAsync(); + if (solutionOpen) + { + OnReady(true); + } + }); + } + + public bool Ready { get; private set; } + + private void OnReady(bool ready) + { + Ready = ready; + ReadyEvent?.Invoke(this, EventArgs.Empty); + } + + private void TUnitChangeNotifier_SolutionOpenedEvent(object sender, EventArgs e) + { + OnReady(true); + } + + private void TUnitChangeNotifier_SolutionClosedEvent(object sender, EventArgs e) + { + addedProjects.Clear(); + if (initializedCache) + { + tUnitProjectCache.Clear(); + initializedCache = false; + } + OnReady(false); + } + + private void TUnitChangeNotifier_ProjectAddedRemovedEvent(object sender, ProjectAddedRemoved e) + { + if (initializedCache) + { + var project = e.Project; + if (e.Added) + { + addedProjects.Add(project); + } + else + { + var removed = addedProjects.Remove(project); + if(!removed) + { + tUnitProjectCache.Remove(e.Project); + } + } + } + } + + private class CpsProjectAndHierarchy { + public CpsProjectAndHierarchy(ConfiguredProject cpsProject, IVsHierarchy hierarchy) + { + CpsProject = cpsProject; + Hierarchy = hierarchy; + } + + public ConfiguredProject CpsProject { get; } + public IVsHierarchy Hierarchy { get; } + } + + private async Task> GetCpsTestProjectsAndHierarchysAsync(IEnumerable projects) + { + List cpsTestProjectsAndHierarchys = new List(); + foreach(var project in projects) + { + var cpsTestProject = await cpsTestProjectService.GetProjectAsync(project); + if (cpsTestProject != null) + { + cpsTestProjectsAndHierarchys.Add(new CpsProjectAndHierarchy(cpsTestProject, project)); + } + } + return cpsTestProjectsAndHierarchys; + } + + private async Task> GetTUnitProjectsAsync(IEnumerable projects) + { + var potentialTUnitProjects = new List(); + var cpsTestProjectAndHierarchys = await GetCpsTestProjectsAndHierarchysAsync(projects); + foreach (var cpsTestProjectAndHierarchy in cpsTestProjectAndHierarchys) + { + var tUnitProject = tUnitProjectFactory.Create(cpsTestProjectAndHierarchy.Hierarchy, cpsTestProjectAndHierarchy.CpsProject); + potentialTUnitProjects.Add(tUnitProject); + } + return potentialTUnitProjects; + } + + public async Task> GetTUnitProjectsAsync(CancellationToken cancellationToken) + { + if (!initializedCache) + { + var solutionProjects = await solutionProjectsProvider.GetLoadedProjectsAsync(cancellationToken); + var potentialTUnitProjects = await GetTUnitProjectsAsync(solutionProjects); + tUnitProjectCache.Initialize(potentialTUnitProjects); + initializedCache = true; + } + else + { + var newTUnitProjects = await GetTUnitProjectsAsync(addedProjects); + foreach(var newTUnitProject in newTUnitProjects) + { + tUnitProjectCache.Add(newTUnitProject); + } + addedProjects.Clear(); + } + + return await tUnitProjectCache.GetTUnitProjectsAsync(cancellationToken); + } + } +} diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitSettingsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitSettingsProvider.cs new file mode 100644 index 00000000..34692b5c --- /dev/null +++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitSettingsProvider.cs @@ -0,0 +1,209 @@ +using FineCodeCoverage.Core.Utilities; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; +using System.ComponentModel.Composition; +using System.IO; +using FineCodeCoverage.Options; +using System.Collections.Generic; +using System.Text; + +namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform +{ + [Export(typeof(ITUnitSettingsProvider))] + internal class TUnitSettingsProvider : ITUnitSettingsProvider + { + private readonly IFileUtil fileUtil; + private readonly IXmlUtils xmlUtils; + private readonly IRunSettingsToConfiguration runSettingsToConfiguration; + private readonly IAppOptionsProvider appOptionsProvider; + private readonly IEnvironment environment; + private int fccRunWhenTestsExceed; + private bool fccRunWhenTestsFail; + + [ImportingConstructor] + public TUnitSettingsProvider( + IFileUtil fileUtil, + IXmlUtils xmlUtils, + IRunSettingsToConfiguration runSettingsToConfiguration, + IAppOptionsProvider appOptionsProvider, + IEnvironment environment + ) + { + this.fileUtil = fileUtil; + this.xmlUtils = xmlUtils; + this.runSettingsToConfiguration = runSettingsToConfiguration; + this.appOptionsProvider = appOptionsProvider; + this.environment = environment; + TakeFCCOptions(appOptionsProvider.Get()); + this.appOptionsProvider.OptionsChanged += TakeFCCOptions; + } + + private void TakeFCCOptions(IAppOptions appOptions) + { + this.fccRunWhenTestsExceed = appOptions.RunWhenTestsExceed; + this.fccRunWhenTestsFail = appOptions.RunWhenTestsFail; + } + + public async Task ProvideAsync(ITUnitCoverageProject tUnitCoverageProject, CancellationToken cancellationToken) + { + await tUnitCoverageProject.CoverageProject.PrepareForCoverageAsync(cancellationToken, false); + var coberturaPath = GetCoberturaPath(tUnitCoverageProject); + var commandLineParseResult = tUnitCoverageProject.CommandLineParseResult; + // todo commandLineParseResult.HasError + string configurationPathArgument = null; + var additionalArgsStringBuilder = new StringBuilder(); + string ignoreExitCodeArg = null; + int? minimumExpectedTests = null; + foreach (var option in commandLineParseResult.Options) + { + switch (option.Name) + { + case "coverage": + case "coverage-output-format": + case "coverage-output"://for now will use own + break; + case "coverage-settings": + case "settings": + var arg = option.Arguments.FirstOrDefault(); + if (arg != null) + { + if (ConfigurationPathArgExists(arg)) + { + configurationPathArgument = arg; + } + } + break; + case "ignore-exit-code": + ignoreExitCodeArg = option.Arguments.FirstOrDefault(); + break; + case "minimum-expected-tests": + var minExpectedTestsArg = option.Arguments.FirstOrDefault(); + if (minExpectedTestsArg != null) + { + if(int.TryParse(minExpectedTestsArg, out var result)) + { + minimumExpectedTests = result; + } + } + break; + default: + AddToAdditionalArgs($"--{option.Name} {string.Join(" ", option.Arguments)}"); + break; + } + } + + AddToAdditionalArgs(GetMinimumExpectedTestsPart(minimumExpectedTests)); + AddToAdditionalArgs(GetIgnoreExitCodePart(ignoreExitCodeArg)); + + var configurationPath = await GetConfigurationPathAsync(tUnitCoverageProject, configurationPathArgument, cancellationToken); + return new TUnitSettings(tUnitCoverageProject.ExePath, configurationPath, coberturaPath, additionalArgsStringBuilder.ToString()); + + bool ConfigurationPathArgExists(string pathArg) + { + pathArg = pathArg.Replace("\"", "").Replace("'", ""); + return fileUtil.Exists(pathArg); + } + + void AddToAdditionalArgs(string part) + { + if (!string.IsNullOrEmpty(part)) + { + additionalArgsStringBuilder.Append($" {part}"); + } + } + } + + private string GetMinimumExpectedTestsPart(int? minimumExpectedTestsArg) + { + // non zero positive integer + if (!minimumExpectedTestsArg.HasValue && fccRunWhenTestsExceed > 1) + { + minimumExpectedTestsArg = fccRunWhenTestsExceed - 1; + } + return minimumExpectedTestsArg.HasValue ? $"--minimum-expected-tests {minimumExpectedTestsArg}" : null; + } + + private string GetIgnoreExitCodePart(string ignoreExitCodeArg) + { + var ignoreExitCodeString = GetIgnoreExitCodeString(ignoreExitCodeArg); + var ignoredExitCodes = GetIgnoredExitCodes(ignoreExitCodeString); + if(!ignoredExitCodes.Contains(2) && fccRunWhenTestsFail) + { + ignoredExitCodes.Add(2); + } + return ignoredExitCodes.Any() ? $"--ignore-exit-code {string.Join(";", ignoredExitCodes)}" : null; + } + + private string GetIgnoreExitCodeString(string ignoreExitCodesArg) + { + var environmentVariableValue = environment.GetEnvironmentVariable("TESTINGPLATFORM_EXITCODE_IGNORE"); + return environmentVariableValue ?? ignoreExitCodesArg ?? ""; + } + + private List GetIgnoredExitCodes(string exitCodes) + { + try + { + var codes = exitCodes.Split(';'); + return codes.Select(code => int.Parse(code)).ToList(); + } + catch + { + return Enumerable.Empty().ToList(); + } + } + + private async Task GetConfigurationPathAsync( + ITUnitCoverageProject tUnitCoverageProject, + string configurationPathArgument, + CancellationToken cancellationToken + ) + { + if (configurationPathArgument != null) + { + if (tUnitCoverageProject.HasCoverageExtension) + { + return configurationPathArgument; + } + + var configurationOrRunSettingsElement = xmlUtils.Load(configurationPathArgument); + var name = configurationOrRunSettingsElement.Name.LocalName; + if (name == "Configuration") return configurationPathArgument; + if (name == "RunSettings") + { + var configurationElement = runSettingsToConfiguration.ConvertToConfiguration(configurationOrRunSettingsElement); + if (configurationElement != null) + { + return WriteConfiguration(tUnitCoverageProject, xmlUtils.Serialize(configurationElement)); + } + } + } + + return await WriteFCCConfigurationAsync(tUnitCoverageProject, cancellationToken); + } + + private async Task WriteFCCConfigurationAsync(ITUnitCoverageProject tUnitCoverageProject, CancellationToken cancellationToken) + { + var configuration = await tUnitCoverageProject.GetConfigurationAsync(cancellationToken); + return WriteConfiguration(tUnitCoverageProject, configuration); + } + + private string WriteConfiguration(ITUnitCoverageProject tUnitCoverageProject, string configuration) + { + var coverageProject = tUnitCoverageProject.CoverageProject; + var configurationPath = Path.Combine(coverageProject.CoverageOutputFolder, coverageProject.Id.ToString() + "config.xml"); + fileUtil.WriteAllText(configurationPath, configuration); + return configurationPath; + } + + private static string GetCoberturaPath(ITUnitCoverageProject tUnitCoverageProject) + { + var coverageProject = tUnitCoverageProject.CoverageProject; + return Path.Combine(coverageProject.CoverageOutputFolder, coverageProject.Id.ToString() + "coverage.xml"); + } + + } + + +} diff --git a/SharedProject/Core/OpenCover/IOpenCoverExeArgumentsProvider.cs b/SharedProject/Core/OpenCover/IOpenCoverExeArgumentsProvider.cs new file mode 100644 index 00000000..51354cd2 --- /dev/null +++ b/SharedProject/Core/OpenCover/IOpenCoverExeArgumentsProvider.cs @@ -0,0 +1,10 @@ +using FineCodeCoverage.Engine.Model; +using System.Collections.Generic; + +namespace FineCodeCoverage.Engine.OpenCover +{ + internal interface IOpenCoverExeArgumentsProvider + { + List Provide(ICoverageProject coverageProject,string msTestPlatformExePath); + } +} diff --git a/SharedProject/Core/OpenCover/OpenCoverExeArgumentsProvider.cs b/SharedProject/Core/OpenCover/OpenCoverExeArgumentsProvider.cs new file mode 100644 index 00000000..760ebf39 --- /dev/null +++ b/SharedProject/Core/OpenCover/OpenCoverExeArgumentsProvider.cs @@ -0,0 +1,191 @@ +using FineCodeCoverage.Engine.Model; +using FineCodeCoverage.Options; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Text.RegularExpressions; + +namespace FineCodeCoverage.Engine.OpenCover +{ + internal static class CommandLineArguments + { + public static string AddQuotes(string value) + { + return $@"""{value}"""; + } + + public static string AddEscapeQuotes(string arg) + { + return $@"\""{arg}\"""; + } + } + [Export(typeof(IOpenCoverExeArgumentsProvider))] + internal class OpenCoverExeArgumentsProvider : IOpenCoverExeArgumentsProvider + { + private enum Delimiter { Semicolon, Space} + + private void AddFilter(ICoverageProject project, List opencoverSettings) + { + var includes = SanitizeExcludesOrIncludes(project.Settings.Include); + var excludes = SanitizeExcludesOrIncludes(project.Settings.Exclude).ToList(); + + var includedModules = project.IncludedReferencedProjects.Select(rp => rp.AssemblyName).ToList(); + if (project.Settings.IncludeTestAssembly && + (includes.Any() || project.IncludedReferencedProjects.Any())) + { + includedModules.Add(project.ProjectName); + } + + var includeFilters = GetExcludesOrIncludes(includes, includedModules, true); + var excludeFilters = GetExcludesOrIncludes(excludes, project.ExcludedReferencedProjects.Select(rp => rp.AssemblyName), false); + AddIncludeAllIfExcludingWithoutIncludes(); + var filters = includeFilters.Concat(excludeFilters).ToList(); + SafeAddToSettingsDelimitedIfAny(opencoverSettings, "filter", filters, Delimiter.Space); + + void AddIncludeAllIfExcludingWithoutIncludes() + { + if (excludeFilters.Any() && !includeFilters.Any()) + { + includeFilters.Add("+[*]*"); + } + } + + List GetExcludesOrIncludes( + IEnumerable excludesOrIncludes,IEnumerable moduleExcludesOrIncludes, bool isInclude) + { + var excludeOrIncludeFilters = new List(); + var includeExcludeSymbol = isInclude ? "+" : "-"; + + foreach (var value in excludesOrIncludes) + { + excludeOrIncludeFilters.Add($@"{includeExcludeSymbol}{value}"); + } + + foreach (var moduleExcludeOrInclude in moduleExcludesOrIncludes) + { + excludeOrIncludeFilters.Add($"{includeExcludeSymbol}[{moduleExcludeOrInclude}]*"); + } + return excludeOrIncludeFilters.Distinct().ToList(); + } + } + + private IEnumerable SanitizeExcludesOrIncludes(IEnumerable excludesOrIncludes) + { + return (excludesOrIncludes ?? new string[0]) + .Where(x => x != null) + .Select(x => x.Trim(' ', '\'', '\"')) + .Where(x => !string.IsNullOrWhiteSpace(x)); + } + + private void SafeAddToSettingsDelimitedIfAny( + List opencoverSettings, + string settingName, + IEnumerable settings, + Delimiter delimiter = Delimiter.Semicolon + ) + { + if (settings.Any()) + { + var delimit = delimiter == Delimiter.Semicolon ? ";" : " "; + opencoverSettings.Add($@"""-{settingName}:{string.Join(delimit, settings)}"""); + } + } + + private void AddExcludeByFile(ICoverageProject project, List opencoverSettings) + { + var excludes = SanitizeExcludesOrIncludes(project.Settings.ExcludeByFile).ToList(); + SafeAddToSettingsDelimitedIfAny(opencoverSettings, "excludebyfile", excludes); + } + + private void AddExcludeByAttribute(ICoverageProject project, List opencoverSettings) + { + var excludeFromCodeCoverageAttributes = new List() + { + // coverlet knows these implicitly + "ExcludeFromCoverage", + "ExcludeFromCodeCoverage" + }; + + var excludes = SanitizeExcludesOrIncludes(project.Settings.ExcludeByAttribute) + .Concat(excludeFromCodeCoverageAttributes) + .SelectMany(exclude => new[] { exclude, GetAlternateName(exclude) }) + .OrderBy(exclude => exclude) + .Select(WildCardIfShortName); + + + SafeAddToSettingsDelimitedIfAny(opencoverSettings, "excludebyattribute", excludes); + + string WildCardIfShortName(string exclude) + { + if(exclude.IndexOf(".") == -1) + { + return $"*.{exclude}"; + } + return exclude; + } + + string GetAlternateName(string exclude) + { + if (exclude.EndsWith("Attribute")) + { + // remove 'Attribute' suffix + return exclude.Substring(0, exclude.Length - 9); + } + else + { + // add 'Attribute' suffix + return $"{exclude}Attribute"; + } + } + + } + + private string GetTargetArgs(ICoverageProject project) + { + var runSettings = !string.IsNullOrWhiteSpace(project.RunSettingsFile) ? $@" /Settings:{CommandLineArguments.AddEscapeQuotes(project.RunSettingsFile)}" : default; + var openCoverTargetArgs = project.Settings.OpenCoverTargetArgs; + var additionalTargetArgs = !string.IsNullOrWhiteSpace(openCoverTargetArgs) ? $" {openCoverTargetArgs}" : default; + return $@"""-targetargs:{CommandLineArguments.AddEscapeQuotes(project.TestDllFile)}{runSettings}{additionalTargetArgs}"""; + } + + private void AddTargetAndTargetArgs(ICoverageProject project, List opencoverSettings, string msTestPlatformExePath) + { + var target = !string.IsNullOrWhiteSpace(project.Settings.OpenCoverTarget) ? project.Settings.OpenCoverTarget : msTestPlatformExePath; + opencoverSettings.Add(CommandLineArguments.AddQuotes($"-target:{target}")); + opencoverSettings.Add(GetTargetArgs(project)); + } + + private string GetRegister(ICoverageProject project) + { + var openCoverRegister = project.Settings.OpenCoverRegister; + if (openCoverRegister == OpenCoverRegister.Default) + { + return $":path{(project.Is64Bit ? "64" : "32")}"; + } + if(openCoverRegister == OpenCoverRegister.NoArg) + { + return ""; + } + return $":{project.Settings.OpenCoverRegister.ToString().ToLower()}"; + } + + public List Provide(ICoverageProject project,string msTestPlatformExePath) + { + var opencoverSettings = new List(); + AddTargetAndTargetArgs(project, opencoverSettings, msTestPlatformExePath); + + opencoverSettings.Add(CommandLineArguments.AddQuotes($"-output:{project.CoverageOutputFile}")); + + AddFilter(project, opencoverSettings); + AddExcludeByFile(project, opencoverSettings); + AddExcludeByAttribute(project, opencoverSettings); + opencoverSettings.Add($"-register{GetRegister(project)}"); + opencoverSettings.Add("-mergebyhash"); + opencoverSettings.Add("-hideskipped:all"); + + return opencoverSettings; + + } + } +} diff --git a/SharedProject/Core/OpenCover/OpenCoverUtil.cs b/SharedProject/Core/OpenCover/OpenCoverUtil.cs index 02405640..36753453 100644 --- a/SharedProject/Core/OpenCover/OpenCoverUtil.cs +++ b/SharedProject/Core/OpenCover/OpenCoverUtil.cs @@ -1,13 +1,13 @@ using System; using System.IO; using System.Linq; -using System.Collections.Generic; using FineCodeCoverage.Engine.Model; using FineCodeCoverage.Engine.MsTestPlatform; using System.Threading.Tasks; using System.ComponentModel.Composition; using FineCodeCoverage.Core.Utilities; using System.Threading; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine.OpenCover { @@ -18,9 +18,10 @@ internal class OpenCoverUtil:IOpenCoverUtil private readonly IMsTestPlatformUtil msTestPlatformUtil; private readonly IProcessUtil processUtil; private readonly ILogger logger; - private readonly IToolFolder toolFolder; - private readonly IToolZipProvider toolZipProvider; - private const string zipPrefix = "openCover"; + private readonly IToolUnzipper toolUnzipper; + private readonly IFileUtil fileUtil; + private readonly IOpenCoverExeArgumentsProvider openCoverExeArgumentsProvider; + private const string zipPrefix = "openCover"; private const string zipDirectoryName = "openCover"; [ImportingConstructor] @@ -28,22 +29,24 @@ public OpenCoverUtil( IMsTestPlatformUtil msTestPlatformUtil, IProcessUtil processUtil, ILogger logger, - IToolFolder toolFolder, - IToolZipProvider toolZipProvider) + IToolUnzipper toolUnzipper, + IFileUtil fileUtil, + IOpenCoverExeArgumentsProvider openCoverExeArgumentsProvider + + ) { this.msTestPlatformUtil = msTestPlatformUtil; this.processUtil = processUtil; this.logger = logger; - this.toolFolder = toolFolder; - this.toolZipProvider = toolZipProvider; + this.toolUnzipper = toolUnzipper; + this.fileUtil = fileUtil; + this.openCoverExeArgumentsProvider = openCoverExeArgumentsProvider; } public void Initialize(string appDataFolder, CancellationToken cancellationToken) { - var zipDestination = toolFolder.EnsureUnzipped(appDataFolder, zipDirectoryName, toolZipProvider.ProvideZip(zipPrefix),cancellationToken); - openCoverExePath = Directory - .GetFiles(zipDestination, "OpenCover.Console.exe", SearchOption.AllDirectories) - .FirstOrDefault(); + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolder, zipDirectoryName, zipPrefix,cancellationToken); + openCoverExePath = fileUtil.GetFiles(zipDestination, "OpenCover.Console.exe", SearchOption.AllDirectories).First(); } private string GetOpenCoverExePath(string customExePath) @@ -55,154 +58,37 @@ private string GetOpenCoverExePath(string customExePath) return openCoverExePath; } - public async Task RunOpenCoverAsync(ICoverageProject project, CancellationToken cancellationToken) + private void DeleteTestPdbIfDoNotIncludeTestAssembly(ICoverageProject project) { - var title = $"OpenCover Run ({project.ProjectName})"; - - var opencoverSettings = new List(); - - opencoverSettings.Add($@" -mergebyhash "); - - opencoverSettings.Add($@" -hideskipped:all "); - - { - // -register: - - var registerValue = "path32"; - - if (project.Is64Bit) - { - registerValue = "path64"; - } - - opencoverSettings.Add($@" -register:{registerValue} "); - } - - { - // -target: - - opencoverSettings.Add($@" ""-target:{msTestPlatformUtil.MsTestPlatformExePath}"" "); - } - - { - // -filter: - - var filters = new List(); - var defaultFilter = "+[*]*"; - - foreach (var value in (project.Settings.Include ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - filters.Add($@"+{value.Replace("\"", "\\\"").Trim(' ', '\'')}"); - } - - foreach (var includedReferencedProject in project.IncludedReferencedProjects) - { - filters.Add($@"+[{includedReferencedProject}]*"); - } - - if (!filters.Any()) - { - filters.Add(defaultFilter); - } - - foreach (var value in (project.Settings.Exclude ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - filters.Add($@"-{value.Replace("\"", "\\\"").Trim(' ', '\'')}"); - } - - foreach (var referencedProjectExcludedFromCodeCoverage in project.ExcludedReferencedProjects) - { - filters.Add($@"-[{referencedProjectExcludedFromCodeCoverage}]*"); - } - - if (filters.Any(x => !x.Equals(defaultFilter))) - { - opencoverSettings.Add($@" ""-filter:{string.Join(" ", filters.Distinct())}"" "); - } - } - - { - // -excludebyfile: - - var excludes = new List(); - - foreach (var value in (project.Settings.ExcludeByFile ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - excludes.Add(value.Replace("\"", "\\\"").Trim(' ', '\'')); - } - - if (excludes.Any()) - { - opencoverSettings.Add($@" ""-excludebyfile:{string.Join(";", excludes)}"" "); - } - } - - { - // -excludebyattribute: - - var excludes = new List() - { - // coverlet knows these implicitly - "ExcludeFromCoverage", - "ExcludeFromCodeCoverage" - }; - - foreach (var value in (project.Settings.ExcludeByAttribute ?? new string[0]).Where(x => !string.IsNullOrWhiteSpace(x))) - { - excludes.Add(value.Replace("\"", "\\\"").Trim(' ', '\'')); - } - - foreach (var exclude in excludes.ToArray()) - { - var excludeAlternateName = default(string); - - if (exclude.EndsWith("Attribute", StringComparison.OrdinalIgnoreCase)) - { - // remove 'Attribute' suffix - excludeAlternateName = exclude.Substring(0, exclude.IndexOf("Attribute", StringComparison.OrdinalIgnoreCase)); - } - else - { - // add 'Attribute' suffix - excludeAlternateName = $"{exclude}Attribute"; - } - - excludes.Add(excludeAlternateName); - } + if (!project.Settings.IncludeTestAssembly) + { + // deleting the pdb of the test assembly seems to work; this is a VERY VERY shameful hack :( - excludes = excludes.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x).ToList(); + var testDllPdbFile = Path.Combine(project.ProjectOutputFolder, Path.GetFileNameWithoutExtension(project.TestDllFile)) + ".pdb"; + fileUtil.DeleteFile(testDllPdbFile); - if (excludes.Any()) - { - opencoverSettings.Add($@" ""-excludebyattribute:(*.{string.Join(")|(*.", excludes)})"" "); - } - } + // filtering out the test-assembly blows up the entire process and nothing gets instrumented or analysed - if (!project.Settings.IncludeTestAssembly) - { - // deleting the pdb of the test assembly seems to work; this is a VERY VERY shameful hack :( - - var testDllPdbFile = Path.Combine(project.ProjectOutputFolder, Path.GetFileNameWithoutExtension(project.TestDllFile)) + ".pdb"; - File.Delete(testDllPdbFile); + //var nameOnlyOfDll = Path.GetFileNameWithoutExtension(project.TestDllFileInWorkFolder); + //filters.Add($@"-[{nameOnlyOfDll}]*"); + } + } - // filtering out the test-assembly blows up the entire process and nothing gets instrumented or analysed - - //var nameOnlyOfDll = Path.GetFileNameWithoutExtension(project.TestDllFileInWorkFolder); - //filters.Add($@"-[{nameOnlyOfDll}]*"); - } + public async Task RunOpenCoverAsync(ICoverageProject project, CancellationToken cancellationToken) + { + DeleteTestPdbIfDoNotIncludeTestAssembly(project); - var runSettings = !string.IsNullOrWhiteSpace(project.RunSettingsFile) ? $@"/Settings:\""{project.RunSettingsFile}\""" : default; - opencoverSettings.Add($@" ""-targetargs:\""{project.TestDllFile}\"" {runSettings}"" "); + var openCoverSettings = openCoverExeArgumentsProvider.Provide(project, msTestPlatformUtil.MsTestPlatformExePath); - opencoverSettings.Add($@" ""-output:{ project.CoverageOutputFile }"" "); + var title = $"OpenCover Run ({project.ProjectName})"; - logger.Log($"{title} Arguments {Environment.NewLine}{string.Join($"{Environment.NewLine}", opencoverSettings)}"); + logger.Log($"{title} Arguments {Environment.NewLine}{string.Join($"{Environment.NewLine}", openCoverSettings)}"); var result = await processUtil .ExecuteAsync(new ExecuteRequest { FilePath = GetOpenCoverExePath(project.Settings.OpenCoverCustomPath), - Arguments = string.Join(" ", opencoverSettings), + Arguments = string.Join(" ", openCoverSettings), WorkingDirectory = project.ProjectOutputFolder },cancellationToken); @@ -211,7 +97,7 @@ public async Task RunOpenCoverAsync(ICoverageProject project, CancellationToken throw new Exception(result.Output); } - logger.Log(title, result.Output); + logger.Log($"{title} - Output", result.Output); } } } diff --git a/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs b/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs new file mode 100644 index 00000000..21d7f35a --- /dev/null +++ b/SharedProject/Core/ReportGenerator/ColourUtilities/LighnessApplier.cs @@ -0,0 +1,337 @@ +using System; + +namespace FineCodeCoverage.Engine.ReportGenerator +{ + /* + Conversions from https://www.codeproject.com/Articles/19045/Manipulating-colors-in-NET-Part-1#hsb + This is potentially better https://github.com/tompazourek/Colourful/ + + */ + public struct RGB + { + /// + /// Gets an empty RGB structure; + /// + public static readonly RGB Empty = new RGB(); + + private int red; + private int green; + private int blue; + + public static bool operator ==(RGB item1, RGB item2) + { + return ( + item1.Red == item2.Red + && item1.Green == item2.Green + && item1.Blue == item2.Blue + ); + } + + public static bool operator !=(RGB item1, RGB item2) + { + return ( + item1.Red != item2.Red + || item1.Green != item2.Green + || item1.Blue != item2.Blue + ); + } + + /// + /// Gets or sets red value. + /// + public int Red + { + get + { + return red; + } + set + { + red = (value > 255) ? 255 : ((value < 0) ? 0 : value); + } + } + + /// + /// Gets or sets red value. + /// + public int Green + { + get + { + return green; + } + set + { + green = (value > 255) ? 255 : ((value < 0) ? 0 : value); + } + } + + /// + /// Gets or sets red value. + /// + public int Blue + { + get + { + return blue; + } + set + { + blue = (value > 255) ? 255 : ((value < 0) ? 0 : value); + } + } + + public RGB(int R, int G, int B) + { + this.red = (R > 255) ? 255 : ((R < 0) ? 0 : R); + this.green = (G > 255) ? 255 : ((G < 0) ? 0 : G); + this.blue = (B > 255) ? 255 : ((B < 0) ? 0 : B); + } + + public override bool Equals(Object obj) + { + if (obj == null || GetType() != obj.GetType()) return false; + + return (this == (RGB)obj); + } + + public override int GetHashCode() + { + return Red.GetHashCode() ^ Green.GetHashCode() ^ Blue.GetHashCode(); + } + } + public static class ColorConversion + { + /// + /// Converts HSL to RGB. + /// + /// Hue, must be in [0, 360]. + /// Saturation, must be in [0, 1]. + /// Luminance, must be in [0, 1]. + public static RGB HSLtoRGB(double h, double s, double l) + { + if (s == 0) + { + // achromatic color (gray scale) + return new RGB( + Convert.ToInt32(Double.Parse(String.Format("{0:0.00}", + l * 255.0))), + Convert.ToInt32(Double.Parse(String.Format("{0:0.00}", + l * 255.0))), + Convert.ToInt32(Double.Parse(String.Format("{0:0.00}", + l * 255.0))) + ); + } + else + { + double q = (l < 0.5) ? (l * (1.0 + s)) : (l + s - (l * s)); + double p = (2.0 * l) - q; + + double Hk = h / 360.0; + double[] T = new double[3]; + T[0] = Hk + (1.0 / 3.0); // Tr + T[1] = Hk; // Tb + T[2] = Hk - (1.0 / 3.0); // Tg + + for (int i = 0; i < 3; i++) + { + if (T[i] < 0) T[i] += 1.0; + if (T[i] > 1) T[i] -= 1.0; + + if ((T[i] * 6) < 1) + { + T[i] = p + ((q - p) * 6.0 * T[i]); + } + else if ((T[i] * 2.0) < 1) //(1.0/6.0)<=T[i] && T[i]<0.5 + { + T[i] = q; + } + else if ((T[i] * 3.0) < 2) // 0.5<=T[i] && T[i]<(2.0/3.0) + { + T[i] = p + (q - p) * ((2.0 / 3.0) - T[i]) * 6.0; + } + else T[i] = p; + } + + return new RGB( + Convert.ToInt32(Double.Parse(String.Format("{0:0.00}", + T[0] * 255.0))), + Convert.ToInt32(Double.Parse(String.Format("{0:0.00}", + T[1] * 255.0))), + Convert.ToInt32(Double.Parse(String.Format("{0:0.00}", + T[2] * 255.0))) + ); + } + } + + public static HSL RGBtoHSL(int red, int green, int blue) + { + double h = 0, s = 0; + + // normalize red, green, blue values + double r = (double)red / 255.0; + double g = (double)green / 255.0; + double b = (double)blue / 255.0; + + double max = Math.Max(r, Math.Max(g, b)); + double min = Math.Min(r, Math.Min(g, b)); + + // hue + if (max == min) + { + h = 0; // undefined + } + else if (max == r && g >= b) + { + h = 60.0 * (g - b) / (max - min); + } + else if (max == r && g < b) + { + h = 60.0 * (g - b) / (max - min) + 360.0; + } + else if (max == g) + { + h = 60.0 * (b - r) / (max - min) + 120.0; + } + else if (max == b) + { + h = 60.0 * (r - g) / (max - min) + 240.0; + } + + // luminance + double l = (max + min) / 2.0; + + // saturation + if (l == 0 || max == min) + { + s = 0; + } + else if (0 < l && l <= 0.5) + { + s = (max - min) / (max + min); + } + else if (l > 0.5) + { + s = (max - min) / (2 - (max + min)); //(max-min > 0)? + } + + return new HSL( + Double.Parse(String.Format("{0:0.##}", h)), + Double.Parse(String.Format("{0:0.##}", s)), + Double.Parse(String.Format("{0:0.##}", l)) + ); + } + } + + public struct HSL + { + /// + /// Gets an empty HSL structure; + /// + public static readonly HSL Empty = new HSL(); + + private double hue; + private double saturation; + private double luminance; + + public static bool operator ==(HSL item1, HSL item2) + { + return ( + item1.Hue == item2.Hue + && item1.Saturation == item2.Saturation + && item1.Luminance == item2.Luminance + ); + } + + public static bool operator !=(HSL item1, HSL item2) + { + return ( + item1.Hue != item2.Hue + || item1.Saturation != item2.Saturation + || item1.Luminance != item2.Luminance + ); + } + + /// + /// Gets or sets the hue component. + /// + public double Hue + { + get + { + return hue; + } + set + { + hue = (value > 360) ? 360 : ((value < 0) ? 0 : value); + } + } + + /// + /// Gets or sets saturation component. + /// + public double Saturation + { + get + { + return saturation; + } + set + { + saturation = (value > 1) ? 1 : ((value < 0) ? 0 : value); + } + } + + /// + /// Gets or sets the luminance component. + /// + public double Luminance + { + get + { + return luminance; + } + set + { + luminance = (value > 1) ? 1 : ((value < 0) ? 0 : value); + } + } + + /// + /// Creates an instance of a HSL structure. + /// + /// Hue value. + /// Saturation value. + /// Lightness value. + public HSL(double h, double s, double l) + { + this.hue = (h > 360) ? 360 : ((h < 0) ? 0 : h); + this.saturation = (s > 1) ? 1 : ((s < 0) ? 0 : s); + this.luminance = (l > 1) ? 1 : ((l < 0) ? 0 : l); + } + + public override bool Equals(Object obj) + { + if (obj == null || GetType() != obj.GetType()) return false; + + return (this == (HSL)obj); + } + + public override int GetHashCode() + { + return Hue.GetHashCode() ^ Saturation.GetHashCode() ^ + Luminance.GetHashCode(); + } + } + public static class LightenssApplier + { + public static System.Drawing.Color Swap(System.Drawing.Color lightnessColor, System.Drawing.Color applyToColor) + { + var hsl = ColorConversion.RGBtoHSL(lightnessColor.R, lightnessColor.G, lightnessColor.B); + var hsl2 = ColorConversion.RGBtoHSL(applyToColor.R, applyToColor.G, applyToColor.B); + hsl2.Luminance = hsl.Luminance; + var rgb = ColorConversion.HSLtoRGB(hsl2.Hue, hsl2.Saturation, hsl2.Luminance); + return System.Drawing.Color.FromArgb(rgb.Red, rgb.Green, rgb.Blue); + } + } +} diff --git a/SharedProject/Core/ReportGenerator/ColourUtilities/LuminanceContrastColourExtensions.cs b/SharedProject/Core/ReportGenerator/ColourUtilities/LuminanceContrastColourExtensions.cs new file mode 100644 index 00000000..5adde47a --- /dev/null +++ b/SharedProject/Core/ReportGenerator/ColourUtilities/LuminanceContrastColourExtensions.cs @@ -0,0 +1,36 @@ +using System; + +namespace FineCodeCoverage.Engine.ReportGenerator +{ + internal static class LuminanceContrastColourExtensions + { + public static double Contrast(this System.Drawing.Color color, System.Drawing.Color color2) + { + var l1 = color.Luminance(); + var l2 = color2.Luminance(); + return l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05); + + } + + public static double Luminance(this System.Drawing.Color color) + { + return Luminance(color.R, color.G, color.B); + } + + private static double Luminance(int r, int g, int b) + { + var lr = LuminanceX(r); + var lg = LuminanceX(g); + var lb = LuminanceX(b); + return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb; + + } + + private static double LuminanceX(int x) + { + x /= 255; + return x <= 0.03928 ? x / 12.92 : Math.Pow((x + 0.055) / 1.055, 2.4); + + } + } +} diff --git a/SharedProject/Core/ReportGenerator/HotspotReader.cs b/SharedProject/Core/ReportGenerator/HotspotReader.cs new file mode 100644 index 00000000..3105d82b --- /dev/null +++ b/SharedProject/Core/ReportGenerator/HotspotReader.cs @@ -0,0 +1,250 @@ +using Esprima.Ast; +using Esprima; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using HtmlAgilityPack; +using System.Linq; +using System.Reflection; + +namespace FineCodeCoverage.Engine.ReportGenerator +{ + public static class HotspotProperties + { + public static readonly List Get = typeof(Hotspot).GetProperties().ToList(); + } + + internal class Metric + { + public string Name { get; set; } + public double? Value { get; set; } + public bool Exceeded { get; set; } + } + internal class Hotspot + { + public string Assembly { get; set; } + public string Class { get; set; } + public string MethodName { get; set; } + public string ShortName { get; set; } + public double FileIndex { get; set; } + public double? Line { get; set; } + public List Metrics { get; set; } + } + class RiskHotspotVars + { + public string RiskHotspotMetrics { get; set; } + public string RiskHotspots { get; set; } + } + internal class HotspotReader + { + public List Read(HtmlDocument doc) + { + var vars = ReadHotspotsVars(doc); + if (vars == null) + { + return new List(); + } + return UseEsprima(vars); + + } + + private List GetMetrics(ArrayExpression arrayExpression, string[] metricNames) + { + return arrayExpression.Elements.Select((elementExpression,index) => + { + var objectExpression = elementExpression as ObjectExpression; + var metric = new Metric { Name = metricNames[index] }; + + var properties = objectExpression.Properties; + properties.ToList().ForEach(node => + { + var property = node as Property; + var propertyName = GetPropertyName(node).ToLower(); + var literalValue = property.Value as Literal; + switch (propertyName) + { + case "exceeded": + metric.Exceeded = literalValue.BooleanValue.Value; + break; + case "value": + if (literalValue.NumericValue.HasValue) + { + metric.Value = literalValue.NumericValue.Value; + } + break; + } + + }); + return metric; + }).ToList(); + + } + + private string GetPropertyName(Node node) + { + var property = node as Property; + var pNameLiteral = property.Key as Literal; + if (pNameLiteral.TokenType != TokenType.StringLiteral) + { + throw new Exception("Unexpected"); + } + return pNameLiteral.StringValue as string; + } + + private IEnumerable GetArrayObjects(ArrayExpression arrayExpression) + { + return arrayExpression.Elements.Select(expression => + { + return expression as ObjectExpression; + }); + } + + private List MapHotspot(ArrayExpression riskHotspots, string[] metricNames) + { + return GetArrayObjects(riskHotspots).Select(objectExpression => + { + var hotspot = new Hotspot(); + var properties = objectExpression.Properties; + properties.ToList().ForEach(node => + { + var property = node as Property; + var pNameLiteral = property.Key as Literal; + var pName = GetPropertyName(node); + var hotspotProperty = HotspotProperties.Get.FirstOrDefault(p => p.Name.ToLower() == pName.ToLower()); + if (hotspotProperty != null) + { + var pNameType = pNameLiteral.Type; + var pValue = property.Value; + var pValueType = pValue.Type; + object value = null; + switch (pValueType) + { + case Nodes.ArrayExpression: + value = GetMetrics(pValue as ArrayExpression,metricNames); + break; + case Nodes.Literal: + var literal = pValue as Literal; + var tokenType = literal.TokenType; + switch (tokenType) + { + case TokenType.BooleanLiteral: + value = literal.BooleanValue.Value; + break; + case TokenType.NumericLiteral: + value = literal.NumericValue.Value; + break; + case TokenType.StringLiteral: + value = literal.StringValue as string; + break; + case TokenType.NullLiteral: + break; + default: + throw new Exception("Unexpected"); + } + + break; + default: + throw new Exception("Unexpected"); + } + hotspotProperty.SetValue(hotspot, value); + + } + }); + + + return hotspot; + }).ToList(); + } + + private List UseEsprima(RiskHotspotVars riskHotspotVars) + { + var metricNames = ParseMetricNames(ParseArray(riskHotspotVars.RiskHotspotMetrics)); + var riskHotspots = ParseArray(riskHotspotVars.RiskHotspots); + return MapHotspot(riskHotspots, metricNames); + } + + private string[] ParseMetricNames(ArrayExpression riskHotspotMetrics) + { + return GetArrayObjects(riskHotspotMetrics).Select(objectExpression => + { + var hotspot = new Hotspot(); + var properties = objectExpression.Properties; + + var nameProperty = properties.ToList().First() as Property; + var value = nameProperty.Value as Literal; + return value.StringValue; + }).ToArray(); + + } + + private ArrayExpression ParseArray(string code) + { + var javascriptParser = new JavaScriptParser(); + var script = javascriptParser.ParseScript(code); + var variableDeclaration = script.ChildNodes.First() as VariableDeclaration; + var declarations = variableDeclaration.Declarations; + var declaration = declarations.First(); + return declaration.Init as ArrayExpression; + } + + private RiskHotspotVars ReadHotspotsVars(HtmlDocument doc) + { + var scriptElements = doc.DocumentNode.Descendants("script"); + if (scriptElements != null) + { + var scriptElement = scriptElements.First(); + var script = scriptElement.InnerText; + + var riskHotspotMetrics = ReadArrayVariableAsString(script, "var riskHotspotMetrics = ["); + if(riskHotspotMetrics == null) + { + return null; + } + var riskhotspots = ReadArrayVariableAsString(script, "var riskHotspots = ["); + if (riskhotspots == null) + { + return null; + } + return new RiskHotspotVars { RiskHotspotMetrics = riskHotspotMetrics, RiskHotspots = riskhotspots }; + } + return null; + } + + // todo change so does not rely on opening bracket + private string ReadArrayVariableAsString(string script, string findString) + { + var riskhotspotsElementIndex = script.IndexOf(findString); + if (riskhotspotsElementIndex != -1) + { + var result = findString; + var toRead = script.Substring(riskhotspotsElementIndex + findString.Length); + using (var sr = new StringReader(toRead)) + { + string line; + var bracketCount = 1; + while ((line = sr.ReadLine()) != null && bracketCount != 0) + { + foreach (var c in line) + { + if (c == '[') + { + bracketCount++; + } + if (c == ']') + { + bracketCount--; + } + } + result += line; + + } + } + return result; + } + return null; + } + + } + +} diff --git a/SharedProject/Core/ReportGenerator/IReportColours.cs b/SharedProject/Core/ReportGenerator/IReportColours.cs index 1798e0f7..4a7392c3 100644 --- a/SharedProject/Core/ReportGenerator/IReportColours.cs +++ b/SharedProject/Core/ReportGenerator/IReportColours.cs @@ -24,6 +24,7 @@ internal interface IReportColours Color CoverageTableInactiveSortColour { get; } Color CoverageTableRowHoverBackgroundColour { get; } + Color CoverageTableRowHoverColour { get; } Color DivHeaderBackgroundColour { get; } diff --git a/SharedProject/Core/ReportGenerator/JsThemeStyling.cs b/SharedProject/Core/ReportGenerator/JsThemeStyling.cs index e3117268..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,14 +6,13 @@ 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; public string LinkColour; public string CoverageTableHeaderFontColour; public string CoverageTableRowHoverBackgroundColour; + public string CoverageTableRowHoverColour; public string DivHeaderBackgroundColour; public string TabBackgroundColour; public string HeaderFontColour; @@ -55,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/ReportColours.cs b/SharedProject/Core/ReportGenerator/ReportColours.cs index 09c23940..932fa024 100644 --- a/SharedProject/Core/ReportGenerator/ReportColours.cs +++ b/SharedProject/Core/ReportGenerator/ReportColours.cs @@ -25,6 +25,8 @@ internal class ReportColours : IReportColours public Color CoverageTableRowHoverBackgroundColour { get; set; } + public Color CoverageTableRowHoverColour { get; set; } + public Color DivHeaderBackgroundColour { get; set; } public Color FontColour { get; set; } @@ -88,6 +90,7 @@ internal class ReportColours : IReportColours public Color ButtonHoverTextColour { get; set; } public Color ButtonPressedTextColour { get; set; } + } } diff --git a/SharedProject/Core/ReportGenerator/ReportColoursExtensions.cs b/SharedProject/Core/ReportGenerator/ReportColoursExtensions.cs index c1f96aca..79c0084a 100644 --- a/SharedProject/Core/ReportGenerator/ReportColoursExtensions.cs +++ b/SharedProject/Core/ReportGenerator/ReportColoursExtensions.cs @@ -6,7 +6,7 @@ namespace FineCodeCoverage.Engine.ReportGenerator { internal static class ReportColoursExtensions { - private class ColourReflection + private class ColourReflection { public PropertyInfo ReportColoursPropertyInfo { get; set; } public FieldInfo JsThemeStylingFieldInfo { get; set; } @@ -50,5 +50,14 @@ public static string ToJsColour(this System.Drawing.Color colour) { return $"rgba({colour.R},{colour.G},{colour.B},{colour.A})"; } + + public static System.Drawing.Color ToColor(string jsColor) + { + var rgba = jsColor.Replace("rgba(", "").Replace(")", "").Split(','); + return System.Drawing.Color.FromArgb(int.Parse(rgba[3]), int.Parse(rgba[0]), int.Parse(rgba[1]), int.Parse(rgba[2])); + + } + } + } diff --git a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs index 78ec4c2e..6fe962fc 100644 --- a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -18,6 +18,8 @@ using Newtonsoft.Json.Linq; using ReportGeneratorPlugins; using System.Threading; +using System.Xml.Linq; +using FineCodeCoverage.Impl; namespace FineCodeCoverage.Engine.ReportGenerator { @@ -35,6 +37,7 @@ internal class ReportGeneratorResult { public string UnifiedHtml { get; set; } public string UnifiedXmlFile { get; set; } + public string HotspotsFile { get; set; } } [Export(typeof(IReportGeneratorUtil))] @@ -45,8 +48,7 @@ internal partial class ReportGeneratorUtil : private readonly IAssemblyUtil assemblyUtil; private readonly IProcessUtil processUtil; private readonly ILogger logger; - private readonly IToolFolder toolFolder; - private readonly IToolZipProvider toolZipProvider; + private readonly IToolUnzipper toolUnzipper; private readonly IReportColoursProvider reportColoursProvider; private readonly IFileUtil fileUtil; private readonly IAppOptionsProvider appOptionsProvider; @@ -56,7 +58,6 @@ internal partial class ReportGeneratorUtil : private const string zipPrefix = "reportGenerator"; private const string zipDirectoryName = "reportGenerator"; - private const string ThemeChangedJSFunctionName = "themeChanged"; private const string CoverageLogJSFunctionName = "coverageLog"; private const string CoverageLogTabName = "Coverage Log"; private const string ShowFCCWorkingJSFunctionName = "showFCCWorking"; @@ -88,16 +89,16 @@ private IReportColours ReportColours public string ReportGeneratorExePath { get; private set; } - private string FontSize => $"{environmentFontDetails.Size * dpiScale.DpiScaleX}px"; - private string FontName => environmentFontDetails.Family.Source; + private string FontSize => environmentFontDetails == null ? "12px" : $"{environmentFontDetails.Size * dpiScale.DpiScaleX}px"; + private string FontName => environmentFontDetails == null ? "Arial" : environmentFontDetails.Family.Source; + private readonly HotspotReader hotspotsReader = new HotspotReader(); [ImportingConstructor] public ReportGeneratorUtil( IAssemblyUtil assemblyUtil, IProcessUtil processUtil, ILogger logger, - IToolFolder toolFolder, - IToolZipProvider toolZipProvider, + IToolUnzipper toolUnzipper, IFileUtil fileUtil, IAppOptionsProvider appOptionsProvider, IReportColoursProvider reportColoursProvider, @@ -112,9 +113,8 @@ IEventAggregator eventAggregator this.assemblyUtil = assemblyUtil; this.processUtil = processUtil; this.logger = logger; - this.toolFolder = toolFolder; - this.toolZipProvider = toolZipProvider; - this.reportColoursProvider = reportColoursProvider; + this.toolUnzipper = toolUnzipper; + this.reportColoursProvider = reportColoursProvider; this.reportColoursProvider.ColoursChanged += ReportColoursProvider_ColoursChanged; this.scriptManager = scriptManager; this.resourceProvider = resourceProvider; @@ -137,7 +137,7 @@ private void ScriptManager_ClearFCCWindowLogsEvent(object sender, EventArgs e) public void Initialize(string appDataFolder, CancellationToken cancellationToken) { - var zipDestination = toolFolder.EnsureUnzipped(appDataFolder, zipDirectoryName, toolZipProvider.ProvideZip(zipPrefix), cancellationToken); + var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolder, zipDirectoryName, zipPrefix, cancellationToken); ReportGeneratorExePath = Directory.GetFiles(zipDestination, "reportGenerator.exe", SearchOption.AllDirectories).FirstOrDefault() ?? Directory.GetFiles(zipDestination, "*reportGenerator*.exe", SearchOption.AllDirectories).FirstOrDefault(); } @@ -153,7 +153,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(); @@ -168,7 +168,7 @@ async Task run(string outputReportType, string inputReports) reportTypeSettings.Add($@"""-reports:{inputReports}"""); reportTypeSettings.Add($@"""-plugins:{typeof(FccLightReportBuilder).Assembly.Location}"""); reportTypeSettings.Add($@"""-reporttypes:{FccLightReportBuilder.REPORT_TYPE}"""); - var (cyclomaticThreshold, crapScoreThreshold, nPathThreshold) = HotspotThresholds(); + var (cyclomaticThreshold, crapScoreThreshold, nPathThreshold) = HotspotThresholds(appOptionsProvider.Get()); reportTypeSettings.Add($@"""riskHotspotsAnalysisThresholds:metricThresholdForCyclomaticComplexity={cyclomaticThreshold}"""); reportTypeSettings.Add($@"""riskHotspotsAnalysisThresholds:metricThresholdForCrapScore={crapScoreThreshold}"""); @@ -180,8 +180,6 @@ async Task run(string outputReportType, string inputReports) throw new Exception($"Unknown reporttype '{outputReportType}'"); } - logger.Log($"{title} Arguments [reporttype:{outputReportType}] {Environment.NewLine}{string.Join($"{Environment.NewLine}", reportTypeSettings)}"); - var result = await processUtil .ExecuteAsync(new ExecuteRequest { @@ -198,7 +196,7 @@ async Task run(string outputReportType, string inputReports) throw new Exception(result.Output); } - logger.Log($"{title} [reporttype:{outputReportType}]", result.Output); + logger.Log($"{title} [reporttype:{outputReportType}] Output", result.Output); } @@ -206,7 +204,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}"; @@ -214,16 +212,38 @@ 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}"; LogCoverageProcess(htmlReportDurationMessage); // result output includes duration for normal log reportGeneratorResult.UnifiedHtml = fileUtil.ReadAllText(unifiedHtmlFile); - return reportGeneratorResult; + var doc = new HtmlDocument + { + OptionFixNestedTags = true, + OptionAutoCloseOnEnd = true + }; + doc.LoadHtml(reportGeneratorResult.UnifiedHtml); + var hotspots = hotspotsReader.Read(doc); + var hotspotsFile = WriteHotspotsToOutputFolder(hotspots, reportOutputFolder); + reportGeneratorResult.HotspotsFile = hotspotsFile; + return reportGeneratorResult; } + private string GetRowHoverLinkColour(bool useLightness = true) + { + if (useLightness) + { + return LightenssApplier.Swap( + ReportColoursExtensions.ToColor(jsReportColours.CoverageTableRowHoverColour), + ReportColoursExtensions.ToColor(jsReportColours.LinkColour) + ).ToJsColour(); + } + + return jsReportColours.CoverageTableRowHoverColour; + } + private void SetInitialTheme(HtmlAgilityPack.HtmlDocument document) { var backgroundColor = jsReportColours.BackgroundColour; @@ -270,9 +290,21 @@ Other option overviewHeaderLinks.Style.Color = jsReportColours.CoverageTableHeaderFontColour; var overviewTrHoverRule = styleRules.First(r => r.SelectorText == ".overview tr:hover"); - overviewTrHoverRule.Style.Background = jsReportColours.CoverageTableRowHoverBackgroundColour; - var expandCollapseIconColor = ReportColours.CoverageTableExpandCollapseIconColour; + /* + Alternative is lighten / darken the background color + ControlPaint.Dark / Light is percentage based - so we can't use it here + */ + + overviewTrHoverRule.Style.Background = jsReportColours.CoverageTableRowHoverBackgroundColour; + + var trHoverTdRule = stylesheet.Add(RuleType.Style); + trHoverTdRule.Text = $".overview tr:hover td {{color:{jsReportColours.CoverageTableRowHoverColour}}}"; + + var trHoverARule = stylesheet.Add(RuleType.Style); + trHoverARule.Text = $".overview tr:hover td a {{color:{GetRowHoverLinkColour()}}}"; + + var expandCollapseIconColor = ReportColours.CoverageTableExpandCollapseIconColour; plusBase64ReportImage.FillSvg(styleRules, expandCollapseIconColor.ToJsColour()); minusBase64ReportImage.FillSvg(styleRules, expandCollapseIconColor.ToJsColour()); @@ -301,9 +333,9 @@ Other option style.InnerHtml = changedCss; } - private string GetStickyTableHead() + private string GetStickyTableHead(IAppOptions appOptions) { - if (!appOptionsProvider.Get().StickyCoverageTable) + if (!appOptions.StickyCoverageTable) { return ""; } @@ -793,63 +825,145 @@ private string HideGroupingCss() "; } - private string ObserveAndHideFullyCovered() + private string CoverageInfoObserver() + { + var code = @" +var coverageInfoObserver = (function(){ + var mutationObserver; + var callbacks = []; + function observe(){ + mutationObserver.observe( + document.querySelector(""coverage-info""), + { attributes: false, childList: true, subtree: true } + ) + } + function cb(record,obs){ + mutationObserver.disconnect(); + for(var i=0;icoverage-bar'); - if(coverageBars.length == 0){ - coverageBars = parent.querySelectorAll('th>coverage-bar'); - } - var shouldHide = true; - // currently there is no option to hide branch coverage. - if(coverageBars.length === 2){ - var branchCoverageBar = coverageBars[1]; - // includes gray - var td = branchCoverageBar.querySelector('table>td.covered100'); - if(!td){ - shouldHide = false; - } - } - if(shouldHide){ - parent.style.setProperty('display', 'none'); - } - } + return ""; } - - } - } -}; - -var observer = new MutationObserver(callback); -observer.observe(targetNode, config); + string fullyQualifiedToName; + switch (appOptions.NamespaceQualification) + { + case NamespaceQualification.AlwaysUnqualified: + case NamespaceQualification.UnqualifiedByNamespace: + fullyQualifiedToName = "var name = fullyQualified.substring(fullyQualified.lastIndexOf(\".\") + 1);"; + break; + case NamespaceQualification.QualifiedByNamespaceLevel: + fullyQualifiedToName = @" +var parts = fullyQualified.split("".""); +var namespaceParts = parts.slice(0,parts.length-1); +var type = parts[parts.length-1]; +var name = type; +if(namespaceParts.length > groupingLevel){ + name = namespaceParts.slice(groupingLevel).join(""."") + ""."" + type; +}"; + break; + default: + throw new Exception($"Unknown GroupingNamespaceQualification '{appOptions.NamespaceQualification}'"); + } + var alwaysUnqualified = appOptions.NamespaceQualification == NamespaceQualification.AlwaysUnqualified; + var code = $@" +var config = {{ attributes: false, childList: true, subtree: true }}; + +var changeQualification = function() {{ + var groupingInput = document.querySelector(""coverage-info .customizebox input""); + if(!groupingInput || groupingInput.value <= 0 && !{alwaysUnqualified.ToString().ToLower()}){{ + return; + }} + + var groupingLevel = groupingInput.value; + var rows = document.querySelectorAll(""coverage-info table tbody tr[class-row]""); + for(var i=0;ifunction clipboardHtml(){ window.clipboardData.setData('Text',document.documentElement.innerHTML)} + +"; + return required ? code : ""; + + } + private string hotspotsPath; + private string WriteHotspotsToOutputFolder(List hotspots, string reportOutputFolder) + { + var rootElement = new XElement("Hotspots", + hotspots.Select(hotspot => + { + return new XElement("Hotspot", + // escaping... + new XElement("Assembly", hotspot.Assembly), + new XElement("Class", hotspot.Class), + new XElement("MethodName", hotspot.MethodName), + new XElement("ShortName", hotspot.ShortName), + new XElement("Line", hotspot.Line.HasValue ? hotspot.Line.Value.ToString() : ""), + // do I need this + new XElement("FileIndex", hotspot.FileIndex), + new XElement("Metrics", hotspot.Metrics.Select(metric => + { + return new XElement("Metric", + new XElement("Name", metric.Name), + new XElement("Exceeded", metric.Exceeded), + new XElement("Value",metric.Value.HasValue ? metric.Value.ToString() : "") + ); + }).ToList() + )); + }).ToList() + ); + hotspotsPath = Path.Combine(reportOutputFolder, "hotspots.xml"); + + rootElement.Save(hotspotsPath); + return hotspotsPath; + + } + + private void RemoveHistoryChartRendering(string html,StringBuilder sb) + { + var start = html.IndexOf("var charts = document.getElementsByClassName('historychart');"); + var end = html.IndexOf("var assemblies = ["); + var toRemove = html.Substring(start, end - start); + sb.Replace(toRemove, @" +var charts = document.getElementsByClassName('historychart'); +for(var i=0;i { - var (cyclomaticThreshold, crapScoreThreshold, nPathThreshold) = HotspotThresholds(); + var (cyclomaticThreshold, crapScoreThreshold, nPathThreshold) = HotspotThresholds(appOptions); var noRiskHotspotsHeader = "No risk hotspots that exceed options :"; var noRiskHotspotsCyclomaticMsg = $"Cyclomatic complexity : {cyclomaticThreshold}"; var noRiskHotspotsNpathMsg =$"NPath complexity : {nPathThreshold}"; @@ -898,10 +1071,10 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo OptionFixNestedTags = true, OptionAutoCloseOnEnd = true }; - + doc.LoadHtml(htmlForProcessing); - SetInitialTheme(doc); + SetInitialTheme(doc); htmlForProcessing = null; doc.DocumentNode.QuerySelectorAll(".footer").ToList().ForEach(x => x.SetAttributeValue("style", "display:none")); @@ -921,8 +1094,8 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo var htmlSb = new StringBuilder(outerHtml); FixGroupingMax(htmlSb); FixCollapse(htmlSb); - - var assembliesSearch = "var assemblies = ["; + RemoveHistoryChartRendering(outerHtml,htmlSb); + var assembliesSearch = "var assemblies = ["; var startIndex = outerHtml.IndexOf(assembliesSearch) + assembliesSearch.Length - 1; var endIndex = outerHtml.IndexOf("var historicCoverageExecutionTimes"); var assembliesToReplace = outerHtml.Substring(startIndex, endIndex - startIndex); @@ -931,13 +1104,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") @@ -982,7 +1155,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(); @@ -1010,11 +1183,12 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo button:hover {{ color : {jsReportColours.ButtonHoverTextColour}; background:{jsReportColours.ButtonHoverColour}; border-color:{jsReportColours.ButtonBorderHoverColour}}} button:active {{ color : {jsReportColours.ButtonPressedTextColour}; background:{jsReportColours.ButtonPressedColour}; border-color:{jsReportColours.ButtonBorderPressedColour}}} table td {{ white-space: nowrap; }} - table.coverage {{ width:150px;height:13px }} body {{ padding-left:3px;padding-right:3px;padding-bottom:3px }} {HideCyclomaticComplexityLink()} body {{ -webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none }} table.overview th, table.overview td {{ white-space: nowrap; word-break: normal; padding-left:10px;padding-right:10px; }} + table.coverage {{ width:150px;height:13px;margin-left:10px;margin-right:10px }} + table.coverage th, table.coverage td {{ padding-left:0px;padding-right:0px}} {GetGroupingCss(namespacedClasses)} table,tr,th,td {{ border: 1px solid;}} input[type=text] {{ color:{jsReportColours.TextBoxTextColour}; background-color:{jsReportColours.TextBoxColour};border-color:{jsReportColours.TextBoxBorderColour} }} @@ -1042,9 +1216,11 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo htmlSb.Replace("", $@"