Skip to content

Commit 06ef9ad

Browse files
daveMuellerMarcoRossignoli
authored andcommitted
[Collectors]Output multiple formats (#533)
Allow multiple output format for collectors
1 parent 19b9611 commit 06ef9ad

15 files changed

+282
-89
lines changed

Documentation/VSTestIntegration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ These are a list of options that are supported by coverlet. These can be specifi
2929

3030
| Option | Summary |
3131
|------------- |------------------------------------------------------------------------------------------|
32-
|Format | Coverage output format. These are either cobertura, json, lcov, opencover or teamcity. |
32+
|Format | Coverage output format. These are either cobertura, json, lcov, opencover or teamcity as well as combinations of these formats. |
3333
|MergeWith | Combine the output of multiple coverage runs into a single result. |
3434
|Exclude | Exclude from code coverage analysing using filter expressions. |
3535
|ExcludeByFile | Ignore specific source files from code coverage. |
@@ -46,7 +46,7 @@ How to specify these options via runsettings?
4646
<DataCollectors>
4747
<DataCollector friendlyName="XPlat code coverage">
4848
<Configuration>
49-
<Format>json</Format>
49+
<Format>json,cobertura</Format>
5050
<MergeWith>/custom/path/result.json</MergeWith>
5151
<Exclude>[coverlet.*.tests?]*,[*]Coverlet.Core*</Exclude> <!-- [Assembly-Filter]Type-Filter -->
5252
<Include>[coverlet.*]*,[*]Coverlet.Core*</Include> <!-- [Assembly-Filter]Type-Filter -->

src/coverlet.collector/DataCollection/AttachmentManager.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.ComponentModel;
33
using System.IO;
4+
45
using coverlet.collector.Resources;
56
using Coverlet.Collector.Utilities;
67
using Coverlet.Collector.Utilities.Interfaces;
@@ -19,31 +20,31 @@ internal class AttachmentManager : IDisposable
1920
private readonly DataCollectionContext _dataCollectionContext;
2021
private readonly IFileHelper _fileHelper;
2122
private readonly IDirectoryHelper _directoryHelper;
22-
private readonly string _reportFileName;
23+
private readonly ICountDownEvent _countDownEvent;
2324
private readonly string _reportDirectory;
2425

25-
public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportFileName)
26+
public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, ICountDownEvent countDownEvent)
2627
: this(dataSink,
2728
dataCollectionContext,
2829
logger,
2930
eqtTrace,
30-
reportFileName,
3131
Guid.NewGuid().ToString(),
3232
new FileHelper(),
33-
new DirectoryHelper())
33+
new DirectoryHelper(),
34+
countDownEvent)
3435
{
3536
}
3637

