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

Structural Coverage (Approximate)

TER1 (Statement): 95.31%
TER2 (Branch): 45.83%
TER3 (LCSAJ): 100.0% (6/6)
Approximate LCSAJ segments: 25

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.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 → 5442 → 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 → 224141 → 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;