TER1 (Statement): 95.31%
TER2 (Branch): 45.83%
TER3 (LCSAJ): 100.0% (6/6)
Approximate LCSAJ segments: 25
● 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::NumericBoundary; 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.36'; 11: 12: =head1 VERSION 13: 14: Version 0.36 15: 16: =cut 17: 18: # -------------------------------------------------- 19: # Mapping of each comparison operator to the list of 20: # operators it should be flipped to when mutating. 21: # Both directions are covered so that e.g. != can be 22: # mutated to == and vice versa. 23: # -------------------------------------------------- 24: my %FLIP = ( 25: '>' => [ '<', '>=', '<=' ], 26: '<' => [ '>', '<=', '>=' ], 27: '>=' => [ '>', '<', '<=' ], 28: '<=' => [ '<', '>', '>=' ], 29: '==' => [ '!=' ], 30: '!=' => [ '==' ], 31: ); 32: 33: =head2 applies_to 34: 35: Returns true if the document contains any comparison operators that this 36: mutator can target (C<E<gt>>, C<E<lt>>, C<E<gt>=>, C<E<lt>=>, C<==>, 37: C<!=>). 38: 39: =cut 40: 41: sub applies_to { ●42 → 44 → 54●42 → 44 → 0 42: my ($self, $doc) = @_; 43: my $ops = $doc->find('PPI::Token::Operator') || []; 44: for my $op (@{$ops}) { 45: next unless exists $FLIP{$op->content()}; 46: my $next_sib = $op->next_sibling(); 47: next if $next_sib && $next_sib->isa('PPI::Token::Symbol'); 48: my $parent = $op->parent(); 49: next unless $parent->isa('PPI::Statement') 50: || $parent->isa('PPI::Structure::Condition') 51: || $parent->isa('PPI::Structure::Block'); 52: return 1;Mutants (Total: 2, Killed: 2, Survived: 0)
53: } ●54 → 54 → 0 54: return 0;
Mutants (Total: 2, Killed: 2, Survived: 0)
55: } 56: 57: =head2 mutate 58: 59: Walk a PPI document and generate one mutant for each comparison operator 60: that can be flipped to reveal a boundary condition not caught by the test 61: suite. For example, C<E<gt>=> is flipped to C<E<gt>>, C<E<lt>>, and 62: C<E<lt>=> in turn, producing three independent mutants. 63: 64: my $mutation = App::Test::Generator::Mutation::NumericBoundary->new; 65: my $doc = PPI::Document->new(\$source); 66: my @mutants = $mutation->mutate($doc); 67: 68: for my $m (@mutants) { 69: print $m->id, ': ', $m->description, "\n"; 70: } 71: 72: =head3 Arguments 73: 74: =over 4 75: 76: =item * C<$self> 77: 78: An instance of C<App::Test::Generator::Mutation::NumericBoundary>. 79: 80: =item * C<$doc> 81: 82: A L<PPI::Document> object representing the parsed source file to mutate. 83: The document is not modified by this method. 84: 85: =back 86: 87: =head3 Returns 88: 89: A list of L<App::Test::Generator::Mutant> objects, one per 90: (operator, flip) pair found in the document. Returns an empty list if no 91: qualifying comparison operators are found. 92: 93: Each mutant carries a C<transform> closure that when called with a fresh 94: L<PPI::Document> copy will replace the targeted operator with its flipped 95: equivalent, targeting the exact operator by line and column number to 96: ensure that multiple comparison operators on the same source line are each 97: mutated independently. 98: 99: =head3 Notes 100: 101: The following operators and their flips are supported: 102: 103: > flips to < >= <= 104: < flips to > <= >= 105: >= flips to > < <= 106: <= flips to < > >= 107: == flips to != 108: != flips to == 109: 110: Mutant IDs include line number, column number, and the flip target to 111: ensure uniqueness even when multiple operators share a source line. 112: 113: =head3 API specification 114: 115: =head4 input 116: 117: { 118: self => { 119: type => OBJECT, 120: isa => 'App::Test::Generator::Mutation::NumericBoundary', 121: }, 122: doc => { 123: type => OBJECT, 124: isa => 'PPI::Document', 125: }, 126: } 127: 128: =head4 output 129: 130: { 131: type => ARRAYREF, 132: elements => { 133: type => OBJECT, 134: isa => 'App::Test::Generator::Mutant', 135: }, 136: } 137: 138: =cut 139: 140: sub mutate { ●141 → 147 → 224●141 → 147 → 0 141: my ($self, $doc) = @_; 142: 143: # Find all operator tokens in the document 144: my $ops = $doc->find('PPI::Token::Operator') || []; 145: my @mutants; 146: 147: for my $op (@{$ops}) { 148: my $original = $op->content(); 149: 150: # Skip readline operators â < immediately followed by 151: # a symbol token is <$fh> not a numeric comparison 152: my $next_sib = $op->next_sibling(); 153: next if $next_sib && $next_sib->isa('PPI::Token::Symbol'); 154: 155: # Only process comparison operators that have defined flips 156: next unless exists $FLIP{$original}; 157: 158: # Only mutate operators that are direct children of 159: # a condition or expression, not list arguments 160: my $parent = $op->parent(); 161: next unless $parent->isa('PPI::Statement') 162: || $parent->isa('PPI::Structure::Condition') 163: || $parent->isa('PPI::Structure::Block'); 164: 165: # Capture location so the transform closure targets the 166: # exact operator rather than the first match on that line 167: my $line = $op->location->[0]; 168: my $col = $op->location->[1]; 169: 170: # Generate one mutant per flip of this operator 171: for my $change (@{ $FLIP{$original} }) { 172: # Build a unique id from location and the specific flip 173: # so multiple operators on the same line don't collide 174: my $id = "NUM_BOUNDARY_${line}_${col}_${change}"; 175: 176: my $mutant = eval { 177: App::Test::Generator::Mutant->new( 178: id => $id, 179: group => "NUM_BOUNDARY:$line", 180: description => "Numeric boundary flip $original to $change", 181: original => $original, 182: line => $line, 183: type => 'comparison', 184: 185: # The transform closure captures line, col, original 186: # and change so it targets precisely the right operator 187: # in the document copy it receives at test time 188: transform => sub { 189: my $doc = $_[0]; 190: my $ops = $doc->find('PPI::Token::Operator') || []; 191: 192: for my $op (@{$ops}) { 193: next unless $op->line_number == $line;
Mutants (Total: 1, Killed: 1, Survived: 0)
194: next unless $op->column_number == $col;
Mutants (Total: 1, Killed: 1, Survived: 0)
195: next unless $op->content eq $original; 196: 197: # Safety check â do not mutate if this looks like 198: # a readline operator (<$fh>) rather than a numeric 199: # comparison. A readline < is immediately followed 200: # by a symbol token starting with $ 201: my $next_sib = $op->next_sibling; 202: if($next_sib && $next_sib->isa('PPI::Token::Symbol')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
203: last; 204: } 205: 206: $op->set_content($change); 207: last; 208: } 209: }, 210: ); 211: }; 212: 213: # If the Mutant construction fails, report clearly rather than 214: # silently dropping the mutant from the results 215: if($@ || !$mutant) {
Mutants (Total: 1, Killed: 1, Survived: 0)
216: warn "Failed to construct mutant $id: $@" if $@; 217: next; 218: } 219: 220: push @mutants, $mutant; 221: } 222: } 223: ●224 → 224 → 0 224: return @mutants;
Mutants (Total: 2, Killed: 2, Survived: 0)
225: } 226: 227: 1;