Skip to content

Commit 3364c96

Browse files
committed
#590, 591: Improved support for Cobertura files generated with 'dotnet test --collect "Code Coverage;Format=Cobertura"'
1 parent f396a74 commit 3364c96

File tree

15 files changed

+1624
-559
lines changed

15 files changed

+1624
-559
lines changed

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "7.0.103",
3+
"version": "7.0.200",
44
"rollForward": "latestMajor"
55
}
66
}

src/Readme.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ For further details take a look at LICENSE.txt.
6666

6767
CHANGELOG
6868

69+
5.1.18.0
70+
71+
* Fix: #590, 591: Improved support for Cobertura files generated with
72+
'dotnet test --collect "Code Coverage;Format=Cobertura"'
73+
6974
5.1.17.0
7075

7176
* Fix: #536: Improved support for C code in Visual Studio coverage input format (*.coveragexml)

src/ReportGenerator.Console.NetCore/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"ReportGenerator.Console.NetCore": {
44
"commandName": "Project",
5-
"commandLineArgs": "-reports:C:\\Users\\danie\\Documents\\Projects\\ReportGenerator\\src\\Testprojects\\CSharp\\Reports\\OpenCover.xml -targetdir:C:\\Users\\danie\\Desktop\\coverage -historydir:C:\\Users\\danie\\Desktop\\coverage\\history -reporttypes:Html;Html_Light;Html_Dark;HtmlSummary;HtmlInline;HtmlInline_AzurePipelines;HtmlInline_AzurePipelines_Light;HtmlInline_AzurePipelines_Dark settings:createSubdirectoryForAllReportTypes=true"
5+
"commandLineArgs": "-reports:C:\\Users\\danie\\Documents\\Projects\\ReportGenerator\\src\\Testprojects\\CSharp\\Reports\\Cobertura_dotnettest.xml -targetdir:C:\\Users\\danie\\Desktop\\dotnet_2 -reporttypes:Html"
66
}
77
}
88
}

src/ReportGenerator.Core.Test/Common/GlobbingFileSearchTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void GetFiles_EmptyDirectory_NoFilesFound()
2828
public void GetFiles_SingleDirectory_XmlFilesFound()
2929
{
3030
var files = GlobbingFileSearch.GetFiles(Path.Combine(FileManager.GetCSharpReportDirectory(), "*.xml")).ToArray();
31-
Assert.Equal(22, files.Length);
31+
Assert.Equal(23, files.Length);
3232
}
3333

3434
[Fact]

src/ReportGenerator.Core.Test/Common/WildCardFileSearchTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void GetFiles_EmptyDirectory_NoFilesFound()
5858
public void GetFiles_SingleDirectory_XmlFilesFound()
5959
{
6060
var files = WildCardFileSearch.GetFiles(Path.Combine(FileManager.GetCSharpReportDirectory(), "*.xml")).ToArray();
61-
Assert.Equal(22, files.Length);
61+
Assert.Equal(23, files.Length);
6262
}
6363

6464
[Fact]

src/ReportGenerator.Core/Parser/CoberturaParser.cs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ internal class CoberturaParser : ParserBase
2828
/// </summary>
2929
private static readonly Regex GenericClassRegex = new Regex("<.*>$", RegexOptions.Compiled);
3030

31+
/// <summary>
32+
/// Regex to analyze if a class name represents an async (generic) class.
33+
/// Format gets generated by 'dotnet test --collect "Code Coverage;Format=Cobertura"'.
34+
/// </summary>
35+
private static readonly Regex AsyncClassRegex = new Regex("^(?<ClassName>.+)\\.<.*>.*__(?:.+(?<GenericTypes><.+>))?", RegexOptions.Compiled);
36+
3137
/// <summary>
3238
/// Regex to analyze if a method name belongs to a lamda expression.
3339
/// </summary>
@@ -36,7 +42,7 @@ internal class CoberturaParser : ParserBase
3642
/// <summary>
3743
/// Regex to analyze if a method name is generated by compiler.
3844
/// </summary>
39-
private static readonly Regex CompilerGeneratedMethodNameRegex = new Regex(@"(?<ClassName>.+)/<(?<CompilerGeneratedName>.+)>.+__.+MoveNext\(\)$", RegexOptions.Compiled);
45+
private static readonly Regex CompilerGeneratedMethodNameRegex = new Regex(@"(?<ClassName>.+)(/|\.)<(?<CompilerGeneratedName>.+)>.+__.+MoveNext\(\)$", RegexOptions.Compiled);
4046

4147
/// <summary>
4248
/// Regex to analyze the branch coverage of a line element.
@@ -127,18 +133,37 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
127133
{
128134
string fullname = c.Attribute("name").Value;
129135
int nestedClassSeparatorIndex = fullname.IndexOf('/');
130-
return nestedClassSeparatorIndex > -1 ? fullname.Substring(0, nestedClassSeparatorIndex) : fullname;
136+
137+
if (nestedClassSeparatorIndex > -1)
138+
{
139+
string className = fullname.Substring(0, nestedClassSeparatorIndex);
140+
return Tuple.Create(className, className);
141+
}
142+
143+
if (fullname.Contains("<"))
144+
{
145+
var match = AsyncClassRegex.Match(fullname);
146+
147+
if (match.Success)
148+
{
149+
return Tuple.Create(
150+
match.Groups["ClassName"].Value,
151+
match.Groups["ClassName"].Value + match.Groups["GenericTypes"].Value);
152+
}
153+
}
154+
155+
return Tuple.Create(fullname, fullname);
131156
})
132-
.Where(name => !name.Contains("$")
133-
&& (!name.Contains("<") || GenericClassRegex.IsMatch(name)))
157+
.Where(c => !c.Item1.Contains("$")
158+
&& (!c.Item1.Contains("<") || GenericClassRegex.IsMatch(c.Item1)))
134159
.Distinct()
135-
.Where(c => this.ClassFilter.IsElementIncludedInReport(c))
136-
.OrderBy(name => name)
160+
.Where(c => this.ClassFilter.IsElementIncludedInReport(c.Item1))
161+
.OrderBy(c => c.Item1)
137162
.ToArray();
138163