37-
public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportFileName, string reportDirectoryName, IFileHelper fileHelper, IDirectoryHelper directoryHelper)
38+
public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportDirectoryName, IFileHelper fileHelper, IDirectoryHelper directoryHelper, ICountDownEvent countDownEvent)
3839
{
3940
// Store input variabless
4041
_dataSink = dataSink;
4142
_dataCollectionContext = dataCollectionContext;
4243
_logger = logger;
4344
_eqtTrace = eqtTrace;
44-
_reportFileName = reportFileName;
4545
_fileHelper = fileHelper;
4646
_directoryHelper = directoryHelper;
47+
_countDownEvent = countDownEvent;
4748

4849
// Report directory to store the coverage reports.
4950
_reportDirectory = Path.Combine(Path.GetTempPath(), reportDirectoryName);
@@ -56,10 +57,11 @@ public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext data
5657
/// Sends coverage report to test platform
5758
/// </summary>
5859
/// <param name="coverageReport">Coverage report</param>
59-
public void SendCoverageReport(string coverageReport)
60+
/// <param name="coverageReportFileName">Coverage report file name</param>
61+
public void SendCoverageReport(string coverageReport, string coverageReportFileName)
6062
{
6163
// Save coverage report to file
62-
string coverageReportPath = this.SaveCoverageReport(coverageReport);
64+
string coverageReportPath = this.SaveCoverageReport(coverageReport, coverageReportFileName);
6365

6466
// Send coverage attachment to test platform.
6567
this.SendAttachment(coverageReportPath);
@@ -73,6 +75,7 @@ public void Dispose()
7375
// Unregister events
7476
try
7577
{
78+
_countDownEvent.Wait();
7679
if (_dataSink != null)
7780
{
7881
_dataSink.SendFileCompleted -= this.OnSendFileCompleted;
@@ -89,21 +92,22 @@ public void Dispose()
8992
/// Saves coverage report to file system
9093
/// </summary>
9194
/// <param name="report">Coverage report</param>
95+
/// <param name="reportFileName">Coverage report file name</param>
9296
/// <returns>Coverage report file path</returns>
93-
private string SaveCoverageReport(string report)
97+
private string SaveCoverageReport(string report, string reportFileName)
9498
{
9599
try
96100
{
97101
_directoryHelper.CreateDirectory(_reportDirectory);
98-
string filePath = Path.Combine(_reportDirectory, _reportFileName);
102+
string filePath = Path.Combine(_reportDirectory, reportFileName);
99103
_fileHelper.WriteAllText(filePath, report);
100104
_eqtTrace.Info("{0}: Saved coverage report to path: '{1}'", CoverletConstants.DataCollectorName, filePath);
101105

102106
return filePath;
103107
}
104108
catch (Exception ex)
105109
{
106-
string errorMessage = string.Format(Resources.FailedToSaveCoverageReport, CoverletConstants.DataCollectorName, _reportFileName, _reportDirectory);
110+
string errorMessage = string.Format(Resources.FailedToSaveCoverageReport, CoverletConstants.DataCollectorName, reportFileName, _reportDirectory);
107111
throw new CoverletDataCollectorException(errorMessage, ex);
108112
}
109113
}
@@ -118,12 +122,14 @@ public void OnSendFileCompleted(object sender, AsyncCompletedEventArgs e)
118122
try
119123
{
120124
_eqtTrace.Verbose("{0}: SendFileCompleted received", CoverletConstants.DataCollectorName);
121-
this.CleanupReportDirectory();
122125
}
123126
catch (Exception ex)
124127
{
125128
_logger.LogWarning(ex.ToString());
126-
this.Dispose();
129+
}
130+
finally
131+
{
132+
_countDownEvent.Signal();
127133
}
128134
}
129135

src/coverlet.collector/DataCollection/CoverageManager.cs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
25
using coverlet.collector.Resources;
36
using Coverlet.Collector.Utilities;
47
using Coverlet.Collector.Utilities.Interfaces;
@@ -17,20 +20,32 @@ internal class CoverageManager
1720

1821
private ICoverageWrapper _coverageWrapper;
1922

20-
public IReporter Reporter { get; }
23+
public IReporter[] Reporters { get; }
2124

2225
public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper)
2326
: this(settings,
24-
new ReporterFactory(settings.ReportFormat).CreateReporter(),
25-
new CoverletLogger(eqtTrace, logger),
26-
coverageWrapper)
27+
settings.ReportFormats.Select(format =>
28+
{
29+
var reporterFactory = new ReporterFactory(format);
30+
if (!reporterFactory.IsValidFormat())
31+
{
32+
eqtTrace.Warning($"Invalid report format '{format}'");
33+
return null;
34+
}
35+
else
36+
{
37+
return reporterFactory.CreateReporter();
38+
}
39+
}).Where(r => r != null).ToArray(),
40+
new CoverletLogger(eqtTrace, logger),
41+
coverageWrapper)
2742
{
2843
}
2944

30-
public CoverageManager(CoverletSettings settings, IReporter reporter, ILogger logger, ICoverageWrapper coverageWrapper)
45+
public CoverageManager(CoverletSettings settings, IReporter[] reporters, ILogger logger, ICoverageWrapper coverageWrapper)
3146
{
3247
// Store input vars
33-
Reporter = reporter;
48+
Reporters = reporters;
3449
_coverageWrapper = coverageWrapper;
3550

3651
// Coverage object
@@ -55,17 +70,14 @@ public void InstrumentModules()
5570
}
5671

5772
/// <summary>
58-
/// Gets coverlet coverage report
73+
/// Gets coverlet coverage reports
5974
/// </summary>
60-
/// <returns>Coverage report</returns>
61-
public string GetCoverageReport()
75+
/// <returns>Coverage reports</returns>
76+
public IEnumerable<(string report, string fileName)> GetCoverageReports()
6277
{
6378
// Get coverage result
6479
CoverageResult coverageResult = this.GetCoverageResult();
65-
66-
// Get coverage report in default format
67-
string coverageReport = this.GetCoverageReport(coverageResult);
68-
return coverageReport;
80+
return this.GetCoverageReports(coverageResult);
6981
}
7082

7183
/// <summary>
@@ -86,15 +98,15 @@ private CoverageResult GetCoverageResult()
8698
}
8799

