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: =head1 VERSION
   24: 
   25: Version 0.46
   26: 
   27: =cut
   28: 
   29: our $VERSION = '0.46';
   30: 
   31: =head1 SYNOPSIS
   32: 
   33:   PERL5OPT='-d:App::Test::Generator::LCSAJ::Runtime -Mblib' prove -l t
   34: 
   35: =head1 DESCRIPTION
   36: 
   37: This module is loaded as a Perl debugger backend using the C<-d:Module> flag.
   38: 
   39: When Perl sees C<-d:App::Test::Generator::LCSAJ::Runtime> it prepends C<Devel::>
   40: and loads C<Devel/App/Test/Generator/LCSAJ/Runtime.pm> from C<@INC>.
   41: The file must therefore live at that path - typically C<lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm>.
   42: 
   43: Perl automatically calls C<DB::DB> before executing each statement while the
   44: debugger is active. We record (file, line) pairs to build runtime hit data for
   45: later LCSAJ analysis.
   46: 
   47: Results are written to C<cover_html/lcsaj_hits/hits_PID.json> at process exit,
   48: one file per process so that parallel test runs do not overwrite each other.
   49: 
   50: =head1 ENVIRONMENT
   51: 
   52: =over 4
   53: 
   54: =item LCSAJ_TARGETS
   55: 
   56: Optional colon-separated list of B<absolute> paths (as produced by C<realpath>)
   57: to restrict recording to specific source files. When empty or unset every
   58: non-internal file is recorded.
   59: 
   60: =back
   61: 
   62: =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 {
โ—[NOT COVERED] 86 โ†’ 89 โ†’ 0   86: 	my $targets_env = $ENV{LCSAJ_TARGETS} // '';
   87: 	$targets_env =~ s/\n//g;
   88: 
   89: 	for my $t (split /:/, $targets_env) {
   90: 		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: 		my $f = $t;
   96: 		$f =~ s{^.*/blib/lib/}{lib/};
   97: 		$f =~ s{^.*/lib/}{lib/};
   98: 		$TARGET{$f} = 1;
   99: 	}
  100: }
  101: 
  102: END {
  103: 	_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: 	my $f = $_[0];
  129: 
  130: 	# Strip everything up to and including blib/lib/ or lib/
  131: 	$f =~ s{^.*/blib/lib/}{lib/};

					
Mutants (Total: 2, Killed: 0, Survived: 2)
132: $f =~ s{^.*/lib/}{lib/}; 133: 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: =head2 DB::DB 162: 163: Perl debugger hook, automatically invoked by the interpreter before every 164: statement while this module is active as a C<-d:> debugger backend. 165: Records a per-(file, line) hit count used later for LCSAJ coverage 166: analysis. 167: 168: =head3 Arguments 169: 170: None. Perl calls this sub directly; the current execution location is
Mutants (Total: 1, Killed: 0, Survived: 1)
171: obtained internally via C<caller(0)>. 172: 173: =head3 Returns 174: 175: Nothing meaningful - this is a void debugger callback. 176: 177: =head3 Side effects 178: 179: Increments C<%HITS{$norm}{$line}> for the normalised path and line number 180: of the statement about to execute. Resolves each distinct raw filename 181: via C<Cwd::abs_path> once, memoising the result in C<%NORM_CACHE>. 182: 183: =head3 Usage example 184: 185: Not called directly - activated via the Perl debugger flag: 186: 187: PERL5OPT='-d:App::Test::Generator::LCSAJ::Runtime -Mblib' prove -l t 188: 189: =head3 API specification 190: 191: =head4 input 192: 193: { } 194: 195: =head4 output 196: 197: { type => UNDEF } 198: 199: =cut 200: 201: sub DB::DB { โ—[NOT COVERED] 202 โ†’ 215 โ†’ 219 202: my (undef, $file, $line) = caller(0); 203: 204: 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: 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: 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: if(%TARGET) { 216: return unless $TARGET{$norm}; 217: } 218: 219: $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: return unless %HITS; 244: 245: # Include PID in filename to support parallel test runs 246: my $out_file = "$OUT_DIR/hits_$$.json"; 247: 248: 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: no autodie qw(open); 254: open my $fh, '>', $out_file or croak "Cannot write $out_file: $!"; 255: 256: print $fh encode_json(\%HITS); 257: close $fh; 258: } 259: 260: 1; 261: 262: __END__ 263: 264: =head1 OUTPUT FORMAT 265: 266: C<cover_html/lcsaj_hits/hits_PID.json> is a JSON object of the form: 267: 268: { 269: "lib/Foo/Bar.pm": { "12": 3, "15": 1, ... }, 270: ... 271: } 272: 273: Keys are lib-relative paths (C<lib/...>); values are objects mapping line 274: numbers (as strings) to hit counts. One file is written per process so 275: parallel test runs produce separate files. 276: 277: =head1 NOTES ON FILE PLACEMENT 278: 279: The C<-d:App::Test::Generator::LCSAJ::Runtime> flag causes Perl to load 280: C<Devel::App::Test::Generator::LCSAJ::Runtime>, which it finds at: 281: 282: lib/Devel/App/Test/Generator/LCSAJ/Runtime.pm 283: 284: Ensure this path is on C<@INC> (C<-Mblib> or C<-Ilib> in PERL5OPT). 285: 286: =head1 SEE ALSO 287: 288: L<Devel::Cover>, L<App::Test::Generator> 289: 290: =head1 AUTHOR 291: 292: Nigel Horne, C<< <njh at nigelhorne.com> >> 293: 294: Portions of this module's initial design and documentation were created 295: with the assistance of AI. 296: 297: =encoding UTF-8 298: 299: =head1 FORMAL SPECIFICATION 300: 301: Let H be the hits relation (file x line) → â„•, T be the target-file set, 302: and I be the internal-file predicate (true only for this module's own 303: source path). 304: 305: ┌ DB_DB ────────────────────────────────────────── 306: │ ΔH 307: │ file? : FilePath 308: │ line? : â„• 309: ├───────────────────────────────────────────────── 310: │ norm == normalize(file?) 311: │ ¬I(norm) ∧ (T = ∅ ∨ norm ∈ T) 312: │ ⟹ H′(norm, line?) = H(norm, line?) + 1 313: │ I(norm) ∨ (T ≠ ∅ ∧ norm ∉ T) 314: │ ⟹ H′ = H 315: └───────────────────────────────────────────────── 316: 317: =head1 LICENCE AND COPYRIGHT 318: 319: Copyright 2025-2026 Nigel Horne. 320: 321: Usage is subject to the terms of GPL2. 322: If you use it, 323: please let me know. 324: 325: =cut