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

Structural Coverage (Approximate)

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

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::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;