lib/App/Test/Generator/Mutator.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 90.38%
TER3 (LCSAJ): 100.0% (4/4)
Approximate LCSAJ segments: 53

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::Mutator;
    2: 
    3: use 5.036;
    4: use autodie qw(:io);    # covers open/close/read/write; excludes system (which legitimately fails)
    5: use Carp qw(croak);
    6: use Config;
    7: use File::Copy::Recursive qw(dircopy);
    8: use File::Spec;
    9: use File::Temp qw(tempdir);
   10: use PPI;
   11: use Readonly;
   12: 
   13: use App::Test::Generator::Mutation::BooleanNegation;
   14: use App::Test::Generator::Mutation::ConditionalInversion;
   15: use App::Test::Generator::Mutation::NumericBoundary;
   16: use App::Test::Generator::Mutation::ReturnUndef;
   17: 
   18: # --------------------------------------------------
   19: # Valid mutation level values
   20: # --------------------------------------------------
   21: Readonly my $LEVEL_FULL => 'full';
   22: Readonly my $LEVEL_FAST => 'fast';
   23: 
   24: # --------------------------------------------------
   25: # Default values for optional constructor arguments
   26: # --------------------------------------------------
   27: Readonly my $DEFAULT_LIB_DIR        => 'lib';
   28: Readonly my $DEFAULT_MUTATION_LEVEL => $LEVEL_FULL;
   29: 
   30: # --------------------------------------------------
   31: # Error message constants — named so test code can
   32: # match against them without duplicating the literal
   33: # --------------------------------------------------
   34: Readonly my $ERR_FILE_REQUIRED     => 'file required';
   35: Readonly my $ERR_WORKSPACE_NOT_SET => 'Workspace not prepared -- call prepare_workspace first';
   36: Readonly my $ERR_RELATIVE_NOT_SET  => 'Relative path not set -- call prepare_workspace first';
   37: 
   38: our $VERSION = '0.46';
   39: 
   40: =head1 NAME
   41: 
   42: App::Test::Generator::Mutator - Generate and apply mutation tests
   43: 
   44: =head1 VERSION
   45: 
   46: Version 0.46
   47: 
   48: =head1 SYNOPSIS
   49: 
   50:     use App::Test::Generator::Mutator;
   51: 
   52:     my $mutator = App::Test::Generator::Mutator->new(
   53:         file           => 'lib/My/Module.pm',
   54:         lib_dir        => 'lib',
   55:         mutation_level => 'fast',
   56:     );
   57: 
   58:     my $mutants = $mutator->generate_mutants();
   59:     printf "Generated %d mutants\n", scalar @{$mutants};
   60: 
   61:     my $workspace = $mutator->prepare_workspace();
   62: 
   63:     for my $m (@{$mutants}) {
   64:         $mutator->apply_mutant($m);
   65:         if($mutator->run_tests()) {
   66:             print "SURVIVED: ${\$m->description}\n";
   67:         } else {
   68:             print "KILLED:   ${\$m->description}\n";
   69:         }
   70:     }
   71: 
   72: =head1 DESCRIPTION
   73: 
   74: B<App::Test::Generator::Mutator> is a mutation engine that programmatically
   75: alters Perl source files to evaluate the effectiveness of a project's test
   76: suite. It analyses modules, generates systematic code mutations (such as
   77: conditional inversions, logical operator changes, and numeric boundary
   78: flips), and applies them within an isolated workspace so tests can be
   79: executed safely against each modified variant.
   80: 
   81: By tracking which mutants are killed (cause tests to fail) versus those that
   82: survive (tests still pass), the module enables calculation of a mutation
   83: score, providing a quantitative measure of how well the test suite detects
   84: unintended behavioural changes.
   85: 
   86: =head2 new
   87: 
   88: Construct a new Mutator for a given source file.
   89: 
   90:     my $mutator = App::Test::Generator::Mutator->new(
   91:         file           => 'lib/My/Module.pm',
   92:         lib_dir        => 'lib',
   93:         mutation_level => 'full',
   94:     );
   95: 
   96: =head3 Arguments
   97: 
   98: =over 4
   99: 
  100: =item * C<file>
  101: 
  102: Path to the Perl source file to mutate. Required. Must exist on disk.
  103: 
  104: =item * C<lib_dir>
  105: 
  106: Root library directory. Optional - defaults to C<lib>.
  107: 
  108: =item * C<mutation_level>
  109: 
  110: Controls the breadth of mutation. C<full> applies all mutations;
  111: C<fast> deduplicates and removes redundant mutants first.
  112: Optional - defaults to C<full>.
  113: 
  114: =back
  115: 
  116: =head3 Returns
  117: 
  118: A blessed hashref. Croaks if C<file> is missing or does not exist.
  119: 
  120: =head3 API specification
  121: 
  122: =head4 input
  123: 
  124:     {
  125:         file           => { type => SCALAR },
  126:         lib_dir        => { type => SCALAR, optional => 1 },
  127:         mutation_level => { type => SCALAR, optional => 1 },
  128:     }
  129: 
  130: =head4 output
  131: 
  132:     {
  133:         type => OBJECT,
  134:         isa  => 'App::Test::Generator::Mutator',
  135:     }
  136: 
  137: =head3 EXAMPLE
  138: 
  139:     my $m = App::Test::Generator::Mutator->new(
  140:         file           => 'lib/Acme/Widget.pm',
  141:         mutation_level => 'fast',
  142:     );
  143: 
  144: =head3 MESSAGES
  145: 
  146: =over 4
  147: 
  148: =item C<file required>
  149: 
  150: C<file> was not supplied.
  151: 
  152: =item C<< file not found: PATH >>
  153: 
  154: C<file> was supplied but does not exist on disk.
  155: 
  156: =back
  157: 
  158: =head3 FORMAL SPECIFICATION
  159: 
  160: Pre:  C<file> is defined ∧ C<-f file>
  161: 
  162: Post: C<ref(result) eq 'App::Test::Generator::Mutator'>
  163:       ∧ C<result-E<gt>{file} eq file>
  164:       ∧ C<result-E<gt>{mutation_level} ∈ {full, fast}>
  165: 
  166: =cut
  167: 
  168: sub new {
  169: 	my ($class, %args) = @_;
  170: 
  171: 	# file is required and must exist on disk
  172: 	croak $ERR_FILE_REQUIRED unless defined $args{file};
  173: 	croak "file not found: $args{file}" unless -f $args{file};
  174: 
  175: 	return bless {

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

176: file => $args{file}, 177: lib_dir => $args{lib_dir} || $DEFAULT_LIB_DIR, 178: mutation_level => $args{mutation_level} || $DEFAULT_MUTATION_LEVEL, 179: 180: # Instantiate all registered mutation strategies 181: mutations => [ 182: App::Test::Generator::Mutation::BooleanNegation->new(), 183: App::Test::Generator::Mutation::ReturnUndef->new(), 184: App::Test::Generator::Mutation::NumericBoundary->new(), 185: App::Test::Generator::Mutation::ConditionalInversion->new(), 186: ], 187: }, $class; 188: } 189: 190: =head2 generate_mutants 191: 192: Parse the target file and generate all mutants by running each registered 193: mutation strategy against the PPI document. 194: 195: my $mutants = $mutator->generate_mutants(); # scalar context → arrayref 196: for my $m (@{$mutants}) { ... } 197: 198: my @mutants = $mutator->generate_mutants(); # list context → flat list (backward-compat) 199: 200: =head3 Arguments 201: 202: None beyond C<$self>. 203: 204: =head3 Returns 205: 206: An arrayref of L<App::Test::Generator::Mutant> objects. In C<fast> mode, 207: redundant and duplicate mutants are removed before returning. 208: Lines within C<## MUTANT_SKIP_BEGIN> / C<## MUTANT_SKIP_END> annotation 209: blocks are excluded from the candidate list entirely. 210: After this method returns, 211: C<$self-E<gt>{skip_lines}> contains a hashref mapping excluded 212: line numbers to 1. 213: 214: =head3 API specification 215: 216: =head4 input 217: 218: { 219: self => { type => OBJECT, isa => 'App::Test::Generator::Mutator' }, 220: } 221: 222: =head4 output 223: 224: { 225: type => ARRAYREF, 226: elements => { type => OBJECT, isa => 'App::Test::Generator::Mutant' }, 227: } 228: 229: =head3 EXAMPLE 230: 231: my $mutants = $mutator->generate_mutants(); 232: printf "%d mutants generated\n", scalar @{$mutants}; 233: 234: for my $m (@{$mutants}) { 235: printf "line %d: %s\n", $m->line, $m->description; 236: } 237: 238: =head3 MESSAGES 239: 240: =over 4 241: 242: =item C<< Unable to parse FILE >> 243: 244: PPI could not parse the source file (syntax error or unreadable file). 245: FILE is the path passed to C<new>. 246: 247: =item C<< FILE: MUTANT_SKIP_BEGIN at line N with no prior MUTANT_SKIP_END >> 248: 249: A C<## MUTANT_SKIP_BEGIN> marker was found while already inside a skip block. 250: 251: =item C<< FILE: MUTANT_SKIP_END at line N with no matching MUTANT_SKIP_BEGIN >> 252: 253: A C<## MUTANT_SKIP_END> marker was found with no preceding C<## MUTANT_SKIP_BEGIN>. 254: 255: =item C<< FILE: MUTANT_SKIP_BEGIN at line N has no matching MUTANT_SKIP_END >> 256: 257: The source file ended while still inside a skip block. 258: 259: =back 260: 261: =head3 FORMAL SPECIFICATION 262: 263: Pre: C<prepare_workspace> need not have been called before this method. 264: 265: Post: C<ref(result) eq 'ARRAY'> 266: ∧ C<∀ m ∈ result: ref(m) eq 'App::Test::Generator::Mutant'> 267: ∧ C<∀ m ∈ result: ¬ skip_lines{m->line}> 268: 269: =head3 PSEUDOCODE 270: 271: parse file with PPI 272: scan lines for MUTANT_SKIP_BEGIN / MUTANT_SKIP_END pairs → skip_lines 273: for each registered mutation strategy: 274: skip if strategy does not apply_to(doc) 275: for each mutant from strategy->mutate(doc): 276: include unless mutant.line ∈ skip_lines 277: if mutation_level == 'fast': 278: deduplicate and remove redundant mutants 279: return arrayref (or flat list in list context) 280: 281: =cut 282: 283: sub generate_mutants { 284 → 295 → 317 284: my $self = $_[0]; 285: 286: # Parse the target file into a PPI document 287: my $doc = PPI::Document->new($self->{file}) or croak "Unable to parse $self->{file}"; 288: 289: # Build set of lines excluded by ## MUTANT_SKIP_BEGIN / ## MUTANT_SKIP_END 290: my %skip_lines; 291: my $in_skip = 0; 292: my $skip_start = 0; 293: my $line_num = 0; 294: 295: for my $line (split /\n/, $doc->serialize()) { 296: $line_num++; 297: 298: # Match only lines where the annotation is the entire content — 299: # prevents false positives in comments or POD that mention the tag

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

300: if($line =~ /^\s*##\s*MUTANT_SKIP_BEGIN\s*$/) { 301: croak "$self->{file}: MUTANT_SKIP_BEGIN at line $line_num with no prior MUTANT_SKIP_END" 302: if $in_skip; 303: $in_skip = 1; 304: $skip_start = $line_num; 305: } 306: $skip_lines{$line_num} = 1 if $in_skip; 307: 308: # Match only lines where the annotation is the entire content — 309: # prevents false positives in comments or POD that mention the tag

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

310: if($line =~ /^\s*##\s*MUTANT_SKIP_END\s*$/) { 311: croak "$self->{file}: MUTANT_SKIP_END at line $line_num with no matching MUTANT_SKIP_BEGIN" 312: unless $in_skip; 313: $in_skip = 0; 314: } 315: } 316: # Unclosed MUTANT_SKIP_BEGIN is fatal 317 → 328 → 336 317: croak "$self->{file}: MUTANT_SKIP_BEGIN at line $skip_start has no matching MUTANT_SKIP_END" if $in_skip; 318: 319: # Store skip lines for use by the report generator 320: $self->{skip_lines} = \%skip_lines; 321: 322: my @mutants; 323: 324: # Run each registered mutation strategy against the document, 325: # excluding any candidates on skip-annotated lines. applies_to() 326: # is a cheap pre-filter -- skip the mutate() walk entirely for 327: # strategies that have nothing to match in this document. 328: for my $mutation (@{$self->{mutations}}) { 329: next unless $mutation->applies_to($doc); 330: push @mutants, grep { !$skip_lines{$_->line} } $mutation->mutate($doc); 331: } 332: 333: # In fast mode deduplicate and remove redundant mutants before returning. 334: # Returns arrayref in scalar context and a flat list in list context so 335: # existing callers (my @m = generate_mutants()) continue to work unchanged. 336: my $result = $self->{mutation_level} eq $LEVEL_FAST 337: ? _dedup_mutants(\@mutants) 338: : \@mutants; 339:

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

340: return wantarray ? @{$result} : $result; 341: } 342: 343: =head2 prepare_workspace 344: 345: Prepare an isolated temporary workspace for a single mutation test run. 346: 347: The entire C<lib_dir> tree is copied into the workspace so that all module 348: dependencies resolve correctly when the test suite runs against the mutant. 349: Only after this copy is complete is the single target file overwritten by 350: C<apply_mutant>. 351: 352: my $workspace = $mutator->prepare_workspace(); 353: $mutator->apply_mutant($mutant); 354: local $ENV{PERL5LIB} = "$workspace/lib"; 355: my $survived = (system('prove', 't') == 0); 356: 357: =head3 Arguments 358: 359: None beyond C<$self>. 360: 361: =head3 Returns 362: 363: A string containing the absolute path to the temporary directory created. 364: The directory is automatically removed when the object goes out of scope 365: via L<File::Temp>'s C<CLEANUP =E<gt> 1> behaviour. 366: 367: =head3 Side effects 368: 369: Creates a temporary directory. Recursively copies C<lib_dir> into it. 370: Sets C<< $self->{_workspace} >>, C<< $self->{_relative} >>, and 371: C<< $self->{_lib_basename} >>. Does not modify C<< $self->{lib_dir} >>. 372: 373: =head3 Notes 374: 375: Call C<prepare_workspace> once per file, then C<apply_mutant> once per 376: mutant within that file. Do not store the returned path beyond the 377: lifetime of the enclosing scope. 378: 379: =head3 API specification 380: 381: =head4 input 382: 383: { 384: self => { type => OBJECT, isa => 'App::Test::Generator::Mutator' }, 385: } 386: 387: =head4 output 388: 389: { 390: type => SCALAR, 391: } 392: 393: =head3 EXAMPLE 394: 395: my $workspace = $mutator->prepare_workspace(); 396: # workspace is an absolute temp dir path 397: # original lib_dir value is unchanged 398: printf "original lib_dir still: %s\n", $mutator->{lib_dir}; 399: 400: =head3 MESSAGES 401: 402: =over 4 403: 404: =item C<dircopy failed: $!> 405: 406: The C<lib_dir> tree could not be copied into the temporary workspace directory, 407: usually a permissions error. 408: 409: =back 410: 411: =head3 FORMAL SPECIFICATION 412: 413: Pre: C<-d self-E<gt>{lib_dir}> 414: ∧ C<self-E<gt>{file}> begins with C<self-E<gt>{lib_dir}> 415: 416: Post: C<-d result> 417: ∧ C<self-E<gt>{_workspace} eq result> 418: ∧ C<self-E<gt>{lib_dir}> unchanged 419: 420: =cut 421: 422: sub prepare_workspace { 423: my $self = $_[0]; 424: 425: # Create a self-cleaning temporary directory 426: my $tmp = tempdir(CLEANUP => 1); 427: 428: # Normalise lib_dir to its final component so workspace paths 429: # are relative regardless of whether an absolute path was passed in 430: my $lib_basename = (File::Spec->splitdir($self->{lib_dir}))[-1]; 431: 432: # Derive the file's path relative to lib_dir for use by apply_mutant 433: my $relative = $self->{file}; 434: $relative =~ s/^\Q$self->{lib_dir}\E\/?//; 435: 436: # Copy the entire lib tree so all dependencies resolve in the workspace 437: dircopy($self->{lib_dir}, File::Spec->catfile($tmp, $lib_basename)) or croak "dircopy failed: $!"; 438: 439: # Store normalised state under private keys — do NOT mutate lib_dir, 440: # which callers may inspect after this call expecting the original value. 441: $self->{_workspace} = $tmp; 442: $self->{_relative} = $relative; 443: $self->{_lib_basename} = $lib_basename; 444:

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

445: return $tmp; 446: } 447: 448: =head2 apply_mutant 449: 450: Apply a single mutant's transform to the target file in the workspace. 451: 452: $mutator->apply_mutant($mutant); 453: 454: =head3 Arguments 455: 456: =over 4 457: 458: =item * C<$mutant> 459: 460: An L<App::Test::Generator::Mutant> object whose C<transform> closure 461: will be applied to the workspace copy of the target file. 462: 463: =back 464: 465: =head3 Returns 466: 467: Nothing. Modifies the workspace copy of the target file in place. 468: 469: =head3 Side effects 470: 471: Overwrites the target file in the workspace with the mutated version. 472: 473: =head3 API specification 474: 475: =head4 input 476: 477: { 478: self => { type => OBJECT, isa => 'App::Test::Generator::Mutator' }, 479: mutant => { type => OBJECT, isa => 'App::Test::Generator::Mutant' }, 480: } 481: 482: =head4 output 483: 484: { type => UNDEF } 485: 486: =head3 EXAMPLE 487: 488: $mutator->prepare_workspace(); 489: for my $m (@{$mutants}) { 490: $mutator->apply_mutant($m); 491: # workspace file is now mutated; run tests against it 492: } 493: 494: =head3 MESSAGES 495: 496: =over 4 497: 498: =item C<Workspace not prepared -- call prepare_workspace first> 499: 500: C<apply_mutant> was called before C<prepare_workspace>. 501: 502: =item C<Relative path not set -- call prepare_workspace first> 503: 504: Internal: the relative-path field was not set by C<prepare_workspace>. 505: 506: =item C<< Failed to parse TARGET >> 507: 508: PPI could not parse the workspace copy of the target file. 509: 510: =back 511: 512: =head3 FORMAL SPECIFICATION 513: 514: Pre: C<self-E<gt>{_workspace}> is defined ∧ C<self-E<gt>{_relative}> is defined 515: ∧ C<ref(mutant-E<gt>{transform}) eq 'CODE'> 516: 517: Post: workspace copy of target file contains the mutated content 518: 519: =cut 520: 521: sub apply_mutant { 522: my ($self, $mutant) = @_; 523: 524: # Workspace must be prepared before applying any mutant 525: my $workspace = $self->{_workspace} 526: or croak $ERR_WORKSPACE_NOT_SET; 527: 528: my $relative = $self->{_relative} 529: or croak $ERR_RELATIVE_NOT_SET; 530: 531: # Construct the full path to the file in the workspace 532: my $target = File::Spec->catfile( 533: $workspace, 534: $self->{_lib_basename}, 535: $relative, 536: ); 537: 538: # Parse the workspace copy and apply the mutation transform 539: my $doc = PPI::Document->new($target) or croak "Failed to parse $target"; 540: 541: $mutant->transform->($doc); 542: 543: $doc->save($target); 544: } 545: 546: =head2 run_tests 547: 548: Run the test suite against the current workspace and return whether all 549: tests passed. 550: 551: my $survived = $mutator->run_tests(); 552: 553: =head3 Arguments 554: 555: None beyond C<$self>. 556: 557: =head3 Returns 558: 559: 1 if all tests passed (mutant survived), 0 if any test failed (mutant 560: killed). 561: 562: =head3 Side effects

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

563: 564: Executes an external process running the test suite. 565: 566: =head3 Notes 567: 568: Uses C<prove> found on PATH. Sets C<PERL5LIB> to include the workspace 569: lib directory before running. 570: 571: =head3 API specification 572: 573: =head4 input 574: 575: { 576: self => { type => OBJECT, isa => 'App::Test::Generator::Mutator' }, 577: } 578: 579: =head4 output 580: 581: { type => SCALAR } 582: 583: =head3 EXAMPLE 584: 585: my $survived = $mutator->run_tests(); 586: if($survived) { 587: print "mutant survived\n"; 588: } else { 589: print "mutant killed\n"; 590: } 591: 592: =head3 FORMAL SPECIFICATION 593: 594: Post: C<result ∈ {0, 1}> 595: ∧ C<result == 1> ⟺ all tests in C<t/> passed against current C<lib/> 596: 597: =cut 598: 599: sub run_tests { 600: my $self = $_[0]; 601:

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

602: # Derive prove from $^X so CPAN smokers that install to a non-PATH 603: # location still resolve the correct perl/prove pair. Config{bin} 604: # is a reliable fallback; bare 'prove' is last resort only. 605: my ($vol, $dir) = File::Spec->splitpath($^X); 606: my $prove = File::Spec->catpath($vol, $dir, 'prove'); 607: $prove = File::Spec->catfile($Config{bin}, 'prove') unless -x $prove; 608: $prove = 'prove' unless -x $prove; 609: 610: return system($prove, '-l', 't') == 0; 611: } 612: 613: # -------------------------------------------------- 614: # _dedup_mutants 615: # 616: # Purpose: Remove duplicate and redundant mutants 617: # from a list, used in fast mutation mode 618: # to reduce the number of mutants to run. 619: # 620: # Entry: $mutants - arrayref of Mutant objects. 621: # 622: # Exit: Returns an arrayref of deduplicated 623: # Mutant objects. 624: # 625: # Side effects: None. 626: # 627: # Notes: Deduplication key uses line, original, 628: # and description rather than the transform

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

629: # coderef, which is not stable as a string.

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

630: # -------------------------------------------------- 631: sub _dedup_mutants { 632 → 636 → 649 632: my ($mutants) = @_; 633: my @rc;

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

634: my %seen;

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

635: 636: for my $m (@{$mutants}) { 637: # Build a stable key from metadata — not from the coderef 638: my $key = join '|',

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

639: $m->line // '', 640: $m->original // '', 641: $m->description // '';

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

642: 643: next if $seen{$key}++;

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

644: next if _is_redundant_mutation($m); 645: 646: push @rc, $m; 647: } 648: 649: return \@rc; 650: } 651: 652: # -------------------------------------------------- 653: # _is_redundant_mutation 654: # 655: # Return true if a mutant is considered 656: # redundant and should be skipped in fast 657: # mutation mode. 658: # 659: # Entry: $m - a Mutant object. 660: # 661: # Exit: Returns 1 if redundant, 0 otherwise. 662: # 663: # Notes: Checks for arithmetic no-ops, double 664: # negation inside conditionals, boolean 665: # literal flips, mutations inside comments, 666: # and equivalent numeric comparisons. 667: # Does not compare transform coderefs — 668: # they are not meaningful as strings. 669: # -------------------------------------------------- 670: sub _is_redundant_mutation { 671 → 681 → 686 671: my ($m) = @_; 672: 673: my $orig = $m->original // ''; 674: 675: # Arithmetic no-ops add nothing to mutation coverage 676: return 1 if $orig =~ /\+\s*0$/; 677: return 1 if $orig =~ /-\s*0$/; 678: 679: # Double negation inside conditionals forces boolean context 680: # in Perl and is not a meaningful mutation 681: if($m->context && $m->context eq 'conditional') { 682: return 1 if $orig =~ /^\!\!/; 683: } 684: 685: # Boolean literal flip on a standalone 1 or 0 is trivial 686: return 1 if $orig =~ /^\s*(?:1|0)\s*$/; 687: 688: # Mutations inside comments are unreachable code 689: return 1 if $m->line_content && $m->line_content =~ /^\s*#/; 690: 691: return 0; 692: } 693: 694: =head1 COMMON PITFALLS 695: 696: =over 4 697: 698: =item Calling C<apply_mutant> before C<prepare_workspace> 699: 700: C<apply_mutant> requires C<prepare_workspace> to have been called first. The 701: workspace holds the isolated copy of C<lib/> that receives mutations. 702: 703: =item Passing an absolute path as C<lib_dir> 704: 705: C<lib_dir> must be a B<relative> path (e.g. C<lib>). An absolute path causes 706: C<apply_mutant> to construct a doubled directory under the workspace and then 707: fail to find the target file. 708: 709: =item Checking C<< $mutator->{workspace} >> after the refactor 710: 711: Internal workspace state is stored in C<_workspace>, C<_relative>, and 712: C<_lib_basename> (note the underscore prefix). The original C<lib_dir> value 713: is never overwritten. Old code that reads C<< $mutator->{workspace} >> or 714: C<< $mutator->{relative} >> (without underscores) will not find these keys. 715: 716: =item Forgetting that C<run_tests> drives C<prove> from C<$^X> 717: 718: C<run_tests> resolves C<prove> from the same Perl binary used to run the script. 719: If you shell out to C<prove> directly in your own integration code, make sure 720: you are using the matching C<prove>. 721: 722: =item C<generate_mutants> in list vs scalar context 723: 724: C<generate_mutants> returns a flat list in list context and an arrayref in 725: scalar context. Assign to an arrayref (C<my $m = $mutator-E<gt>generate_mutants()>) 726: to guarantee you always get a reference regardless of calling context. 727: 728: =back 729: 730: =head1 LIMITATIONS 731: 732: =over 4 733: 734: =item * Single strategy registry 735: 736: The four built-in mutation strategies are hardcoded in C<new>. There is no 737: plugin mechanism for registering additional strategies without subclassing. 738: A future version should accept a C<strategies> arrayref argument. 739: 740: =item * No parallelism 741: 742: C<run_tests> is synchronous. For large test suites or large mutant sets, 743: wall-clock time scales linearly. The in-place mutation strategy also 744: serialises all mutants behind a single file lock. 745: 746: =item * PPI re-parse per apply_mutant call 747: 748: C<apply_mutant> re-parses the workspace copy of the target file for every 749: mutant. For very large single-file modules the PPI parse time may dominate. 750: 751: =item * apply_mutant does not restore on abnormal exit 752: 753: If the process is killed between the write and restore in 754: C<bin/test-generator-mutate>, the project file is left mutated. 755: A C<git restore lib/...> recovers it. 756: 757: =back 758: 759: =head1 SEE ALSO 760: 761: =over 4 762: 763: =item C<bin/test-generator-mutate> 764: 765: =item L<Devel::Mutator> 766: 767: =back 768: 769: =head1 AUTHOR 770: 771: Nigel Horne, C<< <njh at nigelhorne.com> >> 772: 773: =head1 LICENCE AND COPYRIGHT 774: 775: Copyright 2026 Nigel Horne. 776: 777: Usage is subject to the terms of GPL2. 778: If you use it, 779: please let me know. 780: 781: =cut 782: 783: 1;