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 /> -->
-
@@ -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