perlcode-coveragedevel-cover

Odd Devel::Cover "Condition" Coverage with Or (||) & Defined-Or (//) Operators


I was writing some unit tests and decided to give Devel::Cover a try. I'm seeing something that I can't explain when looking at the conditional coverage report, involving use of the Or (||) and Defined-Or (//) operators.

This is the base code that's working as expected:

use strict;
use warnings;

use 5.026;

my($DEF_MIN) = 1;
our($DEF_MAX) = 10;

sub test
{
    my($hashref) = @_;
    $hashref ||= {};

    my($min) = $hashref->{'min'} || 1;
    my($max) = $hashref->{'max'} // 10;

    say qq(MIN: $min / MAX: $max);

    return;
}

test();
test({});
test({ min => 14 });
test({ max => 14 });
test({ min => 10, max => 14 });

When I run that I get the following Condition report:

expected results

Pretty much what I expected.

If I make these change:

my($min) = $hashref->{'min'} || $DEF_MIN;
my($max) = $hashref->{'max'} // $DEF_MAX;

then I get this report, instead:

unexpected results

Why the change from just column A to columns A and B? How can I complete the coverage, or mark it as uncoverable?

As can be seen, it doesn't matter whether I use || or //, and it doesn't matter whether the right hand side is a lexical or a package variable.


Solution

  • The following defines the operation of ||:

    A B Result
    A is false Any value B
    A is true Not evaluated A

    Your test covered both of these paths. Under this model, you'd have full coverage. However, Devel::Cover uses a different model.

    A B Result
    A is false B is false B
    A is false B is true B
    A is true Not evaluated A

    It only considers the expression "fully covered" if the all three of these paths were visited during the test. This makes sense in a boolean context, but the || is used outside of boolean context here.

    Since you didn't run your code with a false value for $DEF_MIN, the first path wasn't visited, and it deems the coverage incomplete.

    When the operand is a literal, it rules out the path where it would expect a true literal to be false as impossible to cover, and removes it. (Same for when it expects a false literal to be true.) That's why there's one less column when using a literal.