lib/App/Test/Generator/LCSAJ/Coverage.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 83.33%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 13

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 App::Test::Generator::LCSAJ::Coverage;
    2: 
    3: use strict;
    4: use warnings;
    5: 
    6: use autodie qw(:all);
    7: use Carp qw(croak);
    8: use JSON::MaybeXS;
    9: 
   10: our $VERSION = '0.36';
   11: 
   12: =head1 NAME
   13: 
   14: App::Test::Generator::LCSAJ::Coverage - Merge LCSAJ path data with runtime hits
   15: 
   16: =head1 VERSION
   17: 
   18: Version 0.36
   19: 
   20: =head1 DESCRIPTION
   21: 
   22: Merges static LCSAJ path data produced by L<App::Test::Generator::LCSAJ>
   23: with runtime line hit data to determine which LCSAJ paths were covered
   24: during test execution. The merged result is written as JSON for
   25: consumption by C<bin/test-generator-index>.
   26: 
   27: =head2 merge
   28: 
   29: Merge a static LCSAJ path JSON file with a runtime line hits JSON file
   30: and write the annotated result to an output file.
   31: 
   32:     App::Test::Generator::LCSAJ::Coverage::merge(
   33:         'cover_db/lcsaj/MyModule.pm.lcsaj.json',
   34:         'cover_db/lcsaj/MyModule.pm.hits.json',
   35:         'cover_db/lcsaj/MyModule.pm.covered.json',
   36:     );
   37: 
   38: =head3 Arguments
   39: 
   40: =over 4
   41: 
   42: =item * C<$lcsaj_file>
   43: 
   44: Path to the C<.lcsaj.json> file produced by
   45: L<App::Test::Generator::LCSAJ>. Required.
   46: 
   47: =item * C<$hits_file>
   48: 
   49: Path to a JSON file mapping line numbers (as strings) to hit counts,
   50: as produced by L<Devel::App::Test::Generator::LCSAJ::Runtime>.
   51: Required.
   52: 
   53: =item * C<$out_file>
   54: 
   55: Path to write the merged output JSON file. Required.
   56: 
   57: =back
   58: 
   59: =head3 Returns
   60: 
   61: Nothing. Writes the annotated LCSAJ path data to C<$out_file>, with
   62: a C<covered> key added to each path record.
   63: 
   64: =head3 Side effects
   65: 
   66: Writes to C<$out_file>. Croaks if any file cannot be read or written.
   67: 
   68: =head3 Notes
   69: 
   70: A path is considered covered if any line in the range C<start..end>
   71: was executed at least once. This is a conservative approximation —
   72: it does not verify that the jump target was actually reached. As a
   73: result, coverage may be slightly overstated for paths where only the
   74: beginning of the sequence was executed.
   75: 
   76: =head3 API specification
   77: 
   78: =head4 input
   79: 
   80:     {
   81:         lcsaj_file => { type => SCALAR },
   82:         hits_file  => { type => SCALAR },
   83:         out_file   => { type => SCALAR },
   84:     }
   85: 
   86: =head4 output
   87: 
   88:     { type => UNDEF }
   89: 
   90: =cut
   91: 
   92: sub merge {
93 → 108 → 12293 → 108 → 0   93: 	my ($lcsaj_file, $hits_file, $out_file) = @_;
   94: 
   95: 	# Validate all three file arguments before attempting any IO
   96: 	croak 'lcsaj_file required' unless defined $lcsaj_file;
   97: 	croak 'hits_file required'  unless defined $hits_file;
   98: 	croak 'out_file required'   unless defined $out_file;
   99: 
  100: 	# Load static LCSAJ path data extracted by App::Test::Generator::LCSAJ
  101: 	my $paths = decode_json(_slurp($lcsaj_file));
  102: 
  103: 	# Load runtime line hit counts from Devel::App::Test::Generator::LCSAJ::Runtime
  104: 	my $hits  = decode_json(_slurp($hits_file));
  105: 
  106: 	# Annotate each path with a covered flag — a path is considered
  107: 	# covered if any line in the start..end range was executed
  108: 	for my $path (@{$paths}) {
  109: 		my $covered = 0;
  110: 
  111: 		for my $line ($path->{start} .. $path->{end}) {
  112: 			if($hits->{$line}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

113: $covered = 1; 114: last; 115: } 116: } 117: 118: $path->{covered} = $covered; 119: } 120: 121: # Write the annotated paths to the output file 122 → 124 → 0 122: open my $fh, '>', $out_file or croak "Cannot write coverage output to $out_file: $!"; 123: print $fh encode_json($paths); 124: close $fh; 125: } 126: 127: # -------------------------------------------------- 128: # _slurp 129: # 130: # Purpose: Read the entire contents of a file and 131: # return it as a string. 132: # 133: # Entry: $file - path to the file to read. 134: # 135: # Exit: Returns the file contents as a scalar 136: # string. Croaks if the file cannot be 137: # opened. 138: # 139: # Side effects: None beyond opening and closing the 140: # file handle. 141: # 142: # Notes: Uses three-argument open for safety with 143: # filenames containing special characters. 144: # Sets $/ to undef to slurp the whole file 145: # in one read, localised to avoid affecting 146: # other code. 147: # -------------------------------------------------- 148: sub _slurp { 149: my $file = $_[0]; 150: 151: open my $fh, '<', $file or croak "Cannot read $file: $!"; 152: 153: # Localise $/ to undef to slurp entire file in one read 154: local $/; 155: return <$fh>; 156: } 157: 158: 1;