139164
var assembly = new Assembly(assemblyName);
140165

141-
Parallel.ForEach(classNames, className => this.ProcessClass(modules, assembly, className));
166+
Parallel.ForEach(classNames, c => this.ProcessClass(modules, assembly, c.Item1, c.Item2));
142167

143168
return assembly;
144169
}
@@ -149,15 +174,17 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
149174
/// <param name="modules">The modules.</param>
150175
/// <param name="assembly">The assembly.</param>
151176
/// <param name="className">Name of the class.</param>
152-
private void ProcessClass(XElement[] modules, Assembly assembly, string className)
177+
/// <param name="classDisplayName">Diesplay name of the class.</param>
178+
private void ProcessClass(XElement[] modules, Assembly assembly, string className, string classDisplayName)
153179
{
154180
var files = modules
155181
.Where(m => m.Attribute("name").Value.Equals(assembly.Name))
156182
.Elements("classes")
157183
.Elements("class")
158184
.Where(c => c.Attribute("name").Value.Equals(className)
159185
|| c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
160-
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal))
186+
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
187+
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))
161188
.Select(c => c.Attribute("filename").Value)
162189
.Distinct()
163190
.ToArray();
@@ -169,11 +196,11 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
169196
// If all files are removed by filters, then the whole class is omitted
170197
if ((files.Length == 0 && !this.FileFilter.HasCustomFilters) || filteredFiles.Length > 0)
171198
{
172-
var @class = new Class(className, assembly);
199+
var @class = new Class(classDisplayName, assembly);
173200

174201
foreach (var file in filteredFiles)
175202
{
176-
@class.AddFile(ProcessFile(modules, @class, file));
203+
@class.AddFile(ProcessFile(modules, @class, className, file));
177204
}
178205

179206
assembly.AddClass(@class);
@@ -185,18 +212,19 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
185212
/// </summary>
186213
/// <param name="modules">The modules.</param>
187214
/// <param name="class">The class.</param>
215+
/// <param name="className">Name of the class.</param>
188216
/// <param name="filePath">The file path.</param>
189217
/// <returns>The <see cref="CodeFile"/>.</returns>
190-
private static CodeFile ProcessFile(XElement[] modules, Class @class, string filePath)
218+
private static CodeFile ProcessFile(XElement[] modules, Class @class, string className, string filePath)
191219
{
192220
var classes = modules
193221
.Where(m => m.Attribute("name").Value.Equals(@class.Assembly.Name))
194222
.Elements("classes")
195223
.Elements("class")
196-
.Where(c => c.Attribute("name").Value.Equals(@class.Name)
197-
|| c.Attribute("name").Value.StartsWith(@class.Name + "$", StringComparison.Ordinal)
198-
|| c.Attribute("name").Value.StartsWith(@class.Name + "/", StringComparison.Ordinal)
199-
|| c.Attribute("name").Value.StartsWith(@class.Name + ".", StringComparison.Ordinal))
224+
.Where(c => c.Attribute("name").Value.Equals(className)
225+
|| c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
226+
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
227+
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))
200228
.Where(c => c.Attribute("filename").Value.Equals(filePath))
201229
.ToArray();
202230

@@ -365,7 +393,7 @@ private static void SetCodeElements(CodeFile codeFile, IEnumerable<XElement> met
365393

366394
codeFile.AddCodeElement(new CodeElement(
367395
methodName,
368-
CodeElementType.Method,
396+
methodName.StartsWith("get_") || methodName.StartsWith("set_") ? CodeElementType.Property : CodeElementType.Method,
369397
firstLine,
370398
lastLine,
371399
codeFile.CoverageQuota(firstLine, lastLine)));
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace Test
5+
{
6+
public class ClassWithLocalFunctions<T1>
7+
{
8+
public class MyNestedClass<T2>
9+
{
10+
public T2 MyProperty { get; set; }
11+
12+
public async Task MyAsyncMethod<T3>(T3 myParam)
13+
{
14+
await MyAsyncLocalFunction<T3>();
15+
16+
async Task MyAsyncLocalFunction<T4>()
17+
{
18+
Console.WriteLine(myParam);
19+
Console.WriteLine(MyProperty);
20+
21+
await Task.CompletedTask;
22+
}
23+
}
24+
}
25+
}
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Net.Http;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Test
6+
{
7+
public class GenericAsyncClass<T>
8+
{
9+
public async Task MyAsyncMethod()
10+
{
11+
}
12+
}
13+
}

src/Testprojects/CSharp/Project_DotNetCore/Test/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public static void Main(string[] args)
4242
catch (System.ArgumentException)
4343
{
4444
}
45+
46+
new GenericAsyncClass<object>().MyAsyncMethod().Wait();
47+
new ClassWithLocalFunctions<object>.MyNestedClass<object>().MyAsyncMethod<object>(null).GetAwaiter().GetResult();
4548
}
4649

4750
private static async void CallAsyncMethod()
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFramework>netcoreapp3.1</TargetFramework>
3+
<TargetFramework>net7.0</TargetFramework>
44
<IsPackable>false</IsPackable>
55
</PropertyGroup>
66
</Project>

0 commit comments

Comments
 (0)