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 + + + + + + + + + +