Skip to content

Commit

Permalink
#590, 591: Improved support for Cobertura files generated with 'dotne…
Browse files Browse the repository at this point in the history
…t test --collect "Code Coverage;Format=Cobertura"'
  • Loading branch information
danielpalme committed Feb 25, 2023
1 parent f396a74 commit 3364c96
Show file tree
Hide file tree
Showing 15 changed files with 1,624 additions and 559 deletions.
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.103",
"version": "7.0.200",
"rollForward": "latestMajor"
}
}
5 changes: 5 additions & 0 deletions src/Readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ For further details take a look at LICENSE.txt.

CHANGELOG

5.1.18.0

* Fix: #590, 591: Improved support for Cobertura files generated with
'dotnet test --collect "Code Coverage;Format=Cobertura"'

5.1.17.0

* Fix: #536: Improved support for C code in Visual Studio coverage input format (*.coveragexml)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"ReportGenerator.Console.NetCore": {
"commandName": "Project",
"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"
"commandLineArgs": "-reports:C:\\Users\\danie\\Documents\\Projects\\ReportGenerator\\src\\Testprojects\\CSharp\\Reports\\Cobertura_dotnettest.xml -targetdir:C:\\Users\\danie\\Desktop\\dotnet_2 -reporttypes:Html"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void GetFiles_EmptyDirectory_NoFilesFound()
public void GetFiles_SingleDirectory_XmlFilesFound()
{
var files = GlobbingFileSearch.GetFiles(Path.Combine(FileManager.GetCSharpReportDirectory(), "*.xml")).ToArray();
Assert.Equal(22, files.Length);
Assert.Equal(23, files.Length);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public void GetFiles_EmptyDirectory_NoFilesFound()
public void GetFiles_SingleDirectory_XmlFilesFound()
{
var files = WildCardFileSearch.GetFiles(Path.Combine(FileManager.GetCSharpReportDirectory(), "*.xml")).ToArray();
Assert.Equal(22, files.Length);
Assert.Equal(23, files.Length);
}

[Fact]
Expand Down
62 changes: 45 additions & 17 deletions src/ReportGenerator.Core/Parser/CoberturaParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ internal class CoberturaParser : ParserBase
/// </summary>
private static readonly Regex GenericClassRegex = new Regex("<.*>$", RegexOptions.Compiled);

/// <summary>
/// Regex to analyze if a class name represents an async (generic) class.
/// Format gets generated by 'dotnet test --collect "Code Coverage;Format=Cobertura"'.
/// </summary>
private static readonly Regex AsyncClassRegex = new Regex("^(?<ClassName>.+)\\.<.*>.*__(?:.+(?<GenericTypes><.+>))?", RegexOptions.Compiled);

/// <summary>
/// Regex to analyze if a method name belongs to a lamda expression.
/// </summary>
Expand All @@ -36,7 +42,7 @@ internal class CoberturaParser : ParserBase
/// <summary>
/// Regex to analyze if a method name is generated by compiler.
/// </summary>
private static readonly Regex CompilerGeneratedMethodNameRegex = new Regex(@"(?<ClassName>.+)/<(?<CompilerGeneratedName>.+)>.+__.+MoveNext\(\)$", RegexOptions.Compiled);
private static readonly Regex CompilerGeneratedMethodNameRegex = new Regex(@"(?<ClassName>.+)(/|\.)<(?<CompilerGeneratedName>.+)>.+__.+MoveNext\(\)$", RegexOptions.Compiled);

/// <summary>
/// Regex to analyze the branch coverage of a line element.
Expand Down Expand Up @@ -127,18 +133,37 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
{
string fullname = c.Attribute("name").Value;
int nestedClassSeparatorIndex = fullname.IndexOf('/');
return nestedClassSeparatorIndex > -1 ? fullname.Substring(0, nestedClassSeparatorIndex) : fullname;
if (nestedClassSeparatorIndex > -1)
{
string className = fullname.Substring(0, nestedClassSeparatorIndex);
return Tuple.Create(className, className);
}
if (fullname.Contains("<"))
{
var match = AsyncClassRegex.Match(fullname);
if (match.Success)
{
return Tuple.Create(
match.Groups["ClassName"].Value,
match.Groups["ClassName"].Value + match.Groups["GenericTypes"].Value);
}
}
return Tuple.Create(fullname, fullname);
})
.Where(name => !name.Contains("$")
&& (!name.Contains("<") || GenericClassRegex.IsMatch(name)))
.Where(c => !c.Item1.Contains("$")
&& (!c.Item1.Contains("<") || GenericClassRegex.IsMatch(c.Item1)))
.Distinct()
.Where(c => this.ClassFilter.IsElementIncludedInReport(c))
.OrderBy(name => name)
.Where(c => this.ClassFilter.IsElementIncludedInReport(c.Item1))
.OrderBy(c => c.Item1)
.ToArray();

