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

Structural Coverage (Approximate)

TER1 (Statement): 96.36%
TER2 (Branch): 60.71%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 29

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::BooleanNegation;
    2: 
    3: use strict;
    4: use warnings;
    5: 
    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 NAME
   13: 
   14: App::Test::Generator::Mutation::BooleanNegation - Negate boolean return
   15: expressions to expose missing assertion coverage
   16: 
   17: =head1 VERSION
   18: 
   19: Version 0.36
   20: 
   21: =head1 METHODS
   22: 
   23: =head2 applies_to
   24: 
   25: Return true if this mutation strategy applies to the given PPI node.
   26: Used by the mutation framework to pre-filter nodes before calling
   27: C<mutate>.
   28: 
   29:     my $applies = $mutation->applies_to($node);
   30: 
   31: =head3 Arguments
   32: 
   33: =over 4
   34: 
   35: =item * C<$node>
   36: 
   37: A L<PPI::Element> node to test.
   38: 
   39: =back
   40: 
   41: =head3 Returns
   42: 
   43: True if the node is a C<PPI::Statement::Return>, false otherwise.
   44: 
   45: =head3 API specification
   46: 
   47: =head4 input
   48: 
   49:     {
   50:         self => { type => OBJECT, isa => 'App::Test::Generator::Mutation::BooleanNegation' },
   51:         node => { type => OBJECT, isa => 'PPI::Element' },
   52:     }
   53: 
   54: =head4 output
   55: 
   56:     { type => SCALAR }
   57: 
   58: =cut
   59: 
   60: sub applies_to {
   61: 	my ($self, $node) = @_;
   62: 
   63: 	# PPI >= 1.270 classifies return as PPI::Statement::Break
   64: 	# rather than PPI::Statement::Return
   65: 	return 0 unless $node->isa('PPI::Statement::Break');

Mutants (Total: 2, Killed: 2, Survived: 0)

66: 67: # Must specifically be a return statement, not last/next/redo 68: my $first = $node->schild(0) or return 0; 69: return $first->content eq 'return';

Mutants (Total: 2, Killed: 2, Survived: 0)

70: } 71: 72: =head2 mutate 73: 74: Walk a PPI document and generate one mutant for each return statement 75: whose expression can be negated. For example, C<return $ok> becomes 76: C<return !($ok)>. 77: 78: my $mutation = App::Test::Generator::Mutation::BooleanNegation->new; 79: my $doc = PPI::Document->new(\$source); 80: my @mutants = $mutation->mutate($doc); 81: 82: for my $m (@mutants) { 83: print $m->id, ': ', $m->description, "\n"; 84: } 85: 86: =head3 Arguments 87: 88: =over 4 89: 90: =item * C<$self> 91: 92: An instance of C<App::Test::Generator::Mutation::BooleanNegation>. 93: 94: =item * C<$doc> 95: 96: A L<PPI::Document> object representing the parsed source to mutate. 97: The document is not modified by this method. 98: 99: =back 100: 101: =head3 Returns 102: 103: A list of L<App::Test::Generator::Mutant> objects, one per qualifying 104: return statement found in the document. Returns an empty list if no 105: return statements with expressions are found. 106: 107: Each mutant carries a C<transform> closure that when called with a 108: fresh L<PPI::Document> copy will wrap the targeted return expression 109: in C<!( )>, negating its boolean value. 110: 111: =head3 Notes 112: 113: Mutant IDs include both line and column number to ensure uniqueness 114: when multiple return statements appear on different lines of the same 115: source file. 116: 117: Only return statements that have an expression child (i.e. not bare 118: C<return;> statements) are mutated. 119: 120: =head3 API specification 121: 122: =head4 input 123: 124: { 125: self => { 126: type => OBJECT, 127: isa => 'App::Test::Generator::Mutation::BooleanNegation', 128: }, 129: doc => { 130: type => OBJECT, 131: isa => 'PPI::Document', 132: }, 133: } 134: 135: =head4 output 136: 137: { 138: type => ARRAYREF, 139: elements => { 140: type => OBJECT, 141: isa => 'App::Test::Generator::Mutant', 142: }, 143: } 144: 145: =cut 146: 147: sub mutate { โ—148 โ†’ 166 โ†’ 252โ—148 โ†’ 166 โ†’ 0 148: my ($self, $doc) = @_; 149: 150: # PPI >= 1.270 classifies return statements as PPI::Statement::Break 151: # (alongside last/next/redo) rather than PPI::Statement::Return. 152: # Use a custom predicate to match only 'return' Break nodes. 153: my $returns = $doc->find(sub { 154: my $node = $_[1]; 155: # Must be a Break statement -- the parent class for return in 156: # newer PPI versions 157: return 0 unless $node->isa('PPI::Statement::Break');

Mutants (Total: 2, Killed: 2, Survived: 0)

158: # Distinguish return from last/next/redo by checking the 159: # first significant child token 160: my $first = $node->schild(0) or return 0; 161: return $first->content eq 'return';

Mutants (Total: 2, Killed: 2, Survived: 0)

162: }) || []; 163: 164: my @mutants; 165: 166: for my $ret (@{$returns}) { 167: # Skip bare return statements with no expression to negate. 168: # Also skip if the only child after 'return' is a semicolon — 169: # PPI may include the statement terminator as a significant child 170: my $expr = $ret->schild(1) or next; 171: next if $expr->isa('PPI::Token::Structure') && $expr->content eq ';'; 172: 173: # Skip structure nodes (e.g. return ($x, $y) gives a 174: # PPI::Structure::List) — set_content only exists on tokens 175: next unless $expr->isa('PPI::Token'); 176: 177: # Skip postfix conditionals — wrapping 'unless ...' in !() is invalid syntax 178: next if $expr->isa('PPI::Token::Word') && $expr->content =~ /^(?:if|unless|while|until|for|foreach)$/; 179: 180: # Capture location so the transform closure targets the 181: # exact statement rather than the first match on that line 182: my $line = $ret->location->[0]; 183: my $col = $ret->location->[1]; 184: 185: # Build a unique ID from line and column so multiple return 186: # statements in the same file never collide 187: my $id = "BOOL_NEGATE_${line}_${col}"; 188: 189: my $mutant = eval { 190: App::Test::Generator::Mutant->new( 191: id => $id, 192: group => "BOOL_NEGATE:$line", 193: description => 'Negate boolean return expression', 194: original => $ret->content, 195: line => $line, 196: type => 'boolean', 197: 198: # The transform closure captures line and col so it 199: # targets precisely the right return statement in the 200: # document copy it receives at test time 201: transform => sub { 202: my $doc = $_[0]; 203: 204: # Locate all return statements in the fresh document copy using 205: # the same PPI::Statement::Break predicate as the outer find -- 206: # PPI >= 1.270 no longer uses PPI::Statement::Return 207: my $rets = $doc->find(sub { 208: my $node = $_[1]; 209: # Match Break nodes only -- covers return/last/next/redo 210: return 0 unless $node->isa('PPI::Statement::Break');

Mutants (Total: 2, Killed: 0, Survived: 2)
211: # Filter to return specifically by inspecting the first token 212: my $first = $node->schild(0) or return 0; 213: return $first->content eq 'return';

Mutants (Total: 2, Killed: 2, Survived: 0)

214: }) || []; 215: 216: for my $ret (@{$rets}) { 217: # Match by line and column to avoid mutating 218: # the wrong return statement 219: next unless $ret->line_number == $line;

Mutants (Total: 1, Killed: 1, Survived: 0)

220: next unless $ret->column_number == $col;

Mutants (Total: 1, Killed: 1, Survived: 0)

221: 222: # Skip bare returns with no expression 223: my $expr = $ret->schild(1) or last; 224: 225: # Skip bare semicolon 226: next if $expr->isa('PPI::Token::Structure') && $expr->content eq ';'; 227: 228: # Skip structure nodes — set_content only exists on tokens 229: next unless $expr->isa('PPI::Token'); 230: 231: # Skip postfix conditionals — wrapping 'unless ...' in !() is invalid syntax 232: next if $expr->isa('PPI::Token::Word') && $expr->content =~ /^(?:if|unless|while|until|for|foreach)$/; 233: 234: my $content = $expr->content(); 235: $expr->set_content("!($content)"); 236: last; 237: } 238: }, 239: ); 240: }; 241: 242: # If the Mutant construction fails, report clearly rather than 243: # silently dropping the mutant from the results 244: if($@ || !$mutant) {

Mutants (Total: 1, Killed: 1, Survived: 0)

245: warn "Failed to construct mutant $id: $@" if $@; 246: next; 247: } 248: 249: push @mutants, $mutant; 250: } 251: โ—252 โ†’ 252 โ†’ 0 252: return @mutants;

Mutants (Total: 2, Killed: 2, Survived: 0)

253: } 254: 255: =head1 AUTHOR 256: 257: Nigel Horne, C<< <njh at nigelhorne.com> >> 258: 259: =head1 LICENCE AND COPYRIGHT 260: 261: Copyright 2026 Nigel Horne. 262: 263: Usage is subject to licence terms. 264: 265: The licence terms of this software are as follows: 266: 267: =over 4 268: 269: =item * Personal single user, single computer use: GPL2 270: 271: =item * All other users (including Commercial, Charity, Educational, 272: Government) must apply in writing for a licence for use from Nigel Horne 273: at the above e-mail. 274: 275: =back 276: 277: =cut 278: 279: 1;