-
Couldn't load subscription status.
- Fork 391
Solution based merging for data collector #1676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9627e72
a6966ba
453d589
00d38bb
7a3a3ce
62e4f58
44785d9
92b0fea
1b912e4
f67b976
c6f5f0e
3e40ef5
ada8b1e
ea1cb6d
5b29253
249ed87
988cb9e
1b7d207
8281be7
d67bb46
90768d6
69777e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| // 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.IO; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using System.Xml; | ||
| using Coverlet.Collector.Utilities; | ||
| using Coverlet.Core; | ||
| using Coverlet.Core.Abstractions; | ||
| 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 | ||
| { | ||
| public class CoverletCoveragePostProcessor : IDataCollectorAttachmentProcessor | ||
| { | ||
| private CoverageResult _coverageResult; | ||
| private ReportFormatParser _reportFormatParser; | ||
| private IMessageLogger _logger; | ||
|
|
||
| public bool SupportsIncrementalProcessing => true; | ||
|
|
||
| public IEnumerable<Uri> GetExtensionUris() => new[] { new Uri(CoverletConstants.DefaultUri) }; | ||
|
|
||
| public Task<ICollection<AttachmentSet>> ProcessAttachmentSetsAsync(XmlElement configurationElement, | ||
| ICollection<AttachmentSet> attachments, IProgress<int> progressReporter, | ||
| IMessageLogger logger, CancellationToken cancellationToken) | ||
| { | ||
| _reportFormatParser ??= new ReportFormatParser(); | ||
| _coverageResult ??= new CoverageResult(); | ||
| _coverageResult.Modules ??= new Modules(); | ||
| _logger = logger; | ||
|
|
||
| string[] formats = _reportFormatParser.ParseReportFormats(configurationElement); | ||
| bool deterministic = _reportFormatParser.ParseDeterministicReport(configurationElement); | ||
| bool useSourceLink = _reportFormatParser.ParseUseSourceLink(configurationElement); | ||
| bool reportMerging = _reportFormatParser.ParseReportMerging(configurationElement); | ||
|
|
||
| AttachDebugger(); | ||
|
|
||
| if (!reportMerging) return Task.FromResult(attachments); | ||
|
|
||
| IList<IReporter> reporters = CreateReporters(formats).ToList(); | ||
|
|
||
| if (attachments.Count > 1) | ||
| { | ||
| _coverageResult.Parameters = new CoverageParameters() {DeterministicReport = deterministic, UseSourceLink = useSourceLink }; | ||
|
|
||
| var fileAttachments = attachments.SelectMany(x => x.Attachments.Where(IsFileAttachment)).ToList(); | ||
| string mergeFilePath = Path.GetDirectoryName(fileAttachments.First().Uri.LocalPath); | ||
|
|
||
| MergeExistingJsonReports(attachments); | ||
|
|
||
| RemoveObsoleteReports(fileAttachments); | ||
|
|
||
| AttachmentSet mergedFileAttachment = WriteCoverageReports(reporters, mergeFilePath, _coverageResult); | ||
|
|
||
| attachments = new List<AttachmentSet> { mergedFileAttachment }; | ||
| } | ||
|
|
||
| return Task.FromResult(attachments); | ||
| } | ||
|
|
||
| private static void RemoveObsoleteReports(List<UriDataAttachment> fileAttachments) | ||
| { | ||
| fileAttachments.ForEach(x => | ||
| { | ||
| string directory = Path.GetDirectoryName(x.Uri.LocalPath); | ||
| if (! string.IsNullOrEmpty(directory) && Directory.Exists(directory)) | ||
| Directory.Delete(directory, true); | ||
| }); | ||
| } | ||
|
|
||
| private void MergeExistingJsonReports(IEnumerable<AttachmentSet> attachments) | ||
| { | ||
| foreach (AttachmentSet attachmentSet in attachments) | ||
| { | ||
| attachmentSet.Attachments.Where(IsFileWithJsonExt).ToList().ForEach(x => | ||
| MergeWithCoverageResult(x.Uri.LocalPath, _coverageResult) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| private AttachmentSet WriteCoverageReports(IEnumerable<IReporter> 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); | ||
| 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; | ||
| } | ||
|
|
||
| private static bool IsFileWithJsonExt(UriDataAttachment x) | ||
| { | ||
| 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) | ||
| { | ||
| string json = File.ReadAllText(filePath); | ||
| coverageResult.Merge(JsonConvert.DeserializeObject<Modules>(json)); | ||
| } | ||
|
|
||
| private string GetCoverageReport(CoverageResult coverageResult, IReporter reporter) | ||
| { | ||
| try | ||
| { | ||
| // empty source root translator returns the original path for deterministic report | ||
| return reporter.Report(coverageResult, new SourceRootTranslator()); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| throw new CoverletDataCollectorException( | ||
| $"{CoverletConstants.DataCollectorName}: Failed to get coverage report", ex); | ||
| } | ||
| } | ||
|
|
||
| private void AttachDebugger() | ||
| { | ||
| if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_POSTPROCESSOR_DEBUG"), out int result) && result == 1) | ||
| { | ||
| Debugger.Launch(); | ||
| Debugger.Break(); | ||
| } | ||
| } | ||
|
|
||
| private IEnumerable<IReporter> CreateReporters(IEnumerable<string> formats) | ||
| { | ||
| IEnumerable<IReporter> 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); | ||
|
|
||
| return reporters; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 => | ||||||||||||||||||||
|
Comment on lines
+37
to
+40
|
||||||||||||||||||||
| if (settings.ReportMerging && ! settings.ReportFormats.Contains("json")) | |
| settings.ReportFormats = settings.ReportFormats.Append("json").ToArray(); | |
| return settings.ReportFormats.Select(format => | |
| IReporter[] updatedReportFormats = settings.ReportFormats; | |
| if (settings.ReportMerging && ! updatedReportFormats.Contains("json")) | |
| updatedReportFormats = updatedReportFormats.Append("json").ToArray(); | |
| return updatedReportFormats.Select(format => |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // 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<string>(); | ||
| 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(); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| internal bool ParseReportMerging(XmlElement configurationElement) | ||
| { | ||
| XmlElement mergeWithElement = configurationElement[CoverletConstants.ReportMerging]; | ||
| bool.TryParse(mergeWithElement?.InnerText, out bool mergeWith); | ||
| return mergeWith; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding checks or logging to ensure that deleting the directory does not inadvertently remove non-report files; additional filtering criteria may improve safety.