coverlet-coverage / coverlet

Cross platform code coverage for .NET
MIT License
2.99k stars 385 forks source link

Closing brace is missed when writing PowerShell cmdlets and calling WriteError #1183

Closed fgimian closed 5 months ago

fgimian commented 3 years ago

Hey there, firstly thank you so much for this incredible tool! 😄

When writing PowerShell cmdlets in C#, it is common to run the inherited WriteError to write non-terminating errors. Sadly it seems that Coverlet does not detect the closing brace in this scenario.

Here's a simply demo example:

using System;
using System.Management.Automation;

namespace Lunette.Cmdlets.Utilities
{
    [Cmdlet(VerbsCommon.Get, "SpecialFolderPath")]
    [OutputType(typeof(string))]
    public class GetSpecialFolderPathCommand : Cmdlet
    {
        [Parameter(Position = 0, Mandatory = true)]
        public string Name { get; set; }

        protected override void ProcessRecord()
        {
            if (Name == "BAD")
            {
                WriteError(new ErrorRecord(
                    new ArgumentException("oh no"),
                    "ItemNotFoundException",
                    ErrorCategory.InvalidArgument,
                    "Hello"));
            }
            else
            {
                WriteObject("hello");
            }
        }
    }
}

And here are the tests:

using System;
using System.Linq;
using Lunette.Cmdlets.Utilities;
using Xunit;

namespace Lunette.Tests.Cmdlets.Utilities
{
    public class GetSpecialFolderPathCommandTests
    {
        [Fact]
        public void Invoke_ValidSpecialFolder_ShouldReturnPath()
        {
            // Arrange
            var cmdlet = new GetSpecialFolderPathCommand()
            {
                Name = "GOOD"
            };

            // Act
            var results = cmdlet.Invoke().OfType<string>().ToList();

            // Assert
            Assert.Equal("hello", results[0]);
        }

        [Fact]
        public void Invoke_InvalidSpecialFolder_ShouldError()
        {
            // Arrange
            var cmdlet = new GetSpecialFolderPathCommand()
            {
                Name = "BAD"
            };

            // Act & Assert
            var exception = Assert.Throws<ArgumentException>(
                () => cmdlet.Invoke().GetEnumerator().MoveNext());
            Assert.Equal("oh no", exception.Message);
        }
    }
}

And as per the coverage report, the closing brace on line 22 is not covered:

image

Any ideas if there's a way to work through this?

Huge thanks! Fotis

fgimian commented 3 years ago

P.S.: I am using a Debug build and have also tried to execute cmdlet.Invoke().OfType<string>().ToList() in the second test to be sure that the enumerator is fully exhausted, but sadly I see the same result.

MarcoRossignoli commented 3 years ago

Which version are you using?Can you try with nightly? https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/ConsumeNightlyBuild.md

fgimian commented 3 years ago

Which version are you using?Can you try with nightly? https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/ConsumeNightlyBuild.md

Thanks so much for the reply Marco. I'm using the latest release available on nuget which is 3.0.3. I'll try the nightly as you recommended and let you know how I go. 😄

fgimian commented 3 years ago

I just attempted this with the latest nightly 3.0.4-preview.32.gdc6edb1dd7 but sadly the problem persists. Would it assist you if I produced a complete minimal project / solution that demonstrated the problem so you can reproduce it?

P.S.: I'm not certain if this is relevant, but I am developing on .NET Core 3.1 as I am targeting PowerShell 7.0.x right now.

fgimian commented 3 years ago

Here's a fully setup solution with related library and test projects.

CoverletProblem.zip

I then invoke tests and Coverlet using:

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput='../lcov.info'

You will see that line 22 is not covered in the lcov output:

sls ",0$" .\lcov.info

I used the latest nightly build in this solution along with .NET 5.0 to rule out a specific issue with .NET Core 3.1.

Please let me know if I can provide any further info to help 😄

Thanks so much! Fotis

MarcoRossignoli commented 3 years ago

Thanks for the repro!

fgimian commented 3 years ago

Hey there Marco, I've done a lot more digging and believe that I can explain what's going on a lot better. I also think I have a better solution for testing cmdlets which I'll post soon.

Ultimately, this is relatively vague territory as Microsoft don't provide a good guide for testing cmdlets written in C# using XUnit so one is left to dig deeper into the PowerShell source to figure out what's going on.

The summary is as follows:

I'll provide further information soon, but just thought I'd share an update with you in the meantime.

Huge thanks Fotis

fgimian commented 3 years ago

Good news, as I suspected, I can reproduce the problem with a more succinct example which I've attached below:

CoverletProblem-v2.zip

The summary of the problem is that Coverlet will detect a closing brace if the method being called throws an exception. However, it will not detect the closing brace if the method calls another method, and the other method throws the exception.

e.g.

if (name == "BAD")
{
    // PROBLEM HERE: If WriteError throws an exception, the closing brace below 
    // won't be counted by Coverlet.
    _handler.WriteError();
}
else
{
    // OK: Coverlet will cover the closing brace if the method directly throws the exception.
    throw new ArgumentException("oh no");
}

I'm assuming this would be extremely hard to cater for, but perhaps you have an idea how to solve it?

Thanks again for all your help and time! Fotis

fgimian commented 3 years ago

And one final update. I also tested this with OpenCover and it had the same problem as Coverlet.

Instructions for doing this via OpenCover (for your reference) are as follows:

# The following instructions assume you've installed OpenCover using the MSI installer
# provided at https://github.com/OpenCover/opencover/releases

# Create a directory for coverage reports
mkdir .coverage

# Run tests using OpenCover
~\AppData\Local\Apps\OpenCover\OpenCover.Console.exe `
    -target:"dotnet.exe" `
    -targetargs:"test" `
    -output:".coverage\coverage.xml" `
    -register:user -filter:"+[CoverletProblem]*"

# Install ReportGenerator globally
dotnet tool install -g dotnet-reportgenerator-globaltool

# Generate the coverage HTML report
ReportGenerator.exe -reports:".coverage\coverage.xml" -targetdir:".coverage"

# View in your browser
ii .coverage\index.html

Cheers Fotis

MarcoRossignoli commented 3 years ago

Thanks for all informations and for the repro!

fgimian commented 3 years ago

In case it's useful to anyone, I've written a more detailed blog post on the approach I've used to overcome this while developing PowerShell cmdlets in C#.

github-actions[bot] commented 1 year ago

This issue is stale because it has been open for 3 months with no activity.

github-actions[bot] commented 5 months ago

This issue was closed because it has been inactive for 9 months since being marked as stale.