Skip to content

Commit fa2b4c6

Browse files
Generate multiple reports if project has multiple target frameworks (#636)
Generate multiple reports if project has multiple target frameworks
1 parent 15b2f95 commit fa2b4c6

File tree

14 files changed

+400
-43
lines changed

14 files changed

+400
-43
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Coverlet.Core.Abstracts
2+
{
3+
internal interface IConsole
4+
{
5+
public void WriteLine(string value);
6+
}
7+
}

src/coverlet.core/DependencyInjection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ private static IServiceProvider InitDefaultServices()
2929
serviceCollection.AddTransient<IRetryHelper, RetryHelper>();
3030
serviceCollection.AddTransient<IProcessExitHandler, ProcessExitHandler>();
3131
serviceCollection.AddTransient<IFileSystem, FileSystem>();
32+
serviceCollection.AddTransient<IConsole, SystemConsole>();
3233

3334
// We need to keep singleton/static semantics
3435
serviceCollection.AddSingleton<IInstrumentationHelper, InstrumentationHelper>();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
using Coverlet.Core.Abstracts;
4+
5+
namespace Coverlet.Core.Helpers
6+
{
7+
public class SystemConsole : IConsole
8+
{
9+
public void WriteLine(string value)
10+
{
11+
Console.WriteLine(value);
12+
}
13+
}
14+
}

src/coverlet.msbuild.tasks/CoverageResultTask.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class CoverageResultTask : Task
2020
private double _threshold;
2121
private string _thresholdType;
2222
private string _thresholdStat;
23+
private string _coverletMultiTargetFrameworksCurrentTFM;
2324
private ITaskItem _instrumenterState;
2425
private MSBuildLogger _logger;
2526

@@ -65,6 +66,12 @@ public ITaskItem InstrumenterState
6566
set { _instrumenterState = value; }
6667
}
6768

