| File: | blib/lib/App/Test/Generator/LCSAJ/Coverage.pm |
| Coverage: | 96.4% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package 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 | ||||||
| 10 | our $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 | ||||||
| 92 | sub 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 | # -------------------------------------------------- | |||||
| 148 | sub _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 | ||||||
| 158 | 1; | |||||