perltestingcode-coveragedevel-cover

Why does Perl's Devel::Cover think some branches and conditions are not covered?


I have this function which takes an array, counts how often each item occurs, and returns an array of unique items, ordered by count first, then alphabetically sorted, and then alphabetically case-insensitive so that order does not change between runs.

use strict;
use warnings;

sub sorted {
    my @elements = @_;

    my %counts;
    foreach my $e (@elements) {
        $counts{$e}++;
    }

    my @sorted = sort { 
        $counts{$b} <=> $counts{$a} or $a cmp $b or lc $a cmp lc $b
    } keys %counts;

    return @sorted;
}

1;

I have this test case for it, and everything works fine:

use strict;
use warnings;
use Test::More;
use module;

is_deeply(['A', 'a', 'c', 'b'], [sorted('a', 'b', 'c', 'a', 'c', 'A', 'A')]);
done_testing();

I run it and use Devel::Cover to collect test coverage numbers. I expected 100% coverage, but branch and condition coverage is short:

HARNESS_PERL_SWITCHES=-MDevel::Cover prove -I. test.t && cover
test.t .. ok   
All tests successful.
Files=1, Tests=1,  1 wallclock secs ( 0.03 usr  0.00 sys +  0.22 cusr  0.02 csys =  0.27 CPU)
Result: PASS
Reading database from ./cover_db


--------- ------ ------ ------ ------ ------ ------ ------
File        stmt   bran   cond    sub    pod   time  total
--------- ------ ------ ------ ------ ------ ------ ------
module.pm  100.0   50.0   66.6  100.0    n/a    0.2   90.4
test.t     100.0    n/a    n/a  100.0    n/a   99.8  100.0
Total      100.0   50.0   66.6  100.0    n/a  100.0   94.8
--------- ------ ------ ------ ------ ------ ------ ------

Checking the HTML report, it shows that some branches and conditions are not covered:

branch coverage

condition coverage

I don't understand why Devel::Cover thinks some branches and conditions were not covered.

It complains that the branch for T F is not covered, which would be the <=> part never being true? I have both 'a' and 'c' twice, so that <=> should return zero for that (F) and non-zero (T) when it compares the 'a' count (2) to the 'b' count (1).

For condition coverage, the report says that the case that both parts of the check are false is not covered. Again, I think I should have that covered because I have both equal counts and equal names.

What test case would I need to add to get 100% branch and condition coverage?

Or, if sort functions like this are tricky for Devel::Cover, how can I tell it to ignore these? I changed the code to

    my @sorted = sort {
        # uncoverable branch left
        # uncoverable condition true
        $counts{$b} <=> $counts{$a} or $a cmp $b or lc $a cmp lc $b
    } keys %counts;

but that did get the same results.


Solution

  • The problem for the coverage is that it never has both the same count of the two items it compares (the <=> condition to be 0) and that they are the same (the first cmp condition to be 0).

    For that we'd need to be comparing an element to itself, but the sorting routine works with keys from the frequency count, not with array elements -- so there is no two of any one element! So an element is never compared to itself and the first two conditons can never both fail.

    One resolution: sort by the actual elements, then select unique ones.

    As for the branch failure, I can't quite pin it down right now but a practical (working) solution is to disengage those tests. Altogether

    package TestMod;
    
    use strict;
    use warnings;    
    use List::Util qw(uniq);
    
    sub sorted {
        my @elements = @_;
    
        my %counts;
        foreach my $e (@elements) {
            $counts{$e}++;
        }
    
        my @sorted = sort { 
            my $cmp;
    
            if ( my $nc = $counts{$b} <=> $counts{$a} ) {
                $cmp = $nc
            }
            elsif ( my $ac = $a cmp $b ) {
                $cmp = $ac
            }
            else { $cmp = lc $a cmp lc $b }
    
            $cmp;
        } @elements;
    
        return uniq @sorted;
    }
    
    1;
    

    Now I get

    main_TestMod.pl .. ok   
    All tests successful.
    Files=1, Tests=1,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.29 cusr  0.02 csys =  0.34 CPU)
    Result: PASS
    Reading database from .../test_coverage/cover_db
    
    
    --------------- ------ ------ ------ ------ ------ ------ ------
    File              stmt   bran   cond    sub    pod   time  total
    --------------- ------ ------ ------ ------ ------ ------ ------
    TestMod.pm       100.0  100.0    n/a  100.0    0.0    2.5   97.3
    main_TestMod.pl  100.0    n/a    n/a  100.0    n/a   97.4  100.0
    Total            100.0  100.0    n/a  100.0    0.0  100.0   98.3
    --------------- ------ ------ ------ ------ ------ ------ ------
    
    
    HTML output written to .../test_coverage/cover_db/coverage.html
    done.
    

    (actual path on my system suppressed)

    Note -- no condition at all now. FWIW: when I leave that one big, typical sort-ish multi-or-ed condition (while sorting elements, not frequency-hash keys) then the condition does have 100% coverage. But the branch fails.


    In this case of straight consecutive tests and no other processing we can also return in each branch (the block in sort is an anonymous sub and one can return)

    package TestMod;
    
    use strict;
    use warnings;    
    use List::Util qw(uniq);
    
    sub sorted {
        my @elements = @_;
    
        my %counts;
        foreach my $e (@elements) {
            $counts{$e}++;
        }
    
        my @sorted = sort { 
            if ( my $nc = $counts{$b} <=> $counts{$a} ) {
                return $nc
            }
            elsif ( my $ac = $a cmp $b ) {
                return $ac
            }
            else { 
                return lc $a cmp lc $b 
            }
        } @elements;
    
        return uniq @sorted;
    }
    
    1;