TER1 (Statement): 100.00%
TER2 (Branch): 68.18%
TER3 (LCSAJ): 100.0% (2/2)
Approximate LCSAJ segments: 23
● 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 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;