danielpalme / ReportGenerator

ReportGenerator converts coverage reports generated by coverlet, OpenCover, dotCover, Visual Studio, NCover, Cobertura, JaCoCo, Clover, gcov or lcov into human readable reports in various formats.
https://reportgenerator.io
Apache License 2.0
2.62k stars 283 forks source link

Static generic methods causing duplicate class record with Microsoft.CodeCoverage in cobertura format #689

Closed JohannesMoersch closed 2 months ago

JohannesMoersch commented 2 months ago

Describe the bug Related to #663 but hopefully this specific scenario is solvable. I have a class with a generic static method inside and this is causing a duplicate record for the parent class to be included. There is one with the generic arguments, and one without. Both records include the code coverage information for the complete class, so it's throwing the overall numbers out. The compiler seems to be generated a nested generic class under the hood to contain the generic method, so I'm guessing this is the source of the issue. The name of the nested class in my case looks like this: MyNamespace.GenerateCustomerMapper.<>c__DisplayClass2_0<TEnum>

To Reproduce This is a simplified version of the code causing the problem.

I'm using dotnet-coverage version 17.11.5 and report generator version 5.3.8. I'm outputting cobertura from dotnet-coverage and inputting that without modification into the report generator.

Write a test that calls TestMethod<object>().

public static class TestClass
{
    public static object? TestMethod<T>()
    {
        return default(T);
    }
}

This is the result I'm getting: image

Thanks for all the work you do!

danielpalme commented 2 months ago

Will have a look in next 1-2 weeks.

danielpalme commented 2 months ago

I tried to reproduce your issue.

I created the following class:

namespace Test
{
    public static class TestClass
    {
        public static object? TestMethod<T>()
        {
            return default(T);
        }
    }
}

Then I executed the following commands:

dotnet tool install --global dotnet-coverage
dotnet-coverage collect -f cobertura "dotnet test CSharp\Project_DotNetCore\UnitTests\UnitTests.csproj"

reportgenerator -reports:output.cobertura.xml -targetdir:issue689 -reporttypes:Html;Cobertura

The resulting Cobertura generated by ReportGenerator file has the following content:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage line-rate="1" branch-rate="1" lines-covered="3" lines-valid="3" branches-covered="0" branches-valid="0" complexity="1" version="0" timestamp="1724011502">
  <sources />
  <packages>
    <package name="UnitTests" line-rate="1" branch-rate="1" complexity="1">
      <classes>
        <class name="Test.TestClass" filename="C:\temp\TestClass.cs" line-rate="1" branch-rate="1" complexity="1">
          <methods>
            <method name="TestMethod&lt;T&gt;" signature="()" line-rate="1" branch-rate="1" complexity="1">
              <lines>
                <line number="6" hits="1" branch="false" />
                <line number="7" hits="1" branch="false" />
                <line number="8" hits="1" branch="false" />
              </lines>
            </method>
          </methods>
          <lines>
            <line number="6" hits="1" branch="false" />
            <line number="7" hits="1" branch="false" />
            <line number="8" hits="1" branch="false" />
          </lines>
        </class>
      </classes>
    </package>
  </packages>
</coverage>

That looks fine for me.

Are you doing something differently? Can you share a coverage file that's causing problems? You can send it privately via email: reportgenerator@palmmedia.de or share it here.

JohannesMoersch commented 2 months ago

Hi, sorry! I made some bad assumptions about the exact origin of the problem, and turns out it's a little more complicated than I thought.

Running this test will produce the scenario I mentioned:

public enum TestEnum
{
    First_0
}

public class TestClass
{
    public static string? Convert<TEnum>(int index) where TEnum : struct
        => Enum.GetNames(typeof(TEnum)).FirstOrDefault(x => x.EndsWith($"_{index}"));

    [Fact]
    public void Test() => Convert<TestEnum>(0);
}

Looks like the additional class that is appearing in the output is generated by the C# compiler and contains the inline lambda expression from the FirstOrDefault method call.

danielpalme commented 2 months ago

Thanks for the sample code. This is the same issue as #663

I wrote a detailed answer here, explaining why I can't fix this issue without causing other issues: