diff --git a/build.proj b/build.proj
index 9eb936186..b73737bed 100644
--- a/build.proj
+++ b/build.proj
@@ -9,6 +9,7 @@
+
@@ -25,11 +26,13 @@
+
+
diff --git a/coverlet.sln b/coverlet.sln
index 7853fb012..8c6728ceb 100644
--- a/coverlet.sln
+++ b/coverlet.sln
@@ -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}
diff --git a/src/coverlet.collector/DataCollection/AttachmentManager.cs b/src/coverlet.collector/DataCollection/AttachmentManager.cs
new file mode 100644
index 000000000..b8b4b206b
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/AttachmentManager.cs
@@ -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
+{
+ ///
+ /// Manages coverage report attachments
+ ///
+ 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;
+ }
+
+ ///
+ /// Sends coverage report to test platform
+ ///
+ /// Coverage report
+ public void SendCoverageReport(string coverageReport)
+ {
+ // Save coverage report to file
+ string coverageReportPath = this.SaveCoverageReport(coverageReport);
+
+ // Send coverage attachment to test platform.
+ this.SendAttachment(coverageReportPath);
+ }
+
+ ///
+ /// Disposes attachment manager
+ ///
+ public void Dispose()
+ {
+ // Unregister events
+ try
+ {
+ if (_dataSink != null)
+ {
+ _dataSink.SendFileCompleted -= this.OnSendFileCompleted;
+ }
+ this.CleanupReportDirectory();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex.ToString());
+ }
+ }
+
+ ///
+ /// Saves coverage report to file system
+ ///
+ /// Coverage report
+ /// Coverage report file path
+ 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);
+ }
+ }
+
+ ///
+ /// SendFileCompleted event handler
+ ///
+ /// Sender
+ /// Event args
+ 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();
+ }
+ }
+
+ ///
+ /// Sends attachment file to test platform
+ ///
+ /// Attachment file path
+ 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);
+ }
+ }
+
+ ///
+ /// Cleans up coverage report directory
+ ///
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/coverlet.collector/DataCollection/CoverageManager.cs b/src/coverlet.collector/DataCollection/CoverageManager.cs
new file mode 100644
index 000000000..1a88a0f9d
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/CoverageManager.cs
@@ -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
+{
+ ///
+ /// Manages coverlet coverage
+ ///
+ 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);
+ }
+
+ ///
+ /// Instrument modules
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Gets coverlet coverage report
+ ///
+ /// Coverage report
+ public string GetCoverageReport()
+ {
+ // Get coverage result
+ CoverageResult coverageResult = this.GetCoverageResult();
+
+ // Get coverage report in default format
+ string coverageReport = this.GetCoverageReport(coverageResult);
+ return coverageReport;
+ }
+
+ ///
+ /// Gets coverlet coverage result
+ ///
+ /// Coverage result
+ private CoverageResult GetCoverageResult()
+ {
+ try
+ {
+ return _coverageWrapper.GetCoverageResult(_coverage);
+ }
+ catch (Exception ex)
+ {
+ string errorMessage = string.Format(Resources.CoverageResultException, CoverletConstants.DataCollectorName);
+ throw new CoverletDataCollectorException(errorMessage, ex);
+ }
+ }
+
+ ///
+ /// Gets coverage report from coverage result
+ ///
+ /// Coverage result
+ /// Coverage report
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/coverlet.collector/DataCollection/CoverageWrapper.cs b/src/coverlet.collector/DataCollection/CoverageWrapper.cs
new file mode 100644
index 000000000..76eece41f
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/CoverageWrapper.cs
@@ -0,0 +1,54 @@
+using Coverlet.Collector.Utilities.Interfaces;
+using Coverlet.Core;
+using Coverlet.Core.Logging;
+
+namespace Coverlet.Collector.DataCollection
+{
+ ///
+ /// Implementation for wrapping over Coverage class in coverlet.core
+ ///
+ internal class CoverageWrapper : ICoverageWrapper
+ {
+ ///
+ /// Creates a coverage object from given coverlet settings
+ ///
+ /// Coverlet settings
+ /// Coverlet logger
+ /// Coverage object
+ 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);
+ }
+
+ ///
+ /// Gets the coverage result from provided coverage object
+ ///
+ /// Coverage
+ /// The coverage result
+ public CoverageResult GetCoverageResult(Coverage coverage)
+ {
+ return coverage.GetCoverageResult();
+ }
+
+ ///
+ /// Prepares modules for getting coverage.
+ /// Wrapper over coverage.PrepareModules
+ ///
+ ///
+ public void PrepareModules(Coverage coverage)
+ {
+ coverage.PrepareModules();
+ }
+ }
+}
diff --git a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs
new file mode 100644
index 000000000..791825361
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs
@@ -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
+{
+ ///
+ /// Coverlet coverage out-proc data collector.
+ ///
+ [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;
+ }
+
+ ///
+ /// Initializes data collector
+ ///
+ /// Configuration element
+ /// Events to register on
+ /// Data sink to send attachments to test platform
+ /// Test platform logger
+ /// Environment context
+ 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;
+ }
+
+ ///
+ /// Disposes the data collector
+ ///
+ /// Disposing flag
+ 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);
+ }
+
+ ///
+ /// SessionStart event handler
+ ///
+ /// Sender
+ /// Event args
+ private void OnSessionStart(object sender, SessionStartEventArgs sessionStartEventArgs)
+ {
+ _eqtTrace.Verbose("{0}: SessionStart received", CoverletConstants.DataCollectorName);
+
+ try
+ {
+ // Get coverlet settings
+ IEnumerable 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);
+ }
+ }
+
+ ///
+ /// SessionEnd event handler
+ ///
+ /// Sender
+ /// Event args
+ 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);
+ }
+ }
+
+ ///
+ /// Gets coverage report file name
+ ///
+ /// Coverage report file name
+ private string GetReportFileName()
+ {
+ string fileName = CoverletConstants.DefaultFileName;
+ string extension = _coverageManager?.Reporter.Extension;
+
+ return extension == null ? fileName : $"{fileName}.{extension}";
+ }
+
+ ///
+ /// Gets test modules
+ ///
+ /// Event args
+ /// Test modules list
+ private IEnumerable GetTestModules(SessionStartEventArgs sessionStartEventArgs)
+ {
+ var testModules = sessionStartEventArgs.GetPropertyValue>(CoverletConstants.TestSourcesPropertyName);
+ if (_eqtTrace.IsInfoEnabled)
+ {
+ _eqtTrace.Info("{0}: TestModules: '{1}'",
+ CoverletConstants.DataCollectorName,
+ string.Join(",", testModules ?? Enumerable.Empty()));
+ }
+
+ return testModules;
+ }
+ }
+}
diff --git a/src/coverlet.collector/DataCollection/CoverletLogger.cs b/src/coverlet.collector/DataCollection/CoverletLogger.cs
new file mode 100644
index 000000000..8304e2cf6
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/CoverletLogger.cs
@@ -0,0 +1,67 @@
+using System;
+using Coverlet.Collector.Utilities;
+using Coverlet.Core.Logging;
+
+namespace Coverlet.Collector.DataCollection
+{
+ ///
+ /// Coverlet logger
+ ///
+ internal class CoverletLogger : ILogger
+ {
+ private readonly TestPlatformEqtTrace _eqtTrace;
+ private readonly TestPlatformLogger _logger;
+
+ public CoverletLogger(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger)
+ {
+ _eqtTrace = eqtTrace;
+ _logger = logger;
+ }
+
+ ///
+ /// Logs error
+ ///
+ /// Error message
+ public void LogError(string message)
+ {
+ _logger.LogWarning(message);
+ }
+
+ ///
+ /// Logs error
+ ///
+ /// Exception to log
+ public void LogError(Exception exception)
+ {
+ _logger.LogWarning(exception.ToString());
+ }
+
+ ///
+ /// Logs information
+ ///
+ /// Information message
+ /// importance
+ public void LogInformation(string message, bool important = false)
+ {
+ _eqtTrace.Info(message);
+ }
+
+ ///
+ /// Logs verbose
+ ///
+ /// Verbose message
+ public void LogVerbose(string message)
+ {
+ _eqtTrace.Verbose(message);
+ }
+
+ ///
+ /// Logs warning
+ ///
+ /// Warning message
+ public void LogWarning(string message)
+ {
+ _eqtTrace.Warning(message);
+ }
+ }
+}
diff --git a/src/coverlet.collector/DataCollection/CoverletSettings.cs b/src/coverlet.collector/DataCollection/CoverletSettings.cs
new file mode 100644
index 000000000..f0a519e33
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/CoverletSettings.cs
@@ -0,0 +1,84 @@
+using System.Linq;
+using System.Text;
+
+namespace Coverlet.Collector.DataCollection
+{
+ ///
+ /// Coverlet settings
+ ///
+ internal class CoverletSettings
+ {
+ ///
+ /// Test module
+ ///
+ public string TestModule { get; set; }
+
+ ///
+ /// Report format
+ ///
+ public string ReportFormat { get; set; }
+
+ ///
+ /// Filters to include
+ ///
+ public string[] IncludeFilters { get; set; }
+
+ ///
+ /// Directories to include
+ ///
+ public string[] IncludeDirectories { get; set; }
+
+ ///
+ /// Filters to exclude
+ ///
+ public string[] ExcludeFilters { get; set; }
+
+ ///
+ /// Source files to exclude
+ ///
+ public string[] ExcludeSourceFiles { get; set; }
+
+ ///
+ /// Attributes to exclude
+ ///
+ public string[] ExcludeAttributes { get; set; }
+
+ ///
+ /// Coverate report path to merge with
+ ///
+ public string MergeWith { get; set; }
+
+ ///
+ /// Use source link flag
+ ///
+ public bool UseSourceLink { get; set; }
+
+ ///
+ /// Single hit flag
+ ///
+ public bool SingleHit { get; set; }
+
+ ///
+ /// Includes test assembly
+ ///
+ 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()));
+ builder.AppendFormat("IncludeDirectories: '{0}', ", string.Join(",", this.IncludeDirectories ?? Enumerable.Empty()));
+ builder.AppendFormat("ExcludeFilters: '{0}', ", string.Join(",", this.ExcludeFilters ?? Enumerable.Empty()));
+ builder.AppendFormat("ExcludeSourceFiles: '{0}', ", string.Join(",", this.ExcludeSourceFiles ?? Enumerable.Empty()));
+ builder.AppendFormat("ExcludeAttributes: '{0}', ", string.Join(",", this.ExcludeAttributes ?? Enumerable.Empty()));
+ 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();
+ }
+ }
+}
diff --git a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs
new file mode 100644
index 000000000..c882755aa
--- /dev/null
+++ b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs
@@ -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
+{
+ ///
+ /// Coverlet settings parser
+ ///
+ internal class CoverletSettingsParser
+ {
+ private readonly TestPlatformEqtTrace _eqtTrace;
+
+ public CoverletSettingsParser(TestPlatformEqtTrace eqtTrace)
+ {
+ _eqtTrace = eqtTrace;
+ }
+
+ ///
+ /// Parser coverlet settings
+ ///
+ /// Configuration element
+ /// Test modules
+ /// Coverlet settings
+ public CoverletSettings Parse(XmlElement configurationElement, IEnumerable 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;
+ }
+
+ ///
+ /// Parses test module
+ ///
+ /// Test modules
+ /// Test module
+ private string ParseTestModule(IEnumerable 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();
+ }
+
+ ///
+ /// Parse report format
+ ///
+ /// Configuration element
+ /// Report format
+ 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;
+ }
+
+ ///
+ /// Parse filters to include
+ ///
+ /// Configuration element
+ /// Filters to include
+ private string[] ParseIncludeFilters(XmlElement configurationElement)
+ {
+ XmlElement includeFiltersElement = configurationElement[CoverletConstants.IncludeFiltersElementName];
+ return includeFiltersElement?.InnerText?.Split(',');
+ }
+
+ ///
+ /// Parse directories to include
+ ///
+ /// Configuration element
+ /// Directories to include
+ private string[] ParseIncludeDirectories(XmlElement configurationElement)
+ {
+ XmlElement includeDirectoriesElement = configurationElement[CoverletConstants.IncludeDirectoriesElementName];
+ return includeDirectoriesElement?.InnerText?.Split(',');
+ }
+
+ ///
+ /// Parse filters to exclude
+ ///
+ /// Configuration element
+ /// Filters to exclude
+ private string[] ParseExcludeFilters(XmlElement configurationElement)
+ {
+ List excludeFilters = new List { CoverletConstants.DefaultExcludeFilter };
+
+ if (configurationElement != null)
+ {
+ XmlElement excludeFiltersElement = configurationElement[CoverletConstants.ExcludeFiltersElementName];
+ string[] filters = excludeFiltersElement?.InnerText?.Split(',');
+ if (filters != null)
+ {
+ excludeFilters.AddRange(filters);
+ }
+ }
+
+ return excludeFilters.ToArray();
+ }
+
+ ///
+ /// Parse source files to exclude
+ ///
+ /// Configuration element
+ /// Source files to exclude
+ private string[] ParseExcludeSourceFiles(XmlElement configurationElement)
+ {
+ XmlElement excludeSourceFilesElement = configurationElement[CoverletConstants.ExcludeSourceFilesElementName];
+ return excludeSourceFilesElement?.InnerText?.Split(',');
+ }
+
+ ///
+ /// Parse attributes to exclude
+ ///
+ /// Configuration element
+ /// Attributes to exclude
+ private string[] ParseExcludeAttributes(XmlElement configurationElement)
+ {
+ XmlElement excludeAttributesElement = configurationElement[CoverletConstants.ExcludeAttributesElementName];
+ return excludeAttributesElement?.InnerText?.Split(',');
+ }
+
+ ///
+ /// Parse merge with attribute
+ ///
+ /// Configuration element
+ /// Merge with attribute
+ private string ParseMergeWith(XmlElement configurationElement)
+ {
+ XmlElement mergeWithElement = configurationElement[CoverletConstants.MergeWithElementName];
+ return mergeWithElement?.InnerText;
+ }
+
+ ///
+ /// Parse use source link flag
+ ///
+ /// Configuration element
+ /// Use source link flag
+ private bool ParseUseSourceLink(XmlElement configurationElement)
+ {
+ XmlElement useSourceLinkElement = configurationElement[CoverletConstants.UseSourceLinkElementName];
+ bool.TryParse(useSourceLinkElement?.InnerText, out bool useSourceLink);
+ return useSourceLink;
+ }
+
+ ///
+ /// Parse single hit flag
+ ///
+ /// Configuration element
+ /// Single hit flag
+ private bool ParseSingleHit(XmlElement configurationElement)
+ {
+ XmlElement singleHitElement = configurationElement[CoverletConstants.SingleHitElementName];
+ bool.TryParse(singleHitElement?.InnerText, out bool singleHit);
+ return singleHit;
+ }
+
+ ///
+ /// Parse include test assembly flag
+ ///
+ /// Configuration element
+ /// Include Test Assembly Flag
+ private bool ParseIncludeTestAssembly(XmlElement configurationElement)
+ {
+ XmlElement includeTestAssemblyElement = configurationElement[CoverletConstants.IncludeTestAssemblyElementName];
+ bool.TryParse(includeTestAssemblyElement?.InnerText, out bool includeTestAssembly);
+ return includeTestAssembly;
+ }
+ }
+}
diff --git a/src/coverlet.collector/Friends.cs b/src/coverlet.collector/Friends.cs
new file mode 100644
index 000000000..5de27ab5b
--- /dev/null
+++ b/src/coverlet.collector/Friends.cs
@@ -0,0 +1,8 @@
+using System.Runtime.CompilerServices;
+
+#region Test Assemblies
+
+[assembly: InternalsVisibleTo("coverlet.collector.tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
+
+#endregion
\ No newline at end of file
diff --git a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs
new file mode 100644
index 000000000..1de4167ca
--- /dev/null
+++ b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/coverlet.collector/Resources/Resources.Designer.cs b/src/coverlet.collector/Resources/Resources.Designer.cs
new file mode 100644
index 000000000..a1af3afe4
--- /dev/null
+++ b/src/coverlet.collector/Resources/Resources.Designer.cs
@@ -0,0 +1,126 @@
+//------------------------------------------------------------------------------
+//
+// 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.
+//
+//------------------------------------------------------------------------------
+
+namespace coverlet.collector.Resources {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // 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() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [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;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: Failed to get coverage report.
+ ///
+ internal static string CoverageReportException {
+ get {
+ return ResourceManager.GetString("CoverageReportException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: Failed to get coverage result.
+ ///
+ internal static string CoverageResultException {
+ get {
+ return ResourceManager.GetString("CoverageResultException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: Failed to cleanup report directory: '{1}'.
+ ///
+ internal static string FailedToCleanupReportDirectory {
+ get {
+ return ResourceManager.GetString("FailedToCleanupReportDirectory", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: Failed to save coverage report '{1}' in directory '{2}'.
+ ///
+ internal static string FailedToSaveCoverageReport {
+ get {
+ return ResourceManager.GetString("FailedToSaveCoverageReport", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: Failed to unload module.
+ ///
+ internal static string FailedToUnloadModule {
+ get {
+ return ResourceManager.GetString("FailedToUnloadModule", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: Failed to instrument modules.
+ ///
+ internal static string InstrumentationException {
+ get {
+ return ResourceManager.GetString("InstrumentationException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}: No test modules found.
+ ///
+ internal static string NoTestModulesFound {
+ get {
+ return ResourceManager.GetString("NoTestModulesFound", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/coverlet.collector/Resources/Resources.resx b/src/coverlet.collector/Resources/Resources.resx
new file mode 100644
index 000000000..af111547e
--- /dev/null
+++ b/src/coverlet.collector/Resources/Resources.resx
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ {0}: Failed to get coverage report
+
+
+ {0}: Failed to get coverage result
+
+
+ {0}: Failed to cleanup report directory: '{1}'
+
+
+ {0}: Failed to save coverage report '{1}' in directory '{2}'
+
+
+ {0}: Failed to unload module
+
+
+ {0}: Failed to instrument modules
+
+
+ {0}: No test modules found
+
+
\ No newline at end of file
diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs
new file mode 100644
index 000000000..d3e6469af
--- /dev/null
+++ b/src/coverlet.collector/Utilities/CoverletConstants.cs
@@ -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";
+ }
+}
diff --git a/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs b/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs
new file mode 100644
index 000000000..e0665673b
--- /dev/null
+++ b/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs
@@ -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)
+ {
+ }
+ }
+}
diff --git a/src/coverlet.collector/Utilities/DirectoryHelper.cs b/src/coverlet.collector/Utilities/DirectoryHelper.cs
new file mode 100644
index 000000000..d03d212cc
--- /dev/null
+++ b/src/coverlet.collector/Utilities/DirectoryHelper.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using Coverlet.Collector.Utilities.Interfaces;
+
+namespace Coverlet.Collector.Utilities
+{
+ ///
+ internal class DirectoryHelper : IDirectoryHelper
+ {
+ ///
+ public bool Exists(string path)
+ {
+ return Directory.Exists(path);
+ }
+
+ ///
+ public void CreateDirectory(string path)
+ {
+ Directory.CreateDirectory(path);
+ }
+
+ ///
+ public void Delete(string path, bool recursive)
+ {
+ Directory.Delete(path, recursive);
+ }
+ }
+}
diff --git a/src/coverlet.collector/Utilities/FileHelper.cs b/src/coverlet.collector/Utilities/FileHelper.cs
new file mode 100644
index 000000000..f6e578380
--- /dev/null
+++ b/src/coverlet.collector/Utilities/FileHelper.cs
@@ -0,0 +1,21 @@
+using System.IO;
+using Coverlet.Collector.Utilities.Interfaces;
+
+namespace Coverlet.Collector.Utilities
+{
+ ///
+ internal class FileHelper : IFileHelper
+ {
+ ///
+ public bool Exists(string path)
+ {
+ return File.Exists(path);
+ }
+
+ ///
+ public void WriteAllText(string path, string contents)
+ {
+ File.WriteAllText(path, contents);
+ }
+ }
+}
diff --git a/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs b/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs
new file mode 100644
index 000000000..d8b2f515e
--- /dev/null
+++ b/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs
@@ -0,0 +1,36 @@
+using Coverlet.Collector.DataCollection;
+using Coverlet.Core;
+using Coverlet.Core.Logging;
+
+namespace Coverlet.Collector.Utilities.Interfaces
+{
+ ///
+ /// 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.
+ ///
+ internal interface ICoverageWrapper
+ {
+ ///
+ /// Creates a coverage object from given coverlet settings
+ ///
+ /// Coverlet settings
+ /// Coverlet logger
+ /// Coverage object
+ Coverage CreateCoverage(CoverletSettings settings, ILogger logger);
+
+ ///
+ /// Gets the coverage result from provided coverage object
+ ///
+ /// Coverage
+ /// The coverage result
+ CoverageResult GetCoverageResult(Coverage coverage);
+
+ ///
+ /// Prepares modules for getting coverage.
+ /// Wrapper over coverage.PrepareModules
+ ///
+ ///
+ void PrepareModules(Coverage coverage);
+
+ }
+}
diff --git a/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs b/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs
new file mode 100644
index 000000000..8e26c0dcf
--- /dev/null
+++ b/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs
@@ -0,0 +1,28 @@
+namespace Coverlet.Collector.Utilities.Interfaces
+{
+ interface IDirectoryHelper
+ {
+ ///
+ /// Determines whether the specified directory exists.
+ ///
+ /// The directory to check.
+ /// 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.
+ bool Exists(string path);
+
+ ///
+ /// Creates all directories and subdirectories in the specified path unless they already exist.
+ ///
+ /// The directory to create.
+ void CreateDirectory(string directory);
+
+ ///
+ /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory.
+ ///
+ /// The name of the directory to remove.
+ /// true to remove directories, subdirectories, and files in path; otherwise, false.
+ void Delete(string path, bool recursive);
+ }
+}
diff --git a/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs b/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs
new file mode 100644
index 000000000..8fd0ab9ea
--- /dev/null
+++ b/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs
@@ -0,0 +1,23 @@
+namespace Coverlet.Collector.Utilities.Interfaces
+{
+ internal interface IFileHelper
+ {
+ ///
+ /// Determines whether the specified file exists.
+ ///
+ /// The file to check.
+ /// 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.
+ bool Exists(string path);
+
+ ///
+ /// 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.
+ ///
+ /// The file to write to.
+ /// The string to write to the file.
+ void WriteAllText(string path, string contents);
+ }
+}
diff --git a/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs b/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs
new file mode 100644
index 000000000..9fcf62c0a
--- /dev/null
+++ b/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs
@@ -0,0 +1,43 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+
+namespace Coverlet.Collector.Utilities
+{
+ ///
+ /// Test platform eqttrace
+ ///
+ internal class TestPlatformEqtTrace
+ {
+ public bool IsInfoEnabled => EqtTrace.IsInfoEnabled;
+ public bool IsVerboseEnabled => EqtTrace.IsVerboseEnabled;
+
+ ///
+ /// Verbose logger
+ ///
+ /// Format
+ /// Args
+ public void Verbose(string format, params object[] args)
+ {
+ EqtTrace.Verbose(format, args);
+ }
+
+ ///
+ /// Warning logger
+ ///
+ /// Format
+ /// Args
+ public void Warning(string format, params object[] args)
+ {
+ EqtTrace.Warning(format, args);
+ }
+
+ ///
+ /// Info logger
+ ///
+ /// Format
+ /// Args
+ public void Info(string format, params object[] args)
+ {
+ EqtTrace.Info(format, args);
+ }
+ }
+}
diff --git a/src/coverlet.collector/Utilities/TestPlatformLogger.cs b/src/coverlet.collector/Utilities/TestPlatformLogger.cs
new file mode 100644
index 000000000..81759807d
--- /dev/null
+++ b/src/coverlet.collector/Utilities/TestPlatformLogger.cs
@@ -0,0 +1,28 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
+
+namespace Coverlet.Collector.Utilities
+{
+ ///
+ /// Test platform logger
+ ///
+ internal class TestPlatformLogger
+ {
+ private readonly DataCollectionLogger _logger;
+ private readonly DataCollectionContext _dataCollectionContext;
+
+ public TestPlatformLogger(DataCollectionLogger logger, DataCollectionContext dataCollectionContext)
+ {
+ _logger = logger;
+ _dataCollectionContext = dataCollectionContext;
+ }
+
+ ///
+ /// Log warning
+ ///
+ /// Warning message
+ public void LogWarning(string warning)
+ {
+ _logger.LogWarning(_dataCollectionContext, warning);
+ }
+ }
+}
diff --git a/src/coverlet.collector/coverlet.collector.csproj b/src/coverlet.collector/coverlet.collector.csproj
new file mode 100644
index 000000000..dae838968
--- /dev/null
+++ b/src/coverlet.collector/coverlet.collector.csproj
@@ -0,0 +1,68 @@
+
+
+
+ netcoreapp2.0
+ coverlet.collector
+ 1.0.0
+
+ coverlet.collector
+ $(AssemblyVersion)
+ coverlet.collector
+ tonerdo
+ MIT
+ http://github.com/tonerdo/coverlet
+ https://raw.githubusercontent.com/tonerdo/coverlet/master/_assets/coverlet-icon.svg?sanitize=true
+ false
+ true
+ Coverlet is a cross platform code coverage library for .NET, with support for line, branch and method coverage.
+ coverage testing unit-test lcov opencover quality
+ git
+ https://github.com/tonerdo/coverlet
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ ResXFileCodeGenerator
+ Resource.Designer.cs
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
diff --git a/src/coverlet.collector/coverlet.collector.nuspec b/src/coverlet.collector/coverlet.collector.nuspec
new file mode 100644
index 000000000..7da3d08a0
--- /dev/null
+++ b/src/coverlet.collector/coverlet.collector.nuspec
@@ -0,0 +1,21 @@
+
+
+
+ coverlet.collector
+ 1.0.0
+ coverlet.collector
+ tonerdo
+ tonerdo
+ false
+ MIT
+ https://raw.githubusercontent.com/tonerdo/coverlet/master/_assets/coverlet-icon.svg?sanitize=true
+ Coverlet is a cross platform code coverage library for .NET, with support for line, branch and method coverage.
+ http://github.com/tonerdo/coverlet
+ coverage testing unit-test lcov opencover quality
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/coverlet.collector/coverlet.collector.targets b/src/coverlet.collector/coverlet.collector.targets
new file mode 100644
index 000000000..cc4376d30
--- /dev/null
+++ b/src/coverlet.collector/coverlet.collector.targets
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ $(VSTestTestAdapterPath);$(MSBuildThisFileDirectory)
+
+
+
diff --git a/test/coverlet.collector.tests/AttachmentManagerTests.cs b/test/coverlet.collector.tests/AttachmentManagerTests.cs
new file mode 100644
index 000000000..39b1776a2
--- /dev/null
+++ b/test/coverlet.collector.tests/AttachmentManagerTests.cs
@@ -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 _mockDataCollectionSink;
+ private DataCollectionContext _dataCollectionContext;
+ private TestPlatformLogger _testPlatformLogger;
+ private TestPlatformEqtTrace _eqtTrace;
+ private Mock _mockFileHelper;
+ private Mock _mockDirectoryHelper;
+ private Mock _mockDataCollectionLogger;
+
+ public AttachmentManagerTests()
+ {
+ _mockDataCollectionSink = new Mock();
+ _mockDataCollectionLogger = new Mock();
+ var testcase = new TestCase { Id = Guid.NewGuid() };
+ _dataCollectionContext = new DataCollectionContext(testcase);
+ _testPlatformLogger = new TestPlatformLogger(_mockDataCollectionLogger.Object, _dataCollectionContext);
+ _eqtTrace = new TestPlatformEqtTrace();
+ _mockFileHelper = new Mock();
+ _mockDirectoryHelper = new Mock();
+
+ _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger,
+ _eqtTrace, "report.cobertura.xml", @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object);
+ }
+
+ [Fact]
+ public void SendCoverageReportShouldSaveReportToFile()
+ {
+ string coverageReport = ""
+ + ""
+ + ""
+ + ""
+ + "";
+
+ _attachmentManager.SendCoverageReport(coverageReport);
+ _mockFileHelper.Verify(x => x.WriteAllText(It.Is(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 = ""
+ + ""
+ + ""
+ + ""
+ + "";
+
+ string message = Assert.Throws(() => _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 = ""
+ + ""
+ + ""
+ + ""
+ + "";
+
+ _attachmentManager.SendCoverageReport(coverageReport);
+
+ _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny()));
+
+ directory.Delete(true);
+ }
+
+ [Fact]
+ public void OnSendFileCompletedShouldCleanUpReportDirectory()
+ {
+ _mockDirectoryHelper.Setup(x => x.Exists(It.Is(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(y => y.Contains(@"E:\temp")), true), Times.Once);
+ }
+
+ [Fact]
+ public void OnSendFileCompletedShouldThrowCoverletDataCollectorExceptionIfUnableToCleanUpReportDirectory()
+ {
+ _mockDirectoryHelper.Setup(x => x.Exists(It.Is(y => y.Contains(@"E:\temp")))).Returns(true);
+ _mockDirectoryHelper.Setup(x => x.Delete(It.Is(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(y => y.Contains("CoverletDataCollectorException: CoverletCoverageDataCollector: Failed to cleanup report directory"))), Times.AtLeastOnce);
+ }
+ }
+}
diff --git a/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs b/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs
new file mode 100644
index 000000000..6e2dbbc82
--- /dev/null
+++ b/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs
@@ -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 _mockDataColectionEvents;
+ private Mock _mockDataCollectionSink;
+ private Mock _mockCoverageWrapper;
+ private XmlElement _configurationElement;
+ private Mock _mockLogger;
+
+ public CoverletCoverageDataCollectorTests()
+ {
+ _mockDataColectionEvents = new Mock();
+ _mockDataCollectionSink = new Mock();
+ _mockLogger = new Mock();
+ _configurationElement = null;
+
+ TestCase testcase = new TestCase { Id = Guid.NewGuid() };
+ _dataCollectionContext = new DataCollectionContext(testcase);
+ _context = new DataCollectionEnvironmentContext(_dataCollectionContext);
+ _mockCoverageWrapper = new Mock();
+ }
+
+ [Fact]
+ public void OnSessionStartShouldInitializeCoverageWithCorrectCoverletSettings()
+ {
+ _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object);
+ _coverletCoverageDataCollector.Initialize(
+ _configurationElement,
+ _mockDataColectionEvents.Object,
+ _mockDataCollectionSink.Object,
+ _mockLogger.Object,
+ _context);
+ IDictionary sessionStartProperties = new Dictionary();
+
+ sessionStartProperties.Add("TestSources", new List { "abc.dll" });
+
+ _mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
+
+ _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => string.Equals(y.TestModule, "abc.dll")), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public void OnSessionStartShouldPrepareModulesForCoverage()
+ {
+ _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object);
+ _coverletCoverageDataCollector.Initialize(
+ _configurationElement,
+ _mockDataColectionEvents.Object,
+ _mockDataCollectionSink.Object,
+ null,
+ _context);
+ IDictionary sessionStartProperties = new Dictionary();
+ Coverage coverage = new Coverage("abc.dll", null, null, null, null, null, true, true, "abc.json", true, It.IsAny());
+
+ sessionStartProperties.Add("TestSources", new List { "abc.dll" });
+ _mockCoverageWrapper.Setup(x => x.CreateCoverage(It.IsAny(), It.IsAny())).Returns(coverage);
+
+ _mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
+
+ _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => y.TestModule.Contains("abc.dll")), It.IsAny()), Times.Once);
+ _mockCoverageWrapper.Verify(x => x.PrepareModules(It.IsAny()), 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 sessionStartProperties = new Dictionary();
+ sessionStartProperties.Add("TestSources", new List { 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()), 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 sessionStartProperties = new Dictionary();
+
+ sessionStartProperties.Add("TestSources", new List { "abc.dll" });
+
+ _mockCoverageWrapper.Setup(x => x.PrepareModules(It.IsAny())).Throws(new FileNotFoundException());
+
+ _mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties));
+
+ _mockLogger.Verify(x => x.LogWarning(_dataCollectionContext,
+ It.Is(y => y.Contains("CoverletDataCollectorException"))));
+ }
+ }
+}
diff --git a/test/coverlet.collector.tests/CoverletSettingsParserTests.cs b/test/coverlet.collector.tests/CoverletSettingsParserTests.cs
new file mode 100644
index 000000000..f392dd4fa
--- /dev/null
+++ b/test/coverlet.collector.tests/CoverletSettingsParserTests.cs
@@ -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(() => _coverletSettingsParser.Parse(null, null)).Message;
+
+ Assert.Equal("CoverletCoverageDataCollector: No test modules found", message);
+ }
+
+ [Fact]
+ public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsEmpty()
+ {
+ string message = Assert.Throws(() => _coverletSettingsParser.Parse(null, Enumerable.Empty())).Message;
+
+ Assert.Equal("CoverletCoverageDataCollector: No test modules found", message);
+ }
+
+ [Fact]
+ public void ParseShouldSelectFirstTestModuleFromTestModulesList()
+ {
+ var testModules = new List { "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 { "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);
+ }
+ }
+}
diff --git a/test/coverlet.collector.tests/coverlet.collector.tests.csproj b/test/coverlet.collector.tests/coverlet.collector.tests.csproj
new file mode 100644
index 000000000..dce1752be
--- /dev/null
+++ b/test/coverlet.collector.tests/coverlet.collector.tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ netcoreapp2.2
+
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+