lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm

Structural Coverage (Approximate)

TER1 (Statement): 90.57%
TER2 (Branch): 81.25%
TER3 (LCSAJ): 0.0% (0/2)
Approximate LCSAJ segments: 17

LCSAJ Legend

โ— Covered โ€” this LCSAJ path was executed during testing.

โ— Not covered โ€” this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start โ†’ end โ†’ jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package Devel::App::Test::Generator::LCSAJ::Runtime;
    2: 
    3: use strict;
    4: use warnings;
    5: use autodie     qw(open close);
    6: use Carp        qw(croak);
    7: use Cwd         qw(abs_path);
    8: use JSON::MaybeXS;
    9: use File::Path  qw(make_path);
   10: 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: =head1 NAME
   20: 
   21: Devel::App::Test::Generator::LCSAJ::Runtime - Debugger backend for LCSAJ coverage
   22: 
   23: =encoding UTF-8
   24: 
   25: =head1 VERSION
   26: 
   27: Version 0.41
   28: 
   29: =cut
   30: 
   31: our $VERSION = '0.41';
   32: 
   33: =head1 SYNOPSIS
   34: 
   35:   PERL5OPT='-d:App::Test::Generator::LCSAJ::Runtime -Mblib' prove -l t
   36: 
   37: =head1 DESCRIPTION
   38: 
   39: This module is loaded as a Perl debugger backend using the C<-d:Module> flag.
   40: 
   41: When Perl sees C<-d:App::Test::Generator::LCSAJ::Runtime> it prepends C<Devel::>
   42: and loads C<Devel/App/Test/Generator/LCSAJ/Runtime.pm> from C<@INC>.
   43: The file must therefore live at that path - typically C<lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm>.
   44: 
   45: Perl automatically calls C<DB::DB> before executing each statement while the
   46: debugger is active. We record (file, line) pairs to build runtime hit data for
   47: later LCSAJ analysis.
   48: 
   49: Results are written to C<cover_html/lcsaj_hits/hits_PID.json> at process exit,
   50: one file per process so that parallel test runs do not overwrite each other.
   51: 
   52: =head1 ENVIRONMENT
   53: 
   54: =over 4
   55: 
   56: =item LCSAJ_TARGETS
   57: 
   58: Optional colon-separated list of B<absolute> paths (as produced by C<realpath>)
   59: to restrict recording to specific source files. When empty or unset every
   60: non-internal file is recorded.
   61: 
   62: =back
   63: 
   64: =cut
   65: 
   66: # --------------------------------------------------
   67: # %HITS       - { normalised_path => { line_number => hit_count } }
   68: # %TARGET     - set of normalised paths to record (empty means record everything)
   69: # %NORM_CACHE - { raw_file => normalised_path }, memoises abs_path()
   70: #               since DB::DB sees the same $file on every consecutive
   71: #               statement within a source file
   72: #
   73: # These must be package globals (our) rather than lexicals because DB::DB
   74: # is called by the Perl debugger infrastructure and needs to access them
   75: # without a closure. Lexical vars would not be visible in DB::DB.
   76: # --------------------------------------------------
   77: our %HITS;
   78: our %TARGET;
   79: our %NORM_CACHE;
   80: 
   81: # --------------------------------------------------
   82: # Populate %TARGET from LCSAJ_TARGETS at compile time.
   83: # The env var contains absolute realpath() output
   84: # separated by colons. Stray newlines from broken
   85: # shell pipelines are stripped defensively.
   86: # --------------------------------------------------
   87: BEGIN {
โ—[NOT COVERED] 88 โ†’ 91 โ†’ 0   88: 	my $targets_env = $ENV{LCSAJ_TARGETS} // '';
   89: 	$targets_env =~ s/\n//g;
   90: 
   91: 	for my $t (split /:/, $targets_env) {
   92: 		next unless length $t;
   93: 
   94: 		# Inline normalisation — cannot call _normalize here since
   95: 		# BEGIN runs before named subs are compiled when BEGIN
   96: 		# appears at the top of the file
   97: 		my $f = $t;
   98: 		$f =~ s{^.*/blib/lib/}{lib/};
   99: 		$f =~ s{^.*/lib/}{lib/};
  100: 		$TARGET{$f} = 1;
  101: 	}
  102: }
  103: 
  104: END {
  105: 	_write_results();
  106: }
  107: 
  108: # --------------------------------------------------
  109: # _normalize
  110: #
  111: # Purpose:    Convert an absolute or build-tree path
  112: #             to a canonical lib-relative form so that
  113: #             paths recorded at runtime match the
  114: #             targets derived from LCSAJ_TARGETS.
  115: #
  116: # Entry:      $path - an absolute or relative file path.
  117: #
  118: # Exit:       Returns a lib-relative path string,
  119: #             e.g. lib/Foo/Bar.pm
  120: #
  121: # Side effects: None.
  122: #
  123: # Notes:      Must be defined before the BEGIN block
  124: #             that calls it, since BEGIN runs at compile
  125: #             time and later subs may not yet be compiled.
  126: #
  127: # Examples:
  128: #   /home/user/proj/blib/lib/Foo/Bar.pm  ->  lib/Foo/Bar.pm
  129: #   /home/user/proj/lib/Foo/Bar.pm       ->  lib/Foo/Bar.pm
  130: # --------------------------------------------------
  131: sub _normalize {

					
Mutants (Total: 2, Killed: 0, Survived: 2)
132: my $f = $_[0]; 133: 134: # Strip everything up to and including blib/lib/ or lib/ 135: $f =~ s{^.*/blib/lib/}{lib/}; 136: $f =~ s{^.*/lib/}{lib/}; 137: return $f; 138: } 139: 140: # -------------------------------------------------- 141: # DB::DB 142: # 143: # Purpose: Called by the Perl debugger before every 144: # statement. Records (file, line) hits for 145: # later LCSAJ coverage analysis. 146: # 147: # Entry: No arguments — caller(0) is used to get 148: # the current file and line number. 149: # 150: # Exit: Returns nothing. Updates %HITS in place. 151: # 152: # Side effects: Increments %HITS{$norm}{$line}. 153: # 154: # Notes: This sub lives in the DB:: package as 155: # required by Perl's debugger protocol. 156: # It is called for every statement executed 157: # while the debugger is active, so it must 158: # be as fast as possible. 159: # Internal files and out-of-target files 160: # are skipped immediately. 161: # abs_path() resolution is memoised in 162: # %NORM_CACHE per raw $file, since the same 163: # file is seen on every consecutive statement. 164: # -------------------------------------------------- 165: =head2 DB::DB 166: 167: Perl debugger hook, automatically invoked by the interpreter before every 168: statement while this module is active as a C<-d:> debugger backend. 169: Records a per-(file, line) hit count used later for LCSAJ coverage 170: analysis.
Mutants (Total: 1, Killed: 0, Survived: 1)
171: 172: =head3 Arguments 173: 174: None. Perl calls this sub directly; the current execution location is 175: obtained internally via C<caller(0)>. 176: 177: =head3 Returns 178: 179: Nothing meaningful — this is a void debugger callback. 180: 181: =head3 Side effects 182: 183: Increments C<%HITS{$norm}{$line}> for the normalised path and line number 184: of the statement about to execute. Resolves each distinct raw filename 185: via C<Cwd::abs_path> once, memoising the result in C<%NORM_CACHE>. 186: 187: =head3 Usage example 188: 189: Not called directly — activated via the Perl debugger flag: 190: 191: PERL5OPT='-d:App::Test::Generator::LCSAJ::Runtime -Mblib' prove -l t 192: 193: =head3 API specification 194: 195: =head4 input 196: 197: { } 198: 199: =head4 output 200: 201: { type => UNDEF } 202: 203: =head3 Formal specification 204: 205: Let H be the hits relation (file x line) → â„•, T be the target-file set, 206: and I be the internal-file predicate (true only for this module's own 207: source path). 208: 209: ┌ DB_DB ────────────────────────────────────────── 210: │ ΔH 211: │ file? : FilePath 212: │ line? : â„• 213: ├───────────────────────────────────────────────── 214: │ norm == normalize(file?) 215: │ ¬I(norm) ∧ (T = ∅ ∨ norm ∈ T) 216: │ ⟹ H′(norm, line?) = H(norm, line?) + 1 217: │ I(norm) ∨ (T ≠ ∅ ∧ norm ∉ T) 218: │ ⟹ H′ = H 219: └───────────────────────────────────────────────── 220: 221: =cut 222: 223: sub DB::DB { โ—[NOT COVERED] 224 โ†’ 237 โ†’ 241 224: my (undef, $file, $line) = caller(0); 225: 226: return unless defined $file && defined $line; 227: 228: # Resolve symlinks and relative components to a stable absolute path, 229: # cached per raw $file to avoid a stat() on every statement 230: my $norm = $NORM_CACHE{$file} //= _normalize(abs_path($file) // $file); 231: 232: # Never record hits inside this module itself — suffix match is used 233: # so it works regardless of CWD or install prefix 234: return if $norm =~ m{(?:^|/)Devel/App/Test/Generator/LCSAJ/Runtime\.pm$}; 235: 236: # If a target list was provided, skip files not in it 237: if(%TARGET) { 238: return unless $TARGET{$norm}; 239: } 240: 241: $HITS{$norm}{$line}++; 242: } 243: 244: # -------------------------------------------------- 245: # _write_results 246: # 247: # Purpose: Serialise %HITS to a per-process JSON 248: # file in the output directory. 249: # 250: # Entry: None. Reads %HITS and $OUT_DIR. 251: # 252: # Exit: Returns nothing. Writes a JSON file. 253: # Returns immediately if %HITS is empty. 254: # 255: # Side effects: Creates $OUT_DIR if absent. 256: # Writes cover_html/lcsaj_hits/hits_PID.json 257: # 258: # Notes: Called from END so it runs even when 259: # prove exits non-zero — mutation tests 260: # are expected to fail. PID is included 261: # in the filename so parallel test runs 262: # produce separate files without collision. 263: # -------------------------------------------------- 264: sub _write_results { 265: return unless %HITS; 266: 267: # Include PID in filename to support parallel test runs 268: my $out_file = "$OUT_DIR/hits_$$.json"; 269: 270: make_path($OUT_DIR) unless -d $OUT_DIR; 271: 272: # autodie is disabled for this open -- under "use autodie qw(open)" 273: # open() never returns false on failure, it throws its own exception 274: # instead, which would silently make the "or croak" below dead code 275: no autodie qw(open); 276: open my $fh, '>', $out_file or croak "Cannot write $out_file: $!"; 277: 278: print $fh encode_json(\%HITS); 279: close $fh; 280: } 281: 282: 1; 283: 284: __END__ 285: 286: =head1 OUTPUT FORMAT 287: 288: C<cover_html/lcsaj_hits/hits_PID.json> is a JSON object of the form: 289: 290: { 291: "lib/Foo/Bar.pm": { "12": 3, "15": 1, ... }, 292: ... 293: } 294: 295: Keys are lib-relative paths (C<lib/...>); values are objects mapping line 296: numbers (as strings) to hit counts. One file is written per process so 297: parallel test runs produce separate files. 298: 299: =head1 NOTES ON FILE PLACEMENT 300: 301: The C<-d:App::Test::Generator::LCSAJ::Runtime> flag causes Perl to load 302: C<Devel::App::Test::Generator::LCSAJ::Runtime>, which it finds at: 303: 304: lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm 305: 306: Ensure this path is on C<@INC> (C<-Mblib> or C<-Ilib> in PERL5OPT). 307: 308: =head1 SEE ALSO 309: 310: L<Devel::Cover>, L<App::Test::Generator> 311: 312: =head1 AUTHOR 313: 314: Nigel Horne, C<< <njh at nigelhorne.com> >> 315: 316: Portions of this module's initial design and documentation were created 317: with the assistance of AI. 318: 319: =head1 LICENCE AND COPYRIGHT 320: 321: Copyright 2025-2026 Nigel Horne. 322: 323: Usage is subject to the terms of GPL2. 324: If you use it, 325: please let me know. 326: 327: =cut