lib/App/Test/Generator/Mutation/ConditionalInversion.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 68.18%
TER3 (LCSAJ): 100.0% (2/2)
Approximate LCSAJ segments: 23

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::Mutation::ConditionalInversion;
    2: 
    3: use strict;
    4: use warnings;
    5: use Carp qw(croak);
    6: use parent 'App::Test::Generator::Mutation::Base';
    7: use App::Test::Generator::Mutant;
    8: use PPI;
    9: 
   10: our $VERSION = '0.41';
   11: 
   12: =head1 VERSION
   13: 
   14: Version 0.41
   15: 
   16: =head1 METHODS
   17: 
   18: =head2 applies_to
   19: 
   20: Returns true if the document contains any C<if> or C<unless> compound
   21: statements that this mutator can target.
   22: 
   23: =head3 Arguments
   24: 
   25: =over 4
   26: 
   27: =item * C<$doc>
   28: 
   29: A L<PPI::Document> object to inspect.
   30: 
   31: =back
   32: 
   33: =head3 Returns
   34: 
   35: A boolean.
   36: 
   37: =head3 API specification
   38: 
   39: =head4 input
   40: 
   41:     {
   42:         self => { type => OBJECT, isa => 'App::Test::Generator::Mutation::ConditionalInversion' },
   43:         doc  => { type => OBJECT, isa => 'PPI::Document' },
   44:     }
   45: 
   46: =head4 output
   47: 
   48:     { type => SCALAR }
   49: 
   50: =cut
   51: 
   52: sub applies_to {
53 → 55 → 61   53: 	my ($self, $doc) = @_;
   54: 	my $compounds = $doc->find('PPI::Statement::Compound') || [];
   55: 	for my $stmt (@{$compounds}) {
   56: 		my $first = $stmt->schild(0);
   57: 		next unless $first && $first->isa('PPI::Token::Word');
   58: 		my $type = $first->content();
   59: 		return 1 if $type eq 'if' || $type eq 'unless';

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

60: } 61: return 0;

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

62: } 63: 64: =head2 mutate 65: 66: Walk a PPI document and generate one mutant for each C<if> or C<unless> 67: statement, inverting the keyword to its opposite. This detects cases where 68: the test suite does not exercise both branches of a conditional. 69: 70: my $mutation = App::Test::Generator::Mutation::ConditionalInversion->new; 71: my $doc = PPI::Document->new(\$source); 72: my @mutants = $mutation->mutate($doc); 73: 74: for my $m (@mutants) { 75: print $m->id, ': ', $m->description, "\n"; 76: } 77: 78: =head3 Arguments 79: 80: =over 4 81: 82: =item * C<$self> 83: 84: An instance of C<App::Test::Generator::Mutation::ConditionalInversion>. 85: 86: =item * C<$doc> 87: 88: A L<PPI::Document> object representing the parsed source file to mutate. 89: The document is not modified by this method. 90: 91: =back 92: 93: =head3 Returns 94: 95: A list of L<App::Test::Generator::Mutant> objects, one per C<if> or 96: C<unless> statement found in the document. Returns an empty list if no 97: qualifying statements are found. 98: 99: Each mutant carries a C<transform> closure that when called with a fresh 100: L<PPI::Document> copy will flip the targeted keyword from C<if> to 101: C<unless> or vice versa, targeting the exact statement by line and column 102: number. 103: 104: =head3 Notes 105: 106: Multiple conditionals on the same source line are each mutated 107: independently. Mutant IDs include both line and column number to ensure 108: uniqueness. 109: 110: Each mutant's optional C<context> field is always set to 111: C<conditional> (every mutant produced by this strategy targets an 112: C<if>/C<unless> keyword); its C<line_content> field holds the raw 113: source text of the mutated line. Both are consumed by 114: L<App::Test::Generator::Mutator>'s fast-mode dedup. 115: 116: =head3 API specification 117: 118: =head4 input 119: 120: { 121: self => { 122: type => OBJECT, 123: isa => 'App::Test::Generator::Mutation::ConditionalInversion', 124: }, 125: doc => { 126: type => OBJECT, 127: isa => 'PPI::Document', 128: }, 129: } 130: 131: =head4 output 132: 133: { 134: type => ARRAYREF, 135: elements => { 136: type => OBJECT, 137: isa => 'App::Test::Generator::Mutant', 138: }, 139: } 140: 141: =cut 142: 143: sub mutate { 144 → 150 → 212 144: my ($self, $doc) = @_; 145: 146: # Find all compound statements in the document 147: my $compounds = $doc->find('PPI::Statement::Compound') || []; 148: my @mutants; 149: 150: for my $stmt (@{$compounds}) { 151: # Only process if and unless statements 152: # Use the actual first token content rather than ->type() since 153: # PPI >= 1.270 returns 'if' for both if and unless via ->type() 154: my $first_word = $stmt->schild(0); 155: next unless $first_word && $first_word->isa('PPI::Token::Word'); 156: my $type = $first_word->content(); 157: next unless $type eq 'if' || $type eq 'unless'; 158: 159: # Verify the statement has a condition block to invert 160: my ($cond) = grep { $_->isa('PPI::Structure::Condition') } $stmt->children; 161: next unless $cond; 162: 163: # Capture location for precise targeting in the transform closure 164: my $line = $stmt->location->[0]; 165: my $col = $stmt->location->[1]; 166: 167: # Determine what the keyword flips to 168: my $flipped = $type eq 'if' ? 'unless' : 'if'; 169: 170: my $mutant = eval { 171: App::Test::Generator::Mutant->new( 172: id => "COND_INV_${line}_${col}", 173: group => "COND_INV:$line", 174: description => "Invert condition $type to $flipped", 175: line => $line, 176: type => 'boolean', 177: original => $cond->content(), 178: context => 'conditional', 179: line_content => $self->_line_content($doc, $line), 180: 181: # Closure captures line, col and flipped so it targets 182: # exactly the right statement in the document copy

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

183: transform => sub {

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

184: my ($doc) = @_; 185: my $stmts = $doc->find('PPI::Statement::Compound') || []; 186: 187: for my $stmt (@{$stmts}) { 188: # Match by line and column to avoid mutating 189: # the wrong conditional on the same line 190: next unless $stmt->location->[0] == $line; 191: next unless $stmt->location->[1] == $col; 192: 193: # Flip the leading keyword 194: my $first = $stmt->schild(0); 195: next unless $first && $first->isa('PPI::Token::Word'); 196: $first->set_content($flipped);

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

197: last; 198: } 199: }, 200: ); 201: }; 202: 203: # Report construction failures clearly rather than silently dropping 204: if($@ || !$mutant) {

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

205: warn "Failed to construct mutant COND_INV_${line}_${col}: $@" if $@; 206: next; 207: } 208: 209: push @mutants, $mutant; 210: } 211: 212: return @mutants; 213: } 214: 215: 1;