TER1 (Statement): 100.00%
TER2 (Branch): 75.00%
TER3 (LCSAJ): 100.0% (2/2)
Approximate LCSAJ segments: 21
● 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.41'; 11: 12: =head1 VERSION 13: 14: Version 0.41 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: =head3 Arguments 40: 41: =over 4 42: 43: =item * C<$doc> 44: 45: A L<PPI::Document> object to inspect. 46: 47: =back 48: 49: =head3 Returns 50: 51: A boolean. 52: 53: =head3 API specification 54: 55: =head4 input 56: 57: { 58: self => { type => OBJECT, isa => 'App::Test::Generator::Mutation::NumericBoundary' }, 59: doc => { type => OBJECT, isa => 'PPI::Document' }, 60: } 61: 62: =head4 output 63: 64: { type => SCALAR } 65: 66: =cut 67: 68: sub applies_to { ●69 → 71 → 79 69: my ($self, $doc) = @_; 70: my $ops = $doc->find('PPI::Token::Operator') || []; 71: for my $op (@{$ops}) { 72: next unless exists $FLIP{$op->content()}; 73: my $parent = $op->parent(); 74: next unless $parent->isa('PPI::Statement') 75: || $parent->isa('PPI::Structure::Condition') 76: || $parent->isa('PPI::Structure::Block'); 77: return 1; 78: } 79: return 0;Mutants (Total: 2, Killed: 2, Survived: 0)
80: } 81:
Mutants (Total: 2, Killed: 2, Survived: 0)
82: =head2 mutate 83: 84: Walk a PPI document and generate one mutant for each comparison operator 85: that can be flipped to reveal a boundary condition not caught by the test 86: suite. For example, C<E<gt>=> is flipped to C<E<gt>>, C<E<lt>>, and 87: C<E<lt>=> in turn, producing three independent mutants. 88: 89: my $mutation = App::Test::Generator::Mutation::NumericBoundary->new; 90: my $doc = PPI::Document->new(\$source); 91: my @mutants = $mutation->mutate($doc); 92: 93: for my $m (@mutants) { 94: print $m->id, ': ', $m->description, "\n"; 95: } 96: 97: =head3 Arguments 98: 99: =over 4 100: 101: =item * C<$self> 102: 103: An instance of C<App::Test::Generator::Mutation::NumericBoundary>. 104: 105: =item * C<$doc> 106: 107: A L<PPI::Document> object representing the parsed source file to mutate. 108: The document is not modified by this method. 109: 110: =back 111: 112: =head3 Returns 113: 114: A list of L<App::Test::Generator::Mutant> objects, one per 115: (operator, flip) pair found in the document. Returns an empty list if no 116: qualifying comparison operators are found. 117: 118: Each mutant carries a C<transform> closure that when called with a fresh 119: L<PPI::Document> copy will replace the targeted operator with its flipped 120: equivalent, targeting the exact operator by line and column number to 121: ensure that multiple comparison operators on the same source line are each 122: mutated independently. 123: 124: =head3 Notes 125: 126: The following operators and their flips are supported: 127: 128: > flips to < >= <= 129: < flips to > <= >= 130: >= flips to > < <= 131: <= flips to < > >= 132: == flips to != 133: != flips to == 134: 135: Mutant IDs include line number, column number, and the flip target to 136: ensure uniqueness even when multiple operators share a source line. 137: 138: Each mutant's optional C<context> field is set to C<conditional> if 139: the operator sits inside (or is itself the keyword of) an 140: C<if>/C<unless>/C<while>/C<until> compound statement, or C<expression> 141: otherwise; its C<line_content> field holds the raw source text of the 142: mutated line. Both are consumed by 143: L<App::Test::Generator::Mutator>'s fast-mode dedup. 144: 145: =head3 API specification 146: 147: =head4 input 148: 149: { 150: self => { 151: type => OBJECT, 152: isa => 'App::Test::Generator::Mutation::NumericBoundary', 153: }, 154: doc => { 155: type => OBJECT, 156: isa => 'PPI::Document', 157: }, 158: } 159: 160: =head4 output 161: 162: { 163: type => ARRAYREF, 164: elements => { 165: type => OBJECT, 166: isa => 'App::Test::Generator::Mutant', 167: }, 168: } 169: 170: =cut 171: 172: sub mutate { ●173 → 179 → 252 173: my ($self, $doc) = @_; 174: 175: # Find all operator tokens in the document 176: my $ops = $doc->find('PPI::Token::Operator') || []; 177: my @mutants; 178: 179: for my $op (@{$ops}) { 180: my $original = $op->content(); 181: 182: # Only process comparison operators that have defined flips 183: next unless exists $FLIP{$original}; 184: 185: # Only mutate operators that are direct children of 186: # a condition or expression, not list arguments 187: my $parent = $op->parent(); 188: next unless $parent->isa('PPI::Statement') 189: || $parent->isa('PPI::Structure::Condition') 190: || $parent->isa('PPI::Structure::Block'); 191: 192: # Capture location so the transform closure targets the 193: # exact operator rather than the first match on that line 194: my $line = $op->location->[0]; 195: my $col = $op->location->[1]; 196: 197: # PPI always wraps a condition's content in a 198: # PPI::Statement::Expression, so the operator's immediate 199: # parent is never literally PPI::Structure::Condition -- 200: # use the shared ancestor-walking helper instead, as 201: # BooleanNegation and ReturnUndef do, to correctly detect 202: # operators inside if/unless/while/until conditions 203: my $context = $self->_in_conditional($op) ? 'conditional' : 'expression'; 204: 205: # Generate one mutant per flip of this operator 206: for my $change (@{ $FLIP{$original} }) { 207: # Build a unique id from location and the specific flip 208: # so multiple operators on the same line don't collide 209: my $id = "NUM_BOUNDARY_${line}_${col}_${change}"; 210: 211: my $mutant = eval { 212: App::Test::Generator::Mutant->new( 213: id => $id, 214: group => "NUM_BOUNDARY:$line", 215: description => "Numeric boundary flip $original to $change", 216: original => $original, 217: line => $line, 218: type => 'comparison', 219: context => $context, 220: line_content => $self->_line_content($doc, $line),
Mutants (Total: 1, Killed: 1, Survived: 0)
221:
Mutants (Total: 1, Killed: 1, Survived: 0)
222: # The transform closure captures line, col, original 223: # and change so it targets precisely the right operator 224: # in the document copy it receives at test time 225: transform => sub { 226: my $doc = $_[0]; 227: my $ops = $doc->find('PPI::Token::Operator') || []; 228: 229: for my $op (@{$ops}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
230: next unless $op->line_number == $line; 231: next unless $op->column_number == $col; 232: next unless $op->content eq $original; 233: 234: $op->set_content($change); 235: last; 236: } 237: }, 238: ); 239: }; 240: 241: # If the Mutant construction fails, report clearly rather than 242: # silently dropping the mutant from the results
Mutants (Total: 1, Killed: 1, Survived: 0)
243: if($@ || !$mutant) { 244: warn "Failed to construct mutant $id: $@" if $@; 245: next; 246: } 247: 248: push @mutants, $mutant; 249: } 250: } 251:
Mutants (Total: 2, Killed: 2, Survived: 0)
252: return @mutants; 253: } 254: 255: 1;