TER1 (Statement): 100.00%
TER2 (Branch): 80.77%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 27
โ 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::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,
211: line => $line, 212: type => 'boolean', 213: context => $self->_in_conditional($ret) ? 'conditional' : 'statement',Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_210_7: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_210_7: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );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;