Skip to content

Commit

Permalink
#663: Improved support for Cobertura files generated by "Microsoft Co…
Browse files Browse the repository at this point in the history
…deCoverage"
  • Loading branch information
danielpalme committed May 31, 2024
1 parent 5d8b6ea commit edb7491
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 18 deletions.
187 changes: 187 additions & 0 deletions src/ReportGenerator.Core.Test/Parser/ClassNameParserTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
using Palmmedia.ReportGenerator.Core.Parser;
using Xunit;

namespace Palmmedia.ReportGenerator.Core.Test.Parser
{
public class ClassNameParserTest
{
[Theory]
// RawMode
[InlineData(
"Test.ClassWithLocalFunctions`1/MyNestedClass`1/<>c__DisplayClass4_0`1/<<MyAsyncMethod>g__MyAsyncLocalFunction|0>d`1",
true,
"Test.ClassWithLocalFunctions`1/MyNestedClass`1/<>c__DisplayClass4_0`1/<<MyAsyncMethod>g__MyAsyncLocalFunction|0>d`1",
"Test.ClassWithLocalFunctions`1/MyNestedClass`1/<>c__DisplayClass4_0`1/<<MyAsyncMethod>g__MyAsyncLocalFunction|0>d`1",
true)]

// Coverlet
[InlineData(
"Test.AsyncClass/<SendAsync>d__0",
false,
"Test.AsyncClass",
"Test.AsyncClass",
true)]
[InlineData(
"Test.ClassWithLocalFunctions`1/MyNestedClass`1",
false,
"Test.ClassWithLocalFunctions`1",
"Test.ClassWithLocalFunctions`1",
true)]
[InlineData(
"Test.ClassWithLocalFunctions`1/MyNestedClass`1/<>c__DisplayClass4_0`1/<<MyAsyncMethod>g__MyAsyncLocalFunction|0>d`1",
false,
"Test.ClassWithLocalFunctions`1",
"Test.ClassWithLocalFunctions`1",
true)]
[InlineData(
"Test.ClassWithLocalFunctions`1/MyNestedClass`1/<MyAsyncMethod>d__4`1",
false,
"Test.ClassWithLocalFunctions`1",
"Test.ClassWithLocalFunctions`1",
true)]
[InlineData(
"Test.GenericAsyncClass`1/<MyAsyncMethod>d__0",
false,
"Test.GenericAsyncClass`1",
"Test.GenericAsyncClass`1",
true)]
[InlineData(
"Test.GenericClass`2",
false,
"Test.GenericClass`2",
"Test.GenericClass`2",
true)]
[InlineData(
"Test.Program/EchoHandler",
false,
"Test.Program",
"Test.Program",
true)]
[InlineData(
"Test.Program/<CallAsyncMethod>d__1",
false,
"Test.Program",
"Test.Program",
true)]
[InlineData(
"Test.TestClass/NestedClass",
false,
"Test.TestClass",
"Test.TestClass",
true)]

// DotNet coverage
[InlineData(
"Test.AsyncClass.<SendAsync>d__0",
false,
"Test.AsyncClass",
"Test.AsyncClass",
true)]
[InlineData(
"Test.GenericAsyncClass.<MyAsyncMethod>d__0<T>",
false,
"Test.GenericAsyncClass",
"Test.GenericAsyncClass<T>",
true)]
[InlineData(
"Test.Program.<CallAsyncMethod>d__1",
false,
"Test.Program",
"Test.Program",
true)]
[InlineData(
"Test.TestClass.<>c",
false,
"Test.TestClass",
"Test.TestClass",
true)]
[InlineData(
"Test.TestClass2.<>c__DisplayClass14_0",
false,
"Test.TestClass2",
"Test.TestClass2",
true)]
[InlineData(
"Test.ClassWithLocalFunctions.MyNestedClass.<MyAsyncMethod>d__4<T1, T2, T3>",
false,
"Test.ClassWithLocalFunctions.MyNestedClass",
"Test.ClassWithLocalFunctions.MyNestedClass<T1, T2, T3>",
true)]
[InlineData(
"Test.ClassWithLocalFunctions.MyNestedClass.<>c__DisplayClass4_0.<<MyAsyncMethod>g__MyAsyncLocalFunction|0>d<T1, T2, T3, T4>",
false,
"Test.ClassWithLocalFunctions.MyNestedClass",
"Test.ClassWithLocalFunctions.MyNestedClass<T1, T2, T3, T4>",
true)]
[InlineData(
"Test.AbstractGenericClass<TModel, TState>",
false,
"Test.AbstractGenericClass<TModel, TState>",
"Test.AbstractGenericClass<TModel, TState>",
true)]
[InlineData(
"Test.AnalyzerTestClass",
false,
"Test.AnalyzerTestClass",
"Test.AnalyzerTestClass",
true)]
[InlineData(
"Test.ClassWithExcludes",
false,
"Test.ClassWithExcludes",
"Test.ClassWithExcludes",
true)]
[InlineData(
"Test.ClassWithLocalFunctions.MyNestedClass<T1, T2>",
false,
"Test.ClassWithLocalFunctions.MyNestedClass<T1, T2>",
"Test.ClassWithLocalFunctions.MyNestedClass<T1, T2>",
true)]
[InlineData(
"Test.GenericClass<TModel, TState>",
false,
"Test.GenericClass<TModel, TState>",
"Test.GenericClass<TModel, TState>",
true)]
[InlineData(
"Test.Program",
false,
"Test.Program",
"Test.Program",
true)]
[InlineData(
"Test.Program.EchoHandler",
false,
"Test.Program.EchoHandler",
"Test.Program.EchoHandler",
true)]
[InlineData(
"Test.TestClass",
false,
"Test.TestClass",
"Test.TestClass",
true)]
[InlineData(
"Test.TestClass.NestedClass",
false,
"Test.TestClass.NestedClass",
"Test.TestClass.NestedClass",
true)]
[InlineData(
"Test.TestClass2",
false,
"Test.TestClass2",
"Test.TestClass2",
true)]
public void ParseClassName(string rawName, bool rawMode, string expectedName,
string expectedDisplayName, bool expectedInclude)
{
var result = ClassNameParser.ParseClassName(rawName, rawMode);

Assert.Equal(expectedName, result.Name);
Assert.Equal(expectedDisplayName, result.DisplayName);
Assert.Equal(rawName, result.RawName);
Assert.Equal(expectedInclude, result.Include);
}
}
}
42 changes: 24 additions & 18 deletions src/ReportGenerator.Core/Parser/ClassNameParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@ namespace Palmmedia.ReportGenerator.Core.Parser
internal static class ClassNameParser
{
/// <summary>
/// Regex to analyze if a class name represents a generic class.
/// Regex to clean class names from compiler generated parts.
/// </summary>
private static readonly Regex GenericClassRegex = new Regex("<.*>$", RegexOptions.Compiled);
private static readonly Regex CleanupRegex = new Regex(".<.*>\\w_?_?\\w*\\d*", 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"'.
/// Regex to analyze if a class name represents a generic class.
/// </summary>
private static readonly Regex AsyncClassRegex = new Regex("^(?<ClassName>.+)\\.<.*>.*__(?:.+(?<GenericTypes><.+>))?", RegexOptions.Compiled);
private static readonly Regex GenericClassRegex = new Regex("(?<ClassName>.+)(?<GenericTypes><.+>)$", RegexOptions.Compiled);

/// <summary>
/// Parses the class name and extracts generic type information.
/// </summary>
/// <param name="rawName">The raw/full name.</param>
/// <param name="rawMode">Indicates whether class names are interpreted (false) or not (true)</param>
/// <param name="rawMode">Indicates whether class names are interpreted (false) or not (true).</param>
/// <returns>The parser result.</returns>
public static ClassNameParserResult ParseClassName(string rawName, bool rawMode)
{
Expand All @@ -47,13 +46,14 @@ public static ClassNameParserResult ParseClassName(string rawName, bool rawMode)

if (rawName.Contains("<"))
{
if (rawName.Contains("__")
|| rawName.Contains(".<>"))
string cleanedClassName = CleanupRegex.Replace(rawName, string.Empty);

if (cleanedClassName.Equals(rawName))
{
return new ClassNameParserResult(string.Empty, string.Empty, rawName, false);
return new ClassNameParserResult(rawName, rawName, rawName, IncludeClass(rawName));
}

var match = AsyncClassRegex.Match(rawName);
var match = GenericClassRegex.Match(cleanedClassName);

if (match.Success)
{
Expand All @@ -63,21 +63,27 @@ public static ClassNameParserResult ParseClassName(string rawName, bool rawMode)
rawName,
IncludeClass(match.Groups["ClassName"].Value));
}
else
{
return new ClassNameParserResult(
cleanedClassName,
cleanedClassName,
rawName,
IncludeClass(cleanedClassName));
}
}

return new ClassNameParserResult(rawName, rawName, rawName, IncludeClass(rawName));
}

/// <summary>
/// Determines whether the given class name should be included in the report.
/// </summary>
/// <param name="name">The name of the class.</param>
/// <returns>True if the class should be included; otherwise, false.</returns>
private static bool IncludeClass(string name)
{
if (name.Contains("$"))
{
return false;
}
else
{
return !name.Contains("<") || GenericClassRegex.IsMatch(name);
}
return !name.Contains("$");
}
}
}

0 comments on commit edb7491

Please sign in to comment.