Merge pull request #410 from vagisha-nidhi/coverletDatacollectors

Coverlet Intergration with VSTest
This commit is contained in:
Toni Solarin-Sodara
2019-05-20 13:23:54 +01:00
committed by GitHub
29 changed files with 1949 additions and 0 deletions
+3
View File
@@ -9,6 +9,7 @@
<RemoveDir Directories="$(OutputPath)" Condition="Exists('$(MSBuildThisFileDirectory)\build')" />
<Exec Command="dotnet build &quot;$(MSBuildThisFileDirectory)src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj&quot; -c $(Configuration)" />
<Exec Command="dotnet build &quot;$(MSBuildThisFileDirectory)src\coverlet.console\coverlet.console.csproj&quot; -c $(Configuration)" />
<Exec Command="dotnet build &quot;$(MSBuildThisFileDirectory)src\coverlet.collector\coverlet.collector.csproj&quot; -c $(Configuration)" />
</Target>
<Target Name="PublishMSBuildTaskProject" AfterTargets="BuildAllProjects">
@@ -25,11 +26,13 @@
<Target Name="RunTests" AfterTargets="CopyMSBuildScripts">
<Exec Command="dotnet test &quot;$(MSBuildThisFileDirectory)test\coverlet.core.tests\coverlet.core.tests.csproj&quot; -c $(Configuration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Include=[coverlet.*]*"/>
<Exec Command="dotnet test &quot;$(MSBuildThisFileDirectory)test\coverlet.collector.tests\coverlet.collector.tests.csproj&quot; -c $(Configuration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Include=[coverlet.*]*"/>
</Target>
<Target Name="CreateNuGetPackage" AfterTargets="RunTests" Condition="$(Configuration) == 'Release'">
<Exec Command="dotnet pack &quot;$(MSBuildThisFileDirectory)src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj&quot; -c $(Configuration) -o $(OutputPath)" />
<Exec Command="dotnet pack &quot;$(MSBuildThisFileDirectory)src\coverlet.console\coverlet.console.csproj&quot; -c $(Configuration) -o $(OutputPath)" />
<Exec Command="dotnet pack &quot;$(MSBuildThisFileDirectory)src\coverlet.collector\coverlet.collector.csproj&quot; -c $(Configuration) -p:NuspecFile=&quot;$(MSBuildThisFileDirectory)src\coverlet.collector\coverlet.collector.nuspec&quot; -o $(OutputPath)" />
</Target>
</Project>
+14
View File
@@ -19,6 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.testsubject", "tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core.performancetest", "test\coverlet.core.performancetest\coverlet.core.performancetest.csproj", "{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.collector", "src\coverlet.collector\coverlet.collector.csproj", "{F5B2C45B-274B-43D6-9565-8B50659CFE56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.collector.tests", "test\coverlet.collector.tests\coverlet.collector.tests.csproj", "{5ED4FA81-8F8C-4211-BA88-7573BD63262E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -49,6 +53,14 @@ Global
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.Build.0 = Release|Any CPU
{F5B2C45B-274B-43D6-9565-8B50659CFE56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5B2C45B-274B-43D6-9565-8B50659CFE56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5B2C45B-274B-43D6-9565-8B50659CFE56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5B2C45B-274B-43D6-9565-8B50659CFE56}.Release|Any CPU.Build.0 = Release|Any CPU
{5ED4FA81-8F8C-4211-BA88-7573BD63262E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5ED4FA81-8F8C-4211-BA88-7573BD63262E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ED4FA81-8F8C-4211-BA88-7573BD63262E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5ED4FA81-8F8C-4211-BA88-7573BD63262E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -60,6 +72,8 @@ Global
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
{AE117FAA-C21D-4F23-917E-0C8050614750} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{F5B2C45B-274B-43D6-9565-8B50659CFE56} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
{5ED4FA81-8F8C-4211-BA88-7573BD63262E} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10}
@@ -0,0 +1,168 @@
using System;
using System.ComponentModel;
using System.IO;
using coverlet.collector.Resources;
using Coverlet.Collector.Utilities;
using Coverlet.Collector.Utilities.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Manages coverage report attachments
/// </summary>
internal class AttachmentManager : IDisposable
{
private readonly DataCollectionSink _dataSink;
private readonly TestPlatformEqtTrace _eqtTrace;
private readonly TestPlatformLogger _logger;
private readonly DataCollectionContext _dataCollectionContext;
private readonly IFileHelper _fileHelper;
private readonly IDirectoryHelper _directoryHelper;
private readonly string _reportFileName;
private readonly string _reportDirectory;
public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportFileName)
: this(dataSink,
dataCollectionContext,
logger,
eqtTrace,
reportFileName,
Guid.NewGuid().ToString(),
new FileHelper(),
new DirectoryHelper())
{
}
public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportFileName, string reportDirectoryName, IFileHelper fileHelper, IDirectoryHelper directoryHelper)
{
// Store input variabless
_dataSink = dataSink;
_dataCollectionContext = dataCollectionContext;
_logger = logger;
_eqtTrace = eqtTrace;
_reportFileName = reportFileName;
_fileHelper = fileHelper;
_directoryHelper = directoryHelper;
// Report directory to store the coverage reports.
_reportDirectory = Path.Combine(Path.GetTempPath(), reportDirectoryName);
// Register events
_dataSink.SendFileCompleted += this.OnSendFileCompleted;
}
/// <summary>
/// Sends coverage report to test platform
/// </summary>
/// <param name="coverageReport">Coverage report</param>
public void SendCoverageReport(string coverageReport)
{
// Save coverage report to file
string coverageReportPath = this.SaveCoverageReport(coverageReport);
// Send coverage attachment to test platform.
this.SendAttachment(coverageReportPath);
}
/// <summary>
/// Disposes attachment manager
/// </summary>
public void Dispose()
{
// Unregister events
try
{
if (_dataSink != null)
{
_dataSink.SendFileCompleted -= this.OnSendFileCompleted;
}
this.CleanupReportDirectory();
}
catch (Exception ex)
{
_logger.LogWarning(ex.ToString());
}
}
/// <summary>
/// Saves coverage report to file system
/// </summary>
/// <param name="report">Coverage report</param>
/// <returns>Coverage report file path</returns>
private string SaveCoverageReport(string report)
{
try
{
_directoryHelper.CreateDirectory(_reportDirectory);
string filePath = Path.Combine(_reportDirectory, _reportFileName);
_fileHelper.WriteAllText(filePath, report);
_eqtTrace.Info("{0}: Saved coverage report to path: '{1}'", CoverletConstants.DataCollectorName, filePath);
return filePath;
}
catch (Exception ex)
{
string errorMessage = string.Format(Resources.FailedToSaveCoverageReport, CoverletConstants.DataCollectorName, _reportFileName, _reportDirectory);
throw new CoverletDataCollectorException(errorMessage, ex);
}
}
/// <summary>
/// SendFileCompleted event handler
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event args</param>
public void OnSendFileCompleted(object sender, AsyncCompletedEventArgs e)
{
try
{
_eqtTrace.Verbose("{0}: SendFileCompleted received", CoverletConstants.DataCollectorName);
this.CleanupReportDirectory();
}
catch (Exception ex)
{
_logger.LogWarning(ex.ToString());
this.Dispose();
}
}
/// <summary>
/// Sends attachment file to test platform
/// </summary>
/// <param name="attachmentPath">Attachment file path</param>
private void SendAttachment(string attachmentPath)
{
if (_fileHelper.Exists(attachmentPath))
{
// Send coverage attachment to test platform.
_eqtTrace.Verbose("{0}: Sending attachment to test platform", CoverletConstants.DataCollectorName);
_dataSink.SendFileAsync(_dataCollectionContext, attachmentPath, false);
}
else
{
_eqtTrace.Warning("{0}: Attachment file does not exist", CoverletConstants.DataCollectorName);
}
}
/// <summary>
/// Cleans up coverage report directory
/// </summary>
private void CleanupReportDirectory()
{
try
{
if (_directoryHelper.Exists(_reportDirectory))
{
_directoryHelper.Delete(_reportDirectory, true);
_eqtTrace.Verbose("{0}: Deleted report directory: '{1}'", CoverletConstants.DataCollectorName, _reportDirectory);
}
}
catch (Exception ex)
{
string errorMessage = string.Format(Resources.FailedToCleanupReportDirectory, CoverletConstants.DataCollectorName, _reportDirectory);
throw new CoverletDataCollectorException(errorMessage, ex);
}
}
}
}
@@ -0,0 +1,106 @@
using System;
using coverlet.collector.Resources;
using Coverlet.Collector.Utilities;
using Coverlet.Collector.Utilities.Interfaces;
using Coverlet.Core;
using Coverlet.Core.Logging;
using Coverlet.Core.Reporters;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Manages coverlet coverage
/// </summary>
internal class CoverageManager
{
private readonly Coverage _coverage;
private ICoverageWrapper _coverageWrapper;
public IReporter Reporter { get; }
public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper)
: this(settings,
new ReporterFactory(settings.ReportFormat).CreateReporter(),
new CoverletLogger(eqtTrace, logger),
coverageWrapper)
{
}
public CoverageManager(CoverletSettings settings, IReporter reporter, ILogger logger, ICoverageWrapper coverageWrapper)
{
// Store input vars
Reporter = reporter;
_coverageWrapper = coverageWrapper;
// Coverage object
_coverage = _coverageWrapper.CreateCoverage(settings, logger);
}
/// <summary>
/// Instrument modules
/// </summary>
public void InstrumentModules()
{
try
{
// Instrument modules
_coverageWrapper.PrepareModules(_coverage);
}
catch (Exception ex)
{
string errorMessage = string.Format(Resources.InstrumentationException, CoverletConstants.DataCollectorName);
throw new CoverletDataCollectorException(errorMessage, ex);
}
}
/// <summary>
/// Gets coverlet coverage report
/// </summary>
/// <returns>Coverage report</returns>
public string GetCoverageReport()
{
// Get coverage result
CoverageResult coverageResult = this.GetCoverageResult();
// Get coverage report in default format
string coverageReport = this.GetCoverageReport(coverageResult);
return coverageReport;
}
/// <summary>
/// Gets coverlet coverage result
/// </summary>
/// <returns>Coverage result</returns>
private CoverageResult GetCoverageResult()
{
try
{
return _coverageWrapper.GetCoverageResult(_coverage);
}
catch (Exception ex)
{
string errorMessage = string.Format(Resources.CoverageResultException, CoverletConstants.DataCollectorName);
throw new CoverletDataCollectorException(errorMessage, ex);
}
}
/// <summary>
/// Gets coverage report from coverage result
/// </summary>
/// <param name="coverageResult">Coverage result</param>
/// <returns>Coverage report</returns>
private string GetCoverageReport(CoverageResult coverageResult)
{
try
{
return Reporter.Report(coverageResult);
}
catch (Exception ex)
{
string errorMessage = string.Format(Resources.CoverageReportException, CoverletConstants.DataCollectorName);
throw new CoverletDataCollectorException(errorMessage, ex);
}
}
}
}
@@ -0,0 +1,54 @@
using Coverlet.Collector.Utilities.Interfaces;
using Coverlet.Core;
using Coverlet.Core.Logging;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Implementation for wrapping over Coverage class in coverlet.core
/// </summary>
internal class CoverageWrapper : ICoverageWrapper
{
/// <summary>
/// Creates a coverage object from given coverlet settings
/// </summary>
/// <param name="settings">Coverlet settings</param>
/// <param name="coverletLogger">Coverlet logger</param>
/// <returns>Coverage object</returns>
public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger)
{
return new Coverage(
settings.TestModule,
settings.IncludeFilters,
settings.IncludeDirectories,
settings.ExcludeFilters,
settings.ExcludeSourceFiles,
settings.ExcludeAttributes,
settings.IncludeTestAssembly,
settings.SingleHit,
settings.MergeWith,
settings.UseSourceLink,
coverletLogger);
}
/// <summary>
/// Gets the coverage result from provided coverage object
/// </summary>
/// <param name="coverage">Coverage</param>
/// <returns>The coverage result</returns>
public CoverageResult GetCoverageResult(Coverage coverage)
{
return coverage.GetCoverageResult();
}
/// <summary>
/// Prepares modules for getting coverage.
/// Wrapper over coverage.PrepareModules
/// </summary>
/// <param name="coverage"></param>
public void PrepareModules(Coverage coverage)
{
coverage.PrepareModules();
}
}
}
@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Coverlet.Collector.Utilities;
using Coverlet.Collector.Utilities.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Coverlet coverage out-proc data collector.
/// </summary>
[DataCollectorTypeUri(CoverletConstants.DefaultUri)]
[DataCollectorFriendlyName(CoverletConstants.FriendlyName)]
public class CoverletCoverageCollector : DataCollector
{
private readonly TestPlatformEqtTrace _eqtTrace;
private DataCollectionEvents _events;
private TestPlatformLogger _logger;
private XmlElement _configurationElement;
private DataCollectionSink _dataSink;
private DataCollectionContext _dataCollectionContext;
private CoverageManager _coverageManager;
private ICoverageWrapper _coverageWrapper;
public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper())
{
}
internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper) : base()
{
_eqtTrace = eqtTrace;
_coverageWrapper = coverageWrapper;
}
/// <summary>
/// Initializes data collector
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <param name="events">Events to register on</param>
/// <param name="dataSink">Data sink to send attachments to test platform</param>
/// <param name="logger">Test platform logger</param>
/// <param name="environmentContext">Environment context</param>
public override void Initialize(
XmlElement configurationElement,
DataCollectionEvents events,
DataCollectionSink dataSink,
DataCollectionLogger logger,
DataCollectionEnvironmentContext environmentContext)
{
if (_eqtTrace.IsInfoEnabled)
{
_eqtTrace.Info("Initializing {0} with configuration: '{1}'", CoverletConstants.DataCollectorName, configurationElement?.OuterXml);
}
// Store input variables
_events = events;
_configurationElement = configurationElement;
_dataSink = dataSink;
_dataCollectionContext = environmentContext.SessionDataCollectionContext;
_logger = new TestPlatformLogger(logger, _dataCollectionContext);
// Register events
_events.SessionStart += OnSessionStart;
_events.SessionEnd += OnSessionEnd;
}
/// <summary>
/// Disposes the data collector
/// </summary>
/// <param name="disposing">Disposing flag</param>
protected override void Dispose(bool disposing)
{
_eqtTrace.Verbose("{0}: Disposing", CoverletConstants.DataCollectorName);
// Unregister events
if (_events != null)
{
_events.SessionStart -= OnSessionStart;
_events.SessionEnd -= OnSessionEnd;
}
// Remove vars
_events = null;
_dataSink = null;
_coverageManager = null;
base.Dispose(disposing);
}
/// <summary>
/// SessionStart event handler
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="sessionStartEventArgs">Event args</param>
private void OnSessionStart(object sender, SessionStartEventArgs sessionStartEventArgs)
{
_eqtTrace.Verbose("{0}: SessionStart received", CoverletConstants.DataCollectorName);
try
{
// Get coverlet settings
IEnumerable<string> testModules = this.GetTestModules(sessionStartEventArgs);
var coverletSettingsParser = new CoverletSettingsParser(_eqtTrace);
CoverletSettings coverletSettings = coverletSettingsParser.Parse(_configurationElement, testModules);
// Get coverage and attachment managers
_coverageManager = new CoverageManager(coverletSettings, _eqtTrace, _logger, _coverageWrapper);
// Instrument modules
_coverageManager.InstrumentModules();
}
catch (Exception ex)
{
_logger.LogWarning(ex.ToString());
this.Dispose(true);
}
}
/// <summary>
/// SessionEnd event handler
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event args</param>
private void OnSessionEnd(object sender, SessionEndEventArgs e)
{
try
{
_eqtTrace.Verbose("{0}: SessionEnd received", CoverletConstants.DataCollectorName);
// Get coverage reports
string coverageReport = _coverageManager?.GetCoverageReport();
// Send result attachments to test platform.
var attachmentManager = new AttachmentManager(_dataSink, _dataCollectionContext, _logger, _eqtTrace, this.GetReportFileName());
attachmentManager?.SendCoverageReport(coverageReport);
}
catch (Exception ex)
{
_logger.LogWarning(ex.ToString());
this.Dispose(true);
}
}
/// <summary>
/// Gets coverage report file name
/// </summary>
/// <returns>Coverage report file name</returns>
private string GetReportFileName()
{
string fileName = CoverletConstants.DefaultFileName;
string extension = _coverageManager?.Reporter.Extension;
return extension == null ? fileName : $"{fileName}.{extension}";
}
/// <summary>
/// Gets test modules
/// </summary>
/// <param name="sessionStartEventArgs">Event args</param>
/// <returns>Test modules list</returns>
private IEnumerable<string> GetTestModules(SessionStartEventArgs sessionStartEventArgs)
{
var testModules = sessionStartEventArgs.GetPropertyValue<IEnumerable<string>>(CoverletConstants.TestSourcesPropertyName);
if (_eqtTrace.IsInfoEnabled)
{
_eqtTrace.Info("{0}: TestModules: '{1}'",
CoverletConstants.DataCollectorName,
string.Join(",", testModules ?? Enumerable.Empty<string>()));
}
return testModules;
}
}
}
@@ -0,0 +1,67 @@
using System;
using Coverlet.Collector.Utilities;
using Coverlet.Core.Logging;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Coverlet logger
/// </summary>
internal class CoverletLogger : ILogger
{
private readonly TestPlatformEqtTrace _eqtTrace;
private readonly TestPlatformLogger _logger;
public CoverletLogger(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger)
{
_eqtTrace = eqtTrace;
_logger = logger;
}
/// <summary>
/// Logs error
/// </summary>
/// <param name="message">Error message</param>
public void LogError(string message)
{
_logger.LogWarning(message);
}
/// <summary>
/// Logs error
/// </summary>
/// <param name="exception">Exception to log</param>
public void LogError(Exception exception)
{
_logger.LogWarning(exception.ToString());
}
/// <summary>
/// Logs information
/// </summary>
/// <param name="message">Information message</param>
/// <param name="important">importance</param>
public void LogInformation(string message, bool important = false)
{
_eqtTrace.Info(message);
}
/// <summary>
/// Logs verbose
/// </summary>
/// <param name="message">Verbose message</param>
public void LogVerbose(string message)
{
_eqtTrace.Verbose(message);
}
/// <summary>
/// Logs warning
/// </summary>
/// <param name="message">Warning message</param>
public void LogWarning(string message)
{
_eqtTrace.Warning(message);
}
}
}
@@ -0,0 +1,84 @@
using System.Linq;
using System.Text;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Coverlet settings
/// </summary>
internal class CoverletSettings
{
/// <summary>
/// Test module
/// </summary>
public string TestModule { get; set; }
/// <summary>
/// Report format
/// </summary>
public string ReportFormat { get; set; }
/// <summary>
/// Filters to include
/// </summary>
public string[] IncludeFilters { get; set; }
/// <summary>
/// Directories to include
/// </summary>
public string[] IncludeDirectories { get; set; }
/// <summary>
/// Filters to exclude
/// </summary>
public string[] ExcludeFilters { get; set; }
/// <summary>
/// Source files to exclude
/// </summary>
public string[] ExcludeSourceFiles { get; set; }
/// <summary>
/// Attributes to exclude
/// </summary>
public string[] ExcludeAttributes { get; set; }
/// <summary>
/// Coverate report path to merge with
/// </summary>
public string MergeWith { get; set; }
/// <summary>
/// Use source link flag
/// </summary>
public bool UseSourceLink { get; set; }
/// <summary>
/// Single hit flag
/// </summary>
public bool SingleHit { get; set; }
/// <summary>
/// Includes test assembly
/// </summary>
public bool IncludeTestAssembly { get; set; }
public override string ToString()
{
var builder = new StringBuilder();
builder.AppendFormat("TestModule: '{0}', ", this.TestModule);
builder.AppendFormat("IncludeFilters: '{0}', ", string.Join(",", this.IncludeFilters ?? Enumerable.Empty<string>()));
builder.AppendFormat("IncludeDirectories: '{0}', ", string.Join(",", this.IncludeDirectories ?? Enumerable.Empty<string>()));
builder.AppendFormat("ExcludeFilters: '{0}', ", string.Join(",", this.ExcludeFilters ?? Enumerable.Empty<string>()));
builder.AppendFormat("ExcludeSourceFiles: '{0}', ", string.Join(",", this.ExcludeSourceFiles ?? Enumerable.Empty<string>()));
builder.AppendFormat("ExcludeAttributes: '{0}', ", string.Join(",", this.ExcludeAttributes ?? Enumerable.Empty<string>()));
builder.AppendFormat("MergeWith: '{0}', ", this.MergeWith);
builder.AppendFormat("UseSourceLink: '{0}'", this.UseSourceLink);
builder.AppendFormat("SingleHit: '{0}'", this.SingleHit);
builder.AppendFormat("IncludeTestAssembly: '{0}'", this.IncludeTestAssembly);
return builder.ToString();
}
}
}
@@ -0,0 +1,206 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using coverlet.collector.Resources;
using Coverlet.Collector.Utilities;
namespace Coverlet.Collector.DataCollection
{
/// <summary>
/// Coverlet settings parser
/// </summary>
internal class CoverletSettingsParser
{
private readonly TestPlatformEqtTrace _eqtTrace;
public CoverletSettingsParser(TestPlatformEqtTrace eqtTrace)
{
_eqtTrace = eqtTrace;
}
/// <summary>
/// Parser coverlet settings
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <param name="testModules">Test modules</param>
/// <returns>Coverlet settings</returns>
public CoverletSettings Parse(XmlElement configurationElement, IEnumerable<string> testModules)
{
var coverletSettings = new CoverletSettings
{
TestModule = this.ParseTestModule(testModules)
};
if (configurationElement != null)
{
coverletSettings.IncludeFilters = this.ParseIncludeFilters(configurationElement);
coverletSettings.IncludeDirectories = this.ParseIncludeDirectories(configurationElement);
coverletSettings.ExcludeAttributes = this.ParseExcludeAttributes(configurationElement);
coverletSettings.ExcludeSourceFiles = this.ParseExcludeSourceFiles(configurationElement);
coverletSettings.MergeWith = this.ParseMergeWith(configurationElement);
coverletSettings.UseSourceLink = this.ParseUseSourceLink(configurationElement);
coverletSettings.SingleHit = this.ParseSingleHit(configurationElement);
coverletSettings.IncludeTestAssembly = this.ParseIncludeTestAssembly(configurationElement);
}
coverletSettings.ReportFormat = this.ParseReportFormat(configurationElement);
coverletSettings.ExcludeFilters = this.ParseExcludeFilters(configurationElement);
if (_eqtTrace.IsVerboseEnabled)
{
_eqtTrace.Verbose("{0}: Initializing coverlet process with settings: \"{1}\"", CoverletConstants.DataCollectorName, coverletSettings.ToString());
}
return coverletSettings;
}
/// <summary>
/// Parses test module
/// </summary>
/// <param name="testModules">Test modules</param>
/// <returns>Test module</returns>
private string ParseTestModule(IEnumerable<string> testModules)
{
// Validate if at least one source present.
if (testModules == null || !testModules.Any())
{
string errorMessage = string.Format(Resources.NoTestModulesFound, CoverletConstants.DataCollectorName);
throw new CoverletDataCollectorException(errorMessage);
}
// Note:
// 1) .NET core test run supports one testModule per run. Coverlet also supports one testModule per run. So, we are using first testSource only and ignoring others.
// 2) If and when .NET full is supported with coverlet OR .NET core starts supporting multiple testModules, revisit this code to use other testModules as well.
return testModules.FirstOrDefault();
}
/// <summary>
/// Parse report format
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Report format</returns>
private string ParseReportFormat(XmlElement configurationElement)
{
string format = string.Empty;
if (configurationElement != null)
{
XmlElement reportFormatElement = configurationElement[CoverletConstants.ReportFormatElementName];
format = reportFormatElement?.InnerText?.Split(',').FirstOrDefault();
}
return string.IsNullOrEmpty(format) ? CoverletConstants.DefaultReportFormat : format;
}
/// <summary>
/// Parse filters to include
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Filters to include</returns>
private string[] ParseIncludeFilters(XmlElement configurationElement)
{
XmlElement includeFiltersElement = configurationElement[CoverletConstants.IncludeFiltersElementName];
return includeFiltersElement?.InnerText?.Split(',');
}
/// <summary>
/// Parse directories to include
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Directories to include</returns>
private string[] ParseIncludeDirectories(XmlElement configurationElement)
{
XmlElement includeDirectoriesElement = configurationElement[CoverletConstants.IncludeDirectoriesElementName];
return includeDirectoriesElement?.InnerText?.Split(',');
}
/// <summary>
/// Parse filters to exclude
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Filters to exclude</returns>
private string[] ParseExcludeFilters(XmlElement configurationElement)
{
List<string> excludeFilters = new List<string> { CoverletConstants.DefaultExcludeFilter };
if (configurationElement != null)
{
XmlElement excludeFiltersElement = configurationElement[CoverletConstants.ExcludeFiltersElementName];
string[] filters = excludeFiltersElement?.InnerText?.Split(',');
if (filters != null)
{
excludeFilters.AddRange(filters);
}
}
return excludeFilters.ToArray();
}
/// <summary>
/// Parse source files to exclude
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Source files to exclude</returns>
private string[] ParseExcludeSourceFiles(XmlElement configurationElement)
{
XmlElement excludeSourceFilesElement = configurationElement[CoverletConstants.ExcludeSourceFilesElementName];
return excludeSourceFilesElement?.InnerText?.Split(',');
}
/// <summary>
/// Parse attributes to exclude
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Attributes to exclude</returns>
private string[] ParseExcludeAttributes(XmlElement configurationElement)
{
XmlElement excludeAttributesElement = configurationElement[CoverletConstants.ExcludeAttributesElementName];
return excludeAttributesElement?.InnerText?.Split(',');
}
/// <summary>
/// Parse merge with attribute
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Merge with attribute</returns>
private string ParseMergeWith(XmlElement configurationElement)
{
XmlElement mergeWithElement = configurationElement[CoverletConstants.MergeWithElementName];
return mergeWithElement?.InnerText;
}
/// <summary>
/// Parse use source link flag
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Use source link flag</returns>
private bool ParseUseSourceLink(XmlElement configurationElement)
{
XmlElement useSourceLinkElement = configurationElement[CoverletConstants.UseSourceLinkElementName];
bool.TryParse(useSourceLinkElement?.InnerText, out bool useSourceLink);
return useSourceLink;
}
/// <summary>
/// Parse single hit flag
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Single hit flag</returns>
private bool ParseSingleHit(XmlElement configurationElement)
{
XmlElement singleHitElement = configurationElement[CoverletConstants.SingleHitElementName];
bool.TryParse(singleHitElement?.InnerText, out bool singleHit);
return singleHit;
}
/// <summary>
/// Parse include test assembly flag
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Include Test Assembly Flag</returns>
private bool ParseIncludeTestAssembly(XmlElement configurationElement)
{
XmlElement includeTestAssemblyElement = configurationElement[CoverletConstants.IncludeTestAssemblyElementName];
bool.TryParse(includeTestAssemblyElement?.InnerText, out bool includeTestAssembly);
return includeTestAssembly;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
using System.Runtime.CompilerServices;
#region Test Assemblies
[assembly: InternalsVisibleTo("coverlet.collector.tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
#endregion
@@ -0,0 +1,86 @@
using System;
using System.Reflection;
using coverlet.collector.Resources;
using Coverlet.Collector.Utilities;
using Coverlet.Core.Instrumentation;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollector.InProcDataCollector;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.InProcDataCollector;
namespace Coverlet.Collector.DataCollection
{
public class CoverletInProcDataCollector : InProcDataCollection
{
public void Initialize(IDataCollectionSink dataCollectionSink)
{
}
public void TestCaseEnd(TestCaseEndArgs testCaseEndArgs)
{
}
public void TestCaseStart(TestCaseStartArgs testCaseStartArgs)
{
}
public void TestSessionEnd(TestSessionEndArgs testSessionEndArgs)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type injectedInstrumentationClass = GetInstrumentationClass(assembly);
if (injectedInstrumentationClass is null)
{
continue;
}
try
{
var unloadModule = injectedInstrumentationClass.GetMethod(nameof(ModuleTrackerTemplate.UnloadModule), new[] { typeof(object), typeof(EventArgs) });
unloadModule.Invoke(null, new[] { null, EventArgs.Empty });
}
catch(Exception ex)
{
// Throw any exception if unload fails
if (EqtTrace.IsErrorEnabled)
{
EqtTrace.Error("{0}: Failed to unload module with error: {1}", CoverletConstants.InProcDataCollectorName, ex);
}
string errorMessage = string.Format(Resources.FailedToUnloadModule, CoverletConstants.InProcDataCollectorName);
throw new CoverletDataCollectorException(errorMessage, ex);
}
}
}
public void TestSessionStart(TestSessionStartArgs testSessionStartArgs)
{
}
private static Type GetInstrumentationClass(Assembly assembly)
{
try
{
foreach (var type in assembly.GetTypes())
{
if (type.Namespace == "Coverlet.Core.Instrumentation.Tracker"
&& type.Name.StartsWith(assembly.GetName().Name + "_"))
{
return type;
}
}
return null;
}
catch(Exception ex)
{
// Avoid crashing if reflection fails.
if (EqtTrace.IsWarningEnabled)
{
EqtTrace.Warning("{0}: Failed to get Instrumentation class with error: {1}", CoverletConstants.InProcDataCollectorName, ex);
}
return null;
}
}
}
}
+126
View File
@@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace coverlet.collector.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("coverlet.collector.Resources.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to {0}: Failed to get coverage report.
/// </summary>
internal static string CoverageReportException {
get {
return ResourceManager.GetString("CoverageReportException", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: Failed to get coverage result.
/// </summary>
internal static string CoverageResultException {
get {
return ResourceManager.GetString("CoverageResultException", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: Failed to cleanup report directory: &apos;{1}&apos;.
/// </summary>
internal static string FailedToCleanupReportDirectory {
get {
return ResourceManager.GetString("FailedToCleanupReportDirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: Failed to save coverage report &apos;{1}&apos; in directory &apos;{2}&apos;.
/// </summary>
internal static string FailedToSaveCoverageReport {
get {
return ResourceManager.GetString("FailedToSaveCoverageReport", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: Failed to unload module.
/// </summary>
internal static string FailedToUnloadModule {
get {
return ResourceManager.GetString("FailedToUnloadModule", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: Failed to instrument modules.
/// </summary>
internal static string InstrumentationException {
get {
return ResourceManager.GetString("InstrumentationException", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: No test modules found.
/// </summary>
internal static string NoTestModulesFound {
get {
return ResourceManager.GetString("NoTestModulesFound", resourceCulture);
}
}
}
}
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CoverageReportException" xml:space="preserve">
<value>{0}: Failed to get coverage report</value>
</data>
<data name="CoverageResultException" xml:space="preserve">
<value>{0}: Failed to get coverage result</value>
</data>
<data name="FailedToCleanupReportDirectory" xml:space="preserve">
<value>{0}: Failed to cleanup report directory: '{1}'</value>
</data>
<data name="FailedToSaveCoverageReport" xml:space="preserve">
<value>{0}: Failed to save coverage report '{1}' in directory '{2}'</value>
</data>
<data name="FailedToUnloadModule" xml:space="preserve">
<value>{0}: Failed to unload module</value>
</data>
<data name="InstrumentationException" xml:space="preserve">
<value>{0}: Failed to instrument modules</value>
</data>
<data name="NoTestModulesFound" xml:space="preserve">
<value>{0}: No test modules found</value>
</data>
</root>
@@ -0,0 +1,24 @@
namespace Coverlet.Collector.Utilities
{
internal static class CoverletConstants
{
public const string FriendlyName = "XPlat code coverage";
public const string DefaultUri = @"datacollector://Microsoft/CoverletCodeCoverage/1.0";
public const string DataCollectorName = "CoverletCoverageDataCollector";
public const string DefaultReportFormat = "cobertura";
public const string DefaultFileName = "coverage";
public const string IncludeFiltersElementName = "Include";
public const string IncludeDirectoriesElementName = "IncludeDirectory";
public const string ExcludeFiltersElementName = "Exclude";
public const string ExcludeSourceFilesElementName = "ExcludeByFile";
public const string ExcludeAttributesElementName = "ExcludeByAttribute";
public const string MergeWithElementName = "MergeWith";
public const string UseSourceLinkElementName = "UseSourceLink";
public const string SingleHitElementName = "SingleHit";
public const string IncludeTestAssemblyElementName = "IncludeTestAssembly";
public const string TestSourcesPropertyName = "TestSources";
public const string ReportFormatElementName = "Format";
public const string DefaultExcludeFilter = "[coverlet.*]*";
public const string InProcDataCollectorName = "CoverletInProcDataCollector";
}
}
@@ -0,0 +1,15 @@
using System;
namespace Coverlet.Collector.Utilities
{
internal class CoverletDataCollectorException : Exception
{
public CoverletDataCollectorException(string message) : base(message)
{
}
public CoverletDataCollectorException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
@@ -0,0 +1,27 @@
using System.IO;
using Coverlet.Collector.Utilities.Interfaces;
namespace Coverlet.Collector.Utilities
{
/// <inheritdoc />
internal class DirectoryHelper : IDirectoryHelper
{
/// <inheritdoc />
public bool Exists(string path)
{
return Directory.Exists(path);
}
/// <inheritdoc />
public void CreateDirectory(string path)
{
Directory.CreateDirectory(path);
}
/// <inheritdoc />
public void Delete(string path, bool recursive)
{
Directory.Delete(path, recursive);
}
}
}
@@ -0,0 +1,21 @@
using System.IO;
using Coverlet.Collector.Utilities.Interfaces;
namespace Coverlet.Collector.Utilities
{
/// <inheritdoc />
internal class FileHelper : IFileHelper
{
/// <inheritdoc />
public bool Exists(string path)
{
return File.Exists(path);
}
/// <inheritdoc />
public void WriteAllText(string path, string contents)
{
File.WriteAllText(path, contents);
}
}
}
@@ -0,0 +1,36 @@
using Coverlet.Collector.DataCollection;
using Coverlet.Core;
using Coverlet.Core.Logging;
namespace Coverlet.Collector.Utilities.Interfaces
{
/// <summary>
/// Wrapper interface for Coverage class in coverlet.core
/// Since the class is not testable, this interface is used to abstract methods for mocking in unit tests.
/// </summary>
internal interface ICoverageWrapper
{
/// <summary>
/// Creates a coverage object from given coverlet settings
/// </summary>
/// <param name="settings">Coverlet settings</param>
/// <param name="coverletLogger">Coverlet logger</param>
/// <returns>Coverage object</returns>
Coverage CreateCoverage(CoverletSettings settings, ILogger logger);
/// <summary>
/// Gets the coverage result from provided coverage object
/// </summary>
/// <param name="coverage">Coverage</param>
/// <returns>The coverage result</returns>
CoverageResult GetCoverageResult(Coverage coverage);
/// <summary>
/// Prepares modules for getting coverage.
/// Wrapper over coverage.PrepareModules
/// </summary>
/// <param name="coverage"></param>
void PrepareModules(Coverage coverage);
}
}
@@ -0,0 +1,28 @@
namespace Coverlet.Collector.Utilities.Interfaces
{
interface IDirectoryHelper
{
/// <summary>
/// Determines whether the specified directory exists.
/// </summary>
/// <param name="path">The directory to check.</param>
/// <returns>true if the caller has the required permissions and path contains the name of an existing directory; otherwise, false.
/// This method also returns false if path is null, an invalid path, or a zero-length string.
/// If the caller does not have sufficient permissions to read the specified file,
/// no exception is thrown and the method returns false regardless of the existence of path.</returns>
bool Exists(string path);
/// <summary>
/// Creates all directories and subdirectories in the specified path unless they already exist.
/// </summary>
/// <param name="directory">The directory to create.</param>
void CreateDirectory(string directory);
/// <summary>
/// Deletes the specified directory and, if indicated, any subdirectories and files in the directory.
/// </summary>
/// <param name="path">The name of the directory to remove.</param>
/// <param name="recursive">true to remove directories, subdirectories, and files in path; otherwise, false.</param>
void Delete(string path, bool recursive);
}
}
@@ -0,0 +1,23 @@
namespace Coverlet.Collector.Utilities.Interfaces
{
internal interface IFileHelper
{
/// <summary>
/// Determines whether the specified file exists.
/// </summary>
/// <param name="path">The file to check.</param>
/// <returns>true if the caller has the required permissions and path contains the name of an existing file; otherwise, false.
/// This method also returns false if path is null, an invalid path, or a zero-length string.
/// If the caller does not have sufficient permissions to read the specified file,
/// no exception is thrown and the method returns false regardless of the existence of path.</returns>
bool Exists(string path);
/// <summary>
/// Creates a new file, writes the specified string to the file, and then closes the file.
/// If the target file already exists, it is overwritten.
/// </summary>
/// <param name="path">The file to write to.</param>
/// <param name="contents">The string to write to the file.</param>
void WriteAllText(string path, string contents);
}
}
@@ -0,0 +1,43 @@
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
namespace Coverlet.Collector.Utilities
{
/// <summary>
/// Test platform eqttrace
/// </summary>
internal class TestPlatformEqtTrace
{
public bool IsInfoEnabled => EqtTrace.IsInfoEnabled;
public bool IsVerboseEnabled => EqtTrace.IsVerboseEnabled;
/// <summary>
/// Verbose logger
/// </summary>
/// <param name="format">Format</param>
/// <param name="args">Args</param>
public void Verbose(string format, params object[] args)
{
EqtTrace.Verbose(format, args);
}
/// <summary>
/// Warning logger
/// </summary>
/// <param name="format">Format</param>
/// <param name="args">Args</param>
public void Warning(string format, params object[] args)
{
EqtTrace.Warning(format, args);
}
/// <summary>
/// Info logger
/// </summary>
/// <param name="format">Format</param>
/// <param name="args">Args</param>
public void Info(string format, params object[] args)
{
EqtTrace.Info(format, args);
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
namespace Coverlet.Collector.Utilities
{
/// <summary>
/// Test platform logger
/// </summary>
internal class TestPlatformLogger
{
private readonly DataCollectionLogger _logger;
private readonly DataCollectionContext _dataCollectionContext;
public TestPlatformLogger(DataCollectionLogger logger, DataCollectionContext dataCollectionContext)
{
_logger = logger;
_dataCollectionContext = dataCollectionContext;
}
/// <summary>
/// Log warning
/// </summary>
/// <param name="warning">Warning message</param>
public void LogWarning(string warning)
{
_logger.LogWarning(_dataCollectionContext, warning);
}
}
}
@@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyTitle>coverlet.collector</AssemblyTitle>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageId>coverlet.collector</PackageId>
<PackageVersion>$(AssemblyVersion)</PackageVersion>
<Title>coverlet.collector</Title>
<Authors>tonerdo</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>http://github.com/tonerdo/coverlet</PackageProjectUrl>
<PackageIconUrl>https://raw.githubusercontent.com/tonerdo/coverlet/master/_assets/coverlet-icon.svg?sanitize=true</PackageIconUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<DevelopmentDependency>true</DevelopmentDependency>
<Description>Coverlet is a cross platform code coverage library for .NET, with support for line, branch and method coverage.</Description>
<PackageTags>coverage testing unit-test lcov opencover quality</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/tonerdo/coverlet</RepositoryUrl>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.TestPlatform.ObjectModel" Version="16.1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="coverlet.collector.targets" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\coverlet.core\coverlet.core.csproj" PrivateAssets="All" />
</ItemGroup>
</Project>
@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<package>
<metadata>
<id>coverlet.collector</id>
<version>1.0.0</version>
<title>coverlet.collector</title>
<authors>tonerdo</authors>
<owners>tonerdo</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<iconUrl>https://raw.githubusercontent.com/tonerdo/coverlet/master/_assets/coverlet-icon.svg?sanitize=true</iconUrl>
<description>Coverlet is a cross platform code coverage library for .NET, with support for line, branch and method coverage.</description>
<projectUrl>http://github.com/tonerdo/coverlet</projectUrl>
<tags>coverage testing unit-test lcov opencover quality</tags>
</metadata>
<files>
<file src="bin\Release\netcoreapp2.0\*.dll" target="build\netcoreapp2.0\" />
<file src="bin\Release\netcoreapp2.0\coverlet.collector.deps.json" target="build\netcoreapp2.0\" />
<file src="coverlet.collector.targets" target="build\netcoreapp2.0\" />
</files>
</package>
@@ -0,0 +1,24 @@
<!--
***********************************************************************************************
coverlet.collector.targets
WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
created a backup copy. Incorrect changes to this file will make it
impossible to load or build your projects from the command-line or the IDE.
***********************************************************************************************
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CopyCoverletDataCollectorFiles" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<CoverletDataCollectorFiles Include="$(MSBuildThisFileDirectory)\*.*" />
</ItemGroup>
<Copy SourceFiles="@(CoverletDataCollectorFiles)" DestinationFolder="$(PublishDir)%(RecursiveDir)" />
</Target>
<Target Name="SetXPlatDataCollectorPath" BeforeTargets="VSTest">
<PropertyGroup>
<VSTestTestAdapterPath>$(VSTestTestAdapterPath);$(MSBuildThisFileDirectory)</VSTestTestAdapterPath>
</PropertyGroup>
</Target>
</Project>
@@ -0,0 +1,110 @@
using System;
using System.ComponentModel;
using System.IO;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
using Xunit;
using Moq;
using Coverlet.Collector.Utilities;
using Coverlet.Collector.Utilities.Interfaces;
using Coverlet.Collector.DataCollection;
namespace Coverlet.Collector.Tests
{
public class AttachmentManagerTests
{
private AttachmentManager _attachmentManager;
private Mock<DataCollectionSink> _mockDataCollectionSink;
private DataCollectionContext _dataCollectionContext;
private TestPlatformLogger _testPlatformLogger;
private TestPlatformEqtTrace _eqtTrace;
private Mock<IFileHelper> _mockFileHelper;
private Mock<IDirectoryHelper> _mockDirectoryHelper;
private Mock<DataCollectionLogger> _mockDataCollectionLogger;
public AttachmentManagerTests()
{
_mockDataCollectionSink = new Mock<DataCollectionSink>();
_mockDataCollectionLogger = new Mock<DataCollectionLogger>();
var testcase = new TestCase { Id = Guid.NewGuid() };
_dataCollectionContext = new DataCollectionContext(testcase);
_testPlatformLogger = new TestPlatformLogger(_mockDataCollectionLogger.Object, _dataCollectionContext);
_eqtTrace = new TestPlatformEqtTrace();
_mockFileHelper = new Mock<IFileHelper>();
_mockDirectoryHelper = new Mock<IDirectoryHelper>();
_attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger,
_eqtTrace, "report.cobertura.xml", @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object);
}
[Fact]
public void SendCoverageReportShouldSaveReportToFile()
{
string coverageReport = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<coverage line-rate=\"1\" branch-rate=\"1\" version=\"1.9\" timestamp=\"1556263787\" lines-covered=\"0\" lines-valid=\"0\" branches-covered=\"0\" branches-valid=\"0\">"
+ "<sources/>"
+ "<packages/>"
+ "</coverage>";
_attachmentManager.SendCoverageReport(coverageReport);
_mockFileHelper.Verify(x => x.WriteAllText(It.Is<string>(y => y.Contains(@"report.cobertura.xml")), coverageReport), Times.Once);
}
[Fact]
public void SendCoverageReportShouldThrowExceptionWhenFailedToSaveReportToFile()
{
_attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger,
_eqtTrace, null, @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object);
string coverageReport = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<coverage line-rate=\"1\" branch-rate=\"1\" version=\"1.9\" timestamp=\"1556263787\" lines-covered=\"0\" lines-valid=\"0\" branches-covered=\"0\" branches-valid=\"0\">"
+ "<sources/>"
+ "<packages/>"
+ "</coverage>";
string message = Assert.Throws<CoverletDataCollectorException>(() => _attachmentManager.SendCoverageReport(coverageReport)).Message;
Assert.Contains("CoverletCoverageDataCollector: Failed to save coverage report", message);
}
[Fact]
public void SendCoverageReportShouldSendAttachmentToTestPlatform()
{
var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
_attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger,
_eqtTrace, "report.cobertura.xml", directory.ToString(), new FileHelper(), _mockDirectoryHelper.Object);
string coverageReport = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<coverage line-rate=\"1\" branch-rate=\"1\" version=\"1.9\" timestamp=\"1556263787\" lines-covered=\"0\" lines-valid=\"0\" branches-covered=\"0\" branches-valid=\"0\">"
+ "<sources/>"
+ "<packages/>"
+ "</coverage>";
_attachmentManager.SendCoverageReport(coverageReport);
_mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny<FileTransferInformation>()));
directory.Delete(true);
}
[Fact]
public void OnSendFileCompletedShouldCleanUpReportDirectory()
{
_mockDirectoryHelper.Setup(x => x.Exists(It.Is<string>(y => y.Contains(@"E:\temp")))).Returns(true);
_mockDataCollectionSink.Raise(x => x.SendFileCompleted += null, new AsyncCompletedEventArgs(null, false, null));
_mockDirectoryHelper.Verify(x => x.Delete(It.Is<string>(y => y.Contains(@"E:\temp")), true), Times.Once);
}
[Fact]
public void OnSendFileCompletedShouldThrowCoverletDataCollectorExceptionIfUnableToCleanUpReportDirectory()
{
_mockDirectoryHelper.Setup(x => x.Exists(It.Is<string>(y => y.Contains(@"E:\temp")))).Returns(true);
_mockDirectoryHelper.Setup(x => x.Delete(It.Is<string>(y => y.Contains(@"E:\temp")), true)).Throws(new FileNotFoundException());
_mockDataCollectionSink.Raise(x => x.SendFileCompleted += null, new AsyncCompletedEventArgs(null, false, null));
_mockDataCollectionLogger.Verify(x => x.LogWarning(_dataCollectionContext,
It.Is<string>(y => y.Contains("CoverletDataCollectorException: CoverletCoverageDataCollector: Failed to cleanup report directory"))), Times.AtLeastOnce);
}
}
}
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
using Moq;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Coverlet.Core;
using Coverlet.Core.Logging;
using Coverlet.Collector.Utilities.Interfaces;
using Coverlet.Collector.Utilities;
using Xunit;
using Coverlet.Collector.DataCollection;
namespace Coverlet.Collector.Tests
{
public class CoverletCoverageDataCollectorTests
{
private DataCollectionEnvironmentContext _context;
private CoverletCoverageCollector _coverletCoverageDataCollector;
private DataCollectionContext _dataCollectionContext;
private Mock<DataCollectionEvents> _mockDataColectionEvents;
private Mock<DataCollectionSink> _mockDataCollectionSink;
private Mock<ICoverageWrapper> _mockCoverageWrapper;
private XmlElement _configurationElement;
private Mock<DataCollectionLogger> _mockLogger;
public CoverletCoverageDataCollectorTests()
{
_mockDataColectionEvents = new Mock<DataCollectionEvents>();
_mockDataCollectionSink = new Mock<DataCollectionSink>();
_mockLogger = new Mock<DataCollectionLogger>();
_configurationElement = null;
TestCase testcase = new TestCase { Id = Guid.NewGuid() };
_dataCollectionContext = new DataCollectionContext(testcase);
_context = new DataCollectionEnvironmentContext(_dataCollectionContext);
_mockCoverageWrapper = new Mock<ICoverageWrapper>();
}
[Fact]
public void OnSessionStartShouldInitializeCoverageWithCorrectCoverletSettings()
{
_coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object);
_coverletCoverageDataCollector.Initialize(
_configurationElement,
_mockDataColectionEvents.Object,
_mockDataCollectionSink.Object,
_mockLogger.Object,
_context);
IDictionary<string, object> sessionStartProperties = new Dictionary<string, object>();
sessionStartProperties.Add("TestSources", new List<string> { "abc.dll" });
_mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
_mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is<CoverletSettings>(y => string.Equals(y.TestModule, "abc.dll")), It.IsAny<ILogger>()), Times.Once);
}
[Fact]
public void OnSessionStartShouldPrepareModulesForCoverage()
{
_coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object);
_coverletCoverageDataCollector.Initialize(
_configurationElement,
_mockDataColectionEvents.Object,
_mockDataCollectionSink.Object,
null,
_context);
IDictionary<string, object> sessionStartProperties = new Dictionary<string, object>();
Coverage coverage = new Coverage("abc.dll", null, null, null, null, null, true, true, "abc.json", true, It.IsAny<ILogger>());
sessionStartProperties.Add("TestSources", new List<string> { "abc.dll" });
_mockCoverageWrapper.Setup(x => x.CreateCoverage(It.IsAny<CoverletSettings>(), It.IsAny<ILogger>())).Returns(coverage);
_mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
_mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is<CoverletSettings>(y => y.TestModule.Contains("abc.dll")), It.IsAny<ILogger>()), Times.Once);
_mockCoverageWrapper.Verify(x => x.PrepareModules(It.IsAny<Coverage>()), Times.Once);
}
[Fact]
public void OnSessionEndShouldSendGetCoverageReportToTestPlatform()
{
_coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper());
_coverletCoverageDataCollector.Initialize(
_configurationElement,
_mockDataColectionEvents.Object,
_mockDataCollectionSink.Object,
_mockLogger.Object,
_context);
string module = GetType().Assembly.Location;
string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true);
File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true);
IDictionary<string, object> sessionStartProperties = new Dictionary<string, object>();
sessionStartProperties.Add("TestSources", new List<string> { Path.Combine(directory.FullName, Path.GetFileName(module)) });
_mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
_mockDataColectionEvents.Raise(x => x.SessionEnd += null, new SessionEndEventArgs());
_mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny<FileTransferInformation>()), Times.Once);
directory.Delete(true);
}
[Fact]
public void OnSessionStartShouldLogWarningIfInstrumentationFailed()
{
_coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object);
_coverletCoverageDataCollector.Initialize(
_configurationElement,
_mockDataColectionEvents.Object,
_mockDataCollectionSink.Object,
_mockLogger.Object,
_context);
IDictionary<string, object> sessionStartProperties = new Dictionary<string, object>();
sessionStartProperties.Add("TestSources", new List<string> { "abc.dll" });
_mockCoverageWrapper.Setup(x => x.PrepareModules(It.IsAny<Coverage>())).Throws(new FileNotFoundException());
_mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
_mockLogger.Verify(x => x.LogWarning(_dataCollectionContext,
It.Is<string>(y => y.Contains("CoverletDataCollectorException"))));
}
}
}
@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Coverlet.Collector.DataCollection;
using Coverlet.Collector.Utilities;
using Xunit;
namespace Coverlet.Collector.Tests
{
public class CoverletSettingsParserTests
{
private CoverletSettingsParser _coverletSettingsParser;
public CoverletSettingsParserTests()
{
_coverletSettingsParser = new CoverletSettingsParser(new TestPlatformEqtTrace());
}
[Fact]
public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsNull()
{
string message = Assert.Throws<CoverletDataCollectorException>(() => _coverletSettingsParser.Parse(null, null)).Message;
Assert.Equal("CoverletCoverageDataCollector: No test modules found", message);
}
[Fact]
public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsEmpty()
{
string message = Assert.Throws<CoverletDataCollectorException>(() => _coverletSettingsParser.Parse(null, Enumerable.Empty<string>())).Message;
Assert.Equal("CoverletCoverageDataCollector: No test modules found", message);
}
[Fact]
public void ParseShouldSelectFirstTestModuleFromTestModulesList()
{
var testModules = new List<string> { "module1.dll", "module2.dll", "module3.dll" };
CoverletSettings coverletSettings = _coverletSettingsParser.Parse(null, testModules);
Assert.Equal("module1.dll", coverletSettings.TestModule);
}
[Fact]
public void ParseShouldCorrectlyParseConfigurationElement()
{
var testModules = new List<string> { "abc.dll" };
var doc = new XmlDocument();
var configElement = doc.CreateElement("Configuration");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeFiltersElementName, "[*]*");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeFiltersElementName, "[coverlet.*.tests?]*");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeDirectoriesElementName, @"E:\temp");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeSourceFilesElementName, "module1.cs,module2.cs");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeAttributesElementName, "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.MergeWithElementName, "/path/to/result.json");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.UseSourceLinkElementName, "false");
this.CreateCoverletNodes(doc, configElement, CoverletConstants.SingleHitElementName, "true");
CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules);
Assert.Equal("abc.dll", coverletSettings.TestModule);
Assert.Equal("[*]*", coverletSettings.IncludeFilters[0]);
Assert.Equal(@"E:\temp", coverletSettings.IncludeDirectories[0]);
Assert.Equal("module1.cs", coverletSettings.ExcludeSourceFiles[0]);
Assert.Equal("module2.cs", coverletSettings.ExcludeSourceFiles[1]);
Assert.Equal("Obsolete", coverletSettings.ExcludeAttributes[0]);
Assert.Equal("GeneratedCodeAttribute", coverletSettings.ExcludeAttributes[1]);
Assert.Equal("/path/to/result.json", coverletSettings.MergeWith);
Assert.Equal("[coverlet.*]*", coverletSettings.ExcludeFilters[0]);
Assert.False(coverletSettings.UseSourceLink);
Assert.True(coverletSettings.SingleHit);
}
private void CreateCoverletNodes(XmlDocument doc, XmlElement configElement, string nodeSetting, string nodeValue)
{
var node = doc.CreateNode("element", nodeSetting, string.Empty);
node.InnerText = nodeValue;
configElement.AppendChild(node);
}
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.10.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\coverlet.core\coverlet.core.csproj" />
<ProjectReference Include="..\..\src\coverlet.collector\coverlet.collector.csproj" />
</ItemGroup>
</Project>