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

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 80.77%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 27

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.41';
   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.41
   20: 
   21: =head1 METHODS
   22: 
   23: =head2 applies_to
   24: 
   25: Return true if the given document contains at least one return
   26: statement this mutation strategy could mutate. Used by
   27: L<App::Test::Generator::Mutator> to pre-filter strategies before
   28: calling C<mutate>, so a document with nothing to mutate skips the
   29: walk entirely.
   30: 
   31:     my $applies = $mutation->applies_to($doc);
   32: 
   33: =head3 Arguments
   34: 
   35: =over 4
   36: 
   37: =item * C<$doc>
   38: 
   39: A L<PPI::Document> object to inspect.
   40: 
   41: =back
   42: 
   43: =head3 Returns
   44: 
   45: True if the document contains a C<return> statement (PPI::Statement::Break
   46: whose first token is C<return>), false otherwise.
   47: 
   48: =head3 API specification
   49: 
   50: =head4 input
   51: 
   52:     {
   53:         self => { type => OBJECT, isa => 'App::Test::Generator::Mutation::BooleanNegation' },
   54:         doc  => { type => OBJECT, isa => 'PPI::Document' },
   55:     }
   56: 
   57: =head4 output
   58: 
   59:     { type => SCALAR }
   60: 
   61: =cut
   62: 
   63: sub applies_to {
   64: 	my ($self, $doc) = @_;
   65: 

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

66: # PPI >= 1.270 classifies return as PPI::Statement::Break rather 67: # than PPI::Statement::Return -- scan the whole document for at 68: # least one qualifying return statement. This must match the 69: # document-level pre-filter contract used by Mutator::generate_mutants

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

70: # (and documented in Mutation::Base) rather than testing a single node, 71: # otherwise every call from generate_mutants would see $doc itself, 72: # which is never a PPI::Statement::Break, and mutate() would never run. 73: my $returns = $doc->find(sub { 74: my $node = $_[1]; 75: return 0 unless $node->isa('PPI::Statement::Break'); 76: my $first = $node->schild(0) or return 0; 77: return $first->content eq 'return'; 78: }) || []; 79: 80: return @{$returns} ? 1 : 0; 81: } 82: 83: =head2 mutate 84: 85: Walk a PPI document and generate one mutant for each return statement 86: whose expression can be negated. For example, C<return $ok> becomes 87: C<return !($ok)>. 88: 89: my $mutation = App::Test::Generator::Mutation::BooleanNegation->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::BooleanNegation>. 104: 105: =item * C<$doc> 106: 107: A L<PPI::Document> object representing the parsed source 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 qualifying 115: return statement found in the document. Returns an empty list if no 116: return statements with expressions are found. 117: 118: Each mutant carries a C<transform> closure that when called with a 119: fresh L<PPI::Document> copy will wrap the targeted return expression 120: in C<!( )>, negating its boolean value. 121: 122: =head3 Notes 123: 124: Mutant IDs include both line and column number to ensure uniqueness 125: when multiple return statements appear on different lines of the same 126: source file. 127: 128: Only return statements that have an expression child (i.e. not bare 129: C<return;> statements) are mutated. 130: 131: Each mutant's optional C<context> field is set to C<conditional> if 132: the return statement sits inside (or is itself the keyword of) an 133: C<if>/C<unless>/C<while>/C<until> compound statement, or C<statement> 134: otherwise; its C<line_content> field holds the raw source text of the 135: mutated line. Both are consumed by 136: L<App::Test::Generator::Mutator>'s fast-mode dedup. 137: 138: =head3 API specification 139: 140: =head4 input 141: 142: { 143: self => { 144: type => OBJECT, 145: isa => 'App::Test::Generator::Mutation::BooleanNegation', 146: }, 147: doc => { 148: type => OBJECT, 149: isa => 'PPI::Document', 150: }, 151: } 152: 153: =head4 output 154: 155: { 156: type => ARRAYREF, 157: elements => {

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

158: type => OBJECT, 159: isa => 'App::Test::Generator::Mutant', 160: }, 161: }

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