var assembly = new Assembly(assemblyName);

Parallel.ForEach(classNames, className => this.ProcessClass(modules, assembly, className));
Parallel.ForEach(classNames, c => this.ProcessClass(modules, assembly, c.Item1, c.Item2));

return assembly;
}
Expand All @@ -149,15 +174,17 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
/// <param name="modules">The modules.</param>
/// <param name="assembly">The assembly.</param>
/// <param name="className">Name of the class.</param>
private void ProcessClass(XElement[] modules, Assembly assembly, string className)
/// <param name="classDisplayName">Diesplay name of the class.</param>
private void ProcessClass(XElement[] modules, Assembly assembly, string className, string classDisplayName)
{
var files = modules
.Where(m => m.Attribute("name").Value.Equals(assembly.Name))
.Elements("classes")
.Elements("class")
.Where(c => c.Attribute("name").Value.Equals(className)
|| c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal))
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))
.Select(c => c.Attribute("filename").Value)
.Distinct()
.ToArray();
Expand All @@ -169,11 +196,11 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
// If all files are removed by filters, then the whole class is omitted
if ((files.Length == 0 && !this.FileFilter.HasCustomFilters) || filteredFiles.Length > 0)
{
var @class = new Class(className, assembly);
var @class = new Class(classDisplayName, assembly);

foreach (var file in filteredFiles)
{
@class.AddFile(ProcessFile(modules, @class, file));
@class.AddFile(ProcessFile(modules, @class, className, file));
}

assembly.AddClass(@class);
Expand All @@ -185,18 +212,19 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
/// </summary>
/// <param name="modules">The modules.</param>
/// <param name="class">The class.</param>
/// <param name="className">Name of the class.</param>
/// <param name="filePath">The file path.</param>
/// <returns>The <see cref="CodeFile"/>.</returns>
private static CodeFile ProcessFile(XElement[] modules, Class @class, string filePath)
private static CodeFile ProcessFile(XElement[] modules, Class @class, string className, string filePath)
{
var classes = modules
.Where(m => m.Attribute("name").Value.Equals(@class.Assembly.Name))
.Elements("classes")
.Elements("class")
.Where(c => c.Attribute("name").Value.Equals(@class.Name)
|| c.Attribute("name").Value.StartsWith(@class.Name + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(@class.Name + "/", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(@class.Name + ".", StringComparison.Ordinal))
.Where(c => c.Attribute("name").Value.Equals(className)
|| c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))
.Where(c => c.Attribute("filename").Value.Equals(filePath))
.ToArray();

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

codeFile.AddCodeElement(new CodeElement(
methodName,
CodeElementType.Method,
methodName.StartsWith("get_") || methodName.StartsWith("set_") ? CodeElementType.Property : CodeElementType.Method,
firstLine,
lastLine,
codeFile.CoverageQuota(firstLine, lastLine)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Threading.Tasks;

namespace Test
{
public class ClassWithLocalFunctions<T1>
{
public class MyNestedClass<T2>
{
public T2 MyProperty { get; set; }

public async Task MyAsyncMethod<T3>(T3 myParam)
{
await MyAsyncLocalFunction<T3>();

async Task MyAsyncLocalFunction<T4>()
{
Console.WriteLine(myParam);
Console.WriteLine(MyProperty);

await Task.CompletedTask;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
public class GenericAsyncClass<T>
{
public async Task MyAsyncMethod()
{
}
}
}
3 changes: 3 additions & 0 deletions src/Testprojects/CSharp/Project_DotNetCore/Test/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public static void Main(string[] args)
catch (System.ArgumentException)
{
}

new GenericAsyncClass<object>().MyAsyncMethod().Wait();
new ClassWithLocalFunctions<object>.MyNestedClass<object>().MyAsyncMethod<object>(null).GetAwaiter().GetResult();
}

private static async void CallAsyncMethod()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="altcover" Version="8.2.833" />
<PackageReference Include="coverlet.msbuild" Version="3.1.0">
<PackageReference Include="altcover" Version="8.6.14" />
<PackageReference Include="coverlet.msbuild" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="Microsoft.CodeCoverage" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down

0 comments on commit 3364c96

Please sign in to comment.