88100
/// <summary>
89-
/// Gets coverage report from coverage result
101+
/// Gets coverage reports from coverage result
90102
/// </summary>
91103
/// <param name="coverageResult">Coverage result</param>
92-
/// <returns>Coverage report</returns>
93-
private string GetCoverageReport(CoverageResult coverageResult)
104+
/// <returns>Coverage reports</returns>
105+
private IEnumerable<(string report, string fileName)> GetCoverageReports(CoverageResult coverageResult)
94106
{
95107
try
96108
{
97-
return Reporter.Report(coverageResult);
109+
return Reporters.Select(reporter => (reporter.Report(coverageResult), Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension)));
98110
}
99111
catch (Exception ex)
100112
{

src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using System.Xml;
56
using Coverlet.Collector.Utilities;
@@ -23,15 +24,17 @@ public class CoverletCoverageCollector : DataCollector
2324
private DataCollectionContext _dataCollectionContext;
2425
private CoverageManager _coverageManager;
2526
private ICoverageWrapper _coverageWrapper;
27+
private ICountDownEventFactory _countDownEventFactory;
2628

27-
public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper())
29+
public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper(), new CollectorCountdownEventFactory())
2830
{
2931
}
3032

31-
internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper) : base()
33+
internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper, ICountDownEventFactory countDownEventFactory) : base()
3234
{
3335
_eqtTrace = eqtTrace;
3436
_coverageWrapper = coverageWrapper;
37+
_countDownEventFactory = countDownEventFactory;
3538
}
3639

3740
/// <summary>
@@ -130,12 +133,23 @@ private void OnSessionEnd(object sender, SessionEndEventArgs e)
130133
_eqtTrace.Verbose("{0}: SessionEnd received", CoverletConstants.DataCollectorName);
131134

132135
// Get coverage reports
133-
string coverageReport = _coverageManager?.GetCoverageReport();
134-
135-
// Send result attachments to test platform.
136-
var attachmentManager = new AttachmentManager(_dataSink, _dataCollectionContext, _logger, _eqtTrace, this.GetReportFileName());
137-
attachmentManager?.SendCoverageReport(coverageReport);
138-
136+
IEnumerable<(string report, string fileName)> coverageReports = _coverageManager?.GetCoverageReports();
137+
138+
if (coverageReports != null && coverageReports.Count() > 0)
139+
{
140+
// Send result attachments to test platform.
141+
using (var attachmentManager = new AttachmentManager(_dataSink, _dataCollectionContext, _logger, _eqtTrace, _countDownEventFactory.Create(coverageReports.Count(), TimeSpan.FromSeconds(30))))
142+
{
143+
foreach ((string report, string fileName) in coverageReports)
144+
{
145+
attachmentManager.SendCoverageReport(report, fileName);
146+
}
147+
}
148+
}
149+
else
150+
{
151+
_eqtTrace.Verbose("{0}: No coverage reports specified", CoverletConstants.DataCollectorName);
152+
}
139153
}
140154
catch (Exception ex)
141155
{
@@ -144,18 +158,6 @@ private void OnSessionEnd(object sender, SessionEndEventArgs e)
144158
}
145159
}
146160