69+
public string CoverletMultiTargetFrameworksCurrentTFM
70+
{
71+
get { return _coverletMultiTargetFrameworksCurrentTFM; }
72+
set { _coverletMultiTargetFrameworksCurrentTFM = value; }
73+
}
74+
6875
public CoverageResultTask()
6976
{
7077
_logger = new MSBuildLogger(Log);
@@ -128,14 +135,14 @@ public override bool Execute()
128135
}
129136
else
130137
{
131-
// Output to file
132-
var filename = Path.GetFileName(_output);
133-
filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename;
134-
filename = Path.HasExtension(filename) ? filename : $"{filename}.{reporter.Extension}";
135-
136-
var report = Path.Combine(directory, filename);
137-
Console.WriteLine($" Generating report '{report}'");
138-
fileSystem.WriteAllText(report, reporter.Report(result));
138+
ReportWriter writer = new ReportWriter(_coverletMultiTargetFrameworksCurrentTFM,
139+
directory,
140+
_output,
141+
reporter,
142+
fileSystem,
143+
DependencyInjection.Current.GetService<IConsole>(),
144+
result);
145+
writer.WriteReport();
139146
}
140147
}
141148

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.msbuild.tasks.snk")]
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
4+
[assembly: AssemblyKeyFile("coverlet.msbuild.tasks.snk")]
5+
[assembly: InternalsVisibleTo("coverlet.core.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.IO;
2+
3+
using Coverlet.Core;
4+
using Coverlet.Core.Abstracts;
5+
using Coverlet.Core.Reporters;
6+
7+
namespace Coverlet.MSbuild.Tasks
8+
{
9+
internal class ReportWriter
10+
{
11+
private readonly string _coverletMultiTargetFrameworksCurrentTFM;
12+
private readonly string _directory;
13+
private readonly string _output;
14+
private readonly IReporter _reporter;
15+
private readonly IFileSystem _fileSystem;
16+
private readonly IConsole _console;
17+
private readonly CoverageResult _result;
18+
19+
public ReportWriter(string coverletMultiTargetFrameworksCurrentTFM, string directory, string output, IReporter reporter, IFileSystem fileSystem, IConsole console, CoverageResult result)
20+
=> (_coverletMultiTargetFrameworksCurrentTFM, _directory, _output, _reporter, _fileSystem, _console, _result) =
21+
(coverletMultiTargetFrameworksCurrentTFM, directory, output, reporter, fileSystem, console, result);
22+
23+
public void WriteReport()
24+
{
25+
string filename = Path.GetFileName(_output);
26+
27+
string separatorPoint = string.IsNullOrEmpty(_coverletMultiTargetFrameworksCurrentTFM) ? "" : ".";
28+
29+
if (filename == string.Empty)
30+
{
31+
// empty filename for instance only directory is passed to CoverletOutput c:\reportpath
32+
// c:\reportpath\coverage.reportedextension
33+
filename = $"coverage.{_coverletMultiTargetFrameworksCurrentTFM}{separatorPoint}{_reporter.Extension}";
34+
}
35+
else if (Path.HasExtension(filename))
36+
{
37+
// filename with extension for instance c:\reportpath\file.ext
38+
// c:\reportpath\file.ext.reportedextension
39+
filename = $"{Path.GetFileNameWithoutExtension(filename)}{separatorPoint}{_coverletMultiTargetFrameworksCurrentTFM}{Path.GetExtension(filename)}.{_reporter.Extension}";
40+
}
41+
else
42+
{
43+
// filename without extension for instance c:\reportpath\file
44+
// c:\reportpath\file.reportedextension
45+
filename = $"{filename}{separatorPoint}{_coverletMultiTargetFrameworksCurrentTFM}.{_reporter.Extension}";
46+
}
47+
48+
string report = Path.Combine(_directory, filename);
49+
_console.WriteLine($" Generating report '{report}'");
50+
_fileSystem.WriteAllText(report, _reporter.Report(_result));
51+
}
52+
}
53+
}

src/coverlet.msbuild.tasks/coverlet.msbuild.targets

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@
3030
Condition="'$(VSTestNoBuild)' != 'true' and '$(CollectCoverage)' == 'true'" />
3131

3232
<Target Name="GenerateCoverageResult">
33+
<PropertyGroup>
34+
<_coverletMultiTargetFrameworksCurrentTFM Condition="'$(TargetFrameworks)' != ''" >$(TargetFramework)</_coverletMultiTargetFrameworksCurrentTFM>
35+
</PropertyGroup>
3336
<Coverlet.MSbuild.Tasks.CoverageResultTask
3437
Output="$(CoverletOutput)"
3538
OutputFormat="$(CoverletOutputFormat)"
3639
Threshold="$(Threshold)"
3740
ThresholdType="$(ThresholdType)"
3841
ThresholdStat="$(ThresholdStat)"
39-
InstrumenterState="$(InstrumenterState)"/>
42+
InstrumenterState="$(InstrumenterState)"
43+
CoverletMultiTargetFrameworksCurrentTFM="$(_coverletMultiTargetFrameworksCurrentTFM)" />
4044
</Target>
4145

4246
<Target Name="GenerateCoverageResultAfterTest"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.IO;
2+
3+
using Coverlet.Core.Abstracts;
4+
using Coverlet.MSbuild.Tasks;
5+
using Moq;
6+
using Xunit;
7+
8+
namespace Coverlet.Core.Reporters.Tests
9+
{
10+
public class Reporters
11+
{
12+
// we use lcov with extension .info and cobertura with extension .cobertura.xml
13+
// to have all possible extension format
14+
// empty coverletOutput is not possible thank's to props default
15+
[Theory]
16+
// single tfm
17+
[InlineData("", "/folder/reportFolder/", "lcov", "/folder/reportFolder/coverage.info")]
18+
[InlineData(null, "/folder/reportFolder/", "cobertura", "/folder/reportFolder/coverage.cobertura.xml")]
19+
[InlineData(null, "/folder/reportFolder/file.ext", "cobertura", "/folder/reportFolder/file.ext.cobertura.xml")]
20+
[InlineData(null, "/folder/reportFolder/file.ext1.ext2", "cobertura", "/folder/reportFolder/file.ext1.ext2.cobertura.xml")]
21+
[InlineData(null, "/folder/reportFolder/file", "cobertura", "/folder/reportFolder/file.cobertura.xml")]
22+
[InlineData(null, "file", "cobertura", "file.cobertura.xml")]
23+
// multiple tfm
24+
[InlineData("netcoreapp2.2", "/folder/reportFolder/", "lcov", "/folder/reportFolder/coverage.netcoreapp2.2.info")]
25+
[InlineData("netcoreapp2.2", "/folder/reportFolder/", "cobertura", "/folder/reportFolder/coverage.netcoreapp2.2.cobertura.xml")]
26+
[InlineData("net472", "/folder/reportFolder/file.ext", "cobertura", "/folder/reportFolder/file.net472.ext.cobertura.xml")]
27+
[InlineData("net472", "/folder/reportFolder/file.ext1.ext2", "cobertura", "/folder/reportFolder/file.ext1.net472.ext2.cobertura.xml")]
28+
[InlineData("netcoreapp2.2", "/folder/reportFolder/file", "cobertura", "/folder/reportFolder/file.netcoreapp2.2.cobertura.xml")]
29+
[InlineData("netcoreapp2.2", "file", "cobertura", "file.netcoreapp2.2.cobertura.xml")]
30+
public void Msbuild_ReportWriter(string coverletMultiTargetFrameworksCurrentTFM, string coverletOutput, string reportFormat, string expectedFileName)
31+
{
32+
Mock<IFileSystem> fileSystem = new Mock<IFileSystem>();
33+
fileSystem.Setup(f => f.WriteAllText(It.IsAny<string>(), It.IsAny<string>()))
34+
.Callback((string path, string contents) =>
35+
{
36+
// Path.Combine depends on OS so we can change only win side to avoid duplication
37+
Assert.Equal(path.Replace('/', Path.DirectorySeparatorChar), expectedFileName.Replace('/', Path.DirectorySeparatorChar));
38+
});
39+
40+
Mock<IConsole> console = new Mock<IConsole>();
41+
42+
ReportWriter reportWriter = new ReportWriter(
43+
coverletMultiTargetFrameworksCurrentTFM,
44+
// mimic code inside CoverageResultTask.cs
45+
Path.GetDirectoryName(coverletOutput),
46+
coverletOutput,
47+
new ReporterFactory(reportFormat).CreateReporter(),
48+
fileSystem.Object,
49+
console.Object,
50+
new CoverageResult() { Modules = new Modules() });
51+
52+
reportWriter.WriteReport();
53+
}
54+
}
55+
}

test/coverlet.core.tests/coverlet.core.tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
<ItemGroup>
2323
<ProjectReference Include="$(RepoRoot)src\coverlet.core\coverlet.core.csproj" />
24+
<ProjectReference Include="$(RepoRoot)src\coverlet.msbuild.tasks\coverlet.msbuild.tasks.csproj" />
2425
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.remoteexecutor\coverlet.tests.remoteexecutor.csproj" />
2526
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.projectsample.empty\coverlet.tests.projectsample.empty.csproj" />
2627
<ProjectReference Include="$(RepoRoot)test\coverlet.tests.projectsample.excludedbyattribute\coverlet.tests.projectsample.excludedbyattribute.csproj" />

test/coverlet.integration.tests/BaseTest.cs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Coverlet.Core;
99
using Newtonsoft.Json;
1010
using NuGet.Packaging;
11+
using Xunit;
1112
using Xunit.Sdk;
1213

1314
namespace Coverlet.Integration.Tests
@@ -52,7 +53,7 @@ private protected string GetPackageVersion(string filter)
5253
return manifest.Metadata.Version.OriginalVersion;
5354
}
5455

55-
private protected ClonedTemplateProject CloneTemplateProject()
56+
private protected ClonedTemplateProject CloneTemplateProject(bool cleanupOnDispose = true)
5657
{
5758
DirectoryInfo finalRoot = Directory.CreateDirectory(Guid.NewGuid().ToString("N"));
5859
foreach (string file in (Directory.GetFiles($"../../../../coverlet.integration.template", "*.cs")
@@ -75,7 +76,7 @@ private protected ClonedTemplateProject CloneTemplateProject()
7576

7677
AddMicrosoftNETTestSdkRef(finalRoot.FullName);
7778

78-
return new ClonedTemplateProject() { Path = finalRoot.FullName };
79+
return new ClonedTemplateProject(finalRoot.FullName, cleanupOnDispose);
7980
}
8081

8182
private protected bool RunCommand(string command, string arguments, out string standardOutput, out string standardError, string workingDirectory = "")
@@ -205,34 +206,101 @@ private protected string AddCollectorRunsettingsFile(string projectPath)
205206
return runsettingsPath;
206207
}
207208

208-
private protected void AssertCoverage(ClonedTemplateProject clonedTemplateProject)
209+
private protected void AssertCoverage(ClonedTemplateProject clonedTemplateProject, string filter = "coverage.json")
209210
{
210-
Modules modules = JsonConvert.DeserializeObject<Modules>(File.ReadAllText(clonedTemplateProject.GetFiles("coverage.json").Single()));
211-
modules
212-
.Document("DeepThought.cs")
213-
.Class("Coverlet.Integration.Template.DeepThought")
214-
.Method("System.Int32 Coverlet.Integration.Template.DeepThought::AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()")
215-
.AssertLinesCovered((6, 1), (7, 1), (8, 1));
211+
bool coverageChecked = false;
212+
foreach (string coverageFile in clonedTemplateProject.GetFiles(filter))
213+
{
214+
JsonConvert.DeserializeObject<Modules>(File.ReadAllText(coverageFile))
215+
.Document("DeepThought.cs")
216+
.Class("Coverlet.Integration.Template.DeepThought")
217+
.Method("System.Int32 Coverlet.Integration.Template.DeepThought::AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()")
218+
.AssertLinesCovered((6, 1), (7, 1), (8, 1));
219+
coverageChecked = true;
220+
}
221+
222+
Assert.True(coverageChecked, "Coverage check fail");
223+
}
224+
225+
private protected void UpdateProjectTargetFramework(ClonedTemplateProject project, params string[] targetFrameworks)
226+
{
227+
if (targetFrameworks is null || targetFrameworks.Length == 0)
228+
{
229+
throw new ArgumentException("Invalid targetFrameworks", nameof(targetFrameworks));
230+
}
231+
232+
if (!File.Exists(project.ProjectFileNamePath))
233+
{
234+
throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj");
235+
}
236+
XDocument xml;
237+
using (var csprojStream = File.OpenRead(project.ProjectFileNamePath))
238+
{
239+
xml = XDocument.Load(csprojStream);
240+
}
241+
242+
xml.Element("Project")
243+
.Element("PropertyGroup")
244+
.Element("TargetFramework")
245+
.Remove();
246+
247+
XElement targetFrameworkElement;
248+
249+
if (targetFrameworks.Length == 1)
250+
{
251+
targetFrameworkElement = new XElement("TargetFramework", targetFrameworks[0]);
252+
}
253+
else
254+
{
255+
targetFrameworkElement = new XElement("TargetFrameworks", string.Join(';', targetFrameworks));
256+
}
257+
258+
xml.Element("Project").Element("PropertyGroup").Add(targetFrameworkElement);
259+
xml.Save(project.ProjectFileNamePath);
260+
}
261+
262+
private protected void PinSDK(ClonedTemplateProject project, string sdkVersion)
263+
{
264+
if (string.IsNullOrEmpty(sdkVersion))
265+
{
266+
throw new ArgumentException("Invalid sdkVersion", nameof(sdkVersion));
267+
}
268+
269+
if (!File.Exists(project.ProjectFileNamePath))
270+
{
271+
throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj");
272+
}
273+
274+
File.WriteAllText(Path.Combine(project.ProjectRootPath, "global.json"), $"{{ \"sdk\": {{ \"version\": \"{sdkVersion}\" }} }}");
216275
}
217276
}
218277

219278
class ClonedTemplateProject : IDisposable
220279
{
221-
public string Path { get; set; }
280+
public string? ProjectRootPath { get; private set; }
281+
public bool _cleanupOnDispose { get; set; }
222282

223283
// We need to have a different asm name to avoid issue with collectors, we filter [coverlet.*]* by default
224284
// https://github.com/tonerdo/coverlet/pull/410#discussion_r284526728
225285
public static string AssemblyName { get; } = "coverletsamplelib.integration.template";
226286
public static string ProjectFileName { get; } = "coverlet.integration.template.csproj";
287+
public string ProjectFileNamePath => Path.Combine(ProjectRootPath, "coverlet.integration.template.csproj");
288+
289+
public ClonedTemplateProject(string projectRootPath, bool cleanupOnDispose) => (ProjectRootPath, _cleanupOnDispose) = (projectRootPath, cleanupOnDispose);
290+
291+
227292

228293
public string[] GetFiles(string filter)
229294
{
230-
return Directory.GetFiles(this.Path, filter, SearchOption.AllDirectories);
295+
return Directory.GetFiles(ProjectRootPath, filter, SearchOption.AllDirectories);
231296
}
232297

233298
public void Dispose()
234299
{
235-
Directory.Delete(Path, true);
300+
if (_cleanupOnDispose)
301+
{
302+
Directory.Delete(ProjectRootPath, true);
303+
}
236304
}
237305
}
238306
}

0 commit comments

Comments
 (0)