From 9627e72507fcffe8bf5c92e3dd53514c4acdd078 Mon Sep 17 00:00:00 2001 From: "David Mueller x." Date: Mon, 3 Jun 2024 23:49:32 +0200 Subject: [PATCH 01/20] initial implementation --- .../CoverletCoveragePostProcessor.cs | 30 +++++++++++++++++++ .../CoverletCoverageCollector.cs | 2 ++ 2 files changed, 32 insertions(+) create mode 100644 src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs new file mode 100644 index 000000000..c868754b3 --- /dev/null +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -0,0 +1,30 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace coverlet.collector.ArtifactPostProcessor +{ + public class CoverletCoveragePostProcessor : IDataCollectorAttachmentProcessor + { + public IEnumerable? GetExtensionUris() + { + throw new NotImplementedException(); + } + + public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, + IMessageLogger logger, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public bool SupportsIncrementalProcessing => true; + } +} diff --git a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs index c95ba217b..791861609 100644 --- a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs +++ b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Xml; +using coverlet.collector.ArtifactPostProcessor; using Coverlet.Collector.Utilities; using Coverlet.Collector.Utilities.Interfaces; using Coverlet.Core.Abstractions; @@ -21,6 +22,7 @@ namespace Coverlet.Collector.DataCollection /// [DataCollectorTypeUri(CoverletConstants.DefaultUri)] [DataCollectorFriendlyName(CoverletConstants.FriendlyName)] + [DataCollectorAttachmentProcessor(typeof(CoverletCoveragePostProcessor))] public class CoverletCoverageCollector : DataCollector { private readonly TestPlatformEqtTrace _eqtTrace; From a6966babc921bb2a2a326feb40945cf242470557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 28 Jun 2024 22:13:56 +0200 Subject: [PATCH 02/20] prototyping --- .../CoverletCoveragePostProcessor.cs | 62 ++++++++++++++++++- .../Utilities/CoverletConstants.cs | 2 +- src/coverlet.core/Coverage.cs | 2 +- .../Helpers/DummySourceRootTranslator.cs | 33 ++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/coverlet.core/Helpers/DummySourceRootTranslator.cs diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index c868754b3..c46b90307 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -3,28 +3,86 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Xml; +using Coverlet.Collector.Utilities; +using Coverlet.Core; +using Coverlet.Core.Abstractions; +using coverlet.core.Helpers; +using Coverlet.Core.Reporters; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Newtonsoft.Json; namespace coverlet.collector.ArtifactPostProcessor { public class CoverletCoveragePostProcessor : IDataCollectorAttachmentProcessor { + private readonly CoverageResult _coverageResult = new(); + + public CoverletCoveragePostProcessor() + { + _coverageResult.Modules = new Modules(); + } + +#pragma warning disable CS8632 public IEnumerable? GetExtensionUris() +#pragma warning restore CS8632 { - throw new NotImplementedException(); + var uri = new Uri(CoverletConstants.DefaultUri); + return new[] { uri }; } public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) { - throw new NotImplementedException(); + AttachDebugger(); + + foreach (AttachmentSet attachmentSet in attachments) + { + foreach (UriDataAttachment uriAttachment in attachmentSet.Attachments) + { + string json = File.ReadAllText(uriAttachment.Uri.LocalPath); + + _coverageResult.Merge(JsonConvert.DeserializeObject(json)); + } + } + + (string report, string fileName) report = GetCoverageReport(_coverageResult); + string reportDirectory = Path.Combine(Path.GetTempPath(), new Guid().ToString()); + + Directory.CreateDirectory(reportDirectory); + string filePath = Path.Combine(reportDirectory, report.fileName); + File.WriteAllText(filePath, report.report); + + return Task.FromResult(new List {new(new Uri(filePath), report.fileName) } as ICollection); } public bool SupportsIncrementalProcessing => true; + + private void AttachDebugger() + { + Debugger.Launch(); + Debugger.Break(); + } + + private (string report, string fileName) GetCoverageReport(CoverageResult coverageResult) + { + var reporterFactory = new ReporterFactory("json"); + IReporter reporter = reporterFactory.CreateReporter(); + + try + { + return (reporter.Report(coverageResult, new DummySourceRootTranslator()), Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); + } + catch (Exception ex) + { + throw new CoverletDataCollectorException($"{CoverletConstants.DataCollectorName}: Failed to get coverage report", ex); + } + } } } diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs index 5ce4a79ef..52989938a 100644 --- a/src/coverlet.collector/Utilities/CoverletConstants.cs +++ b/src/coverlet.collector/Utilities/CoverletConstants.cs @@ -8,7 +8,7 @@ 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 DefaultReportFormat = "json"; public const string DefaultFileName = "coverage"; public const string IncludeFiltersElementName = "Include"; public const string IncludeDirectoriesElementName = "IncludeDirectory"; diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 07c0297fa..16b53320a 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -108,7 +108,7 @@ public CoveragePrepareResult PrepareModules() _parameters.IncludeFilters = _parameters.IncludeFilters?.Where(f => _instrumentationHelper.IsValidFilterExpression(f)).ToArray(); IReadOnlyList validModules = _instrumentationHelper.SelectModules(modules, _parameters.IncludeFilters, _parameters.ExcludeFilters).ToList(); - foreach (var excludedModule in modules.Except(validModules)) + foreach (string excludedModule in modules.Except(validModules)) { _logger.LogVerbose($"Excluded module: '{excludedModule}'"); } diff --git a/src/coverlet.core/Helpers/DummySourceRootTranslator.cs b/src/coverlet.core/Helpers/DummySourceRootTranslator.cs new file mode 100644 index 000000000..58336e890 --- /dev/null +++ b/src/coverlet.core/Helpers/DummySourceRootTranslator.cs @@ -0,0 +1,33 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Coverlet.Core.Abstractions; +using Coverlet.Core.Helpers; + +namespace coverlet.core.Helpers +{ + internal class DummySourceRootTranslator : ISourceRootTranslator + { + public bool AddMappingInCache(string originalFileName, string targetFileName) + { + throw new NotImplementedException(); + } + + public string ResolveFilePath(string originalFileName) + { + throw new NotImplementedException(); + } + + public string ResolveDeterministicPath(string originalFileName) + { + throw new NotImplementedException(); + } + + public IReadOnlyList ResolvePathRoot(string pathRoot) + { + throw new NotImplementedException(); + } + } +} From 453d58999409b785729004ea707848d369a2394b Mon Sep 17 00:00:00 2001 From: "David Mueller x." Date: Wed, 10 Jul 2024 00:41:53 +0200 Subject: [PATCH 03/20] nit --- .../CoverletCoveragePostProcessor.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index c46b90307..41bc91b7b 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.ObjectModel; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -17,6 +17,7 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Newtonsoft.Json; +using System.Linq; namespace coverlet.collector.ArtifactPostProcessor { @@ -40,7 +41,12 @@ public CoverletCoveragePostProcessor() public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) { - AttachDebugger(); + if(attachments.Count <= 1) + { + return Task.FromResult(attachments); + } + + System.Diagnostics.Debugger.Launch(); foreach (AttachmentSet attachmentSet in attachments) { @@ -59,17 +65,12 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co string filePath = Path.Combine(reportDirectory, report.fileName); File.WriteAllText(filePath, report.report); - return Task.FromResult(new List {new(new Uri(filePath), report.fileName) } as ICollection); + attachments = new[] { new AttachmentSet(new Uri(filePath), report.fileName) }.Concat(attachments).ToList(); + return Task.FromResult(attachments); } public bool SupportsIncrementalProcessing => true; - private void AttachDebugger() - { - Debugger.Launch(); - Debugger.Break(); - } - private (string report, string fileName) GetCoverageReport(CoverageResult coverageResult) { var reporterFactory = new ReporterFactory("json"); From 00d38bb8d4e5908b42f8f0686abe03e3b3be5bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Sun, 14 Jul 2024 22:30:14 +0200 Subject: [PATCH 04/20] added some todos --- .../CoverletCoveragePostProcessor.cs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index c46b90307..bf523efa9 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -22,6 +22,7 @@ namespace coverlet.collector.ArtifactPostProcessor { public class CoverletCoveragePostProcessor : IDataCollectorAttachmentProcessor { + private string _resultsDirectory; private readonly CoverageResult _coverageResult = new(); public CoverletCoveragePostProcessor() @@ -37,29 +38,37 @@ public CoverletCoveragePostProcessor() return new[] { uri }; } - public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, + public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, + ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) { - AttachDebugger(); + + + if (attachments.Count > 1) + { + AttachDebugger(); + } foreach (AttachmentSet attachmentSet in attachments) { - foreach (UriDataAttachment uriAttachment in attachmentSet.Attachments) + foreach (UriDataAttachment uriAttachment in + attachmentSet.Attachments) // check if the already merged report should be added here { + _resultsDirectory ??= + Directory.GetParent(uriAttachment.Uri.LocalPath)!.FullName; //null-forgiving operator needs to change + string json = File.ReadAllText(uriAttachment.Uri.LocalPath); - _coverageResult.Merge(JsonConvert.DeserializeObject(json)); + _coverageResult.Merge(JsonConvert.DeserializeObject(json)); // is it possible to only use json here? } } (string report, string fileName) report = GetCoverageReport(_coverageResult); - string reportDirectory = Path.Combine(Path.GetTempPath(), new Guid().ToString()); - Directory.CreateDirectory(reportDirectory); - string filePath = Path.Combine(reportDirectory, report.fileName); + string filePath = Path.Combine(_resultsDirectory, report.fileName); File.WriteAllText(filePath, report.report); - return Task.FromResult(new List {new(new Uri(filePath), report.fileName) } as ICollection); + return Task.FromResult(attachments); } public bool SupportsIncrementalProcessing => true; @@ -72,16 +81,20 @@ private void AttachDebugger() private (string report, string fileName) GetCoverageReport(CoverageResult coverageResult) { - var reporterFactory = new ReporterFactory("json"); + var reporterFactory = + new ReporterFactory("json"); // the last merged file should be in the format the user specified IReporter reporter = reporterFactory.CreateReporter(); try { - return (reporter.Report(coverageResult, new DummySourceRootTranslator()), Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); + // check if we need the sourceRootTranslator here + return (reporter.Report(coverageResult, new DummySourceRootTranslator()), + Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); } catch (Exception ex) { - throw new CoverletDataCollectorException($"{CoverletConstants.DataCollectorName}: Failed to get coverage report", ex); + throw new CoverletDataCollectorException( + $"{CoverletConstants.DataCollectorName}: Failed to get coverage report", ex); } } } From 62e4f58fddd304afaed35053899a6f1c0357fa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Wed, 24 Jul 2024 00:43:42 +0200 Subject: [PATCH 05/20] further development for json format --- .../CoverletCoveragePostProcessor.cs | 103 ++++++++++++------ .../DataCollection/CoverletSettingsParser.cs | 21 +--- .../Utilities/CoverletConstants.cs | 1 + .../Utilities/ReportFormatParser.cs | 29 +++++ 4 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 src/coverlet.collector/Utilities/ReportFormatParser.cs diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 79231f454..15c8daae4 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -17,75 +17,87 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Newtonsoft.Json; -using System.Linq; namespace coverlet.collector.ArtifactPostProcessor { public class CoverletCoveragePostProcessor : IDataCollectorAttachmentProcessor { - private string _resultsDirectory; private readonly CoverageResult _coverageResult = new(); + private ReportFormatParser _reportFormatParser; + private IMessageLogger _logger; public CoverletCoveragePostProcessor() { _coverageResult.Modules = new Modules(); } -#pragma warning disable CS8632 - public IEnumerable? GetExtensionUris() -#pragma warning restore CS8632 - { - var uri = new Uri(CoverletConstants.DefaultUri); - return new[] { uri }; - } + public bool SupportsIncrementalProcessing => true; + + public IEnumerable GetExtensionUris() => new[] { new Uri(CoverletConstants.DefaultUri) }; public Task> ProcessAttachmentSetsAsync(XmlElement configurationElement, ICollection attachments, IProgress progressReporter, IMessageLogger logger, CancellationToken cancellationToken) { - + _reportFormatParser ??= new ReportFormatParser(); + _logger = logger; if (attachments.Count > 1) { - AttachDebugger(); + System.Diagnostics.Debugger.Launch(); } + // think about what configuration is mandatory and how to exit if not there + string[] formats = _reportFormatParser.ParseReportFormats(configurationElement); + string mergeDirectory = ParseMergeDirectory(configurationElement); + IReporter reporter = CreateReporter(formats); + string fileName = Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension); + + if (mergeDirectory == null) return Task.FromResult(attachments); + foreach (AttachmentSet attachmentSet in attachments) { - foreach (UriDataAttachment uriAttachment in - attachmentSet.Attachments) // check if the already merged report should be added here + foreach (UriDataAttachment uriAttachment in attachmentSet.Attachments) { - _resultsDirectory ??= - Directory.GetParent(uriAttachment.Uri.LocalPath)!.FullName; //null-forgiving operator needs to change - - string json = File.ReadAllText(uriAttachment.Uri.LocalPath); - - _coverageResult.Merge(JsonConvert.DeserializeObject(json)); // is it possible to only use json here? + MergeWithCoverageResult(uriAttachment.Uri.LocalPath); } } - (string report, string fileName) report = GetCoverageReport(_coverageResult); + Directory.CreateDirectory(mergeDirectory); + string filePath = Path.Combine(mergeDirectory, fileName); + + MergeIntermediateResultWhenExist(filePath); - string filePath = Path.Combine(_resultsDirectory, report.fileName); - File.WriteAllText(filePath, report.report); + string report = GetCoverageReport(_coverageResult, reporter); + + File.WriteAllText(filePath, report); - // attachments = new[] { new AttachmentSet(new Uri(filePath), report.fileName) }.Concat(attachments).ToList(); return Task.FromResult(attachments); } - public bool SupportsIncrementalProcessing => true; + private void MergeIntermediateResultWhenExist(string filePath) + { + if (File.Exists(filePath)) + { + MergeWithCoverageResult(filePath); + } + } - private (string report, string fileName) GetCoverageReport(CoverageResult coverageResult) + private void MergeWithCoverageResult(string filePath) { - var reporterFactory = - new ReporterFactory("json"); // the last merged file should be in the format the user specified - IReporter reporter = reporterFactory.CreateReporter(); + string json = File.ReadAllText(filePath); + _coverageResult.Merge(JsonConvert.DeserializeObject(json)); +// think about merging reports with different format -> e.g. reportgenerator core +// or maybe we can deserialize different reports into Modules??? +// or always create additionally the specified format and overwrite intermediate results + } + private string GetCoverageReport(CoverageResult coverageResult, IReporter reporter) + { try { // check if we need the sourceRootTranslator here - return (reporter.Report(coverageResult, new DummySourceRootTranslator()), - Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); + return reporter.Report(coverageResult, new DummySourceRootTranslator()); } catch (Exception ex) { @@ -93,5 +105,34 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co $"{CoverletConstants.DataCollectorName}: Failed to get coverage report", ex); } } + + private IReporter CreateReporter(IEnumerable formats) + { + IEnumerable reporters = formats.Select(format => + { + var reporterFactory = new ReporterFactory(format); + if (!reporterFactory.IsValidFormat()) + { + _logger.SendMessage(TestMessageLevel.Warning, $"Invalid report format '{format}'"); + return null; + } + return reporterFactory.CreateReporter(); + }).Where(r => r != null); + + // if we want to consider the case where multiple reporters are specified, the first one should be taken + IReporter reporter = reporters.FirstOrDefault(); + + // current prototype only with json format + var reporterFactory = new ReporterFactory("json"); + reporter = reporterFactory.CreateReporter(); + + return reporter; + } + + private static string ParseMergeDirectory(XmlElement configurationElement) + { + XmlElement mergeWithElement = configurationElement[CoverletConstants.MergeDirectory]; + return mergeWithElement?.InnerText; + } } } diff --git a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs index 733dacfcc..edf17b59f 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs @@ -15,10 +15,12 @@ namespace Coverlet.Collector.DataCollection internal class CoverletSettingsParser { private readonly TestPlatformEqtTrace _eqtTrace; + private readonly ReportFormatParser _reportFormatParser; public CoverletSettingsParser(TestPlatformEqtTrace eqtTrace) { _eqtTrace = eqtTrace; + _reportFormatParser = new ReportFormatParser(); } /// @@ -50,7 +52,7 @@ public CoverletSettings Parse(XmlElement configurationElement, IEnumerable testModules) return testModules.FirstOrDefault(); } - /// - /// Parse report formats - /// - /// Configuration element - /// Report formats - private static string[] ParseReportFormats(XmlElement configurationElement) - { - string[] formats = Array.Empty(); - if (configurationElement != null) - { - XmlElement reportFormatElement = configurationElement[CoverletConstants.ReportFormatElementName]; - formats = SplitElement(reportFormatElement); - } - - return formats is null || formats.Length == 0 ? new[] { CoverletConstants.DefaultReportFormat } : formats; - } - /// /// Parse filters to include /// diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs index 52989938a..9d6b77861 100644 --- a/src/coverlet.collector/Utilities/CoverletConstants.cs +++ b/src/coverlet.collector/Utilities/CoverletConstants.cs @@ -27,5 +27,6 @@ internal static class CoverletConstants public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute"; public const string DeterministicReport = "DeterministicReport"; public const string ExcludeAssembliesWithoutSources = "ExcludeAssembliesWithoutSources"; + public const string MergeDirectory = "MergeDirectory"; } } diff --git a/src/coverlet.collector/Utilities/ReportFormatParser.cs b/src/coverlet.collector/Utilities/ReportFormatParser.cs new file mode 100644 index 000000000..1200ea642 --- /dev/null +++ b/src/coverlet.collector/Utilities/ReportFormatParser.cs @@ -0,0 +1,29 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Linq; + +namespace Coverlet.Collector.Utilities +{ + internal class ReportFormatParser + { + internal string[] ParseReportFormats(XmlElement configurationElement) + { + string[] formats = Array.Empty(); + if (configurationElement != null) + { + XmlElement reportFormatElement = configurationElement[CoverletConstants.ReportFormatElementName]; + formats = SplitElement(reportFormatElement); + } + + return formats is null || formats.Length == 0 ? new[] { CoverletConstants.DefaultReportFormat } : formats; + } + + private static string[] SplitElement(XmlElement element) + { + return element?.InnerText?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => value.Trim()).ToArray(); + } + } +} From 44785d938bb67e59abaf2e2a72a7c18e4903b1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Thu, 25 Jul 2024 00:57:55 +0200 Subject: [PATCH 06/20] refactorings --- .../CoverletCoveragePostProcessor.cs | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 15c8daae4..decf7043b 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -22,15 +22,10 @@ namespace coverlet.collector.ArtifactPostProcessor { public class CoverletCoveragePostProcessor : IDataCollectorAttachmentProcessor { - private readonly CoverageResult _coverageResult = new(); + private CoverageResult _coverageResult; private ReportFormatParser _reportFormatParser; private IMessageLogger _logger; - public CoverletCoveragePostProcessor() - { - _coverageResult.Modules = new Modules(); - } - public bool SupportsIncrementalProcessing => true; public IEnumerable GetExtensionUris() => new[] { new Uri(CoverletConstants.DefaultUri) }; @@ -40,6 +35,8 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co IMessageLogger logger, CancellationToken cancellationToken) { _reportFormatParser ??= new ReportFormatParser(); + _coverageResult ??= new CoverageResult(); + _coverageResult.Modules ??= new Modules(); _logger = logger; if (attachments.Count > 1) @@ -50,43 +47,59 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co // think about what configuration is mandatory and how to exit if not there string[] formats = _reportFormatParser.ParseReportFormats(configurationElement); string mergeDirectory = ParseMergeDirectory(configurationElement); - IReporter reporter = CreateReporter(formats); - string fileName = Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension); + // validate that json reports where created otherwise we cannot merge them + IList reporters = CreateReporter(formats).ToList(); + // add more validation logic here, e.g. for specified json reports if (mergeDirectory == null) return Task.FromResult(attachments); + MergeExistingJsonReports(attachments, mergeDirectory); + + WriteCoverageReports(reporters, mergeDirectory, _coverageResult); + + return Task.FromResult(attachments); + } + + private void MergeExistingJsonReports(ICollection attachments, string mergeDirectory) + { + string jsonFileName = Path.ChangeExtension(CoverletConstants.DefaultFileName, "json"); + foreach (AttachmentSet attachmentSet in attachments) { foreach (UriDataAttachment uriAttachment in attachmentSet.Attachments) { - MergeWithCoverageResult(uriAttachment.Uri.LocalPath); + MergeWithCoverageResult(uriAttachment.Uri.LocalPath, _coverageResult); } } Directory.CreateDirectory(mergeDirectory); - string filePath = Path.Combine(mergeDirectory, fileName); + string jsonFilePath = Path.Combine(mergeDirectory, jsonFileName); - MergeIntermediateResultWhenExist(filePath); - - string report = GetCoverageReport(_coverageResult, reporter); - - File.WriteAllText(filePath, report); + MergeIntermediateResultWhenExist(jsonFilePath, _coverageResult); + } - return Task.FromResult(attachments); + private void WriteCoverageReports(IEnumerable reporters, string directory, CoverageResult coverageResult) + { + foreach (IReporter reporter in reporters) + { + string report = GetCoverageReport(coverageResult, reporter); + string filePath = Path.Combine(directory, Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); + File.WriteAllText(filePath, report); + } } - private void MergeIntermediateResultWhenExist(string filePath) + private void MergeIntermediateResultWhenExist(string filePath, CoverageResult coverageResult) { if (File.Exists(filePath)) { - MergeWithCoverageResult(filePath); + MergeWithCoverageResult(filePath, coverageResult); } } - private void MergeWithCoverageResult(string filePath) + private void MergeWithCoverageResult(string filePath, CoverageResult coverageResult) { string json = File.ReadAllText(filePath); - _coverageResult.Merge(JsonConvert.DeserializeObject(json)); + coverageResult.Merge(JsonConvert.DeserializeObject(json)); // think about merging reports with different format -> e.g. reportgenerator core // or maybe we can deserialize different reports into Modules??? // or always create additionally the specified format and overwrite intermediate results @@ -106,7 +119,7 @@ private string GetCoverageReport(CoverageResult coverageResult, IReporter report } } - private IReporter CreateReporter(IEnumerable formats) + private IEnumerable CreateReporter(IEnumerable formats) { IEnumerable reporters = formats.Select(format => { @@ -119,14 +132,7 @@ private IReporter CreateReporter(IEnumerable formats) return reporterFactory.CreateReporter(); }).Where(r => r != null); - // if we want to consider the case where multiple reporters are specified, the first one should be taken - IReporter reporter = reporters.FirstOrDefault(); - - // current prototype only with json format - var reporterFactory = new ReporterFactory("json"); - reporter = reporterFactory.CreateReporter(); - - return reporter; + return reporters; } private static string ParseMergeDirectory(XmlElement configurationElement) From 92b0fea6ebf58a8b65ea697b380a9aa71eef9c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Sat, 27 Jul 2024 00:09:41 +0200 Subject: [PATCH 07/20] nit --- .../CoverletCoveragePostProcessor.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index decf7043b..ca028e49e 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -39,37 +39,29 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co _coverageResult.Modules ??= new Modules(); _logger = logger; - if (attachments.Count > 1) - { - System.Diagnostics.Debugger.Launch(); - } - - // think about what configuration is mandatory and how to exit if not there string[] formats = _reportFormatParser.ParseReportFormats(configurationElement); string mergeDirectory = ParseMergeDirectory(configurationElement); - // validate that json reports where created otherwise we cannot merge them - IList reporters = CreateReporter(formats).ToList(); - // add more validation logic here, e.g. for specified json reports - if (mergeDirectory == null) return Task.FromResult(attachments); + if (mergeDirectory == null || !formats.Contains("json")) + return Task.FromResult(attachments); - MergeExistingJsonReports(attachments, mergeDirectory); + IList reporters = CreateReporters(formats).ToList(); + MergeExistingJsonReports(attachments, mergeDirectory); WriteCoverageReports(reporters, mergeDirectory, _coverageResult); return Task.FromResult(attachments); } - private void MergeExistingJsonReports(ICollection attachments, string mergeDirectory) + private void MergeExistingJsonReports(IEnumerable attachments, string mergeDirectory) { string jsonFileName = Path.ChangeExtension(CoverletConstants.DefaultFileName, "json"); foreach (AttachmentSet attachmentSet in attachments) { - foreach (UriDataAttachment uriAttachment in attachmentSet.Attachments) - { - MergeWithCoverageResult(uriAttachment.Uri.LocalPath, _coverageResult); - } + attachmentSet.Attachments.Where(IsFileWithJsonExt).ToList().ForEach(x => + MergeWithCoverageResult(x.Uri.LocalPath, _coverageResult) + ); } Directory.CreateDirectory(mergeDirectory); @@ -78,6 +70,11 @@ private void MergeExistingJsonReports(ICollection attachments, st MergeIntermediateResultWhenExist(jsonFilePath, _coverageResult); } + private static bool IsFileWithJsonExt(UriDataAttachment x) + { + return x.Uri.IsFile && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json"); + } + private void WriteCoverageReports(IEnumerable reporters, string directory, CoverageResult coverageResult) { foreach (IReporter reporter in reporters) @@ -100,9 +97,6 @@ private void MergeWithCoverageResult(string filePath, CoverageResult coverageRes { string json = File.ReadAllText(filePath); coverageResult.Merge(JsonConvert.DeserializeObject(json)); -// think about merging reports with different format -> e.g. reportgenerator core -// or maybe we can deserialize different reports into Modules??? -// or always create additionally the specified format and overwrite intermediate results } private string GetCoverageReport(CoverageResult coverageResult, IReporter reporter) @@ -119,7 +113,7 @@ private string GetCoverageReport(CoverageResult coverageResult, IReporter report } } - private IEnumerable CreateReporter(IEnumerable formats) + private IEnumerable CreateReporters(IEnumerable formats) { IEnumerable reporters = formats.Select(format => { From 1b912e4669698cb538a9d1890a554dfde0c172e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Sat, 27 Jul 2024 00:12:50 +0200 Subject: [PATCH 08/20] nit --- src/coverlet.collector/Utilities/CoverletConstants.cs | 2 +- src/coverlet.core/Coverage.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs index 9d6b77861..60adfe962 100644 --- a/src/coverlet.collector/Utilities/CoverletConstants.cs +++ b/src/coverlet.collector/Utilities/CoverletConstants.cs @@ -8,7 +8,7 @@ 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 = "json"; + public const string DefaultReportFormat = "cobertura"; public const string DefaultFileName = "coverage"; public const string IncludeFiltersElementName = "Include"; public const string IncludeDirectoriesElementName = "IncludeDirectory"; diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 16b53320a..07c0297fa 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -108,7 +108,7 @@ public CoveragePrepareResult PrepareModules() _parameters.IncludeFilters = _parameters.IncludeFilters?.Where(f => _instrumentationHelper.IsValidFilterExpression(f)).ToArray(); IReadOnlyList validModules = _instrumentationHelper.SelectModules(modules, _parameters.IncludeFilters, _parameters.ExcludeFilters).ToList(); - foreach (string excludedModule in modules.Except(validModules)) + foreach (var excludedModule in modules.Except(validModules)) { _logger.LogVerbose($"Excluded module: '{excludedModule}'"); } From f67b97627aa4fae6da784ff048e2c4b22da67458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Sat, 31 Aug 2024 00:15:57 +0200 Subject: [PATCH 09/20] adaptions --- .../CoverletCoveragePostProcessor.cs | 51 +++++++++---------- .../Utilities/CoverletConstants.cs | 1 - .../Utilities/ReportFormatParser.cs | 14 +++++ 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index ca028e49e..8c38788b4 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -17,6 +17,7 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Newtonsoft.Json; +using System.Diagnostics; namespace coverlet.collector.ArtifactPostProcessor { @@ -39,40 +40,43 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co _coverageResult.Modules ??= new Modules(); _logger = logger; + //Debugger.Launch(); + //Debugger.Break(); + string[] formats = _reportFormatParser.ParseReportFormats(configurationElement); - string mergeDirectory = ParseMergeDirectory(configurationElement); + bool deterministic = _reportFormatParser.ParseDeterministicReport(configurationElement); + bool useSourceLink = _reportFormatParser.ParseUseSourceLink(configurationElement); - if (mergeDirectory == null || !formats.Contains("json")) - return Task.FromResult(attachments); + if (!formats.Contains("json")) return Task.FromResult(attachments); IList reporters = CreateReporters(formats).ToList(); - MergeExistingJsonReports(attachments, mergeDirectory); - WriteCoverageReports(reporters, mergeDirectory, _coverageResult); + if (attachments.Count > 1) + { + _coverageResult.Parameters = new CoverageParameters() {DeterministicReport = deterministic, UseSourceLink = useSourceLink }; + var attachmentDirectories = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt).Select(y => Path.GetDirectoryName(y.Uri.LocalPath))).ToList(); + MergeExistingJsonReports(attachments); + WriteCoverageReports(reporters, attachmentDirectories.First(), _coverageResult); + RemoveObsoleteReports(attachmentDirectories); + attachments = new List { attachments.First() }; + } return Task.FromResult(attachments); } - private void MergeExistingJsonReports(IEnumerable attachments, string mergeDirectory) + private void RemoveObsoleteReports(List attachmentDirectories) { - string jsonFileName = Path.ChangeExtension(CoverletConstants.DefaultFileName, "json"); + attachmentDirectories.Skip(1).ToList().ForEach(x => Directory.Delete(x, true)); + } + private void MergeExistingJsonReports(IEnumerable attachments) + { foreach (AttachmentSet attachmentSet in attachments) { attachmentSet.Attachments.Where(IsFileWithJsonExt).ToList().ForEach(x => MergeWithCoverageResult(x.Uri.LocalPath, _coverageResult) ); } - - Directory.CreateDirectory(mergeDirectory); - string jsonFilePath = Path.Combine(mergeDirectory, jsonFileName); - - MergeIntermediateResultWhenExist(jsonFilePath, _coverageResult); - } - - private static bool IsFileWithJsonExt(UriDataAttachment x) - { - return x.Uri.IsFile && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json"); } private void WriteCoverageReports(IEnumerable reporters, string directory, CoverageResult coverageResult) @@ -85,12 +89,9 @@ private void WriteCoverageReports(IEnumerable reporters, string direc } } - private void MergeIntermediateResultWhenExist(string filePath, CoverageResult coverageResult) + private static bool IsFileWithJsonExt(UriDataAttachment x) { - if (File.Exists(filePath)) - { - MergeWithCoverageResult(filePath, coverageResult); - } + return x.Uri.IsFile && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json"); } private void MergeWithCoverageResult(string filePath, CoverageResult coverageResult) @@ -128,11 +129,5 @@ private IEnumerable CreateReporters(IEnumerable formats) return reporters; } - - private static string ParseMergeDirectory(XmlElement configurationElement) - { - XmlElement mergeWithElement = configurationElement[CoverletConstants.MergeDirectory]; - return mergeWithElement?.InnerText; - } } } diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs index 60adfe962..5ce4a79ef 100644 --- a/src/coverlet.collector/Utilities/CoverletConstants.cs +++ b/src/coverlet.collector/Utilities/CoverletConstants.cs @@ -27,6 +27,5 @@ internal static class CoverletConstants public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute"; public const string DeterministicReport = "DeterministicReport"; public const string ExcludeAssembliesWithoutSources = "ExcludeAssembliesWithoutSources"; - public const string MergeDirectory = "MergeDirectory"; } } diff --git a/src/coverlet.collector/Utilities/ReportFormatParser.cs b/src/coverlet.collector/Utilities/ReportFormatParser.cs index 1200ea642..16571a2a8 100644 --- a/src/coverlet.collector/Utilities/ReportFormatParser.cs +++ b/src/coverlet.collector/Utilities/ReportFormatParser.cs @@ -25,5 +25,19 @@ private static string[] SplitElement(XmlElement element) { return element?.InnerText?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => value.Trim()).ToArray(); } + + internal bool ParseUseSourceLink(XmlElement configurationElement) + { + XmlElement useSourceLinkElement = configurationElement[CoverletConstants.UseSourceLinkElementName]; + bool.TryParse(useSourceLinkElement?.InnerText, out bool useSourceLink); + return useSourceLink; + } + + internal bool ParseDeterministicReport(XmlElement configurationElement) + { + XmlElement deterministicReportElement = configurationElement[CoverletConstants.DeterministicReport]; + bool.TryParse(deterministicReportElement?.InnerText, out bool deterministicReport); + return deterministicReport; + } } } From c6f5f0e35963341580169cc651e6161a60b9de01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Mon, 2 Sep 2024 01:33:59 +0200 Subject: [PATCH 10/20] added new parameter, removed requirement for json format setting --- .../CoverletCoveragePostProcessor.cs | 17 ++++++---- .../DataCollection/CoverageManager.cs | 34 ++++++++++++------- .../DataCollection/CoverletSettings.cs | 6 ++++ .../DataCollection/CoverletSettingsParser.cs | 1 + .../Utilities/CoverletConstants.cs | 1 + .../Utilities/ReportFormatParser.cs | 7 ++++ 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 8c38788b4..417d35cc2 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -17,7 +17,6 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Newtonsoft.Json; -using System.Diagnostics; namespace coverlet.collector.ArtifactPostProcessor { @@ -40,14 +39,12 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co _coverageResult.Modules ??= new Modules(); _logger = logger; - //Debugger.Launch(); - //Debugger.Break(); - string[] formats = _reportFormatParser.ParseReportFormats(configurationElement); bool deterministic = _reportFormatParser.ParseDeterministicReport(configurationElement); bool useSourceLink = _reportFormatParser.ParseUseSourceLink(configurationElement); + bool reportMerging = _reportFormatParser.ParseReportMerging(configurationElement); - if (!formats.Contains("json")) return Task.FromResult(attachments); + if (!reportMerging) return Task.FromResult(attachments); IList reporters = CreateReporters(formats).ToList(); @@ -57,16 +54,22 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co var attachmentDirectories = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt).Select(y => Path.GetDirectoryName(y.Uri.LocalPath))).ToList(); MergeExistingJsonReports(attachments); WriteCoverageReports(reporters, attachmentDirectories.First(), _coverageResult); - RemoveObsoleteReports(attachmentDirectories); + RemoveObsoleteReports(attachmentDirectories, formats); attachments = new List { attachments.First() }; } return Task.FromResult(attachments); } - private void RemoveObsoleteReports(List attachmentDirectories) + // proper documentation for the whole feature + // integration tests? maybe in coverlet.integration.tests.Collectors? + // double check that new parameter is only useable for collectors + private void RemoveObsoleteReports(List attachmentDirectories, string[] formats) { attachmentDirectories.Skip(1).ToList().ForEach(x => Directory.Delete(x, true)); + if (!formats.Contains("json")) + Directory.GetFiles(attachmentDirectories.First()) + .Where(x => Path.GetExtension(x).Equals(".json")).ToList().ForEach(File.Delete); } private void MergeExistingJsonReports(IEnumerable attachments) diff --git a/src/coverlet.collector/DataCollection/CoverageManager.cs b/src/coverlet.collector/DataCollection/CoverageManager.cs index f0453501c..467414f18 100644 --- a/src/coverlet.collector/DataCollection/CoverageManager.cs +++ b/src/coverlet.collector/DataCollection/CoverageManager.cs @@ -26,24 +26,32 @@ internal class CoverageManager public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) : this(settings, - settings.ReportFormats.Select(format => - { - var reporterFactory = new ReporterFactory(format); - if (!reporterFactory.IsValidFormat()) - { - eqtTrace.Warning($"Invalid report format '{format}'"); - return null; - } - else - { - return reporterFactory.CreateReporter(); - } - }).Where(r => r != null).ToArray(), + CreateReporters(settings, eqtTrace), new CoverletLogger(eqtTrace, logger), coverageWrapper, instrumentationHelper, fileSystem, sourceRootTranslator, cecilSymbolHelper) { } + private static IReporter[] CreateReporters(CoverletSettings settings, TestPlatformEqtTrace eqtTrace) + { + if (settings.ReportMerging && ! settings.ReportFormats.Contains("json")) + settings.ReportFormats = settings.ReportFormats.Append("json").ToArray(); + + return settings.ReportFormats.Select(format => + { + var reporterFactory = new ReporterFactory(format); + if (!reporterFactory.IsValidFormat()) + { + eqtTrace.Warning($"Invalid report format '{format}'"); + return null; + } + else + { + return reporterFactory.CreateReporter(); + } + }).Where(r => r != null).ToArray(); + } + public CoverageManager(CoverletSettings settings, IReporter[] reporters, ILogger logger, ICoverageWrapper coverageWrapper, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) { diff --git a/src/coverlet.collector/DataCollection/CoverletSettings.cs b/src/coverlet.collector/DataCollection/CoverletSettings.cs index 0c80687f9..5080374a4 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettings.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettings.cs @@ -86,6 +86,11 @@ internal class CoverletSettings /// public string ExcludeAssembliesWithoutSources { get; set; } + /// + /// Report merging flag + /// + public bool ReportMerging { get; set; } + public override string ToString() { var builder = new StringBuilder(); @@ -104,6 +109,7 @@ public override string ToString() builder.AppendFormat("DoesNotReturnAttributes: '{0}'", string.Join(",", DoesNotReturnAttributes ?? Enumerable.Empty())); builder.AppendFormat("DeterministicReport: '{0}'", DeterministicReport); builder.AppendFormat("ExcludeAssembliesWithoutSources: '{0}'", ExcludeAssembliesWithoutSources); + builder.AppendFormat("ReportMerging: '{0}'", ReportMerging); return builder.ToString(); } diff --git a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs index edf17b59f..488687263 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs @@ -50,6 +50,7 @@ public CoverletSettings Parse(XmlElement configurationElement, IEnumerable Date: Tue, 3 Sep 2024 01:02:59 +0200 Subject: [PATCH 11/20] refactoring (untested) --- .../CoverletCoveragePostProcessor.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 417d35cc2..207fbc2fb 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -51,10 +51,14 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co if (attachments.Count > 1) { _coverageResult.Parameters = new CoverageParameters() {DeterministicReport = deterministic, UseSourceLink = useSourceLink }; - var attachmentDirectories = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt).Select(y => Path.GetDirectoryName(y.Uri.LocalPath))).ToList(); + + var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt)).ToList(); + string mergeFilePath = fileAttachments.First().Uri.LocalPath; + MergeExistingJsonReports(attachments); - WriteCoverageReports(reporters, attachmentDirectories.First(), _coverageResult); - RemoveObsoleteReports(attachmentDirectories, formats); + WriteCoverageReports(reporters, mergeFilePath, _coverageResult); + RemoveObsoleteReports(fileAttachments); + attachments = new List { attachments.First() }; } @@ -64,12 +68,9 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co // proper documentation for the whole feature // integration tests? maybe in coverlet.integration.tests.Collectors? // double check that new parameter is only useable for collectors - private void RemoveObsoleteReports(List attachmentDirectories, string[] formats) + private void RemoveObsoleteReports(List fileAttachments) { - attachmentDirectories.Skip(1).ToList().ForEach(x => Directory.Delete(x, true)); - if (!formats.Contains("json")) - Directory.GetFiles(attachmentDirectories.First()) - .Where(x => Path.GetExtension(x).Equals(".json")).ToList().ForEach(File.Delete); + fileAttachments.ForEach(x => File.Delete(x.Uri.LocalPath)); } private void MergeExistingJsonReports(IEnumerable attachments) From ea1cb6d326a614afa3658dafb4cc353e23226bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 7 Feb 2025 21:46:56 +0100 Subject: [PATCH 12/20] debug --- .../CoverletCoveragePostProcessor.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 207fbc2fb..9ef2e8ab0 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -17,6 +17,7 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Newtonsoft.Json; +using System.Diagnostics; namespace coverlet.collector.ArtifactPostProcessor { @@ -55,6 +56,7 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt)).ToList(); string mergeFilePath = fileAttachments.First().Uri.LocalPath; + // does merge only work for json extensions, how are they created now if isn't specified in runsettings MergeExistingJsonReports(attachments); WriteCoverageReports(reporters, mergeFilePath, _coverageResult); RemoveObsoleteReports(fileAttachments); @@ -106,6 +108,8 @@ private void MergeWithCoverageResult(string filePath, CoverageResult coverageRes private string GetCoverageReport(CoverageResult coverageResult, IReporter reporter) { + AttachDebugger(); + try { // check if we need the sourceRootTranslator here @@ -118,6 +122,15 @@ private string GetCoverageReport(CoverageResult coverageResult, IReporter report } } + private void AttachDebugger() + { + if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_ATTACHMENT_DEBUG"), out int result) && result == 1) + { + Debugger.Launch(); + Debugger.Break(); + } + } + private IEnumerable CreateReporters(IEnumerable formats) { IEnumerable reporters = formats.Select(format => From 5b2925304605b73845f2e04cfa5cfd5b163adbe0 Mon Sep 17 00:00:00 2001 From: "David Mueller x." Date: Sun, 9 Feb 2025 23:54:52 +0100 Subject: [PATCH 13/20] adaptions --- .../CoverletCoveragePostProcessor.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 9ef2e8ab0..565607911 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -45,6 +45,8 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co bool useSourceLink = _reportFormatParser.ParseUseSourceLink(configurationElement); bool reportMerging = _reportFormatParser.ParseReportMerging(configurationElement); + AttachDebugger(); + if (!reportMerging) return Task.FromResult(attachments); IList reporters = CreateReporters(formats).ToList(); @@ -54,11 +56,13 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co _coverageResult.Parameters = new CoverageParameters() {DeterministicReport = deterministic, UseSourceLink = useSourceLink }; var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt)).ToList(); - string mergeFilePath = fileAttachments.First().Uri.LocalPath; + string mergeFilePath = Path.GetDirectoryName(fileAttachments.First().Uri.LocalPath); // does merge only work for json extensions, how are they created now if isn't specified in runsettings MergeExistingJsonReports(attachments); WriteCoverageReports(reporters, mergeFilePath, _coverageResult); + // check if we can remove more than just the json extension files + // maybe don't remove the merged json file as it is printed to the console RemoveObsoleteReports(fileAttachments); attachments = new List { attachments.First() }; @@ -90,6 +94,7 @@ private void WriteCoverageReports(IEnumerable reporters, string direc foreach (IReporter reporter in reporters) { string report = GetCoverageReport(coverageResult, reporter); + //throws exceptions -- check what the problem is string filePath = Path.Combine(directory, Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); File.WriteAllText(filePath, report); } @@ -108,8 +113,6 @@ private void MergeWithCoverageResult(string filePath, CoverageResult coverageRes private string GetCoverageReport(CoverageResult coverageResult, IReporter reporter) { - AttachDebugger(); - try { // check if we need the sourceRootTranslator here From 249ed8711760c689ff85a151202af11e2d6ee2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 14 Feb 2025 02:21:23 +0100 Subject: [PATCH 14/20] some changes --- .../CoverletCoveragePostProcessor.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 565607911..2ec9897ab 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -55,17 +55,20 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co { _coverageResult.Parameters = new CoverageParameters() {DeterministicReport = deterministic, UseSourceLink = useSourceLink }; - var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileWithJsonExt)).ToList(); + var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileAttachment)).ToList(); string mergeFilePath = Path.GetDirectoryName(fileAttachments.First().Uri.LocalPath); // does merge only work for json extensions, how are they created now if isn't specified in runsettings MergeExistingJsonReports(attachments); - WriteCoverageReports(reporters, mergeFilePath, _coverageResult); + + RemoveObsoleteReports(fileAttachments); + + var mergedFileAttachment = WriteCoverageReports(reporters, mergeFilePath, _coverageResult); // check if we can remove more than just the json extension files // maybe don't remove the merged json file as it is printed to the console - RemoveObsoleteReports(fileAttachments); - attachments = new List { attachments.First() }; + attachments = new List { mergedFileAttachment }; + // create new attachment set with only the merged file (add new parameter to pass in the output directory; if not specified use first one) } return Task.FromResult(attachments); @@ -89,20 +92,28 @@ private void MergeExistingJsonReports(IEnumerable attachments) } } - private void WriteCoverageReports(IEnumerable reporters, string directory, CoverageResult coverageResult) + private AttachmentSet WriteCoverageReports(IEnumerable reporters, string directory, CoverageResult coverageResult) { + var attachment = new AttachmentSet(new Uri(CoverletConstants.DefaultUri), string.Empty); foreach (IReporter reporter in reporters) { string report = GetCoverageReport(coverageResult, reporter); //throws exceptions -- check what the problem is string filePath = Path.Combine(directory, Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); File.WriteAllText(filePath, report); + attachment.Attachments.Add(new UriDataAttachment(new Uri(filePath),string.Empty)); } + return attachment; } private static bool IsFileWithJsonExt(UriDataAttachment x) { - return x.Uri.IsFile && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json"); + return IsFileAttachment(x) && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json"); + } + + private static bool IsFileAttachment(UriDataAttachment x) + { + return x.Uri.IsFile; } private void MergeWithCoverageResult(string filePath, CoverageResult coverageResult) From 988cb9e1a68371f6e2d7ca93d74fd91720454bb2 Mon Sep 17 00:00:00 2001 From: "David Mueller x." Date: Sun, 16 Feb 2025 00:34:57 +0100 Subject: [PATCH 15/20] changes --- .../CoverletCoveragePostProcessor.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 2ec9897ab..a0816c2aa 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -63,12 +63,11 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co RemoveObsoleteReports(fileAttachments); - var mergedFileAttachment = WriteCoverageReports(reporters, mergeFilePath, _coverageResult); + AttachmentSet mergedFileAttachment = WriteCoverageReports(reporters, mergeFilePath, _coverageResult); // check if we can remove more than just the json extension files // maybe don't remove the merged json file as it is printed to the console attachments = new List { mergedFileAttachment }; - // create new attachment set with only the merged file (add new parameter to pass in the output directory; if not specified use first one) } return Task.FromResult(attachments); @@ -79,7 +78,12 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co // double check that new parameter is only useable for collectors private void RemoveObsoleteReports(List fileAttachments) { - fileAttachments.ForEach(x => File.Delete(x.Uri.LocalPath)); + fileAttachments.ForEach(x => + { + string directory = Path.GetDirectoryName(x.Uri.LocalPath); + if (! string.IsNullOrEmpty(directory)) + Directory.Delete(directory, true); + }); } private void MergeExistingJsonReports(IEnumerable attachments) @@ -98,10 +102,10 @@ private AttachmentSet WriteCoverageReports(IEnumerable reporters, str foreach (IReporter reporter in reporters) { string report = GetCoverageReport(coverageResult, reporter); - //throws exceptions -- check what the problem is - string filePath = Path.Combine(directory, Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)); - File.WriteAllText(filePath, report); - attachment.Attachments.Add(new UriDataAttachment(new Uri(filePath),string.Empty)); + var file = new FileInfo(Path.Combine(directory, Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension))); + file.Directory?.Create(); + File.WriteAllText(file.FullName, report); + attachment.Attachments.Add(new UriDataAttachment(new Uri(file.FullName),string.Empty)); } return attachment; } From 1b7d2074f00400e344b2189e1b5966f2ef0e4f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Wed, 19 Feb 2025 01:27:07 +0100 Subject: [PATCH 16/20] tests + cleanup --- .../ArtifactPostProcessor/CoverletCoveragePostProcessor.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index a0816c2aa..0700adc42 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -64,8 +64,6 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co RemoveObsoleteReports(fileAttachments); AttachmentSet mergedFileAttachment = WriteCoverageReports(reporters, mergeFilePath, _coverageResult); - // check if we can remove more than just the json extension files - // maybe don't remove the merged json file as it is printed to the console attachments = new List { mergedFileAttachment }; } @@ -81,7 +79,7 @@ private void RemoveObsoleteReports(List fileAttachments) fileAttachments.ForEach(x => { string directory = Path.GetDirectoryName(x.Uri.LocalPath); - if (! string.IsNullOrEmpty(directory)) + if (! string.IsNullOrEmpty(directory) && Directory.Exists(directory)) Directory.Delete(directory, true); }); } From 8281be77e407094fb993fb5469f990e592dc54c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Thu, 20 Feb 2025 00:49:48 +0100 Subject: [PATCH 17/20] solution for deterministic cobertura reports --- .../CoverletCoveragePostProcessor.cs | 7 ++-- .../Helpers/DummySourceRootTranslator.cs | 33 ------------------- .../Helpers/SourceRootTranslator.cs | 7 ++++ 3 files changed, 10 insertions(+), 37 deletions(-) delete mode 100644 src/coverlet.core/Helpers/DummySourceRootTranslator.cs diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 0700adc42..2aaf07965 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -11,13 +11,13 @@ using Coverlet.Collector.Utilities; using Coverlet.Core; using Coverlet.Core.Abstractions; -using coverlet.core.Helpers; using Coverlet.Core.Reporters; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Newtonsoft.Json; using System.Diagnostics; +using Coverlet.Core.Helpers; namespace coverlet.collector.ArtifactPostProcessor { @@ -58,7 +58,6 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileAttachment)).ToList(); string mergeFilePath = Path.GetDirectoryName(fileAttachments.First().Uri.LocalPath); - // does merge only work for json extensions, how are they created now if isn't specified in runsettings MergeExistingJsonReports(attachments); RemoveObsoleteReports(fileAttachments); @@ -128,8 +127,8 @@ private string GetCoverageReport(CoverageResult coverageResult, IReporter report { try { - // check if we need the sourceRootTranslator here - return reporter.Report(coverageResult, new DummySourceRootTranslator()); + // empty source root translator returns the original path for deterministic report + return reporter.Report(coverageResult, new SourceRootTranslator()); } catch (Exception ex) { diff --git a/src/coverlet.core/Helpers/DummySourceRootTranslator.cs b/src/coverlet.core/Helpers/DummySourceRootTranslator.cs deleted file mode 100644 index 58336e890..000000000 --- a/src/coverlet.core/Helpers/DummySourceRootTranslator.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Toni Solarin-Sodara -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using Coverlet.Core.Abstractions; -using Coverlet.Core.Helpers; - -namespace coverlet.core.Helpers -{ - internal class DummySourceRootTranslator : ISourceRootTranslator - { - public bool AddMappingInCache(string originalFileName, string targetFileName) - { - throw new NotImplementedException(); - } - - public string ResolveFilePath(string originalFileName) - { - throw new NotImplementedException(); - } - - public string ResolveDeterministicPath(string originalFileName) - { - throw new NotImplementedException(); - } - - public IReadOnlyList ResolvePathRoot(string pathRoot) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/coverlet.core/Helpers/SourceRootTranslator.cs b/src/coverlet.core/Helpers/SourceRootTranslator.cs index d5a0dfcc7..ad4323a6d 100644 --- a/src/coverlet.core/Helpers/SourceRootTranslator.cs +++ b/src/coverlet.core/Helpers/SourceRootTranslator.cs @@ -24,11 +24,18 @@ internal class SourceRootTranslator : ISourceRootTranslator private readonly Dictionary> _sourceToDeterministicPathMapping; private Dictionary _resolutionCacheFiles; + public SourceRootTranslator() + { + _sourceRootMapping = new Dictionary>(); + _sourceToDeterministicPathMapping = new Dictionary>(); + } + public SourceRootTranslator(ILogger logger, IFileSystem fileSystem) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _sourceRootMapping = new Dictionary>(); + _sourceToDeterministicPathMapping = new Dictionary>(); } public SourceRootTranslator(string sourceMappingFile, ILogger logger, IFileSystem fileSystem) From d67bb464c4f7ee82076c669641a570767d4aca5d Mon Sep 17 00:00:00 2001 From: "David Mueller x." Date: Sun, 2 Mar 2025 23:14:36 +0100 Subject: [PATCH 18/20] added some documentation --- Documentation/VSTestIntegration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/VSTestIntegration.md b/Documentation/VSTestIntegration.md index 3c8be75e1..1f8161728 100644 --- a/Documentation/VSTestIntegration.md +++ b/Documentation/VSTestIntegration.md @@ -121,6 +121,7 @@ These are a list of options that are supported by coverlet. These can be specifi | DoesNotReturnAttribute | Methods marked with these attributes are known not to return, statements following them will be excluded from coverage | | DeterministicReport | Generates deterministic report in context of deterministic build. Take a look at [documentation](DeterministicBuild.md) for further informations. | ExcludeAssembliesWithoutSources | Specifies whether to exclude assemblies without source. Options are either MissingAll, MissingAny or None. Default is MissingAll.| +| ReportMerging | Automatically merge coverage reports of multiple projects.| How to specify these options via runsettings? @@ -143,6 +144,7 @@ How to specify these options via runsettings? true false MissingAll,MissingAny,None + false From 90768d6b7f30f97c06ef357a9f166d818bf546e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Tue, 4 Mar 2025 23:48:36 +0100 Subject: [PATCH 19/20] docs --- Documentation/Changelog.md | 5 ++++- Documentation/VSTestIntegration.md | 2 +- .../ArtifactPostProcessor/CoverletCoveragePostProcessor.cs | 5 +---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index 5dfebc247..7e3f47b0b 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -6,7 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Release date 2024-01-20 +### Improvements +- Implement solution based merging for data collector [#1307](https://github.com/coverlet-coverage/coverlet/issues/1307) + +## Release date 2025-01-20 ### Packages coverlet.msbuild 6.0.4 coverlet.console 6.0.4 diff --git a/Documentation/VSTestIntegration.md b/Documentation/VSTestIntegration.md index 1f8161728..0354e119a 100644 --- a/Documentation/VSTestIntegration.md +++ b/Documentation/VSTestIntegration.md @@ -121,7 +121,7 @@ These are a list of options that are supported by coverlet. These can be specifi | DoesNotReturnAttribute | Methods marked with these attributes are known not to return, statements following them will be excluded from coverage | | DeterministicReport | Generates deterministic report in context of deterministic build. Take a look at [documentation](DeterministicBuild.md) for further informations. | ExcludeAssembliesWithoutSources | Specifies whether to exclude assemblies without source. Options are either MissingAll, MissingAny or None. Default is MissingAll.| -| ReportMerging | Automatically merge coverage reports of multiple projects.| +| ReportMerging | Automatically merge coverage reports if coverage is calculated for multiple projects. Default is false.| How to specify these options via runsettings? diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index 2aaf07965..bbff1eb75 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -70,10 +70,7 @@ public Task> ProcessAttachmentSetsAsync(XmlElement co return Task.FromResult(attachments); } - // proper documentation for the whole feature - // integration tests? maybe in coverlet.integration.tests.Collectors? - // double check that new parameter is only useable for collectors - private void RemoveObsoleteReports(List fileAttachments) + private static void RemoveObsoleteReports(List fileAttachments) { fileAttachments.ForEach(x => { From 69777e95c6a6590f72bd85dcc67595b7d7f5c91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Wed, 5 Mar 2025 00:35:51 +0100 Subject: [PATCH 20/20] added troubleshoot documentation --- Documentation/Troubleshooting.md | 9 +++++++++ .../CoverletCoveragePostProcessor.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Documentation/Troubleshooting.md b/Documentation/Troubleshooting.md index 5a3e682cc..941115117 100644 --- a/Documentation/Troubleshooting.md +++ b/Documentation/Troubleshooting.md @@ -265,3 +265,12 @@ To enable exceptions log for in-process data collectors ```shell set COVERLET_DATACOLLECTOR_INPROC_EXCEPTIONLOG_ENABLED=1 ``` + +## Enable collector post processing debugging +Post processing is uses for automatically merging coverage reports with the `ReportMerging` option enabled. You can live attach and debug the post processor `COVERLET_DATACOLLECTOR_POSTPROCESSOR_DEBUG`. + +You will be asked to attach a debugger through UI popup. + +```shell + set COVERLET_DATACOLLECTOR_POSTPROCESSOR_DEBUG=1 +``` diff --git a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs index bbff1eb75..5151cc362 100644 --- a/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs +++ b/src/coverlet.collector/ArtifactPostProcessor/CoverletCoveragePostProcessor.cs @@ -136,7 +136,7 @@ private string GetCoverageReport(CoverageResult coverageResult, IReporter report private void AttachDebugger() { - if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_ATTACHMENT_DEBUG"), out int result) && result == 1) + if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_POSTPROCESSOR_DEBUG"), out int result) && result == 1) { Debugger.Launch(); Debugger.Break();