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: =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 {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: 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: 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: =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