162: 163: =cut 164: 165: sub mutate { โ—166 โ†’ 184 โ†’ 271 166: my ($self, $doc) = @_; 167: 168: # PPI >= 1.270 classifies return statements as PPI::Statement::Break 169: # (alongside last/next/redo) rather than PPI::Statement::Return. 170: # Use a custom predicate to match only 'return' Break nodes. 171: my $returns = $doc->find(sub { 172: my $node = $_[1]; 173: # Must be a Break statement -- the parent class for return in 174: # newer PPI versions 175: return 0 unless $node->isa('PPI::Statement::Break'); 176: # Distinguish return from last/next/redo by checking the 177: # first significant child token 178: my $first = $node->schild(0) or return 0; 179: return $first->content eq 'return'; 180: }) || []; 181: 182: my @mutants; 183: 184: for my $ret (@{$returns}) { 185: # Skip bare return statements with no expression to negate, 186: # and bare returns with only a postfix conditional/loop 187: # modifier (return if $cond; has nothing to negate) 188: my @expr = _return_expr_span($ret); 189: next unless @expr; 190: 191: # Skip a lone structure node (e.g. return ($x, $y) gives a 192: # single PPI::Structure::List child) — wrapping it is not 193: # useful and there is nothing simple to splice around 194: next if @expr == 1 && !$expr[0]->isa('PPI::Token'); 195: 196: # Capture location so the transform closure targets the 197: # exact statement rather than the first match on that line 198: my $line = $ret->location->[0]; 199: my $col = $ret->location->[1]; 200: 201: # Build a unique ID from line and column so multiple return 202: # statements in the same file never collide 203: my $id = "BOOL_NEGATE_${line}_${col}"; 204: 205: my $mutant = eval { 206: App::Test::Generator::Mutant->new( 207: id => $id, 208: group => "BOOL_NEGATE:$line", 209: description => 'Negate boolean return expression', 210: original => $ret->content,

Mutants (Total: 2, Killed: 0, Survived: 2)
211: line => $line, 212: type => 'boolean', 213: context => $self->_in_conditional($ret) ? 'conditional' : 'statement',

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

214: line_content => $self->_line_content($doc, $line), 215: 216: # The transform closure captures line and col so it 217: # targets precisely the right return statement in the 218: # document copy it receives at test time 219: transform => sub {

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

220: my $doc = $_[0];

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

221: 222: # Locate all return statements in the fresh document copy using 223: # the same PPI::Statement::Break predicate as the outer find -- 224: # PPI >= 1.270 no longer uses PPI::Statement::Return 225: my $rets = $doc->find(sub { 226: my $node = $_[1]; 227: # Match Break nodes only -- covers return/last/next/redo 228: return 0 unless $node->isa('PPI::Statement::Break'); 229: # Filter to return specifically by inspecting the first token 230: my $first = $node->schild(0) or return 0; 231: return $first->content eq 'return'; 232: }) || []; 233: 234: for my $ret (@{$rets}) { 235: # Match by line and column to avoid mutating 236: # the wrong return statement 237: next unless $ret->line_number == $line; 238: next unless $ret->column_number == $col; 239: 240: # Skip bare returns with no expression 241: my @expr = _return_expr_span($ret); 242: last unless @expr; 243: 244: # Skip a lone structure node

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

245: last if @expr == 1 && !$expr[0]->isa('PPI::Token'); 246: 247: # Wrap the whole expression span in !(...) rather 248: # than just its first token -- $self->{x} is three 249: # significant children (Symbol, Operator, Structure) 250: # and wrapping only the leading $self produced the 251: # broken mutant 'return !($self)->{x};' 252: $expr[0]->insert_before(PPI::Token::Operator->new('!'));

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

253: $expr[0]->insert_before(PPI::Token::Structure->new('(')); 254: $expr[-1]->insert_after(PPI::Token::Structure->new(')')); 255: last; 256: } 257: }, 258: ); 259: }; 260: 261: # If the Mutant construction fails, report clearly rather than 262: # silently dropping the mutant from the results 263: if($@ || !$mutant) { 264: warn "Failed to construct mutant $id: $@" if $@; 265: next; 266: } 267: 268: push @mutants, $mutant; 269: } 270: 271: return @mutants; 272: } 273: 274: # -------------------------------------------------- 275: # Purpose: identify the PPI elements making up the expression 276: # being returned by a 'return' statement, excluding 277: # the leading 'return' keyword, the trailing statement 278: # terminator, and any postfix conditional/loop modifier 279: # (if/unless/while/until/for/foreach) and its condition. 280: # Entry: a PPI::Statement::Break node already confirmed to be 281: # a 'return' statement. 282: # Exit: a list of the significant child elements making up 283: # the return expression, or an empty list for a bare 284: # return (with or without a postfix modifier). 285: # Side effects: none. 286: # -------------------------------------------------- 287: sub _return_expr_span { โ—288 โ†’ 293 โ†’ 297 288: my ($ret) = @_; 289: 290: my @children = $ret->schildren; 291: shift @children; 292: 293: if(@children && $children[-1]->isa('PPI::Token::Structure') && $children[-1]->content eq ';') { 294: pop @children; 295: } 296: โ—297 โ†’ 297 โ†’ 305 297: for my $i (0 .. $#children) { 298: my $child = $children[$i]; 299: next unless $child->isa('PPI::Token::Word'); 300: next unless $child->content =~ /^(?:if|unless|while|until|for|foreach)$/; 301: @children = @children[0 .. $i - 1]; 302: last; 303: } 304: 305: return @children; 306: } 307: 308: =head1 AUTHOR 309: 310: Nigel Horne, C<< <njh at nigelhorne.com> >> 311: 312: =head1 LICENCE AND COPYRIGHT 313: 314: Copyright 2026 Nigel Horne. 315: 316: Usage is subject to licence terms. 317: 318: The licence terms of this software are as follows: 319: 320: =over 4 321: 322: =item * Personal single user, single computer use: GPL2 323: 324: =item * All other users (including Commercial, Charity, Educational, 325: Government) must apply in writing for a licence for use from Nigel Horne 326: at the above e-mail. 327: 328: =back 329: 330: =cut 331: 332: 1;