TER1 (Statement): 100.00%
TER2 (Branch): 90.38%
TER3 (LCSAJ): 100.0% (4/4)
Approximate LCSAJ segments: 53
● 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::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;