147-
/// <summary>
148-
/// Gets coverage report file name
149-
/// </summary>
150-
/// <returns>Coverage report file name</returns>
151-
private string GetReportFileName()
152-
{
153-
string fileName = CoverletConstants.DefaultFileName;
154-
string extension = _coverageManager?.Reporter.Extension;
155-
156-
return extension == null ? fileName : $"{fileName}.{extension}";
157-
}
158-
159161
/// <summary>
160162
/// Gets test modules
161163
/// </summary>

src/coverlet.collector/DataCollection/CoverletSettings.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ internal class CoverletSettings
1414
public string TestModule { get; set; }
1515

1616
/// <summary>
17-
/// Report format
17+
/// Report formats
1818
/// </summary>
19-
public string ReportFormat { get; set; }
19+
public string[] ReportFormats { get; set; }
2020

2121
/// <summary>
2222
/// Filters to include

src/coverlet.collector/DataCollection/CoverletSettingsParser.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Xml;
45
using coverlet.collector.Resources;
@@ -43,7 +44,7 @@ public CoverletSettings Parse(XmlElement configurationElement, IEnumerable<strin
4344
coverletSettings.IncludeTestAssembly = this.ParseIncludeTestAssembly(configurationElement);
4445
}
4546

46-
coverletSettings.ReportFormat = this.ParseReportFormat(configurationElement);
47+
coverletSettings.ReportFormats = this.ParseReportFormats(configurationElement);
4748
coverletSettings.ExcludeFilters = this.ParseExcludeFilters(configurationElement);
4849

4950
if (_eqtTrace.IsVerboseEnabled)
@@ -75,19 +76,21 @@ private string ParseTestModule(IEnumerable<string> testModules)
7576
}
7677

7778
/// <summary>
78-
/// Parse report format
79+
/// Parse report formats
7980
/// </summary>
8081
/// <param name="configurationElement">Configuration element</param>
81-
/// <returns>Report format</returns>
82-
private string ParseReportFormat(XmlElement configurationElement)
82+
/// <returns>Report formats</returns>
83+
private string[] ParseReportFormats(XmlElement configurationElement)
8384
{
84-
string format = string.Empty;
85+
string[] formats = Array.Empty<string>();
8586
if (configurationElement != null)
8687
{
8788
XmlElement reportFormatElement = configurationElement[CoverletConstants.ReportFormatElementName];
88-
format = reportFormatElement?.InnerText?.Split(',').FirstOrDefault();
89+
formats = reportFormatElement?.InnerText?.Split(',').Select(format => format.Trim())
90+
.Where(format => !string.IsNullOrEmpty(format)).ToArray();
8991
}
90-
return string.IsNullOrEmpty(format) ? CoverletConstants.DefaultReportFormat : format;
92+
93+
return formats is null || formats.Length == 0 ? new[] {CoverletConstants.DefaultReportFormat} : formats;
9194
}
9295

9396
/// <summary>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Threading;
3+
4+
using Coverlet.Collector.Utilities.Interfaces;
5+
6+
namespace Coverlet.Collector.Utilities
7+
{
8+
internal class CollectorCountdownEventFactory : ICountDownEventFactory
9+
{
10+
public ICountDownEvent Create(int count, TimeSpan waitTimeout)
11+
{
12+
return new CollectorCountdownEvent(count, waitTimeout);
13+
}
14+
}
15+
16+
internal class CollectorCountdownEvent : ICountDownEvent
17+
{
18+
private readonly CountdownEvent _countDownEvent;
19+
private readonly TimeSpan _waitTimeout;
20+
21+
public CollectorCountdownEvent(int count, TimeSpan waitTimeout)
22+
{
23+
_countDownEvent = new CountdownEvent(count);
24+
_waitTimeout = waitTimeout;
25+
}
26+
27+
public void Signal()
28+
{
29+
_countDownEvent.Signal();
30+
}
31+
32+
public void Wait()
33+
{
34+
// We wait on another thread to avoid to block forever
35+
// We could use Task/Task.Delay timeout trick but this api and collector are sync so to
36+
// avoid too much GetAwaiter()/GetResult() I prefer keep code simple.
37+
// This thread is created only one time where we pass coverage files
38+
var waitOnThread = new Thread(() => _countDownEvent.Wait());
39+
waitOnThread.Start();
40+
waitOnThread.Join(_waitTimeout);
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)