Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Documentation/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Documentation/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in documentation: "uses" should be "used". The sentence should read "Post processing is used for automatically merging coverage reports..."

Suggested change
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`.
Post processing is used for automatically merging coverage reports with the `ReportMerging` option enabled. You can live attach and debug the post processor `COVERLET_DATACOLLECTOR_POSTPROCESSOR_DEBUG`.

Copilot uses AI. Check for mistakes.

You will be asked to attach a debugger through UI popup.

```shell
set COVERLET_DATACOLLECTOR_POSTPROCESSOR_DEBUG=1
```
2 changes: 2 additions & 0 deletions Documentation/VSTestIntegration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 if coverage is calculated for multiple projects. Default is false.|

How to specify these options via runsettings?

Expand All @@ -143,6 +144,7 @@ How to specify these options via runsettings?
<SkipAutoProps>true</SkipAutoProps>
<DeterministicReport>false</DeterministicReport>
<ExcludeAssembliesWithoutSources>MissingAll,MissingAny,None</ExcludeAssembliesWithoutSources>
<ReportMerging>false</ReportMerging>
</Configuration>
</DataCollector>
</DataCollectors>
Expand Down
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,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progressReporter parameter is not used anywhere in the method. For operations that process multiple attachments (potentially a large number in solution-wide scenarios), progress reporting would be valuable for user feedback. Consider reporting progress after processing each attachment or major operation (e.g., after merging each JSON report, after writing each report format).

Copilot uses AI. Check for mistakes.
IMessageLogger logger, CancellationToken cancellationToken)
Comment on lines +34 to +36
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cancellationToken parameter is not used anywhere in the method. While the current implementation uses synchronous operations, the method signature suggests async cancellation support. Consider either utilizing the cancellationToken for long-running operations (like file I/O, JSON deserialization, and directory deletion), or document why it's not needed. At minimum, cancellationToken.ThrowIfCancellationRequested() should be called before expensive operations.

Copilot uses AI. Check for mistakes.
{
_reportFormatParser ??= new ReportFormatParser();
_coverageResult ??= new CoverageResult();
_coverageResult.Modules ??= new Modules();
_logger = logger;
Comment on lines +38 to +41
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread safety concern: The class uses instance fields (_coverageResult, _reportFormatParser, _logger) that are initialized with null-coalescing operators and potentially shared across multiple calls to ProcessAttachmentSetsAsync. If this method is called concurrently or multiple times, there's potential for race conditions or state corruption. Since SupportsIncrementalProcessing is set to true (line 30), the class instance may be reused. Consider whether these fields should be method-local variables instead, or document thread-safety guarantees.

Copilot uses AI. Check for mistakes.

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();
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all report formats are invalid, the reporters list will be empty, but the code proceeds to use it without checking. When WriteCoverageReports is called with an empty reporters list (line 65), it will create an empty AttachmentSet, which may not be the desired behavior. Consider checking if the reporters list is empty and either logging a warning or returning early.

Copilot uses AI. Check for mistakes.

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);

Comment on lines +59 to +60
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug: calling First() on an empty collection will throw an InvalidOperationException. If there are no file attachments in the collection, this will cause a runtime error. Consider adding a check to ensure fileAttachments is not empty before calling First(), or use FirstOrDefault() with a null check.

Suggested change
string mergeFilePath = Path.GetDirectoryName(fileAttachments.First().Uri.LocalPath);
var firstFileAttachment = fileAttachments.FirstOrDefault();
if (firstFileAttachment == null)
{
_logger?.SendMessage(TestMessageLevel.Warning, "No file attachments found to merge coverage reports.");
return Task.FromResult(attachments);
}
string mergeFilePath = Path.GetDirectoryName(firstFileAttachment.Uri.LocalPath);

Copilot uses AI. Check for mistakes.
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))
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing inconsistency: there's an extra space after the exclamation mark in this condition. Consider removing it for consistency with standard formatting conventions (e.g., !string.IsNullOrEmpty(directory) instead of ! string.IsNullOrEmpty(directory)).

Suggested change
if (! string.IsNullOrEmpty(directory) && Directory.Exists(directory))
if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory))

Copilot uses AI. Check for mistakes.
Directory.Delete(directory, true);
Comment on lines +78 to +79
Copy link

Copilot AI Jun 9, 2025

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.

Suggested change
if (! string.IsNullOrEmpty(directory) && Directory.Exists(directory))
Directory.Delete(directory, true);
if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory))
{
var files = Directory.GetFiles(directory);
var reportFiles = files.Where(file => file.EndsWith(".json") || file.EndsWith(".xml")).ToList();
if (reportFiles.Count == files.Length) // Ensure all files are report-related
{
Console.WriteLine($"Deleting directory: {directory}. Files: {string.Join(", ", reportFiles)}");
Directory.Delete(directory, true);
}
else
{
Console.WriteLine($"Skipping directory: {directory}. Contains non-report files.");
}
}

Copilot uses AI. Check for mistakes.
});
}

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");
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String comparison is case-sensitive. The extension comparison with ".json" won't match files with uppercase extensions like ".JSON" or ".Json". Consider using Path.GetExtension(x.Uri.AbsolutePath).Equals(".json", StringComparison.OrdinalIgnoreCase) for case-insensitive comparison.

Suggested change
return IsFileAttachment(x) && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json");
return IsFileAttachment(x) && Path.GetExtension(x.Uri.AbsolutePath).Equals(".json", StringComparison.OrdinalIgnoreCase);

Copilot uses AI. Check for mistakes.
}

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));
Comment on lines +119 to +120
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: JSON deserialization can fail for various reasons (malformed JSON, incompatible schema, file read errors). If deserialization fails, it will throw an exception that propagates up without a meaningful error message. Consider wrapping this in a try-catch block and providing a more descriptive error message that includes the file path.

Suggested change
string json = File.ReadAllText(filePath);
coverageResult.Merge(JsonConvert.DeserializeObject<Modules>(json));
try
{
string json = File.ReadAllText(filePath);
Modules modules = JsonConvert.DeserializeObject<Modules>(json);
coverageResult.Merge(modules);
}
catch (Exception ex)
{
throw new CoverletDataCollectorException(
$"{CoverletConstants.DataCollectorName}: Failed to merge coverage result from file '{filePath}'",
ex);
}

Copilot uses AI. Check for mistakes.
}

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;
}
}
}
Comment on lines +1 to +162
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new CoverletCoveragePostProcessor class, which is the core of this feature, lacks test coverage. Given the complexity of the attachment processing logic (merging JSON reports, handling file deletions, creating merged attachments), this class should have comprehensive unit tests to cover various scenarios such as: multiple attachments, empty attachments, malformed JSON files, missing directories, and the interaction with the merging flag.

Copilot uses AI. Check for mistakes.
34 changes: 21 additions & 13 deletions src/coverlet.collector/DataCollection/CoverageManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing inconsistency: there's an extra space after the exclamation mark in this condition. Consider removing it for consistency with standard formatting conventions (e.g., !settings.ReportFormats.Contains("json") instead of ! settings.ReportFormats.Contains("json")).

Suggested change
if (settings.ReportMerging && ! settings.ReportFormats.Contains("json"))
if (settings.ReportMerging && !settings.ReportFormats.Contains("json"))

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String comparison is case-sensitive. The check for "json" format won't match if the user specifies "JSON" or "Json" in the configuration. Since report format names in coverlet are consistently lowercase (as seen in the reporter classes), consider using a case-insensitive comparison or converting the format to lowercase before comparison: settings.ReportFormats.Any(f => f.Equals("json", StringComparison.OrdinalIgnoreCase)).

Suggested change
if (settings.ReportMerging && ! settings.ReportFormats.Contains("json"))
if (settings.ReportMerging && !settings.ReportFormats.Any(f => f.Equals("json", StringComparison.OrdinalIgnoreCase)))

Copilot uses AI. Check for mistakes.
settings.ReportFormats = settings.ReportFormats.Append("json").ToArray();

return settings.ReportFormats.Select(format =>
Comment on lines +37 to +40
Copy link

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutating settings.ReportFormats in place may introduce unintended side effects; consider assigning the updated report formats to a new local variable to preserve immutability.

Suggested change
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 =>

Copilot uses AI. Check for mistakes.
{
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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +22,7 @@ namespace Coverlet.Collector.DataCollection
/// </summary>
[DataCollectorTypeUri(CoverletConstants.DefaultUri)]
[DataCollectorFriendlyName(CoverletConstants.FriendlyName)]
[DataCollectorAttachmentProcessor(typeof(CoverletCoveragePostProcessor))]
public class CoverletCoverageCollector : DataCollector
{
private readonly TestPlatformEqtTrace _eqtTrace;
Expand Down
6 changes: 6 additions & 0 deletions src/coverlet.collector/DataCollection/CoverletSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ internal class CoverletSettings
/// </summary>
public string ExcludeAssembliesWithoutSources { get; set; }

/// <summary>
/// Report merging flag
/// </summary>
public bool ReportMerging { get; set; }

public override string ToString()
{
var builder = new StringBuilder();
Expand All @@ -104,6 +109,7 @@ public override string ToString()
builder.AppendFormat("DoesNotReturnAttributes: '{0}'", string.Join(",", DoesNotReturnAttributes ?? Enumerable.Empty<string>()));
builder.AppendFormat("DeterministicReport: '{0}'", DeterministicReport);
builder.AppendFormat("ExcludeAssembliesWithoutSources: '{0}'", ExcludeAssembliesWithoutSources);
builder.AppendFormat("ReportMerging: '{0}'", ReportMerging);

return builder.ToString();
}
Expand Down
22 changes: 4 additions & 18 deletions src/coverlet.collector/DataCollection/CoverletSettingsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/// <summary>
Expand Down Expand Up @@ -48,9 +50,10 @@ public CoverletSettings Parse(XmlElement configurationElement, IEnumerable<strin
coverletSettings.DoesNotReturnAttributes = ParseDoesNotReturnAttributes(configurationElement);
coverletSettings.DeterministicReport = ParseDeterministicReport(configurationElement);
coverletSettings.ExcludeAssembliesWithoutSources = ParseExcludeAssembliesWithoutSources(configurationElement);
coverletSettings.ReportMerging = _reportFormatParser.ParseReportMerging(configurationElement);
}

coverletSettings.ReportFormats = ParseReportFormats(configurationElement);
coverletSettings.ReportFormats = _reportFormatParser.ParseReportFormats(configurationElement);
coverletSettings.ExcludeFilters = ParseExcludeFilters(configurationElement);

if (_eqtTrace.IsVerboseEnabled)
Expand Down Expand Up @@ -80,23 +83,6 @@ private static string ParseTestModule(IEnumerable<string> testModules)
return testModules.FirstOrDefault();
}

/// <summary>
/// Parse report formats
/// </summary>
/// <param name="configurationElement">Configuration element</param>
/// <returns>Report formats</returns>
private static 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;
}

/// <summary>
/// Parse filters to include
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/coverlet.collector/Utilities/CoverletConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ReportMerging = "ReportMerging";
}
}
50 changes: 50 additions & 0 deletions src/coverlet.collector/Utilities/ReportFormatParser.cs
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;
}
}
}
Comment on lines +1 to +50
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ReportFormatParser class and its methods (ParseReportFormats, ParseUseSourceLink, ParseDeterministicReport, ParseReportMerging) lack test coverage. Given that other utility classes in the collector have comprehensive tests (e.g., CoverletSettingsParserTests), this new parser should have similar test coverage to ensure correct parsing of XML configuration elements.

Copilot uses AI. Check for mistakes.
Loading
Loading