diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj
index b453a90c..cc79c541 100644
--- a/FineCodeCoverage/FineCodeCoverage.csproj
+++ b/FineCodeCoverage/FineCodeCoverage.csproj
@@ -125,6 +125,10 @@
ZippedTools\coverlet.console.6.0.4.zip
true
+
+ ZippedTools\dotnet-coverage.17.14.2.zip
+ true
+
@@ -165,6 +169,9 @@
3.11.0
+
+ 3.1.4097
+
16.9.20
@@ -179,6 +186,9 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+ 16.10.31321.278
+
16.10.31320.204
@@ -189,6 +199,12 @@
13.0.3
+
+ 5.11.6
+
+
+ 5.11.6
+
3.13.1
@@ -201,6 +217,9 @@
3.3.0
+
+ 2.0.0-beta4.22272.1
+
7.0.0
diff --git a/FineCodeCoverage2022/FineCodeCoverage2022.csproj b/FineCodeCoverage2022/FineCodeCoverage2022.csproj
index 5c12b842..db52bb71 100644
--- a/FineCodeCoverage2022/FineCodeCoverage2022.csproj
+++ b/FineCodeCoverage2022/FineCodeCoverage2022.csproj
@@ -109,6 +109,10 @@
ZippedTools\coverlet.console.6.0.4.zip
true
+
+ ZippedTools\dotnet-coverage.17.14.2.zip
+ true
+
Designer
VsixManifestGenerator
@@ -188,6 +192,12 @@
13.0.3
+
+ 17.13.2
+
+
+ 17.13.2
+
1.0.0
@@ -200,6 +210,9 @@
3.3.0
+
+ 2.0.0-beta4.22272.1
+
8.0.0
diff --git a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs
index d76d22ce..ed5da6d3 100644
--- a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs
+++ b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs
@@ -9,6 +9,7 @@
using FineCodeCoverage.Engine.Model;
using FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage;
using FineCodeCoverage.Impl;
+using FineCodeCoverage.Impl.TestContainerDiscovery;
using FineCodeCoverage.Options;
using Microsoft.VisualStudio.TestWindow.Extensibility;
using Moq;
@@ -120,6 +121,8 @@ private void SetUpOptions(Action> setupAppOptions)
public void SetUp()
{
mocker = new AutoMoqer();
+ var mockCoverageCollectableFromTestExplorer = mocker.GetMock();
+ mockCoverageCollectableFromTestExplorer.Setup(coverageCollectableFromTestExplorer => coverageCollectableFromTestExplorer.IsCollectableAsync()).ReturnsAsync(true);
testContainerDiscoverer = mocker.Create();
testContainerDiscoverer.RunAsync = (taskProvider) =>
{
diff --git a/README.md b/README.md
index 7209046b..61bed377 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,17 @@ or download from [releases](https://github.com/FortuneN/FineCodeCoverage/release
For .Net
FCC supports the new [Microsoft.Testing.Platform](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-platform-intro) for MsTest, NUnit and xUnit.
-Support for TUnit will be available shortly but will require running tests differently.
+
+Unfortunately the workaround FCC uses for MSTest, NUnit and xUnit does not apply to TUnit.
+TUnit has its own dedicated button on the FCC tool window toolbar and cannot be driven from the test explorer window.
+As the test explorer window is not used FCC cannot intercept your runsettings ( see later).
+Microsoft.Testing.Platform has the [Microsoft code coverage extension](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-extensions-code-coverage#microsoft-code-coverage). If you add the package it will be used, otherwise FCC will run dotnet-coverage.
+The Microsoft code coverage extension accepts runsettings or [configuration](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/dotnet-coverage#settings) and dotnet-coverage only accepts configuration.
+FCC will supply the necessary settings using FCC's settings system ( below ) but if you want to supply your own, FCC will read the test project file.
+If either the FCCTestingPlatformCommandLineArguments or TestingPlatformCommandLineArguments property is present FCC will use these [arguments](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro?tabs=dotnetcli#options) except ones pertaining to coverage other than --coverage-settings ( or --settings) as long as the
+path specified exists.
+
+Note that TUnit blocks coverage of your tests in the GlobalSetup.cs file if using the project template.
When not using Microsoft.Testing.Platform you have added test adapters through nuget packages. For instance, the NUnit Test Adapter extension is not sufficient.
@@ -23,8 +33,7 @@ When not using Microsoft.Testing.Platform you have added test adapters through n
## Introduction
Fine Code Coverage provides code coverage using one of 3 different coverage tools. In previous releases there were two coverage tools being utilised, OpenCover and Coverlet that will be referred to as 'old coverage'.
-Microsoft now provides a free coverage solution that you can choose to use by setting the Visual Studio Fine Code Coverage enumeration option RunMsCodeCoverage. This will probably be the preferred coverage
-tool for most developers.
+Microsoft now provides a free coverage solution that you can choose to use by setting the Visual Studio Fine Code Coverage enumeration option RunMsCodeCoverage. This will probably be the preferred coverage tool for most developers. This is not necessary for TUnit.
With the old coverage it was possible for FCC to provide an abstraction over each tool's exclusion / inclusion options. This abstraction does not work for MS code coverage.
Thus you will find that there are separate configuration options for Ms coverage vs old coverage and options that are common to the two.
@@ -32,7 +41,7 @@ Assembly level exclusions and inclusions can be achieved - see ExcludeAssemblies
Configuration is ( mostly ) determined from Visual Studio options, finecodecoverage-settings.xml files and project msbuild properties. All of these settings are optional.
For options that have a project scope, these settings form a hierarchy where lower levels override or, for collections, override or merge with the level above. This is described in detail further on.
-Regardless of the coverage tool employed the process begins with FCC reacting to the test explorer in visual studio. One of the 3 coverage tools provides the coverage results and the results can be opened from buttons on the Fine Code Coverage Tool Window.
+Aside from TUnit projects, the process begins with FCC reacting to the test explorer in visual studio. One of the 3 coverage tools provides the coverage results and the results can be opened from buttons on the Fine Code Coverage Tool Window.
This coverage is not dynamic and represents the coverage obtained from the last time you executed tests. When the coverage becomes outdated, you can click the 'FCC Clear UI' button in Tools or run coverage again.
Details of how FCC is progressing with code coverage can be found in the Coverage Log tab in the Fine Code Coverage Tool Window with more detailed logs in the FCC Output Window Pane. If you experience issues then providing the logs from the output window will help to understand the nature of the problem.
diff --git a/Shared Files/OutputToolWindowPackage.cs b/Shared Files/OutputToolWindowPackage.cs
index b155fb60..7edb157e 100644
--- a/Shared Files/OutputToolWindowPackage.cs
+++ b/Shared Files/OutputToolWindowPackage.cs
@@ -1,12 +1,13 @@
// ------------------------------------------------------------------------------
//
-// This file was generated by VSIX Synchronizer
+// This file was generated by VSIX Synchronizer 1.0.44
+// Available from https://marketplace.visualstudio.com/items?itemName=MadsKristensen.VsixSynchronizer64
//
// ------------------------------------------------------------------------------
namespace FineCodeCoverage
{
using System;
-
+
///
/// Helper class that exposes all GUIDs used across VS Package.
///
@@ -24,6 +25,7 @@ internal sealed partial class PackageGuids
public const string guidClearUICommandImageString = "8252a6d7-bcf3-4518-ae22-ad20ef8d4b63";
public static Guid guidClearUICommandImage = new Guid(guidClearUICommandImageString);
}
+
///
/// Helper class that encapsulates all CommandIDs uses across VS Package.
///
@@ -37,7 +39,9 @@ internal sealed partial class PackageIds
public const int cmdidOpenCoberturaCommand = 0x0101;
public const int cmdidOpenHotspotsCommand = 0x0102;
public const int cmdidToggleCoverageIndicatorsCommand = 0x0103;
+ public const int cmdidCollectTUnitCommand = 0x010C;
+ public const int cmdidCancelCollectTUnitCommand = 0x010D;
public const int outputToolWindowCommandImageIndex = 0x0001;
public const int clearUICommandImageIndex = 0x0001;
}
-}
\ No newline at end of file
+}
diff --git a/Shared Files/OutputToolWindowPackage.vsct b/Shared Files/OutputToolWindowPackage.vsct
index 12bc3203..560ab8b0 100644
--- a/Shared Files/OutputToolWindowPackage.vsct
+++ b/Shared Files/OutputToolWindowPackage.vsct
@@ -91,6 +91,30 @@
FCC.OpenHotspots
+
+
@@ -134,6 +158,8 @@
+
+
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/SharedProject/Core/FCCEngine.cs b/SharedProject/Core/FCCEngine.cs
index 1c28e405..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;
@@ -67,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]
@@ -82,12 +84,14 @@ public FCCEngine(
ISolutionEvents solutionEvents,
IAppOptionsProvider appOptionsProvider,
IEventAggregator eventAggregator,
- IDisposeAwareTaskRunner disposeAwareTaskRunner
+ IDisposeAwareTaskRunner disposeAwareTaskRunner,
+ ITUnitCoverageRunner tUnitCoverageRunner
)
{
this.solutionEvents = solutionEvents;
this.eventAggregator = eventAggregator;
this.disposeAwareTaskRunner = disposeAwareTaskRunner;
+ this.tUnitCoverageRunner = tUnitCoverageRunner;
solutionEvents.AfterClosing += (s,args) => ClearUI(false);
appOptionsProvider.OptionsChanged += (appOptions) =>
{
@@ -121,6 +125,7 @@ public void Initialize(CancellationToken cancellationToken)
msTestPlatformUtil.Initialize(AppDataFolderPath, cancellationToken);
coverageUtilManager.Initialize(AppDataFolderPath, cancellationToken);
msCodeCoverageRunSettingsService.Initialize(AppDataFolderPath, this,cancellationToken);
+ tUnitCoverageRunner.Initialize(AppDataFolderPath, cancellationToken);
}
public void ClearUI(bool clearOutputWindowHistory = true)
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/TemplatedRunSettingsService.cs b/SharedProject/Core/MsTestPlatform/CodeCoverage/TemplatedRunSettingsService.cs
index 3381ff73..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
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs
index aa349900..e59d05b0 100644
--- a/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/DisableTestingPlatformServerCapabilityGlobalPropertiesProvider.cs
@@ -1,5 +1,6 @@
using FineCodeCoverage.Engine.Model;
using FineCodeCoverage.Options;
+using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Build;
@@ -61,18 +62,12 @@ private async Task IsTUnitAsync()
private async Task ProjectEnabledAsync()
{
- var coverageProject = await GetCoverageProjectAsync();
- if (coverageProject != null)
- {
- var isTUnit = await IsTUnitAsync();
- if (isTUnit)
- {
- return false;
- }
- var projectSettings = await coverageProjectSettingsManager.GetSettingsAsync(coverageProject);
- return projectSettings.Enabled;
- }
- return true;
+ 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()
@@ -93,23 +88,18 @@ private async Task ProjectEnabledAsync()
return null;
}
- private async Task GetCoverageProjectAsync()
+ private CoverageProject GetCoverageProject(Guid projectGuid)
{
- var projectGuid = await GetProjectGuidAsync();
- if (projectGuid.HasValue)
+ return new CoverageProject(appOptionsProvider, null, coverageProjectSettingsManager, null)
{
- return new CoverageProject(appOptionsProvider, null, coverageProjectSettingsManager, null)
- {
- Id = projectGuid.Value,
- ProjectFile = unconfiguredProject.FullPath
- };
- }
- return null;
+ Id = projectGuid,
+ ProjectFile = unconfiguredProject.FullPath
+ };
}
public override async Task> GetGlobalPropertiesAsync(CancellationToken cancellationToken)
{
- if (!AllProjectsDisabled() && await ProjectEnabledAsync())
+ if (!await IsTUnitAsync() && !AllProjectsDisabled() && await ProjectEnabledAsync())
{
return Empty.PropertiesMap.Add("DisableTestingPlatformServerCapability", "true");
}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/BuildHelper.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/BuildHelper.cs
new file mode 100644
index 00000000..c07c3138
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/BuildHelper.cs
@@ -0,0 +1,147 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.ComponentModel.Composition;
+using System.Threading;
+using Microsoft;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal class BuildStartEnd : IVsUpdateSolutionEvents {
+ public event EventHandler BuildEvent;
+
+ public int UpdateSolution_Begin(ref int pfCancelUpdate)
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
+ {
+ BuildEvent?.Invoke(this, new BuildStartEndArgs(false));
+ return VSConstants.S_OK;
+ }
+
+ public int UpdateSolution_StartUpdate(ref int pfCancelUpdate)
+ {
+ BuildEvent?.Invoke(this, new BuildStartEndArgs(true));
+ return VSConstants.S_OK;
+ }
+
+ public int UpdateSolution_Cancel()
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
+ {
+ return VSConstants.S_OK;
+ }
+ }
+
+ internal class BuildCompletionHandler : IVsUpdateSolutionEvents, IDisposable
+ {
+ private readonly TaskCompletionSource _tcs = new TaskCompletionSource();
+ private readonly CancellationTokenRegistration registration;
+ public BuildCompletionHandler(CancellationToken cancellationToken)
+ {
+ registration = cancellationToken.Register(() => _tcs.TrySetCanceled());
+ }
+
+ ///
+ /// Task that completes when the build finishes.
+ ///
+ public Task BuildCompleted => _tcs.Task;
+
+ public int UpdateSolution_Begin(ref int pfCancelUpdate) => VSConstants.S_OK;
+
+ public int UpdateSolution_Cancel() => VSConstants.S_OK;
+
+ public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
+ {
+ var cancelled = fCancelCommand == 1;
+ var nonFailed = fSucceeded == 1;
+ var anySucceeded = fModified == 1;
+
+ // Signal the task completion.
+ _tcs.TrySetResult(fSucceeded != 0);
+ return VSConstants.S_OK;
+ }
+
+ public int UpdateSolution_StartUpdate(ref int pfCancelUpdate) => VSConstants.S_OK;
+
+ public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy) => VSConstants.S_OK;
+
+ public void Dispose()
+ {
+ registration.Dispose();
+ }
+ }
+
+ [Export(typeof(IBuildHelper))]
+ internal class BuildHelper : IBuildHelper
+ {
+ private IVsSolutionBuildManager2 solutionBuildManager2;
+ private BuildStartEnd buildStartEnd;
+ private bool building;
+ public event EventHandler ExternalBuildEvent;
+
+ [ImportingConstructor]
+ public BuildHelper(
+ [Import(typeof(SVsServiceProvider))]
+ IServiceProvider serviceProvider
+ )
+ {
+#pragma warning disable VSTHRD102 // Implement internal logic asynchronously
+ ThreadHelper.JoinableTaskFactory.Run(async () =>
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ this.solutionBuildManager2 = serviceProvider.GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager2;
+ Assumes.Present(this.solutionBuildManager2);
+ buildStartEnd = new BuildStartEnd();
+ this.solutionBuildManager2.AdviseUpdateSolutionEvents(buildStartEnd, out var cookie);
+ buildStartEnd.BuildEvent += BuildStartEnd_BuildEvent;
+ });
+#pragma warning restore VSTHRD102 // Implement internal logic asynchronously
+ }
+
+ private void BuildStartEnd_BuildEvent(object sender, BuildStartEndArgs e)
+ {
+ if (!building)
+ {
+ ExternalBuildEvent?.Invoke(this, e);
+ }
+ }
+
+ public async Task BuildAsync(List projects,CancellationToken cancellationToken)
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ var buildHandler = new BuildCompletionHandler(cancellationToken);
+ int hr = solutionBuildManager2.AdviseUpdateSolutionEvents(buildHandler, out uint cookie);
+ ErrorHandler.ThrowOnFailure(hr);
+ var succeeded = false;
+ try
+ {
+ building = true;
+ var result = solutionBuildManager2.StartSimpleUpdateSolutionConfiguration(
+ (uint)VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_BUILD, 0, 0);
+ ErrorHandler.ThrowOnFailure(result);
+ succeeded = await buildHandler.BuildCompleted;
+ }
+ catch (OperationCanceledException)
+ {
+ solutionBuildManager2.CancelUpdateSolutionConfiguration();
+ throw;
+ }
+ finally
+ {
+ building = false;
+ buildHandler.Dispose();
+ solutionBuildManager2.UnadviseUpdateSolutionEvents(cookie);
+ }
+ return succeeded;
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CPSTestProjectService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CPSTestProjectService.cs
new file mode 100644
index 00000000..756e70fc
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CPSTestProjectService.cs
@@ -0,0 +1,23 @@
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using System.ComponentModel.Composition;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.ProjectSystem;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ICPSTestProjectService))]
+ internal class CPSTestProjectService : ICPSTestProjectService
+ {
+ public async Task GetProjectAsync(IVsHierarchy hierarchy)
+ {
+ if (!hierarchy.IsCapabilityMatch("TestContainer"))
+ {
+ return null;
+ }
+ var unconfiguredProject = await hierarchy.AsUnconfiguredProjectAsync();
+ if (unconfiguredProject == null) return null;
+ return await unconfiguredProject.GetSuggestedConfiguredProjectAsync();
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseOption.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseOption.cs
new file mode 100644
index 00000000..37daea1a
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseOption.cs
@@ -0,0 +1,24 @@
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ /*
+ adjusted from
+ https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/CommandLine/OptionRecord.cs
+ */
+ internal sealed class CommandLineParseOption
+ {
+ public CommandLineParseOption(string name, string[] arguments)
+ {
+ Name = name;
+ Arguments = arguments;
+ }
+ ///
+ /// Gets the name of the option.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the arguments of the option.
+ ///
+ public string[] Arguments { get; }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseResult.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseResult.cs
new file mode 100644
index 00000000..0c2f8936
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParseResult.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ /*
+ adjusted from
+ https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/CommandLine/ParseResult.cs
+ */
+ internal class CommandLineParseResult
+ {
+ public CommandLineParseResult(
+ IReadOnlyList options,
+ IReadOnlyList errors)
+ {
+ Options = options;
+ Errors = errors;
+ }
+
+ public static CommandLineParseResult Empty { get; } = new CommandLineParseResult(Enumerable.Empty().ToList(),Enumerable.Empty().ToList());
+
+ public const char OptionPrefix = '-';
+
+ public IReadOnlyList Options { get; }
+ public IReadOnlyList Errors { get; }
+
+ public bool HasError => Errors.Count > 0;
+
+ public bool IsOptionSet(string optionName)
+ => Options.Any(o => o.Name.Equals(optionName.Trim(OptionPrefix), StringComparison.OrdinalIgnoreCase));
+
+ public bool TryGetOptionArgumentList(string optionName, out string[] arguments)
+ {
+ optionName = optionName.Trim(OptionPrefix);
+ IEnumerable result = Options.Where(x => x.Name == optionName);
+ if (result.Any())
+ {
+ arguments = result.SelectMany(x => x.Arguments).ToArray();
+ return true;
+ }
+
+ arguments = null;
+ return false;
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParser.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParser.cs
new file mode 100644
index 00000000..b1925e88
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/CommandLineParser.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.CommandLine.Parsing;
+using System.ComponentModel.Composition;
+using System.Linq;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ICommandLineParser
+ {
+ CommandLineParseResult Parse(string argumentsString);
+ }
+
+ /*
+ Adjusted from
+ https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs
+ without escaping
+ */
+ [Export(typeof(ICommandLineParser))]
+ internal class CommandLineParser : ICommandLineParser
+ {
+ public CommandLineParseResult Parse(string argumentsString)
+ {
+ var args = CommandLineStringSplitter.Instance.Split(argumentsString).ToList();
+ if (!args.Any())
+ {
+ return CommandLineParseResult.Empty;
+ }
+ return Parse(args);
+ }
+
+ public CommandLineParseResult Parse(string[] args)
+ => Parse(args.ToList());
+
+ private CommandLineParseResult Parse(List args)
+ {
+ List options = new List();
+ List errors = new List();
+
+ string currentOption = null;
+ string currentArg = null;
+ string toolName = null;
+ List currentOptionArguments = new List();
+ for (int i = 0; i < args.Count; i++)
+ {
+ if (args[i].StartsWith("@") && ResponseFileHelper.TryReadResponseFile(args[i].Substring(1), errors, out string[] newArguments))
+ {
+ args.InsertRange(i + 1, newArguments);
+ continue;
+ }
+ bool argumentHandled = false;
+ currentArg = args[i];
+
+ while (!argumentHandled)
+ {
+ if (currentArg is null)
+ {
+ errors.Add($"UnexpectedNullArgument {i}");
+ break;
+ }
+
+ // we accept as start for options -- and - all the rest are arguments to the previous option
+ if ((args[i].Length > 1 && currentArg[0].Equals('-') && !currentArg[1].Equals('-')) ||
+ (args[i].Length > 2 && currentArg[0].Equals('-') && currentArg[1].Equals('-') && !currentArg[2].Equals('-')))
+ {
+ if (currentOption is null)
+ {
+ ParseOptionAndSeparators(args[i], out currentOption, out currentArg);
+ argumentHandled = currentArg is null;
+ }
+ else
+ {
+ options.Add(new CommandLineParseOption(currentOption, currentOptionArguments.ToArray()));
+ currentOptionArguments.Clear();
+ ParseOptionAndSeparators(args[i], out currentOption, out currentArg);
+ argumentHandled = true;
+ }
+ }
+ else
+ {
+ // If it's the first argument and it doesn't start with - then it's the tool name
+ if (i == 0 && !args[0][0].Equals('-'))
+ {
+ toolName = currentArg;
+ }
+ else if (currentOption is null)
+ {
+ errors.Add($"UnexpectedArgument {args[i]}");
+ }
+ else
+ {
+ currentOptionArguments.Add(currentArg.Trim());
+ currentArg = null;
+ }
+
+ argumentHandled = true;
+ }
+ }
+ }
+
+ if (currentOption != null)
+ {
+ if (currentArg != null)
+ {
+ currentOptionArguments.Add(currentArg.Trim());
+ }
+
+ options.Add(new CommandLineParseOption(currentOption, currentOptionArguments.ToArray()));
+ }
+
+ return new CommandLineParseResult(options, errors);
+ }
+
+ private static void ParseOptionAndSeparators(string arg, out string currentOption, out string currentArg)
+ {
+ var delimiterIndex = arg.IndexOfAny(new char[] { ':', '=', ' ' });
+ if (delimiterIndex == -1)
+ {
+ currentOption = arg;
+ currentArg = null;
+ }
+ else
+ {
+ currentOption = arg.Substring(0, delimiterIndex);
+ currentArg = arg.Substring(++delimiterIndex);
+ }
+ currentOption = currentOption.TrimStart('-');
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IBuildHelper.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IBuildHelper.cs
new file mode 100644
index 00000000..c67957d3
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IBuildHelper.cs
@@ -0,0 +1,22 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal class BuildStartEndArgs
+ {
+ public BuildStartEndArgs(bool isStart)
+ {
+ IsStart = isStart;
+ }
+
+ public bool IsStart { get; }
+ }
+ internal interface IBuildHelper
+ {
+ event EventHandler ExternalBuildEvent;
+ Task BuildAsync(List projects, System.Threading.CancellationToken cancellationToken);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ICPSTestProjectService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ICPSTestProjectService.cs
new file mode 100644
index 00000000..b01fbfe8
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ICPSTestProjectService.cs
@@ -0,0 +1,11 @@
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ICPSTestProjectService
+ {
+ Task GetProjectAsync(IVsHierarchy hierarchy);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/INugetProjectServiceProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/INugetProjectServiceProvider.cs
new file mode 100644
index 00000000..65d0b471
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/INugetProjectServiceProvider.cs
@@ -0,0 +1,10 @@
+using Microsoft.VisualStudio.Threading;
+using NuGet.VisualStudio.Contracts;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface INugetProjectServiceProvider
+ {
+ AsyncLazy LazyNugetProjectService { get; }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IRunSettingsToConfiguration.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IRunSettingsToConfiguration.cs
new file mode 100644
index 00000000..d1f89b05
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IRunSettingsToConfiguration.cs
@@ -0,0 +1,9 @@
+using System.Xml.Linq;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface IRunSettingsToConfiguration
+ {
+ XElement ConvertToConfiguration(XElement runSettingsElement);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ISolutionProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ISolutionProjectsProvider.cs
new file mode 100644
index 00000000..500cb448
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ISolutionProjectsProvider.cs
@@ -0,0 +1,13 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ISolutionProjectsProvider
+ {
+ Task IsSolutionOpenAsync();
+ Task> GetLoadedProjectsAsync(CancellationToken cancellationToken );
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitChangeNotifier.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitChangeNotifier.cs
new file mode 100644
index 00000000..25dbdd5d
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitChangeNotifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using System;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal class ProjectAddedRemoved
+ {
+ public ProjectAddedRemoved(bool added, IVsHierarchy project)
+ {
+ Added = added;
+ Project = project;
+ }
+ public bool Added { get; }
+ public IVsHierarchy Project { get; }
+ }
+
+ internal interface ITUnitChangeNotifier
+ {
+ event EventHandler ProjectAddedRemovedEvent;
+ event EventHandler SolutionClosedEvent;
+ event EventHandler SolutionOpenedEvent;
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverage.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverage.cs
new file mode 100644
index 00000000..7d8ca2b6
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverage.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ interface ITUnitCoverage
+ {
+ bool Ready { get; }
+
+ event EventHandler CollectingChangedEvent;
+ event EventHandler ReadyEvent;
+ void CollectCoverage();
+ void Cancel();
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageProjectFactory.cs
new file mode 100644
index 00000000..3b0e50ba
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageProjectFactory.cs
@@ -0,0 +1,22 @@
+using FineCodeCoverage.Engine.Model;
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ITUnitCoverageProject
+ {
+ string ExePath { get; }
+ Task GetConfigurationAsync(CancellationToken cancellationToken);
+ ICoverageProject CoverageProject { get; }
+ IVsHierarchy VsHierarchy { get; }
+ bool HasCoverageExtension { get; }
+ CommandLineParseResult CommandLineParseResult { get; }
+ }
+ internal interface ITUnitCoverageProjectFactory
+ {
+ Task CreateTUnitCoverageProjectAsync(
+ ITUnitProject tUnitProject,CancellationToken cancellationToken);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageRunner.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageRunner.cs
new file mode 100644
index 00000000..48b109c1
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitCoverageRunner.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal class TUnitSettings
+ {
+ public TUnitSettings(
+ string exePath,
+ string settingsPath,
+ string outputPath,
+ string additionalArgs
+ )
+ {
+ ExePath = exePath;
+ SettingsPath = settingsPath;
+ OutputPath = outputPath;
+ AdditionalArgs = additionalArgs;
+ }
+
+ public string ExePath { get; }
+ public string SettingsPath { get; }
+ public string OutputPath { get; }
+ public string AdditionalArgs { get; }
+ }
+
+ internal interface ITUnitCoverageRunner
+ {
+ event EventHandler ReadyEvent;
+ void Initialize(string appDataFolderPath, CancellationToken cancellationToken);
+ Task RunAsync(
+ TUnitSettings tUnitSettings,
+ bool hasCoverageExtension,
+ bool showWindow = false,
+ CancellationToken cancellationToken = default(CancellationToken));
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitInstalledPackagesService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitInstalledPackagesService.cs
new file mode 100644
index 00000000..f3a9c37c
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitInstalledPackagesService.cs
@@ -0,0 +1,28 @@
+using NuGet.VisualStudio.Contracts;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+using System.Collections.Immutable;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal class TUnitInstalledPackageResult
+ {
+ public TUnitInstalledPackageResult(InstalledPackageResultStatus status, bool hasCoverageExtension, bool hasTunit)
+ {
+ Status = status;
+ HasCoverageExtension = hasCoverageExtension;
+ HasTUnit = hasTunit;
+ }
+
+ public bool HasTUnit { get; }
+ public bool HasCoverageExtension { get; }
+ public InstalledPackageResultStatus Status { get; }
+ }
+
+ interface ITUnitInstalledPackagesService
+ {
+ TUnitInstalledPackageResult GetTUnitInstalledPackages(IImmutableDictionary> packageReferenceItems);
+ Task GetTUnitInstalledPackagesAsync(Guid projectGuid, CancellationToken cancellationToken);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectCache.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectCache.cs
new file mode 100644
index 00000000..8ab9025b
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectCache.cs
@@ -0,0 +1,16 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ITUnitProjectCache
+ {
+ void Initialize(List tUnitProjects);
+ Task> GetTUnitProjectsAsync(CancellationToken cancellationToken);
+ void Remove(IVsHierarchy project);
+ void Add(ITUnitProject iTUnitProject);
+ void Clear();
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectFactory.cs
new file mode 100644
index 00000000..0f1377b4
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectFactory.cs
@@ -0,0 +1,22 @@
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.Shell.Interop;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ITUnitProject : IDisposable
+ {
+ bool IsTUnit { get;} // probably will not change
+ bool HasCoverageExtension { get;} // could change
+ IVsHierarchy Hierarchy { get; }
+ Task UpdateStateAsync(CancellationToken cancellationToken);
+ CommandLineParseResult CommandLineParseResult { get; }
+ }
+
+ internal interface ITUnitProjectFactory
+ {
+ ITUnitProject Create(IVsHierarchy project, ConfiguredProject configuredProject);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectsProvider.cs
new file mode 100644
index 00000000..d0cefd4a
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitProjectsProvider.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ITUnitProjectsProvider
+ {
+ bool Ready { get; }
+
+ event EventHandler ReadyEvent;
+ Task> GetTUnitProjectsAsync(CancellationToken cancellationToken);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitSettingsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitSettingsProvider.cs
new file mode 100644
index 00000000..082a86ad
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ITUnitSettingsProvider.cs
@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal interface ITUnitSettingsProvider
+ {
+ Task ProvideAsync(ITUnitCoverageProject tUnitCoverageProject, CancellationToken cancellationToken);
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IVsHierarchyExtensions.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IVsHierarchyExtensions.cs
new file mode 100644
index 00000000..698c7c98
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/IVsHierarchyExtensions.cs
@@ -0,0 +1,69 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio;
+using System;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.ProjectSystem.Properties;
+using Microsoft.VisualStudio.ProjectSystem;
+using EnvDTE;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal static class IVsHierarchyExtensions
+ {
+ // https://github.com/microsoft/VSProjectSystem/blob/master/doc/automation/finding_CPS_in_a_VS_project.md
+ public async static Task AsUnconfiguredProjectAsync(this IVsHierarchy hier)
+ {
+ if (hier is IVsBrowseObjectContext context)
+ {
+ return context.UnconfiguredProject;
+ }
+ else
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ object pvar;
+ if (ErrorHandler.Succeeded(hier.GetProperty(4294967294U, -2027, out pvar)) && pvar is Project project)
+ {
+ context = project.Object as IVsBrowseObjectContext;
+ return context.UnconfiguredProject;
+ }
+ }
+
+ return null;
+ }
+
+ public static Project ToProject(this IVsHierarchy hierarchy)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ // Retrieve the automation object from the root of the hierarchy.
+ int hr = hierarchy.GetProperty(
+ VSConstants.VSITEMID_ROOT,
+ (int)__VSHPROPID.VSHPROPID_ExtObject,
+ out object extObject);
+
+ if (ErrorHandler.Succeeded(hr) && extObject is Project project)
+ {
+ return project;
+ }
+
+ return null;
+ }
+
+ public static Guid GetGuid(this IVsHierarchy hierarchy)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ int hr = hierarchy.GetGuidProperty(
+ VSConstants.VSITEMID_ROOT,
+ (int)__VSHPROPID.VSHPROPID_ProjectIDGuid,
+ out Guid projectGuid);
+
+ return projectGuid;
+ }
+
+ public async static Task GetGuidAsync(this IVsHierarchy hierarchy)
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ return GetGuid(hierarchy);
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/NugetProjectServiceProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/NugetProjectServiceProvider.cs
new file mode 100644
index 00000000..4de28763
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/NugetProjectServiceProvider.cs
@@ -0,0 +1,34 @@
+using FineCodeCoverage.Core.Utilities;
+using Microsoft.ServiceHub.Framework;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.ServiceBroker;
+using Microsoft.VisualStudio.Threading;
+using NuGet.VisualStudio.Contracts;
+using System;
+using System.ComponentModel.Composition;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(INugetProjectServiceProvider))]
+ internal class NugetProjectServiceProvider : INugetProjectServiceProvider
+ {
+ public AsyncLazy LazyNugetProjectService { get; }
+
+ [ImportingConstructor]
+ public NugetProjectServiceProvider(
+ [Import(typeof(SVsServiceProvider))]
+ IServiceProvider serviceProvider
+ )
+ {
+ LazyNugetProjectService = new AsyncLazy(async () =>
+ {
+ var brokeredServiceContainer = serviceProvider.GetService();
+ IServiceBroker serviceBroker = brokeredServiceContainer.GetFullAccessServiceBroker();
+#pragma warning disable ISB001 // Dispose of proxies
+ INuGetProjectService nugetProjectService = await serviceBroker.GetProxyAsync(NuGetServices.NuGetProjectServiceV1);
+#pragma warning restore ISB001 // Dispose of proxies
+ return nugetProjectService;
+ }, ThreadHelper.JoinableTaskFactory);
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ResponseFileHelper.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ResponseFileHelper.cs
new file mode 100644
index 00000000..1ed67900
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/ResponseFileHelper.cs
@@ -0,0 +1,161 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ internal static class ResponseFileHelper
+ {
+ private enum Boundary
+ {
+ TokenStart,
+ WordEnd,
+ QuoteStart,
+ QuoteEnd,
+ }
+
+ internal static bool TryReadResponseFile(string rspFilePath, ICollection errors, out string[] newArguments)
+ {
+ try
+ {
+ newArguments = ExpandResponseFile(rspFilePath).ToArray();
+ return true;
+ }
+ catch (FileNotFoundException)
+ {
+ errors.Add($"ResponseFileNotFound {rspFilePath}");
+ }
+ catch (IOException e)
+ {
+ errors.Add($"FailedToReadResponseFile {rspFilePath} {e.ToString()}");
+ }
+
+ newArguments = null;
+ return false;
+
+ // Local functions
+ IEnumerable ExpandResponseFile(string filePath)
+ {
+ string[] lines = File.ReadAllLines(filePath);
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ string line = lines[i];
+
+ foreach (string p in SplitLine(line))
+ {
+ yield return p;
+ }
+ }
+ }
+
+ IEnumerable SplitLine(string line)
+ {
+ string arg = line.Trim();
+
+ if (arg.Length == 0 || arg[0] == '#')
+ {
+ yield break;
+ }
+
+ foreach (string word in SplitCommandLine(arg))
+ {
+ yield return word;
+ }
+ }
+ }
+
+ public static IEnumerable SplitCommandLine(string commandLine)
+ {
+ int startTokenIndex = 0;
+
+ int pos = 0;
+
+ Boundary seeking = Boundary.TokenStart;
+ Boundary seekingQuote = Boundary.QuoteStart;
+
+ while (pos < commandLine.Length)
+ {
+ char c = commandLine[pos];
+
+ if (char.IsWhiteSpace(c))
+ {
+ if (seekingQuote == Boundary.QuoteStart)
+ {
+ switch (seeking)
+ {
+ case Boundary.WordEnd:
+ yield return CurrentToken();
+ startTokenIndex = pos;
+ seeking = Boundary.TokenStart;
+ break;
+
+ case Boundary.TokenStart:
+ startTokenIndex = pos;
+ break;
+ }
+ }
+ }
+ else if (c == '\"')
+ {
+ if (seeking == Boundary.TokenStart)
+ {
+ switch (seekingQuote)
+ {
+ case Boundary.QuoteEnd:
+ yield return CurrentToken();
+ startTokenIndex = pos;
+ seekingQuote = Boundary.QuoteStart;
+ break;
+
+ case Boundary.QuoteStart:
+ startTokenIndex = pos + 1;
+ seekingQuote = Boundary.QuoteEnd;
+ break;
+ }
+ }
+ else
+ {
+ switch (seekingQuote)
+ {
+ case Boundary.QuoteEnd:
+ seekingQuote = Boundary.QuoteStart;
+ break;
+
+ case Boundary.QuoteStart:
+ seekingQuote = Boundary.QuoteEnd;
+ break;
+ }
+ }
+ }
+ else if (seeking == Boundary.TokenStart && seekingQuote == Boundary.QuoteStart)
+ {
+ seeking = Boundary.WordEnd;
+ startTokenIndex = pos;
+ }
+
+ Advance();
+
+ if (IsAtEndOfInput())
+ {
+ switch (seeking)
+ {
+ case Boundary.TokenStart:
+ break;
+ default:
+ yield return CurrentToken();
+ break;
+ }
+ }
+ }
+
+ void Advance() => pos++;
+
+ string CurrentToken() => commandLine.Substring(startTokenIndex, IndexOfEndOfToken()).Replace("\"", string.Empty);
+
+ int IndexOfEndOfToken() => pos - startTokenIndex;
+
+ bool IsAtEndOfInput() => pos == commandLine.Length;
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/RunSettingsToConfiguration.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/RunSettingsToConfiguration.cs
new file mode 100644
index 00000000..e4eff207
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/RunSettingsToConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(IRunSettingsToConfiguration))]
+ internal class RunSettingsToConfiguration : IRunSettingsToConfiguration
+ {
+ public XElement ConvertToConfiguration(XElement runSettingsElement)
+ {
+ var dataCollectorsElement = runSettingsElement.Element("DataCollectionRunSettings").Element("DataCollectors");
+ var codeCoverageDataCollectorElement = dataCollectorsElement.Elements().FirstOrDefault(dataCollectorElement =>
+ {
+ var friendlyName = dataCollectorElement.Attribute((XName)"friendlyName")?.Value ?? string.Empty;
+ return friendlyName.Equals("Code Coverage", StringComparison.OrdinalIgnoreCase);
+ });
+ return codeCoverageDataCollectorElement?.Element("Configuration");
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/SolutionProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/SolutionProjectsProvider.cs
new file mode 100644
index 00000000..7930ea5c
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/SolutionProjectsProvider.cs
@@ -0,0 +1,71 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.ComponentModel.Composition;
+using System.Threading;
+using Microsoft;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+
+ [Export(typeof(ISolutionProjectsProvider))]
+ internal class SolutionProjectsProvider : ISolutionProjectsProvider
+ {
+ private readonly IServiceProvider serviceProvider;
+
+ [ImportingConstructor]
+ public SolutionProjectsProvider(
+ [Import(typeof(SVsServiceProvider))]
+ IServiceProvider serviceProvider
+ )
+ {
+ this.serviceProvider = serviceProvider;
+ }
+
+ public async Task> GetLoadedProjectsAsync(CancellationToken cancellationToken)
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
+ return GetProjects(vsSolution, __VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION);
+ }
+
+ public async Task IsSolutionOpenAsync()
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
+ Assumes.Present(vsSolution);
+ vsSolution.GetProperty((int)__VSPROPID.VSPROPID_IsSolutionOpen, out var isSolutionOpen);
+ return (bool)isSolutionOpen;
+ }
+
+ private List GetProjects(IVsSolution vsSolution, __VSENUMPROJFLAGS flags)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ var projects = new List();
+ var result = vsSolution.GetProjectEnum((uint)flags, Guid.Empty, out var enumHierarchies);
+ if (result == VSConstants.S_OK)
+ {
+ IVsHierarchy[] rgelt = new IVsHierarchy[1];
+ uint fetched = 0;
+ while (enumHierarchies.Next(1, rgelt, out fetched) == VSConstants.S_OK && fetched > 0)
+ {
+ int hr = rgelt[0].GetGuidProperty(
+ VSConstants.VSITEMID_ROOT,
+ (int)__VSHPROPID.VSHPROPID_TypeGuid,
+ out var typeGuid
+ );
+
+ if (typeGuid != VSConstants.GUID_ItemType_VirtualFolder)
+ {
+ projects.Add(rgelt[0]);
+ }
+ }
+ }
+ return projects;
+ }
+ }
+
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitChangeNotifier.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitChangeNotifier.cs
new file mode 100644
index 00000000..f09facdc
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitChangeNotifier.cs
@@ -0,0 +1,100 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio;
+using System;
+using System.ComponentModel.Composition;
+using Microsoft;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitChangeNotifier))]
+ internal class TUnitChangeNotifier : ITUnitChangeNotifier, IVsSolutionEvents
+ {
+ public event EventHandler ProjectAddedRemovedEvent;
+ public event EventHandler SolutionClosedEvent;
+ public event EventHandler SolutionOpenedEvent;
+
+ [ImportingConstructor]
+ public TUnitChangeNotifier(
+ [Import(typeof(SVsServiceProvider))]
+ IServiceProvider serviceProvider
+ )
+ {
+#pragma warning disable VSTHRD102 // Implement internal logic asynchronously
+ ThreadHelper.JoinableTaskFactory.Run(async () =>
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
+ Assumes.Present(vsSolution);
+ vsSolution.AdviseSolutionEvents(this, out uint _);
+ });
+#pragma warning restore VSTHRD102 // Implement internal logic asynchronously
+ }
+
+
+ #region solution events
+ public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded)
+ {
+ if (fAdded == 1)
+ {
+ ProjectAddedRemovedEvent?.Invoke(this, new ProjectAddedRemoved(true, pHierarchy));
+ }
+ return VSConstants.S_OK;
+ }
+
+ public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved)
+ {
+ if (fRemoved == 1)
+ {
+ ProjectAddedRemovedEvent?.Invoke(this, new ProjectAddedRemoved(false, pHierarchy));
+ }
+ return VSConstants.S_OK;
+ }
+
+ public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy)
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy)
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel)
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel)
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
+ {
+ SolutionOpenedEvent?.Invoke(this, EventArgs.Empty);
+ return VSConstants.S_OK;
+ }
+
+ public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel)
+ {
+ return VSConstants.S_OK;
+ }
+
+ public int OnBeforeCloseSolution(object pUnkReserved)
+ {
+ SolutionClosedEvent?.Invoke(this, EventArgs.Empty);
+ return VSConstants.S_OK;
+ }
+
+ public int OnAfterCloseSolution(object pUnkReserved)
+ {
+ return VSConstants.S_OK;
+ }
+ #endregion
+
+
+ }
+
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnitConstants.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitConstants.cs
similarity index 64%
rename from SharedProject/Core/MsTestPlatform/TestingPlatform/TUnitConstants.cs
rename to SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitConstants.cs
index 694f1ff6..aaea4f36 100644
--- a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnitConstants.cs
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitConstants.cs
@@ -3,5 +3,6 @@
internal abstract class TUnitConstants
{
public const string TUnitPackageId = "TUnit";
+ public const string CodeCoveragePackageId = "Microsoft.Testing.Extensions.CodeCoverage";
}
}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverage.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverage.cs
new file mode 100644
index 00000000..cecb9e84
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverage.cs
@@ -0,0 +1,232 @@
+using FineCodeCoverage.Core.Utilities;
+using FineCodeCoverage.Engine;
+using FineCodeCoverage.Engine.ReportGenerator;
+using FineCodeCoverage.Impl;
+using FineCodeCoverage.Impl.TestContainerDiscovery;
+using FineCodeCoverage.Output;
+using Microsoft.CodeAnalysis;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Task = System.Threading.Tasks.Task;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitCoverage))]
+ [Export(typeof(ICoverageCollectableFromTestExplorer))]
+ internal class TUnitCoverage : ITUnitCoverage, ICoverageCollectableFromTestExplorer
+ {
+ private readonly ITUnitProjectsProvider tUnitProjectsProvider;
+ private readonly IBuildHelper buildHelper;
+ private readonly ITUnitCoverageProjectFactory tUnitCoverageProjectFactory;
+ private readonly ITUnitCoverageRunner tUnitCoverageRunner;
+ private readonly ICoverageToolOutputManager coverageToolOutputManager;
+ private readonly IFCCEngine fccEngine;
+ private readonly ITUnitSettingsProvider tUnitSettingsProvider;
+ private readonly IDisposeAwareTaskRunner disposeAwareTaskRunner;
+ private readonly IEventAggregator eventAggregator;
+ private readonly ILogger logger;
+ private readonly IReportGeneratorUtil reportGeneratorUtil;
+ private int coverageRunNumber = 1;
+ private ICancellationTokenSource cancellationTokenSource;
+ private bool runnerReady;
+ private bool projectsProviderReady;
+ private bool externalBuildInProgress;
+
+ public event EventHandler ReadyEvent;
+ public event EventHandler CollectingChangedEvent;
+
+ [ImportingConstructor]
+ public TUnitCoverage(
+ ITUnitProjectsProvider tUnitProjectsProvider,
+ IBuildHelper buildHelper,
+ ITUnitCoverageProjectFactory tUnitCoverageProjectFactory,
+ ITUnitCoverageRunner tUnitCoverageRunner,
+ ICoverageToolOutputManager coverageToolOutputManager,
+ IFCCEngine fccEngine,
+ ITUnitSettingsProvider tUnitSettingsProvider,
+ IDisposeAwareTaskRunner disposeAwareTaskRunner,
+ IEventAggregator eventAggregator,
+ ILogger logger,
+ IReportGeneratorUtil reportGeneratorUtil
+ )
+ {
+ buildHelper.ExternalBuildEvent += BuildHelper_ExternalBuildEvent;
+ tUnitProjectsProvider.ReadyEvent += TUnitProjectsProvider_ReadyEvent;
+ tUnitCoverageRunner.ReadyEvent += TUnitRunner_ReadyEvent;
+ projectsProviderReady = tUnitProjectsProvider.Ready;
+ this.tUnitProjectsProvider = tUnitProjectsProvider;
+ this.buildHelper = buildHelper;
+ this.tUnitCoverageProjectFactory = tUnitCoverageProjectFactory;
+ this.tUnitCoverageRunner = tUnitCoverageRunner;
+ this.coverageToolOutputManager = coverageToolOutputManager;
+ this.fccEngine = fccEngine;
+ this.tUnitSettingsProvider = tUnitSettingsProvider;
+ this.disposeAwareTaskRunner = disposeAwareTaskRunner;
+ this.eventAggregator = eventAggregator;
+ this.logger = logger;
+ this.reportGeneratorUtil = reportGeneratorUtil;
+ }
+
+ private void BuildHelper_ExternalBuildEvent(object sender, BuildStartEndArgs e)
+ {
+ externalBuildInProgress = e.IsStart;
+ OnReady();
+ }
+
+ private void TUnitRunner_ReadyEvent(object sender, EventArgs e)
+ {
+ runnerReady = true;
+ OnReady();
+ }
+
+ private void TUnitProjectsProvider_ReadyEvent(object sender, EventArgs e)
+ {
+ projectsProviderReady = tUnitProjectsProvider.Ready;
+ OnReady();
+ }
+
+ public bool Ready => runnerReady && projectsProviderReady && !externalBuildInProgress;
+ private void OnReady()
+ {
+ ReadyEvent?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected void OnCollectingChanged(bool collecting)
+ {
+ CollectingChangedEvent?.Invoke(this, collecting);
+ }
+
+ public void Cancel()
+ {
+ try
+ {
+ if(cancellationTokenSource?.IsCancellationRequested == false)
+ {
+ cancellationTokenSource.Cancel();
+ }
+ }
+ catch (ObjectDisposedException) { }
+ }
+
+ public void CollectCoverage()
+ {
+ Cancel();
+ cancellationTokenSource = disposeAwareTaskRunner.CreateLinkedTokenSource();
+ disposeAwareTaskRunner.RunAsyncFunc(CollectCoverageAsync);
+ }
+
+ private void LogCoverageStarting()
+ {
+ reportGeneratorUtil.LogCoverageProcess($"Coverage Starting - {coverageRunNumber++}");
+ logger.Log(StatusMarkerProvider.Get($"Coverage Starting - {coverageRunNumber++}"));
+ }
+
+ private async Task> GetEnabledTUnitProjectsAsync(CancellationToken cancellationToken)
+ {
+ var tUnitProjects = await tUnitProjectsProvider.GetTUnitProjectsAsync(cancellationToken);
+ var tUnitCoverageProjects = await Task.WhenAll(tUnitProjects.Select(tUnitProject => tUnitCoverageProjectFactory.CreateTUnitCoverageProjectAsync(tUnitProject, cancellationToken)));
+ return tUnitCoverageProjects.Where(tp => tp.CoverageProject.Settings.Enabled).ToList();
+ }
+
+ private async Task CollectCoverageAsync()
+ {
+ var cancellationToken = cancellationTokenSource.Token;
+ LogCoverageStarting();
+ this.eventAggregator.SendMessage(new TestExecutionStartingMessage());
+
+ OnCollectingChanged(true);//order important
+
+ var raiseCoverageEndedMessage = true;
+ try
+ {
+ var tUnitCoverageProjects = await GetEnabledTUnitProjectsAsync(cancellationToken);
+ if (tUnitCoverageProjects.Any())
+ {
+ var success = await BuildAndCollectAsync(tUnitCoverageProjects, cancellationToken);
+ raiseCoverageEndedMessage = !success;
+ }
+ else
+ {
+ logger.Log("No enabled Tunit test projects.");
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ logger.Log("Coverage collection cancelled");
+ }
+ catch(Exception exc)
+ {
+ logger.Log(exc.ToString());
+ }
+ if (raiseCoverageEndedMessage)
+ {
+ reportGeneratorUtil.EndOfCoverageRun();
+ }
+ cancellationTokenSource.Dispose();
+ cancellationTokenSource = null;
+ OnCollectingChanged(false);
+ }
+
+ private async Task BuildAndCollectAsync(List tUnitCoverageProjects, CancellationToken cancellationToken)
+ {
+ var success = false;
+ logger.Log("Starting build");
+ var buildSuccess = await buildHelper.BuildAsync(tUnitCoverageProjects.ConvertAll(tp => tp.VsHierarchy), cancellationToken);
+ if (buildSuccess)
+ {
+ success = await CollectCoverageAsync(tUnitCoverageProjects, cancellationToken);
+ }
+ else
+ {
+ logger.Log("Unsuccessful build. Not collecting coverage");
+ }
+ return success;
+ }
+
+ private async Task CollectCoverageAsync(List tUnitCoverageProjects, CancellationToken cancellationToken)
+ {
+ logger.Log($"Collecting coverage for {tUnitCoverageProjects.Count} enabled TUnit test projects with coverage extension");
+
+ var coverageProjects = tUnitCoverageProjects.ConvertAll(tUnitCoverageProject => tUnitCoverageProject.CoverageProject);
+ cancellationToken.ThrowIfCancellationRequested();
+ coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects);
+
+ var runAllProjects = true;
+ List coberturaFiles = new List();
+ foreach (var tUnitCoverageProject in tUnitCoverageProjects)
+ {
+ var tUnitSettings = await tUnitSettingsProvider.ProvideAsync(tUnitCoverageProject, cancellationToken);
+ var success = await tUnitCoverageRunner.RunAsync(tUnitSettings, tUnitCoverageProject.HasCoverageExtension, false, cancellationToken);
+ if (success)
+ {
+ coberturaFiles.Add(tUnitSettings.OutputPath);
+ }
+ else
+ {
+ runAllProjects = false;
+ break;
+ }
+ }
+
+ if (runAllProjects)
+ {
+ fccEngine.RunAndProcessReport(coberturaFiles.ToArray(), null);
+ }
+ else
+ {
+ logger.Log("Not collecting coverage due to unsuccessful test");
+ }
+ return runAllProjects;
+ }
+
+ async Task ICoverageCollectableFromTestExplorer.IsCollectableAsync()
+ {
+ var tunitProjects = await tUnitProjectsProvider.GetTUnitProjectsAsync(CancellationToken.None);
+ return !tunitProjects.Any();
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageProjectFactory.cs
new file mode 100644
index 00000000..5d55f739
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageProjectFactory.cs
@@ -0,0 +1,147 @@
+using FineCodeCoverage.Engine.Model;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio;
+using System.Threading.Tasks;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using FineCodeCoverage.Engine.MsTestPlatform.CodeCoverage;
+using System.Xml.Linq;
+using System;
+using FineCodeCoverage.Core.Utilities;
+using Microsoft;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitCoverageProjectFactory))]
+ internal class TUnitCoverageProjectFactory : ITUnitCoverageProjectFactory
+ {
+ private readonly ICoverageProjectFactory coverageProjectFactory;
+ private readonly ITemplatedRunSettingsService templatedRunSettingsService;
+ private readonly IServiceProvider serviceProvider;
+ private readonly IXmlUtils xmlUtils;
+ private readonly IRunSettingsToConfiguration runSettingsToConfiguration;
+
+ class TUnitCoverageProject : ITUnitCoverageProject
+ {
+ private readonly Func> configurationProvider;
+
+ public TUnitCoverageProject(
+ string exePath,
+ ICoverageProject coverageProject,
+ IVsHierarchy vsHierarchy,
+ CommandLineParseResult commandLineParseResult,
+ Func> configurationProvider,
+ bool hasCoverageExtension
+ )
+ {
+ ExePath = exePath;
+ CoverageProject = coverageProject;
+ VsHierarchy = vsHierarchy;
+ CommandLineParseResult = commandLineParseResult;
+ this.configurationProvider = configurationProvider;
+ HasCoverageExtension = hasCoverageExtension;
+ }
+ public string ExePath { get; }
+ public Task GetConfigurationAsync(CancellationToken cancellationToken)
+ {
+ return configurationProvider(cancellationToken);
+ }
+ public ICoverageProject CoverageProject { get; }
+ public IVsHierarchy VsHierarchy { get; }
+ public CommandLineParseResult CommandLineParseResult { get; }
+ public bool HasCoverageExtension { get; }
+ }
+
+ [ImportingConstructor]
+ public TUnitCoverageProjectFactory(
+ ICoverageProjectFactory coverageProjectFactory,
+ ITemplatedRunSettingsService templatedRunSettingsService,
+ [Import(typeof(SVsServiceProvider))]
+ IServiceProvider serviceProvider,
+ IXmlUtils xmlUtils,
+ IRunSettingsToConfiguration runSettingsToConfiguration
+ )
+ {
+ this.coverageProjectFactory = coverageProjectFactory;
+ this.templatedRunSettingsService = templatedRunSettingsService;
+ this.serviceProvider = serviceProvider;
+ this.xmlUtils = xmlUtils;
+ this.runSettingsToConfiguration = runSettingsToConfiguration;
+ }
+
+ private async Task CreateCoverageProjectAsync(
+ IVsHierarchy project,
+ CancellationToken cancellationToken)
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ var coverageProject = coverageProjectFactory.Create();
+ project.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_Name, out var projectName);
+ coverageProject.ProjectName = projectName.ToString();
+ project.GetGuidProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_CmdUIGuid, out var projectGuid);
+ coverageProject.Id = projectGuid;
+ project.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID4.VSHPROPID_TargetFrameworkMoniker, out var targetFrameworkMoniker);
+ cancellationToken.ThrowIfCancellationRequested();
+ if (project is IVsBuildPropertyStorage buildPropertyStorage)
+ {
+ //todo configuration parameter for Debug
+ int hr = buildPropertyStorage.GetPropertyValue("TargetPath", null, 1, out var outputFile);
+ ErrorHandler.ThrowOnFailure(hr);
+ coverageProject.TestDllFile = outputFile;
+ }//todo throw if not
+ cancellationToken.ThrowIfCancellationRequested();
+ if (project is IVsProject vsProject)
+ {
+ int hr = vsProject.GetMkDocument(VSConstants.VSITEMID_ROOT, out var projectFilePath);
+ ErrorHandler.ThrowOnFailure(hr);
+ coverageProject.ProjectFile = projectFilePath;
+ }//todo throw if not
+
+ return coverageProject;
+ }
+
+ private async Task GetSolutionDirectoryAsync(CancellationToken cancellationToken)
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ var vsSolution = serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
+ Assumes.Present(vsSolution);
+ vsSolution.GetSolutionInfo(out string solutionDirectory, out var _, out var __);
+ return solutionDirectory;
+ }
+
+ private async Task GetConfigurationElementAsync(ICoverageProject coverageProject, CancellationToken ct)
+ {
+ var solutionDirectory = await GetSolutionDirectoryAsync(ct);
+ var runSettings = templatedRunSettingsService.CreateProjectsRunSettings(new ICoverageProject[] { coverageProject }, solutionDirectory, "")[0].RunSettings;
+ return runSettingsToConfiguration.ConvertToConfiguration(XElement.Parse(runSettings));
+ }
+
+ public async Task CreateTUnitCoverageProjectAsync(
+ ITUnitProject tUnitProject,
+ CancellationToken cancellationToken)
+ {
+ var coverageProject = await CreateCoverageProjectAsync(tUnitProject.Hierarchy, cancellationToken);
+ var exePath = Path.ChangeExtension(coverageProject.TestDllFile, ".exe");
+
+ Func> configurationProvider = async (ct) =>
+ {
+ var configurationElement = await GetConfigurationElementAsync(coverageProject, ct);
+ if (coverageProject.Settings.IncludeTestAssembly)
+ {
+ configurationElement.Add(new XElement("IncludeTestAssembly", true));
+ }
+ return xmlUtils.Serialize(configurationElement);
+ };
+
+ return new TUnitCoverageProject(
+ exePath,
+ coverageProject,
+ tUnitProject.Hierarchy,
+ tUnitProject.CommandLineParseResult,
+ configurationProvider,
+ tUnitProject.HasCoverageExtension);
+ }
+ }
+
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageRunner.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageRunner.cs
new file mode 100644
index 00000000..e90db0b9
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitCoverageRunner.cs
@@ -0,0 +1,148 @@
+using FineCodeCoverage.Core.Initialization;
+using FineCodeCoverage.Core.Utilities;
+using FineCodeCoverage.Output;
+using Microsoft.VisualStudio.Threading;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitCoverageRunner))]
+ internal class TUnitCoverageRunner : ITUnitCoverageRunner
+ {
+ private const string zipDirectoryName = "dotnet-coverage";
+ private const string zipPrefix = "dotnet-coverage";
+ private readonly ILogger logger;
+ private readonly IToolUnzipper toolUnzipper;
+ private const int successExitCode = 0;
+ private readonly Dictionary nonSuccessExitCodeMessages = new Dictionary
+ {
+ { 2, "At least one test failure." },
+ { 3, "Test session was aborted." },
+ { 4, "Setup of used extension is invalid."},
+ { 5, "Command line arguments are invalid."},
+ { 6, "Test session is using a non-implemented feature." },
+ { 7, "Test session was unable to complete successfully, and likely crashed. It's possible that this was caused by a test session that was run via a test controller's extension point."},
+ // todo check the source for this one as may be the minimum expected tests setting
+ { 8, "Test session ran 0 tests." },
+ { 9, "Minimum execution policy for the executed tests was violated." },
+ { 10, "The test adapter failed to run tests for an infrastructure reason unrelated to the test's self. An example is failing to create a fixture needed by tests." },
+ { 11, "The test process will exit if dependent process exits" },
+ { 12, "Test session was unable to run because the client does not support any of the supported protocol versions." },
+ { 13, "Test session was stopped due to reaching the specified number of maximum failed tests using --maximum-failed-tests command-line option." }
+ };
+
+ public event EventHandler ReadyEvent;
+
+ [ImportingConstructor]
+ public TUnitCoverageRunner(
+ ILogger logger,
+ IToolUnzipper toolUnzipper
+ )
+ {
+ this.logger = logger;
+ this.toolUnzipper = toolUnzipper;
+ }
+
+ private (string,string) GetExeAndArgs(
+ TUnitSettings tUnitSettings,
+ bool hasCoverageExtension
+ )
+ {
+ var path = hasCoverageExtension ? tUnitSettings.ExePath : dotnetCoverageExePath;
+ var args = hasCoverageExtension ? $"--disable-logo --coverage --coverage-output-format cobertura --coverage-settings \"{tUnitSettings.SettingsPath}\" --coverage-output \"{tUnitSettings.OutputPath}\"" :
+ $"collect \"{tUnitSettings.ExePath}\" --disable-logo -f cobertura -o \"{tUnitSettings.OutputPath}\" -s \"{tUnitSettings.SettingsPath}\" --nologo";
+ args = $"{args} {tUnitSettings.AdditionalArgs}";
+ return (path, args);
+ }
+
+ private CancellationToken cancellationToken;
+ private string dotnetCoverageExePath;
+
+ public async Task RunAsync(
+ TUnitSettings tUnitSettings,
+ bool hasCoverageExtension,
+ bool showWindow = false,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ this.cancellationToken = cancellationToken;
+ var (path,args) = GetExeAndArgs(tUnitSettings, hasCoverageExtension);
+ // could have FCC option - hide-test-output or just allow them to supply their own
+ logger.Log("Executing TUnit", path, "Arguments", args);
+ using (var process = new Process())
+ {
+ process.StartInfo = new ProcessStartInfo
+ {
+ FileName = path,
+ Arguments = args,
+ UseShellExecute = false,
+ CreateNoWindow = !showWindow,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+ process.OutputDataReceived += Process_OutputDataReceived;
+ process.ErrorDataReceived += Process_ErrorDataReceived;
+ cancellationToken.ThrowIfCancellationRequested();
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ await process.WaitForExitAsync(cancellationToken);
+ process.WaitForExit(1000); // Ensures all output is handled
+
+ /*
+ from https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro?tabs=dotnetcli#run-and-debug-tests
+ The app exits with a nonzero exit code if there's an error, which is typical for most executables. For more information on the known exit codes, see Microsoft.Testing.Platform exit codes.
+ Tip
+ You can ignore a specific exit code using the --ignore-exit-code command line option.
+
+ */
+ LogNonSuccessExitCode(process.ExitCode);
+ logger.Log("-----------");
+ return process.ExitCode == successExitCode;
+ }
+ }
+
+ private void LogNonSuccessExitCode(int exitCode)
+ {
+ if(exitCode != successExitCode)
+ {
+ string message = $"Non success exit code : {exitCode}.";
+ if(nonSuccessExitCodeMessages.TryGetValue(exitCode, out var msg))
+ {
+ message = $"{message} {msg}";
+ }
+ logger.Log(message);
+ }
+ }
+
+ private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ logger.Log($"Error: {e.Data}");
+ }
+ }
+
+ private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ logger.Log(e.Data);
+ }
+ }
+
+ public void Initialize(string appDataFolderPath, CancellationToken cancellationToken)
+ {
+ var zipDestination = toolUnzipper.EnsureUnzipped(appDataFolderPath, zipDirectoryName, zipPrefix, cancellationToken);
+ dotnetCoverageExePath = Directory.GetFiles(zipDestination, "dotnet-coverage.exe", SearchOption.AllDirectories).First();
+ ReadyEvent?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitInstalledPackagesService.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitInstalledPackagesService.cs
new file mode 100644
index 00000000..d839d410
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitInstalledPackagesService.cs
@@ -0,0 +1,86 @@
+using Microsoft.VisualStudio.Threading;
+using NuGet.VisualStudio.Contracts;
+using System;
+using System.Threading.Tasks;
+using System.Threading;
+using System.ComponentModel.Composition;
+using System.Collections.Immutable;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+
+ [Export(typeof(ITUnitInstalledPackagesService))]
+ internal class TUnitInstalledPackagesService : ITUnitInstalledPackagesService
+ {
+ private readonly AsyncLazy lazyNugetProjectService;
+
+ [ImportingConstructor]
+ public TUnitInstalledPackagesService(
+ INugetProjectServiceProvider nugetProjectServiceProvider
+ )
+ {
+ this.lazyNugetProjectService = nugetProjectServiceProvider.LazyNugetProjectService;
+ }
+
+ public TUnitInstalledPackageResult GetTUnitInstalledPackages(IImmutableDictionary> packageReferenceItems)
+ {
+ if(packageReferenceItems == null)
+ {
+ return new TUnitInstalledPackageResult(InstalledPackageResultStatus.Unknown, false, false);
+ }
+
+ var hasTUnit = false;
+ var hasCoverageExtension = false;
+ foreach (var packageReference in packageReferenceItems)
+ {
+ var id = packageReference.Key;
+ if (id == TUnitConstants.TUnitPackageId)
+ {
+ hasTUnit = true;
+ continue;
+ }
+ if (id == TUnitConstants.CodeCoveragePackageId)
+ {
+ hasCoverageExtension = true;
+ }
+ if (hasTUnit && hasCoverageExtension)
+ {
+ break;
+ }
+ }
+ return new TUnitInstalledPackageResult(InstalledPackageResultStatus.Successful, hasCoverageExtension, hasTUnit);
+ }
+
+ public async Task GetTUnitInstalledPackagesAsync(Guid projectGuid, CancellationToken cancellationToken)
+ {
+ var nugetProjectService = await lazyNugetProjectService.GetValueAsync();
+ var result = await nugetProjectService.GetInstalledPackagesAsync(projectGuid, cancellationToken);
+ if (result.Status == InstalledPackageResultStatus.Successful)
+ {
+ var hasTUnit = false;
+ var hasCoverageExtension = false;
+ foreach (var package in result.Packages)
+ {
+ var id = package.Id;
+ if (id == TUnitConstants.TUnitPackageId)
+ {
+ hasTUnit = true;
+ continue;
+ }
+ if (id == TUnitConstants.CodeCoveragePackageId)
+ {
+ hasCoverageExtension = true;
+ }
+ if (hasTUnit && hasCoverageExtension)
+ {
+ break;
+ }
+ }
+ return new TUnitInstalledPackageResult(result.Status, hasCoverageExtension, hasTUnit);
+ }
+ return new TUnitInstalledPackageResult(result.Status, false, false);
+ }
+ }
+
+
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectCache.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectCache.cs
new file mode 100644
index 00000000..a3a1198b
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectCache.cs
@@ -0,0 +1,54 @@
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitProjectCache))]
+ internal class TUnitProjectCache : ITUnitProjectCache
+ {
+ private Dictionary projectLookup;
+ public void Add(ITUnitProject tUnitProject)
+ {
+ projectLookup.Add(tUnitProject.Hierarchy, tUnitProject);
+ }
+
+ public void Clear()
+ {
+ foreach(var tUnitproject in projectLookup.Values)
+ {
+ tUnitproject.Dispose();
+ }
+ projectLookup = null;
+ }
+
+ public async Task> GetTUnitProjectsAsync(CancellationToken cancellationToken)
+ {
+ var tUnitProjects = new List();
+ foreach (var project in projectLookup.Values)
+ {
+ await project.UpdateStateAsync(cancellationToken);
+ if (project.IsTUnit)
+ {
+ tUnitProjects.Add(project);
+ }
+ }
+ return tUnitProjects;
+
+ }
+
+ public void Initialize(List tUnitProjects)
+ {
+ projectLookup = tUnitProjects.ToDictionary(p => p.Hierarchy);
+ }
+
+ public void Remove(IVsHierarchy project)
+ {
+ projectLookup[project].Dispose();
+ projectLookup.Remove(project);
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectFactory.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectFactory.cs
new file mode 100644
index 00000000..afacebd0
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectFactory.cs
@@ -0,0 +1,217 @@
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.ProjectSystem.Properties;
+using Microsoft.VisualStudio.Shell.Interop;
+using NuGet.VisualStudio.Contracts;
+using System;
+using System.Collections.Immutable;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitProjectFactory))]
+ internal class TUnitProjectFactory : ITUnitProjectFactory
+ {
+ private readonly ITUnitInstalledPackagesService tUnitInstalledPackagesService;
+ private readonly ICommandLineParser commandLineParser;
+
+ class TUnitProject : ITUnitProject, IDisposable
+ {
+ private readonly ITUnitInstalledPackagesService tUnitInstalledPackagesService;
+ private readonly ICommandLineParser commandLineParser;
+ private IImmutableDictionary> packageReferenceItems;
+ private bool requiresUpdate = true;
+ private bool disposedValue;
+ private readonly IProjectProperties commonProperties;
+ private readonly IDisposable packageChangeSubscription;
+ private const string FCCTestingPlatformCommandLineArgumentsPropertyName = "FCCTestingPlatformCommandLineArguments";
+ private const string TestingPlatformCommandLineArgumentsPropertyName = "TestingPlatformCommandLineArguments";
+
+
+ /*
+ in VS2022 there is also
+ https://learn.microsoft.com/en-us/visualstudio/extensibility/visualstudio.extensibility/project/project?view=vs-2022
+ https://learn.microsoft.com/en-us/visualstudio/extensibility/project-visual-studio-sdk?view=vs-2022 o
+ */
+
+ public TUnitProject(
+ ITUnitInstalledPackagesService tUnitInstalledPackagesService,
+ ICommandLineParser commandLineParser,
+ ConfiguredProject configuredProject,
+ IVsHierarchy hierarchy
+ )
+ {
+ commonProperties = configuredProject.Services.ProjectPropertiesProvider.GetCommonProperties();
+ this.Hierarchy = hierarchy;
+ this.tUnitInstalledPackagesService = tUnitInstalledPackagesService;
+ this.commandLineParser = commandLineParser;
+ this.packageChangeSubscription = this.SubscribeToPackageReferenceChanges(configuredProject);
+ }
+
+ /*
+ cannot use GetEvaluatedPropertyValueAsync as absence returns empty string
+ */
+ private async Task UseFCCTestingPlatformCommandLineArgumentsPropertyNameAsync()
+ {
+ var propertyNames = await commonProperties.GetPropertyNamesAsync();
+ var hasTestingPlatformCommandLineArgumentsPropertyName = false;
+ foreach (var propertyName in propertyNames)
+ {
+ if(propertyName == FCCTestingPlatformCommandLineArgumentsPropertyName)
+ {
+ return true;
+ }
+ if(propertyName == TestingPlatformCommandLineArgumentsPropertyName)
+ {
+ hasTestingPlatformCommandLineArgumentsPropertyName = true;
+ }
+ }
+ if (hasTestingPlatformCommandLineArgumentsPropertyName)
+ {
+ return false;
+ }
+ return null;
+ }
+
+ private async Task ParseTestingPlatformCommandLineArgumentsAsync()
+ {
+ var useFCCTestingPlatformCommandLineArgumentsPropertyName = await UseFCCTestingPlatformCommandLineArgumentsPropertyNameAsync();
+ if (!useFCCTestingPlatformCommandLineArgumentsPropertyName.HasValue)
+ {
+ CommandLineParseResult = CommandLineParseResult.Empty;
+ }
+ else
+ {
+ var propertyName = useFCCTestingPlatformCommandLineArgumentsPropertyName.Value ? FCCTestingPlatformCommandLineArgumentsPropertyName : TestingPlatformCommandLineArgumentsPropertyName;
+ var testingPlatformCommandLineArguments = await commonProperties.GetEvaluatedPropertyValueAsync(propertyName);
+
+ CommandLineParseResult = commandLineParser.Parse(testingPlatformCommandLineArguments);
+ }
+ }
+
+ private IDisposable SubscribeToPackageReferenceChanges(ConfiguredProject configuredProject)
+ {
+ // there is ActiveConfiguredProjectSubscription but not available in 2019
+ var subscriptionService = configuredProject.Services.ProjectSubscription;
+ var receivingBlock = new ActionBlock>(ProjectUpdateAsync);
+ return subscriptionService.JointRuleSource.SourceBlock.LinkTo(receivingBlock, ruleNames: new string[] { "PackageReference" });
+ }
+
+ /*
+ Idea was to use Nuget api, but
+ IVsPackageInstallerEvents
+ These events are only raised for packages.config projects.
+ To get updates for both packages.config and PackageReference use IVsNuGetProjectUpdateEvents instead.
+
+ But IVsNuGetProjectUpdateEvents shipped in version 6.2 - Visual Studio 2022
+
+ --
+ Also note that IVSProject4 has PackageReferences but the project is IVSProject !
+ and cannot get change event from VSProjectEvents.ReferencesEvents
+ */
+
+ /*
+ if did not want real-time changes then could have used configuredProject.Services.PackageReferences
+ public interface IPackageReference : IReference
+ {
+ }
+ */
+
+ private Task ProjectUpdateAsync(IProjectVersionedValue update)
+ {
+ // if need to switch to the main thread will need CPS IThreadHandling
+ // This runs on a background thread.
+ packageReferenceItems = update.Value.CurrentState["PackageReference"].Items;
+ requiresUpdate = true;
+ return Task.CompletedTask;
+ }
+ public bool IsTUnit { get; private set; }
+ public bool HasCoverageExtension { get; private set; }
+ public IVsHierarchy Hierarchy { get; }
+
+ public CommandLineParseResult CommandLineParseResult { get; private set; } = CommandLineParseResult.Empty;
+ public async Task UpdateStateAsync(CancellationToken cancellationToken)
+ {
+ if (requiresUpdate)
+ {
+ var installedPackagesResult = await tUnitInstalledPackagesService.GetTUnitInstalledPackagesAsync(await Hierarchy.GetGuidAsync(), cancellationToken);
+ if (installedPackagesResult.Status != InstalledPackageResultStatus.Successful)
+ {
+ // fallback but not transitive
+ // the data flow block should get data immediately
+ installedPackagesResult = tUnitInstalledPackagesService.GetTUnitInstalledPackages(packageReferenceItems);
+ }
+
+ IsTUnit = installedPackagesResult.HasTUnit;
+ HasCoverageExtension = installedPackagesResult.HasCoverageExtension;
+
+ requiresUpdate = false;
+ }
+
+ if (IsTUnit)
+ {
+ /*
+ alternative is
+ var projectSnapshotService = configuredProject.Services.ProjectSnapshotService;
+ var receivingBlock = new ActionBlock>((pvv) =>
+ {
+ var projectInstance = pvv.Value.ProjectInstance;
+ var argsProperty = projectInstance.GetProperty(FCCTestingPlatformCommandLineArgumentsPropertyName);
+ if (argsProperty == null)
+ {
+ argsProperty = projectInstance.GetProperty(TestingPlatformCommandLineArgumentsPropertyName);
+ }
+ if(argsProperty != null)
+ {
+ var value = argsProperty.EvaluatedValue;
+ }
+
+ });
+ return projectSnapshotService.SourceBlock.LinkTo(receivingBlock);
+
+ */
+ await ParseTestingPlatformCommandLineArgumentsAsync();
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ packageChangeSubscription.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+
+ [ImportingConstructor]
+ public TUnitProjectFactory(
+ ITUnitInstalledPackagesService tUnitInstalledPackagesService,
+ ICommandLineParser commandLineParser
+ )
+ {
+ this.tUnitInstalledPackagesService = tUnitInstalledPackagesService;
+ this.commandLineParser = commandLineParser;
+ }
+ public ITUnitProject Create(IVsHierarchy hierarchy,ConfiguredProject configuredProject)
+ {
+ return new TUnitProject(tUnitInstalledPackagesService, commandLineParser, configuredProject, hierarchy);
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectsProvider.cs
new file mode 100644
index 00000000..1e943792
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitProjectsProvider.cs
@@ -0,0 +1,155 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.ComponentModel.Composition;
+using System;
+using System.Threading;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.Shell;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitProjectsProvider))]
+ internal class TUnitProjectsProvider : ITUnitProjectsProvider
+ {
+ private readonly ISolutionProjectsProvider solutionProjectsProvider;
+ private readonly ICPSTestProjectService cpsTestProjectService;
+ private readonly ITUnitChangeNotifier tUnitChangeNotifier;
+ private readonly ITUnitProjectFactory tUnitProjectFactory;
+ private readonly ITUnitProjectCache tUnitProjectCache;
+ private bool initializedCache;
+ private readonly List addedProjects = new List();
+
+ public event EventHandler ReadyEvent;
+
+ [ImportingConstructor]
+ public TUnitProjectsProvider(
+ ISolutionProjectsProvider solutionProjectsProvider,
+ ICPSTestProjectService cpsTestProjectService,
+ ITUnitChangeNotifier tUnitChangeNotifier,
+ ITUnitProjectFactory tUnitProjectFactory,
+ ITUnitProjectCache tUnitProjectCache
+ )
+ {
+ tUnitChangeNotifier.ProjectAddedRemovedEvent += TUnitChangeNotifier_ProjectAddedRemovedEvent;
+ tUnitChangeNotifier.SolutionClosedEvent += TUnitChangeNotifier_SolutionClosedEvent;
+ tUnitChangeNotifier.SolutionOpenedEvent += TUnitChangeNotifier_SolutionOpenedEvent;
+ this.solutionProjectsProvider = solutionProjectsProvider;
+ this.cpsTestProjectService = cpsTestProjectService;
+ this.tUnitChangeNotifier = tUnitChangeNotifier;
+ this.tUnitProjectFactory = tUnitProjectFactory;
+ this.tUnitProjectCache = tUnitProjectCache;
+ ThreadHelper.JoinableTaskFactory.Run(async () =>
+ {
+ var solutionOpen = await solutionProjectsProvider.IsSolutionOpenAsync();
+ if (solutionOpen)
+ {
+ OnReady(true);
+ }
+ });
+ }
+
+ public bool Ready { get; private set; }
+
+ private void OnReady(bool ready)
+ {
+ Ready = ready;
+ ReadyEvent?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void TUnitChangeNotifier_SolutionOpenedEvent(object sender, EventArgs e)
+ {
+ OnReady(true);
+ }
+
+ private void TUnitChangeNotifier_SolutionClosedEvent(object sender, EventArgs e)
+ {
+ addedProjects.Clear();
+ if (initializedCache)
+ {
+ tUnitProjectCache.Clear();
+ initializedCache = false;
+ }
+ OnReady(false);
+ }
+
+ private void TUnitChangeNotifier_ProjectAddedRemovedEvent(object sender, ProjectAddedRemoved e)
+ {
+ if (initializedCache)
+ {
+ var project = e.Project;
+ if (e.Added)
+ {
+ addedProjects.Add(project);
+ }
+ else
+ {
+ var removed = addedProjects.Remove(project);
+ if(!removed)
+ {
+ tUnitProjectCache.Remove(e.Project);
+ }
+ }
+ }
+ }
+
+ private class CpsProjectAndHierarchy {
+ public CpsProjectAndHierarchy(ConfiguredProject cpsProject, IVsHierarchy hierarchy)
+ {
+ CpsProject = cpsProject;
+ Hierarchy = hierarchy;
+ }
+
+ public ConfiguredProject CpsProject { get; }
+ public IVsHierarchy Hierarchy { get; }
+ }
+
+ private async Task> GetCpsTestProjectsAndHierarchysAsync(IEnumerable projects)
+ {
+ List cpsTestProjectsAndHierarchys = new List();
+ foreach(var project in projects)
+ {
+ var cpsTestProject = await cpsTestProjectService.GetProjectAsync(project);
+ if (cpsTestProject != null)
+ {
+ cpsTestProjectsAndHierarchys.Add(new CpsProjectAndHierarchy(cpsTestProject, project));
+ }
+ }
+ return cpsTestProjectsAndHierarchys;
+ }
+
+ private async Task> GetTUnitProjectsAsync(IEnumerable projects)
+ {
+ var potentialTUnitProjects = new List();
+ var cpsTestProjectAndHierarchys = await GetCpsTestProjectsAndHierarchysAsync(projects);
+ foreach (var cpsTestProjectAndHierarchy in cpsTestProjectAndHierarchys)
+ {
+ var tUnitProject = tUnitProjectFactory.Create(cpsTestProjectAndHierarchy.Hierarchy, cpsTestProjectAndHierarchy.CpsProject);
+ potentialTUnitProjects.Add(tUnitProject);
+ }
+ return potentialTUnitProjects;
+ }
+
+ public async Task> GetTUnitProjectsAsync(CancellationToken cancellationToken)
+ {
+ if (!initializedCache)
+ {
+ var solutionProjects = await solutionProjectsProvider.GetLoadedProjectsAsync(cancellationToken);
+ var potentialTUnitProjects = await GetTUnitProjectsAsync(solutionProjects);
+ tUnitProjectCache.Initialize(potentialTUnitProjects);
+ initializedCache = true;
+ }
+ else
+ {
+ var newTUnitProjects = await GetTUnitProjectsAsync(addedProjects);
+ foreach(var newTUnitProject in newTUnitProjects)
+ {
+ tUnitProjectCache.Add(newTUnitProject);
+ }
+ addedProjects.Clear();
+ }
+
+ return await tUnitProjectCache.GetTUnitProjectsAsync(cancellationToken);
+ }
+ }
+}
diff --git a/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitSettingsProvider.cs b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitSettingsProvider.cs
new file mode 100644
index 00000000..34692b5c
--- /dev/null
+++ b/SharedProject/Core/MsTestPlatform/TestingPlatform/TUnit/TUnitSettingsProvider.cs
@@ -0,0 +1,209 @@
+using FineCodeCoverage.Core.Utilities;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Threading;
+using System.ComponentModel.Composition;
+using System.IO;
+using FineCodeCoverage.Options;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FineCodeCoverage.Core.MsTestPlatform.TestingPlatform
+{
+ [Export(typeof(ITUnitSettingsProvider))]
+ internal class TUnitSettingsProvider : ITUnitSettingsProvider
+ {
+ private readonly IFileUtil fileUtil;
+ private readonly IXmlUtils xmlUtils;
+ private readonly IRunSettingsToConfiguration runSettingsToConfiguration;
+ private readonly IAppOptionsProvider appOptionsProvider;
+ private readonly IEnvironment environment;
+ private int fccRunWhenTestsExceed;
+ private bool fccRunWhenTestsFail;
+
+ [ImportingConstructor]
+ public TUnitSettingsProvider(
+ IFileUtil fileUtil,
+ IXmlUtils xmlUtils,
+ IRunSettingsToConfiguration runSettingsToConfiguration,
+ IAppOptionsProvider appOptionsProvider,
+ IEnvironment environment
+ )
+ {
+ this.fileUtil = fileUtil;
+ this.xmlUtils = xmlUtils;
+ this.runSettingsToConfiguration = runSettingsToConfiguration;
+ this.appOptionsProvider = appOptionsProvider;
+ this.environment = environment;
+ TakeFCCOptions(appOptionsProvider.Get());
+ this.appOptionsProvider.OptionsChanged += TakeFCCOptions;
+ }
+
+ private void TakeFCCOptions(IAppOptions appOptions)
+ {
+ this.fccRunWhenTestsExceed = appOptions.RunWhenTestsExceed;
+ this.fccRunWhenTestsFail = appOptions.RunWhenTestsFail;
+ }
+
+ public async Task ProvideAsync(ITUnitCoverageProject tUnitCoverageProject, CancellationToken cancellationToken)
+ {
+ await tUnitCoverageProject.CoverageProject.PrepareForCoverageAsync(cancellationToken, false);
+ var coberturaPath = GetCoberturaPath(tUnitCoverageProject);
+ var commandLineParseResult = tUnitCoverageProject.CommandLineParseResult;
+ // todo commandLineParseResult.HasError
+ string configurationPathArgument = null;
+ var additionalArgsStringBuilder = new StringBuilder();
+ string ignoreExitCodeArg = null;
+ int? minimumExpectedTests = null;
+ foreach (var option in commandLineParseResult.Options)
+ {
+ switch (option.Name)
+ {
+ case "coverage":
+ case "coverage-output-format":
+ case "coverage-output"://for now will use own
+ break;
+ case "coverage-settings":
+ case "settings":
+ var arg = option.Arguments.FirstOrDefault();
+ if (arg != null)
+ {
+ if (ConfigurationPathArgExists(arg))
+ {
+ configurationPathArgument = arg;
+ }
+ }
+ break;
+ case "ignore-exit-code":
+ ignoreExitCodeArg = option.Arguments.FirstOrDefault();
+ break;
+ case "minimum-expected-tests":
+ var minExpectedTestsArg = option.Arguments.FirstOrDefault();
+ if (minExpectedTestsArg != null)
+ {
+ if(int.TryParse(minExpectedTestsArg, out var result))
+ {
+ minimumExpectedTests = result;
+ }
+ }
+ break;
+ default:
+ AddToAdditionalArgs($"--{option.Name} {string.Join(" ", option.Arguments)}");
+ break;
+ }
+ }
+
+ AddToAdditionalArgs(GetMinimumExpectedTestsPart(minimumExpectedTests));
+ AddToAdditionalArgs(GetIgnoreExitCodePart(ignoreExitCodeArg));
+
+ var configurationPath = await GetConfigurationPathAsync(tUnitCoverageProject, configurationPathArgument, cancellationToken);
+ return new TUnitSettings(tUnitCoverageProject.ExePath, configurationPath, coberturaPath, additionalArgsStringBuilder.ToString());
+
+ bool ConfigurationPathArgExists(string pathArg)
+ {
+ pathArg = pathArg.Replace("\"", "").Replace("'", "");
+ return fileUtil.Exists(pathArg);
+ }
+
+ void AddToAdditionalArgs(string part)
+ {
+ if (!string.IsNullOrEmpty(part))
+ {
+ additionalArgsStringBuilder.Append($" {part}");
+ }
+ }
+ }
+
+ private string GetMinimumExpectedTestsPart(int? minimumExpectedTestsArg)
+ {
+ // non zero positive integer
+ if (!minimumExpectedTestsArg.HasValue && fccRunWhenTestsExceed > 1)
+ {
+ minimumExpectedTestsArg = fccRunWhenTestsExceed - 1;
+ }
+ return minimumExpectedTestsArg.HasValue ? $"--minimum-expected-tests {minimumExpectedTestsArg}" : null;
+ }
+
+ private string GetIgnoreExitCodePart(string ignoreExitCodeArg)
+ {
+ var ignoreExitCodeString = GetIgnoreExitCodeString(ignoreExitCodeArg);
+ var ignoredExitCodes = GetIgnoredExitCodes(ignoreExitCodeString);
+ if(!ignoredExitCodes.Contains(2) && fccRunWhenTestsFail)
+ {
+ ignoredExitCodes.Add(2);
+ }
+ return ignoredExitCodes.Any() ? $"--ignore-exit-code {string.Join(";", ignoredExitCodes)}" : null;
+ }
+
+ private string GetIgnoreExitCodeString(string ignoreExitCodesArg)
+ {
+ var environmentVariableValue = environment.GetEnvironmentVariable("TESTINGPLATFORM_EXITCODE_IGNORE");
+ return environmentVariableValue ?? ignoreExitCodesArg ?? "";
+ }
+
+ private List GetIgnoredExitCodes(string exitCodes)
+ {
+ try
+ {
+ var codes = exitCodes.Split(';');
+ return codes.Select(code => int.Parse(code)).ToList();
+ }
+ catch
+ {
+ return Enumerable.Empty().ToList();
+ }
+ }
+
+ private async Task GetConfigurationPathAsync(
+ ITUnitCoverageProject tUnitCoverageProject,
+ string configurationPathArgument,
+ CancellationToken cancellationToken
+ )
+ {
+ if (configurationPathArgument != null)
+ {
+ if (tUnitCoverageProject.HasCoverageExtension)
+ {
+ return configurationPathArgument;
+ }
+
+ var configurationOrRunSettingsElement = xmlUtils.Load(configurationPathArgument);
+ var name = configurationOrRunSettingsElement.Name.LocalName;
+ if (name == "Configuration") return configurationPathArgument;
+ if (name == "RunSettings")
+ {
+ var configurationElement = runSettingsToConfiguration.ConvertToConfiguration(configurationOrRunSettingsElement);
+ if (configurationElement != null)
+ {
+ return WriteConfiguration(tUnitCoverageProject, xmlUtils.Serialize(configurationElement));
+ }
+ }
+ }
+
+ return await WriteFCCConfigurationAsync(tUnitCoverageProject, cancellationToken);
+ }
+
+ private async Task WriteFCCConfigurationAsync(ITUnitCoverageProject tUnitCoverageProject, CancellationToken cancellationToken)
+ {
+ var configuration = await tUnitCoverageProject.GetConfigurationAsync(cancellationToken);
+ return WriteConfiguration(tUnitCoverageProject, configuration);
+ }
+
+ private string WriteConfiguration(ITUnitCoverageProject tUnitCoverageProject, string configuration)
+ {
+ var coverageProject = tUnitCoverageProject.CoverageProject;
+ var configurationPath = Path.Combine(coverageProject.CoverageOutputFolder, coverageProject.Id.ToString() + "config.xml");
+ fileUtil.WriteAllText(configurationPath, configuration);
+ return configurationPath;
+ }
+
+ private static string GetCoberturaPath(ITUnitCoverageProject tUnitCoverageProject)
+ {
+ var coverageProject = tUnitCoverageProject.CoverageProject;
+ return Path.Combine(coverageProject.CoverageOutputFolder, coverageProject.Id.ToString() + "coverage.xml");
+ }
+
+ }
+
+
+}
diff --git a/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs b/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs
index 2285e8f6..494d9f66 100644
--- a/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs
+++ b/SharedProject/Core/Utilities/DisposeAwareTaskRunner.cs
@@ -7,11 +7,44 @@
namespace FineCodeCoverage.Core.Utilities
{
-
+ internal interface ICancellationTokenSource : IDisposable
+ {
+ CancellationToken Token { get; }
+ bool IsCancellationRequested { get; }
+
+ void Cancel();
+ }
+
+ internal class CancellationTokenSourceWrapper : ICancellationTokenSource
+ {
+ private readonly CancellationTokenSource cancellationTokenSource;
+
+ public CancellationTokenSourceWrapper(CancellationTokenSource cancellationTokenSource)
+ {
+ this.cancellationTokenSource = cancellationTokenSource;
+ }
+
+ public CancellationToken Token => cancellationTokenSource.Token;
+
+ public bool IsCancellationRequested => cancellationTokenSource.IsCancellationRequested;
+
+ public void Cancel()
+ {
+ cancellationTokenSource.Cancel();
+ }
+
+ public void Dispose()
+ {
+ cancellationTokenSource.Dispose();
+ }
+ }
+
internal interface IDisposeAwareTaskRunner
{
void RunAsyncFunc(Func taskProvider);
CancellationToken DisposalToken { get; }
+ ICancellationTokenSource CreateLinkedTokenSource();
+ bool IsVsShutdown { get; }
}
[Export(typeof(IDisposeAwareTaskRunner))]
@@ -33,6 +66,13 @@ internal DisposeAwareTaskRunner()
///
public CancellationToken DisposalToken => this.disposeCancellationTokenSource.Token;
+ public bool IsVsShutdown => DisposalToken.IsCancellationRequested;
+
+ public ICancellationTokenSource CreateLinkedTokenSource()
+ {
+ return new CancellationTokenSourceWrapper(CancellationTokenSource.CreateLinkedTokenSource(DisposalToken));
+ }
+
public void Dispose()
{
this.Dispose(true);
diff --git a/SharedProject/Core/Utilities/IEnvironment.cs b/SharedProject/Core/Utilities/IEnvironment.cs
new file mode 100644
index 00000000..6c1b80db
--- /dev/null
+++ b/SharedProject/Core/Utilities/IEnvironment.cs
@@ -0,0 +1,7 @@
+namespace FineCodeCoverage.Core.Utilities
+{
+ interface IEnvironment
+ {
+ string GetEnvironmentVariable(string variable);
+ }
+}
diff --git a/SharedProject/Core/Utilities/IXmlUtils.cs b/SharedProject/Core/Utilities/IXmlUtils.cs
new file mode 100644
index 00000000..8d050095
--- /dev/null
+++ b/SharedProject/Core/Utilities/IXmlUtils.cs
@@ -0,0 +1,10 @@
+using System.Xml.Linq;
+
+namespace FineCodeCoverage.Core.Utilities
+{
+ interface IXmlUtils
+ {
+ XElement Load(string path);
+ string Serialize(XElement xmlElement);
+ }
+}
diff --git a/SharedProject/Core/Utilities/SystemEnvironment.cs b/SharedProject/Core/Utilities/SystemEnvironment.cs
new file mode 100644
index 00000000..95d95a15
--- /dev/null
+++ b/SharedProject/Core/Utilities/SystemEnvironment.cs
@@ -0,0 +1,14 @@
+using System;
+using System.ComponentModel.Composition;
+
+namespace FineCodeCoverage.Core.Utilities
+{
+ [Export(typeof(IEnvironment))]
+ internal class SystemEnvironment : IEnvironment
+ {
+ public string GetEnvironmentVariable(string variable)
+ {
+ return Environment.GetEnvironmentVariable(variable);
+ }
+ }
+}
diff --git a/SharedProject/Core/Utilities/XmlUtils.cs b/SharedProject/Core/Utilities/XmlUtils.cs
new file mode 100644
index 00000000..f6b61fea
--- /dev/null
+++ b/SharedProject/Core/Utilities/XmlUtils.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.Composition;
+using System.Xml.Linq;
+
+namespace FineCodeCoverage.Core.Utilities
+{
+ [Export(typeof(IXmlUtils))]
+ internal class XmlUtils : IXmlUtils
+ {
+ public XElement Load(string path)
+ {
+ return XElement.Load(path);
+ }
+
+ public string Serialize(XElement xmlElement)
+ {
+ return new XDocument(new XDeclaration("1.0", "utf-8", "yes"), xmlElement).ToString();
+ }
+ }
+}
diff --git a/SharedProject/Impl/TestContainerDiscovery/ICoverageCollectableFromTestExplorer.cs b/SharedProject/Impl/TestContainerDiscovery/ICoverageCollectableFromTestExplorer.cs
new file mode 100644
index 00000000..e204e45f
--- /dev/null
+++ b/SharedProject/Impl/TestContainerDiscovery/ICoverageCollectableFromTestExplorer.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace FineCodeCoverage.Impl.TestContainerDiscovery
+{
+ internal interface ICoverageCollectableFromTestExplorer
+ {
+ Task IsCollectableAsync();
+ }
+}
diff --git a/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs b/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs
index 46fc205b..58761f79 100644
--- a/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs
+++ b/SharedProject/Impl/TestContainerDiscovery/TestContainerDiscoverer.cs
@@ -15,6 +15,7 @@
using FineCodeCoverage.Core.Initialization;
using FineCodeCoverage.Core.Utilities;
using FineCodeCoverage.Output;
+using FineCodeCoverage.Impl.TestContainerDiscovery;
namespace FineCodeCoverage.Impl
{
@@ -35,6 +36,7 @@ internal class TestContainerDiscoverer : ITestContainerDiscoverer
private readonly IReportGeneratorUtil reportGeneratorUtil;
private readonly IMsCodeCoverageRunSettingsService msCodeCoverageRunSettingsService;
private readonly IEventAggregator eventAggregator;
+ private readonly ICoverageCollectableFromTestExplorer coverageCollectableFromTestExplorer;
internal Dictionary> testOperationStateChangeHandlers;
private bool cancelling;
private MsCodeCoverageCollectionStatus msCodeCoverageCollectionStatus;
@@ -64,13 +66,15 @@ public TestContainerDiscoverer
IAppOptionsProvider appOptionsProvider,
IReportGeneratorUtil reportGeneratorUtil,
IMsCodeCoverageRunSettingsService msCodeCoverageRunSettingsService,
- IEventAggregator eventAggregator
+ IEventAggregator eventAggregator,
+ ICoverageCollectableFromTestExplorer coverageCollectableFromTestExplorer
)
{
this.appOptionsProvider = appOptionsProvider;
this.reportGeneratorUtil = reportGeneratorUtil;
this.msCodeCoverageRunSettingsService = msCodeCoverageRunSettingsService;
this.eventAggregator = eventAggregator;
+ this.coverageCollectableFromTestExplorer = coverageCollectableFromTestExplorer;
this.fccEngine = fccEngine;
this.testOperationStateInvocationManager = testOperationStateInvocationManager;
this.testOperationFactory = testOperationFactory;
@@ -270,7 +274,7 @@ private async Task TestExecutionCancelAndFinishedAsync(IOperation operation)
private async Task OperationState_StateChangedAsync(OperationStateChangedEventArgs e)
{
if (testOperationStateChangeHandlers.TryGetValue(e.State, out var handler)) {
- if (testOperationStateInvocationManager.CanInvoke(e.State))
+ if (await coverageCollectableFromTestExplorer.IsCollectableAsync() && testOperationStateInvocationManager.CanInvoke(e.State))
{
await handler(e.Operation);
}
diff --git a/SharedProject/Output/CancelCollectTUnitCommand.cs b/SharedProject/Output/CancelCollectTUnitCommand.cs
new file mode 100644
index 00000000..8fa27dca
--- /dev/null
+++ b/SharedProject/Output/CancelCollectTUnitCommand.cs
@@ -0,0 +1,76 @@
+using System;
+using System.ComponentModel.Design;
+using EnvDTE80;
+using FineCodeCoverage.Core.MsTestPlatform.TestingPlatform;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Task = System.Threading.Tasks.Task;
+
+namespace FineCodeCoverage.Output
+{
+ ///
+ /// Command handler
+ ///
+ internal sealed class CancelCollectTUnitCommand
+ {
+ ///
+ /// Command ID.
+ ///
+ public const int CommandId = PackageIds.cmdidCancelCollectTUnitCommand;
+
+ ///
+ /// Command menu group (command set GUID).
+ ///
+ public static readonly Guid CommandSet = PackageGuids.guidOutputToolWindowPackageCmdSet;
+
+ private readonly MenuCommand command;
+ private readonly ITUnitCoverage tUnitCoverage;
+
+ public static CancelCollectTUnitCommand Instance
+ {
+ get;
+ private set;
+ }
+
+
+ public static async Task InitializeAsync(AsyncPackage package, ITUnitCoverage tUnitCoverage)
+ {
+ // Switch to the main thread - the call to AddCommand in the constructor requires
+ // the UI thread.
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
+
+ OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
+ var dte = ServiceProvider.GlobalProvider.GetService(typeof(SDTE)) as DTE2;
+ Instance = new CancelCollectTUnitCommand(commandService, tUnitCoverage);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Adds our command handlers for menu (commands must exist in the command table file)
+ ///
+ /// Command service to add command to, not null.
+ private CancelCollectTUnitCommand(OleMenuCommandService commandService, ITUnitCoverage tUnitCoverage)
+ {
+ commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
+
+ var menuCommandID = new CommandID(CommandSet, CommandId);
+ this.command = new MenuCommand(this.Execute, menuCommandID);
+ this.command.Visible = false;
+ tUnitCoverage.CollectingChangedEvent += (_, collecting) => this.command.Visible = collecting;
+ commandService.AddCommand(command);
+ this.tUnitCoverage = tUnitCoverage;
+ }
+
+ ///
+ /// This function is the callback used to execute the command when the menu item is clicked.
+ /// See the constructor to see how the menu item is associated with this function using
+ /// OleMenuCommandService service and MenuCommand class.
+ ///
+ /// Event sender.
+ /// Event args.
+ private void Execute(object sender, EventArgs e)
+ {
+ tUnitCoverage.Cancel();
+ }
+ }
+}
diff --git a/SharedProject/Output/CollectTUnitCommand.cs b/SharedProject/Output/CollectTUnitCommand.cs
new file mode 100644
index 00000000..f3d8b4c1
--- /dev/null
+++ b/SharedProject/Output/CollectTUnitCommand.cs
@@ -0,0 +1,80 @@
+using System;
+using System.ComponentModel.Design;
+using EnvDTE80;
+using FineCodeCoverage.Core.MsTestPlatform.TestingPlatform;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Task = System.Threading.Tasks.Task;
+
+namespace FineCodeCoverage.Output
+{
+ ///
+ /// Command handler
+ ///
+ internal sealed class CollectTUnitCommand
+ {
+ ///
+ /// Command ID.
+ ///
+ public const int CommandId = PackageIds.cmdidCollectTUnitCommand;
+
+ ///
+ /// Command menu group (command set GUID).
+ ///
+ public static readonly Guid CommandSet = PackageGuids.guidOutputToolWindowPackageCmdSet;
+
+ private readonly MenuCommand command;
+ private readonly ITUnitCoverage tUnitCoverage;
+
+ public static CollectTUnitCommand Instance
+ {
+ get;
+ private set;
+ }
+
+
+ public static async Task InitializeAsync(AsyncPackage package, ITUnitCoverage tUnitCoverage)
+ {
+ // Switch to the main thread - the call to AddCommand in the constructor requires
+ // the UI thread.
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
+
+ OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
+ var dte = ServiceProvider.GlobalProvider.GetService(typeof(SDTE)) as DTE2;
+ Instance = new CollectTUnitCommand(commandService, tUnitCoverage);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Adds our command handlers for menu (commands must exist in the command table file)
+ ///
+ /// Command service to add command to, not null.
+ private CollectTUnitCommand(OleMenuCommandService commandService, ITUnitCoverage tUnitCoverage)
+ {
+ commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
+
+ var menuCommandID = new CommandID(CommandSet, CommandId);
+ this.command = new MenuCommand(this.Execute, menuCommandID);
+ this.command.Enabled = tUnitCoverage.Ready;
+ tUnitCoverage.CollectingChangedEvent += (_, collecting) => this.command.Visible = !collecting;
+ tUnitCoverage.ReadyEvent += (_, __) =>
+ {
+ this.command.Enabled = tUnitCoverage.Ready;
+ };
+ commandService.AddCommand(command);
+ this.tUnitCoverage = tUnitCoverage;
+ }
+
+ ///
+ /// This function is the callback used to execute the command when the menu item is clicked.
+ /// See the constructor to see how the menu item is associated with this function using
+ /// OleMenuCommandService service and MenuCommand class.
+ ///
+ /// Event sender.
+ /// Event args.
+ private void Execute(object sender, EventArgs e)
+ {
+ tUnitCoverage.CollectCoverage();
+ }
+ }
+}
diff --git a/SharedProject/Output/OutputToolWindowPackage.cs b/SharedProject/Output/OutputToolWindowPackage.cs
index e72e1040..1b69bf7b 100644
--- a/SharedProject/Output/OutputToolWindowPackage.cs
+++ b/SharedProject/Output/OutputToolWindowPackage.cs
@@ -12,6 +12,7 @@
using EnvDTE80;
using FineCodeCoverage.Core.Utilities;
using FineCodeCoverage.Core.Initialization;
+using FineCodeCoverage.Core.MsTestPlatform.TestingPlatform;
namespace FineCodeCoverage.Output
{
@@ -99,6 +100,9 @@ await OutputToolWindowCommand.InitializeAsync(
componentModel.GetService(),
componentModel.GetService()
);
+ var tUnitCoveraage = componentModel.GetService();
+ await CollectTUnitCommand.InitializeAsync(this, tUnitCoveraage);
+ await CancelCollectTUnitCommand.InitializeAsync(this, tUnitCoveraage);
await componentModel.GetService().InitializeAsync(cancellationToken);
}
diff --git a/SharedProject/SharedProject.projitems b/SharedProject/SharedProject.projitems
index 65bdd1bb..789a7a50 100644
--- a/SharedProject/SharedProject.projitems
+++ b/SharedProject/SharedProject.projitems
@@ -135,7 +135,40 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -169,6 +202,7 @@
+
@@ -177,6 +211,7 @@
+
@@ -189,6 +224,7 @@
+
@@ -201,6 +237,7 @@
+
@@ -387,6 +424,7 @@
+
@@ -415,6 +453,8 @@
+
+