File Coverage

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

linestmtbrancondsubtimecode
1package App::Test::Generator::LCSAJ::Coverage;
2
3
4
4
4
74113
4
50
use strict;
4
4
4
4
6
3
78
use warnings;
5
6
4
4
4
342
11309
10
use autodie qw(:all);
7
4
4
4
18542
3
83
use Carp qw(croak);
8
4
4
4
10
3
415
use JSON::MaybeXS;
9
10our $VERSION = '0.41';
11
12 - 90
=head1 NAME

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

=head1 VERSION

Version 0.41

=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
16
106607
        my ($lcsaj_file, $hits_file, $out_file) = @_;
94
95        # Validate all three file arguments before attempting any IO
96
16
39
        croak 'lcsaj_file required' unless defined $lcsaj_file;
97
14
28
        croak 'hits_file required'  unless defined $hits_file;
98
12
23
        croak 'out_file required'   unless defined $out_file;
99
100        # Load static LCSAJ path data extracted by App::Test::Generator::LCSAJ
101
10
13
        my $paths = decode_json(_slurp($lcsaj_file));
102
103        # Load runtime line hit counts from Devel::App::Test::Generator::LCSAJ::Runtime
104
9
13
        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
9
9
9
14
        for my $path (@{$paths}) {
109
9
9
                my $covered = 0;
110
111
9
15
                for my $line ($path->{start} .. $path->{end}) {
112
27
27
                        if($hits->{$line}) {
113
4
2
                                $covered = 1;
114
4
4
                                last;
115                        }
116                }
117
118
9
11
                $path->{covered} = $covered;
119        }
120
121        # Write the annotated paths to the output file. autodie is disabled
122        # for this open so the custom error message below is actually
123        # reachable -- under "use autodie qw(:all)" open() never returns
124        # false on failure, it throws its own exception instead, which
125        # would silently make the "or croak" dead code.
126
4
4
4
10
0
9
        no autodie qw(open);
127
9
219
        open my $fh, '>', $out_file or croak "Cannot write coverage output to $out_file: $!";
128
8
39
        print $fh encode_json($paths);
129
8
22
        close $fh;
130}
131
132# --------------------------------------------------
133# _slurp
134#
135# Purpose:    Read the entire contents of a file and
136#             return it as a string.
137#
138# Entry:      $file - path to the file to read.
139#
140# Exit:       Returns the file contents as a scalar
141#             string. Croaks if the file cannot be
142#             opened.
143#
144# Side effects: None beyond opening and closing the
145#               file handle.
146#
147# Notes:      Uses three-argument open for safety with
148#             filenames containing special characters.
149#             Sets $/ to undef to slurp the whole file
150#             in one read, localised to avoid affecting
151#             other code.
152# --------------------------------------------------
153sub _slurp {
154
21
2242
        my $file = $_[0];
155
156        # autodie is disabled for this open for the same reason as in
157        # merge() above -- it would otherwise make the custom "Cannot
158        # read" message unreachable dead code.
159
4
4
4
618
4
5
        no autodie qw(open);
160
21
213
        open my $fh, '<', $file or croak "Cannot read $file: $!";
161
162        # Localise $/ to undef to slurp entire file in one read
163
19
30
        local $/;
164
19
202
        return <$fh>;
165}
166
1671;