File Coverage

File:blib/lib/App/Test/Generator/LCSAJ/Coverage.pm
Coverage:96.4%

linestmtbrancondsubtimecode
1package App::Test::Generator::LCSAJ::Coverage;
2
3
2
2
2
74904
2
25
use strict;
4
2
2
2
3
2
41
use warnings;
5
6
2
2
2
348
11530
3
use autodie qw(:all);
7
2
2
2
14516
2
39
use Carp qw(croak);
8
2
2
2
5
1
351
use JSON::MaybeXS;
9
10our $VERSION = '0.36';
11
12 - 90
=head1 NAME

App::Test::Generator::LCSAJ::Coverage - Merge LCSAJ path data with runtime hits

=head1 VERSION

Version 0.36

=head1 DESCRIPTION

Merges static LCSAJ path data produced by L<App::Test::Generator::LCSAJ>
with runtime line hit data to determine which LCSAJ paths were covered
during test execution. The merged result is written as JSON for
consumption by C<bin/test-generator-index>.

=head2 merge

Merge a static LCSAJ path JSON file with a runtime line hits JSON file
and write the annotated result to an output file.

    App::Test::Generator::LCSAJ::Coverage::merge(
        'cover_db/lcsaj/MyModule.pm.lcsaj.json',
        'cover_db/lcsaj/MyModule.pm.hits.json',
        'cover_db/lcsaj/MyModule.pm.covered.json',
    );

=head3 Arguments

=over 4

=item * C<$lcsaj_file>

Path to the C<.lcsaj.json> file produced by
L<App::Test::Generator::LCSAJ>. Required.

=item * C<$hits_file>

Path to a JSON file mapping line numbers (as strings) to hit counts,
as produced by L<Devel::App::Test::Generator::LCSAJ::Runtime>.
Required.

=item * C<$out_file>

Path to write the merged output JSON file. Required.

=back

=head3 Returns

Nothing. Writes the annotated LCSAJ path data to C<$out_file>, with
a C<covered> key added to each path record.

=head3 Side effects

Writes to C<$out_file>. Croaks if any file cannot be read or written.

=head3 Notes

A path is considered covered if any line in the range C<start..end>
was executed at least once. This is a conservative approximation —
it does not verify that the jump target was actually reached. As a
result, coverage may be slightly overstated for paths where only the
beginning of the sequence was executed.

=head3 API specification

=head4 input

    {
        lcsaj_file => { type => SCALAR },
        hits_file  => { type => SCALAR },
        out_file   => { type => SCALAR },
    }

=head4 output

    { type => UNDEF }

=cut
91
92sub merge {
93
10
99002
        my ($lcsaj_file, $hits_file, $out_file) = @_;
94
95        # Validate all three file arguments before attempting any IO
96
10
26
        croak 'lcsaj_file required' unless defined $lcsaj_file;
97
9
13
        croak 'hits_file required'  unless defined $hits_file;
98
8
12
        croak 'out_file required'   unless defined $out_file;
99
100        # Load static LCSAJ path data extracted by App::Test::Generator::LCSAJ
101
7
10
        my $paths = decode_json(_slurp($lcsaj_file));
102
103        # Load runtime line hit counts from Devel::App::Test::Generator::LCSAJ::Runtime
104
7
10
        my $hits  = decode_json(_slurp($hits_file));
105
106        # Annotate each path with a covered flag — a path is considered
107        # covered if any line in the start..end range was executed
108
7
7
6
9
        for my $path (@{$paths}) {
109
8
8
                my $covered = 0;
110
111
8
12
                for my $line ($path->{start} .. $path->{end}) {
112
25
25
                        if($hits->{$line}) {
113
4
4
                                $covered = 1;
114
4
3
                                last;
115                        }
116                }
117
118
8
10
                $path->{covered} = $covered;
119        }
120
121        # Write the annotated paths to the output file
122
7
8
        open my $fh, '>', $out_file or croak "Cannot write coverage output to $out_file: $!";
123
7
501
        print $fh encode_json($paths);
124
7
13
        close $fh;
125}
126
127# --------------------------------------------------
128# _slurp
129#
130# Purpose:    Read the entire contents of a file and
131#             return it as a string.
132#
133# Entry:      $file - path to the file to read.
134#
135# Exit:       Returns the file contents as a scalar
136#             string. Croaks if the file cannot be
137#             opened.
138#
139# Side effects: None beyond opening and closing the
140#               file handle.
141#
142# Notes:      Uses three-argument open for safety with
143#             filenames containing special characters.
144#             Sets $/ to undef to slurp the whole file
145#             in one read, localised to avoid affecting
146#             other code.
147# --------------------------------------------------
148sub _slurp {
149
14
15
        my $file = $_[0];
150
151
14
21
        open my $fh, '<', $file or croak "Cannot read $file: $!";
152
153        # Localise $/ to undef to slurp entire file in one read
154
14
2394
        local $/;
155
14
174
        return <$fh>;
156}
157
1581;