TER1 (Statement): 90.57%
TER2 (Branch): 81.25%
TER3 (LCSAJ): 0.0% (0/2)
Approximate LCSAJ segments: 17
โ 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.
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/};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 isMutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_131_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_131_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );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: =cutMutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_170_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes