| File: | blib/lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm |
| Coverage: | 85.1% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Devel::App::Test::Generator::LCSAJ::Runtime; | |||||
| 2 | ||||||
| 3 | 2 2 2 | 69135 2 22 | use strict; | |||
| 4 | 2 2 2 | 3 2 39 | use warnings; | |||
| 5 | 2 2 2 | 180 5947 5 | use autodie qw(open close); | |||
| 6 | 2 2 2 | 432 2 43 | use Carp qw(croak); | |||
| 7 | 2 2 2 | 4 2 38 | use Cwd qw(abs_path); | |||
| 8 | 2 2 2 | 2 4 44 | use JSON::MaybeXS; | |||
| 9 | 2 2 2 | 4 1 43 | use File::Path qw(make_path); | |||
| 10 | 2 2 2 | 264 1658 218 | use Readonly; | |||
| 11 | ||||||
| 12 | # -------------------------------------------------- | |||||
| 13 | # Output directory for per-process hit JSON files. | |||||
| 14 | # One file is written per process (PID) so parallel | |||||
| 15 | # test runs do not overwrite each other's output. | |||||
| 16 | # -------------------------------------------------- | |||||
| 17 | Readonly my $OUT_DIR => 'cover_html/lcsaj_hits'; | |||||
| 18 | ||||||
| 19 - 27 | =head1 NAME Devel::App::Test::Generator::LCSAJ::Runtime - Debugger backend for LCSAJ coverage =head1 VERSION Version 0.46 =cut | |||||
| 28 | ||||||
| 29 | our $VERSION = '0.46'; | |||||
| 30 | ||||||
| 31 - 62 | =head1 SYNOPSIS PERL5OPT='-d:App::Test::Generator::LCSAJ::Runtime -Mblib' prove -l t =head1 DESCRIPTION This module is loaded as a Perl debugger backend using the C<-d:Module> flag. When Perl sees C<-d:App::Test::Generator::LCSAJ::Runtime> it prepends C<Devel::> and loads C<Devel/App/Test/Generator/LCSAJ/Runtime.pm> from C<@INC>. The file must therefore live at that path - typically C<lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm>. Perl automatically calls C<DB::DB> before executing each statement while the debugger is active. We record (file, line) pairs to build runtime hit data for later LCSAJ analysis. Results are written to C<cover_html/lcsaj_hits/hits_PID.json> at process exit, one file per process so that parallel test runs do not overwrite each other. =head1 ENVIRONMENT =over 4 =item LCSAJ_TARGETS Optional colon-separated list of B<absolute> paths (as produced by C<realpath>) to restrict recording to specific source files. When empty or unset every non-internal file is recorded. =back =cut | |||||
| 63 | ||||||
| 64 | # -------------------------------------------------- | |||||
| 65 | # %HITS - { normalised_path => { line_number => hit_count } } | |||||
| 66 | # %TARGET - set of normalised paths to record (empty means record everything) | |||||
| 67 | # %NORM_CACHE - { raw_file => normalised_path }, memoises abs_path() | |||||
| 68 | # since DB::DB sees the same $file on every consecutive | |||||
| 69 | # statement within a source file | |||||
| 70 | # | |||||
| 71 | # These must be package globals (our) rather than lexicals because DB::DB | |||||
| 72 | # is called by the Perl debugger infrastructure and needs to access them | |||||
| 73 | # without a closure. Lexical vars would not be visible in DB::DB. | |||||
| 74 | # -------------------------------------------------- | |||||
| 75 | our %HITS; | |||||
| 76 | our %TARGET; | |||||
| 77 | our %NORM_CACHE; | |||||
| 78 | ||||||
| 79 | # -------------------------------------------------- | |||||
| 80 | # Populate %TARGET from LCSAJ_TARGETS at compile time. | |||||
| 81 | # The env var contains absolute realpath() output | |||||
| 82 | # separated by colons. Stray newlines from broken | |||||
| 83 | # shell pipelines are stripped defensively. | |||||
| 84 | # -------------------------------------------------- | |||||
| 85 | BEGIN { | |||||
| 86 | 2 | 8 | my $targets_env = $ENV{LCSAJ_TARGETS} // ''; | |||
| 87 | 2 | 2 | $targets_env =~ s/\n//g; | |||
| 88 | ||||||
| 89 | 2 | 279 | for my $t (split /:/, $targets_env) { | |||
| 90 | 0 | 0 | next unless length $t; | |||
| 91 | ||||||
| 92 | # Inline normalisation â cannot call _normalize here since | |||||
| 93 | # BEGIN runs before named subs are compiled when BEGIN | |||||
| 94 | # appears at the top of the file | |||||
| 95 | 0 | 0 | my $f = $t; | |||
| 96 | 0 | 0 | $f =~ s{^.*/blib/lib/}{lib/}; | |||
| 97 | 0 | 0 | $f =~ s{^.*/lib/}{lib/}; | |||
| 98 | 0 | 0 | $TARGET{$f} = 1; | |||
| 99 | } | |||||
| 100 | } | |||||
| 101 | ||||||
| 102 | END { | |||||
| 103 | 2 | 3718 | _write_results(); | |||
| 104 | } | |||||
| 105 | ||||||
| 106 | # -------------------------------------------------- | |||||
| 107 | # _normalize | |||||
| 108 | # | |||||
| 109 | # Convert an absolute or build-tree path | |||||
| 110 | # to a canonical lib-relative form so that | |||||
| 111 | # paths recorded at runtime match the | |||||
| 112 | # targets derived from LCSAJ_TARGETS. | |||||
| 113 | # | |||||
| 114 | # Entry: $path - an absolute or relative file path. | |||||
| 115 | # | |||||
| 116 | # Exit: Returns a lib-relative path string, | |||||
| 117 | # e.g. lib/Foo/Bar.pm | |||||
| 118 | # | |||||
| 119 | # Notes: Must be defined before the BEGIN block | |||||
| 120 | # that calls it, since BEGIN runs at compile | |||||
| 121 | # time and later subs may not yet be compiled. | |||||
| 122 | # | |||||
| 123 | # Examples: | |||||
| 124 | # /home/user/proj/blib/lib/Foo/Bar.pm -> lib/Foo/Bar.pm | |||||
| 125 | # /home/user/proj/lib/Foo/Bar.pm -> lib/Foo/Bar.pm | |||||
| 126 | # -------------------------------------------------- | |||||
| 127 | sub _normalize { | |||||
| 128 | 8 | 1253 | my $f = $_[0]; | |||
| 129 | ||||||
| 130 | # Strip everything up to and including blib/lib/ or lib/ | |||||
| 131 | 8 | 18 | $f =~ s{^.*/blib/lib/}{lib/}; | |||
| 132 | 8 | 14 | $f =~ s{^.*/lib/}{lib/}; | |||
| 133 | 8 | 18 | return $f; | |||
| 134 | } | |||||
| 135 | ||||||
| 136 | # -------------------------------------------------- | |||||
| 137 | # DB::DB | |||||
| 138 | # | |||||
| 139 | # Called by the Perl debugger before every | |||||
| 140 | # statement. Records (file, line) hits for | |||||
| 141 | # later LCSAJ coverage analysis. | |||||
| 142 | # | |||||
| 143 | # Entry: No arguments â caller(0) is used to get | |||||
| 144 | # the current file and line number. | |||||
| 145 | # | |||||
| 146 | # Exit: Returns nothing. Updates %HITS in place. | |||||
| 147 | # | |||||
| 148 | # Side effects: Increments %HITS{$norm}{$line}. | |||||
| 149 | # | |||||
| 150 | # Notes: This sub lives in the DB:: package as | |||||
| 151 | # required by Perl's debugger protocol. | |||||
| 152 | # It is called for every statement executed | |||||
| 153 | # while the debugger is active, so it must | |||||
| 154 | # be as fast as possible. | |||||
| 155 | # Internal files and out-of-target files | |||||
| 156 | # are skipped immediately. | |||||
| 157 | # abs_path() resolution is memoised in | |||||
| 158 | # %NORM_CACHE per raw $file, since the same | |||||
| 159 | # file is seen on every consecutive statement. | |||||
| 160 | # -------------------------------------------------- | |||||
| 161 - 199 | =head2 DB::DB
Perl debugger hook, automatically invoked by the interpreter before every
statement while this module is active as a C<-d:> debugger backend.
Records a per-(file, line) hit count used later for LCSAJ coverage
analysis.
=head3 Arguments
None. Perl calls this sub directly; the current execution location is
obtained internally via C<caller(0)>.
=head3 Returns
Nothing meaningful - this is a void debugger callback.
=head3 Side effects
Increments C<%HITS{$norm}{$line}> for the normalised path and line number
of the statement about to execute. Resolves each distinct raw filename
via C<Cwd::abs_path> once, memoising the result in C<%NORM_CACHE>.
=head3 Usage example
Not called directly - activated via the Perl debugger flag:
PERL5OPT='-d:App::Test::Generator::LCSAJ::Runtime -Mblib' prove -l t
=head3 API specification
=head4 input
{ }
=head4 output
{ type => UNDEF }
=cut | |||||
| 200 | ||||||
| 201 | sub DB::DB { | |||||
| 202 | 7 | 89193 | my (undef, $file, $line) = caller(0); | |||
| 203 | ||||||
| 204 | 7 | 66 | return unless defined $file && defined $line; | |||
| 205 | ||||||
| 206 | # Resolve symlinks and relative components to a stable absolute path, | |||||
| 207 | # cached per raw $file to avoid a stat() on every statement | |||||
| 208 | 7 | 37 | my $norm = $NORM_CACHE{$file} //= _normalize(abs_path($file) // $file); | |||
| 209 | ||||||
| 210 | # Never record hits inside this module itself â suffix match is used | |||||
| 211 | # so it works regardless of CWD or install prefix | |||||
| 212 | 7 | 15 | return if $norm =~ m{(?:^|/)Devel/App/Test/Generator/LCSAJ/Runtime\.pm$}; | |||
| 213 | ||||||
| 214 | # If a target list was provided, skip files not in it | |||||
| 215 | 6 | 9 | if(%TARGET) { | |||
| 216 | 2 | 4 | return unless $TARGET{$norm}; | |||
| 217 | } | |||||
| 218 | ||||||
| 219 | 5 | 11 | $HITS{$norm}{$line}++; | |||
| 220 | } | |||||
| 221 | ||||||
| 222 | # -------------------------------------------------- | |||||
| 223 | # _write_results | |||||
| 224 | # | |||||
| 225 | # Serialise %HITS to a per-process JSON | |||||
| 226 | # file in the output directory. | |||||
| 227 | # | |||||
| 228 | # Entry: None. Reads %HITS and $OUT_DIR. | |||||
| 229 | # | |||||
| 230 | # Exit: Returns nothing. Writes a JSON file. | |||||
| 231 | # Returns immediately if %HITS is empty. | |||||
| 232 | # | |||||
| 233 | # Side effects: Creates $OUT_DIR if absent. | |||||
| 234 | # Writes cover_html/lcsaj_hits/hits_PID.json | |||||
| 235 | # | |||||
| 236 | # Notes: Called from END so it runs even when | |||||
| 237 | # prove exits non-zero â mutation tests | |||||
| 238 | # are expected to fail. PID is included | |||||
| 239 | # in the filename so parallel test runs | |||||
| 240 | # produce separate files without collision. | |||||
| 241 | # -------------------------------------------------- | |||||
| 242 | sub _write_results { | |||||
| 243 | 5 | 6059 | return unless %HITS; | |||
| 244 | ||||||
| 245 | # Include PID in filename to support parallel test runs | |||||
| 246 | 2 | 12 | my $out_file = "$OUT_DIR/hits_$$.json"; | |||
| 247 | ||||||
| 248 | 2 | 12 | make_path($OUT_DIR) unless -d $OUT_DIR; | |||
| 249 | ||||||
| 250 | # autodie is disabled for this open -- under "use autodie qw(open)" | |||||
| 251 | # open() never returns false on failure, it throws its own exception | |||||
| 252 | # instead, which would silently make the "or croak" below dead code | |||||
| 253 | 2 2 2 | 4 5 5 | no autodie qw(open); | |||
| 254 | 2 | 237 | open my $fh, '>', $out_file or croak "Cannot write $out_file: $!"; | |||
| 255 | ||||||
| 256 | 1 | 11 | print $fh encode_json(\%HITS); | |||
| 257 | 1 | 5 | close $fh; | |||
| 258 | } | |||||
| 259 | ||||||
| 260 | 1; | |||||
| 261 | ||||||