TER1 (Statement): 91.91%
TER2 (Branch): 80.25%
TER3 (LCSAJ): 97.8% (45/46)
Approximate LCSAJ segments: 163
โ 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::CoverageGuidedFuzzer; 2: 3: use strict; 4: use warnings; 5: use Carp qw(croak); 6: use feature 'state'; 7: use Readonly; 8: 9: # -------------------------------------------------- 10: # Fuzzing loop parameters 11: # -------------------------------------------------- 12: Readonly my $DEFAULT_ITERATIONS => 100; 13: Readonly my $CORPUS_MUTATE_RATIO => 0.70; # 70% mutate, 30% explore 14: Readonly my $RANDOM_KEEP_RATIO => 0.20; # keep 20% random when no coverage 15: Readonly my $EDGE_CASE_RATIO => 0.40; # 40% chance to use declared edge case 16: Readonly my $INT_BOUNDARY_RATIO => 0.30; # 30% chance to use boundary int 17: Readonly my $STR_BOUNDARY_RATIO => 0.30; # 30% chance to use boundary length 18: Readonly my $SEED_CORPUS_SIZE => 5; # initial random inputs to seed corpus 19: Readonly my $DEFAULT_MAX_STR_LEN => 64; 20: Readonly my $DEFAULT_MAX_ARRAY => 4; # max elements in random array (0..N) 21: Readonly my $INT32_MAX => 2**31 - 1; 22: Readonly my $INT32_MIN => -(2**31); 23: 24: # -------------------------------------------------- 25: # Type name constants â used in schema dispatch 26: # -------------------------------------------------- 27: Readonly my $TYPE_INTEGER => 'integer'; 28: Readonly my $TYPE_NUMBER => 'number'; 29: Readonly my $TYPE_BOOLEAN => 'boolean'; 30: Readonly my $TYPE_ARRAY => 'arrayref'; 31: Readonly my $TYPE_HASH => 'hashref'; 32: Readonly my $TYPE_STRING => 'string'; 33: 34: # -------------------------------------------------- 35: # JSON module preference order 36: # -------------------------------------------------- 37: Readonly my @JSON_MODULES => qw(JSON::MaybeXS JSON); 38: 39: our $VERSION = '0.36'; 40: 41: =head1 NAME 42: 43: App::Test::Generator::CoverageGuidedFuzzer - AFL-style coverage-guided fuzzing for App::Test::Generator 44: 45: =head1 VERSION 46: 47: Version 0.36 48: 49: =head1 SYNOPSIS 50: 51: use App::Test::Generator::CoverageGuidedFuzzer; 52: 53: my $fuzzer = App::Test::Generator::CoverageGuidedFuzzer->new( 54: schema => $yaml_schema, 55: target_sub => \&My::Module::validate, 56: iterations => 200, 57: seed => 42, 58: ); 59: 60: my $report = $fuzzer->run(); 61: $fuzzer->save_corpus('t/corpus/validate.json'); 62: 63: =head1 DESCRIPTION 64: 65: Implements coverage-guided fuzzing on top of App::Test::Generator's 66: existing schema-driven input generation. Instead of purely random 67: generation it: 68: 69: =over 4 70: 71: =item 1. Generates or mutates a structured input 72: 73: =item 2. Runs the target sub under Devel::Cover to capture branch hits 74: 75: =item 3. Keeps inputs that discover new branches in a corpus 76: 77: =item 4. Preferentially mutates corpus entries in future iterations 78: 79: =back 80: 81: This is the Perl equivalent of what AFL/libFuzzer do at the byte level, 82: but operating on typed, schema-validated Perl data structures. 83: 84: =head1 METHODS 85: 86: =head2 new 87: 88: Construct a new coverage-guided fuzzer. 89: 90: my $fuzzer = App::Test::Generator::CoverageGuidedFuzzer->new( 91: schema => $yaml_schema, 92: target_sub => \&My::Module::validate, 93: iterations => 200, 94: seed => 42, 95: instance => $obj, # optional pre-built object for method calls 96: ); 97: 98: =head3 Arguments 99: 100: =over 4 101: 102: =item * C<schema> 103: 104: A hashref representing the parsed YAML schema for the target function. 105: Required. 106: 107: =item * C<target_sub> 108: 109: A CODE reference to the function under test. Required. 110: 111: =item * C<iterations> 112: 113: Number of fuzzing iterations to run. Optional - defaults to 100. 114: 115: =item * C<seed> 116: 117: Random seed for reproducible runs. Optional - defaults to C<time()>. 118: 119: =item * C<instance> 120: 121: An optional pre-built object to use as the invocant when calling the 122: target sub as a method. 123: 124: =back 125: 126: =head3 Returns 127: 128: A blessed hashref. Croaks if C<schema> or C<target_sub> is missing. 129: 130: =head3 API specification 131: 132: =head4 input 133: 134: { 135: schema => { type => HASHREF }, 136: target_sub => { type => CODEREF }, 137: iterations => { type => SCALAR, optional => 1 }, 138: seed => { type => SCALAR, optional => 1 }, 139: instance => { type => OBJECT, optional => 1 }, 140: } 141: 142: =head4 output 143: 144: { 145: type => OBJECT, 146: isa => 'App::Test::Generator::CoverageGuidedFuzzer', 147: } 148: 149: =cut 150: 151: sub new { โ152 โ 182 โ 186โ152 โ 182 โ 0 152: my ($class, %args) = @_; 153: 154: croak 'schema required' unless $args{schema}; 155: croak 'target_sub required' unless $args{target_sub}; 156: 157: my $self = bless { 158: schema => $args{schema}, 159: target_sub => $args{target_sub}, 160: instance => $args{instance}, 161: iterations => $args{iterations} // $DEFAULT_ITERATIONS, 162: seed => $args{seed} // time(), 163: corpus => [], # [{input => ..., coverage => {...}}] 164: covered => {}, # "file:line:branch" => 1 165: bugs => [], # [{input => ..., error => ...}] 166: stats => { 167: total => 0, 168: interesting => 0, 169: bugs => 0, 170: coverage => 0, 171: }, 172: _cover_available => undef, 173: }, $class; 174: 175: srand($self->{seed}); 176: 177: # Probe for Devel::Cover availability once at construction time 178: $self->{_cover_available} = eval { require Devel::Cover; 1 } ? 1 : 0; 179: 180: # Warn once per process if coverage guidance is unavailable 181: state $cover_warned = 0; 182: if(!$self->{_cover_available} && !$cover_warned++) {183: warn 'Devel::Cover not available; fuzzing without coverage guidance.'; 184: } 185: โ186 โ 186 โ 0 186: return $self;Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_182_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 2, Killed: 2, Survived: 0)
187: } 188: 189: =head2 run 190: 191: Run the coverage-guided fuzzing loop and return a summary report. 192: 193: my $report = $fuzzer->run(); 194: printf "Branches covered: %d\n", $report->{branches_covered}; 195: printf "Bugs found: %d\n", $report->{bugs_found}; 196: 197: =head3 Arguments 198: 199: None beyond C<$self>. 200: 201: =head3 Returns 202: 203: A hashref with keys C<total_iterations>, C<interesting_inputs>, 204: C<corpus_size>, C<branches_covered>, C<bugs_found>, and C<bugs>. 205: 206: =head3 API specification 207: 208: =head4 input 209: 210: { 211: self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' }, 212: } 213: 214: =head4 output 215: 216: { 217: type => HASHREF, 218: keys => { 219: total_iterations => { type => SCALAR }, 220: interesting_inputs => { type => SCALAR }, 221: corpus_size => { type => SCALAR }, 222: branches_covered => { type => SCALAR }, 223: bugs_found => { type => SCALAR }, 224: bugs => { type => ARRAYREF }, 225: }, 226: } 227: 228: =cut 229: 230: sub run { โ231 โ 237 โ 253โ231 โ 237 โ 0 231: my ($self) = @_; 232: 233: # Phase 1: seed the corpus with a small set of random inputs 234: $self->_seed_corpus(); 235: 236: # Phase 2: main fuzzing loop â alternate between mutation and exploration 237: for my $i (1 .. $self->{iterations}) { 238: my $input; 239: 240: if(@{ $self->{corpus} } && rand() < $CORPUS_MUTATE_RATIO) {
241: # Mutate a randomly chosen corpus entry 242: my $parent = $self->{corpus}[ int(rand(@{ $self->{corpus} })) ]; 243: $input = $self->_mutate($parent->{input}); 244: } else { 245: # Fresh random generation for exploration 246: $input = $self->_generate_random(); 247: } 248: 249: $self->_run_one($input); 250: $self->{stats}{total}++; 251: } 252: โ253 โ 254 โ 0 253: $self->{stats}{coverage} = scalar keys %{ $self->{covered} }; 254: return $self->_build_report();Mutants (Total: 4, Killed: 0, Survived: 4)
- NUM_BOUNDARY_240_37_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_240_37_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_240_37_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- COND_INV_240_3: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 2, Killed: 2, Survived: 0)
255: } 256: 257: =head2 corpus 258: 259: Return the accumulated corpus as an arrayref of hashrefs with keys 260: C<input> and C<coverage>. 261: 262: my $corpus = $fuzzer->corpus(); 263: 264: =head3 API specification 265: 266: =head4 input 267: 268: { self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' } } 269: 270: =head4 output 271: 272: { type => ARRAYREF } 273: 274: =cut 275: 276: sub corpus { $_[0]->{corpus} } 277: 278: =head2 bugs 279: 280: Return bugs found as an arrayref of hashrefs with keys C<input> and 281: C<error>. 282: 283: my $bugs = $fuzzer->bugs(); 284: 285: =head3 API specification 286: 287: =head4 input 288: 289: { self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' } } 290: 291: =head4 output 292: 293: { type => ARRAYREF } 294: 295: =cut 296: 297: sub bugs { $_[0]->{bugs} } 298: 299: =head2 save_corpus 300: 301: Serialise the corpus to a JSON file for replay or extension on future 302: runs. 303: 304: $fuzzer->save_corpus('t/corpus/validate.json'); 305: 306: =head3 Arguments 307: 308: =over 4 309: 310: =item * C<$path> 311: 312: Path to write the JSON corpus file. Required. 313: 314: =back 315: 316: =head3 Returns 317: 318: Nothing. Croaks if the file cannot be written or no JSON module is 319: available. 320: 321: =head3 Side effects 322: 323: Writes a JSON file to C<$path>. 324: 325: =head3 API specification 326: 327: =head4 input 328: 329: { 330: self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' }, 331: path => { type => SCALAR }, 332: } 333: 334: =head4 output 335: 336: { type => UNDEF } 337: 338: =cut 339: 340: sub save_corpus { 341: my ($self, $path) = @_; 342: 343: croak 'path required' unless defined $path; 344: 345: my $json = _load_json_module(); 346: 347: open my $fh, '>', $path 348: or croak "Cannot write corpus to $path: $!"; 349: 350: print $fh $json->new->pretty->encode({ 351: seed => $self->{seed}, 352: corpus => [ map { { input => $_->{input} } } @{ $self->{corpus} } ], 353: bugs => $self->{bugs}, 354: }); 355: 356: close $fh; 357: } 358: 359: =head2 load_corpus 360: 361: Load a previously saved corpus JSON file, pre-seeding the fuzzer so 362: it continues from where it left off. 363: 364: $fuzzer->load_corpus('t/corpus/validate.json'); 365: 366: =head3 Arguments 367: 368: =over 4 369: 370: =item * C<$path> 371: 372: Path to the JSON corpus file to load. Required. 373: 374: =back 375: 376: =head3 Returns 377: 378: Nothing. Croaks if the file cannot be read or no JSON module is 379: available. 380: 381: =head3 Side effects 382: 383: Appends loaded entries to C<< $self->{corpus} >>. 384: 385: =head3 API specification 386: 387: =head4 input 388: 389: { 390: self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' }, 391: path => { type => SCALAR }, 392: } 393: 394: =head4 output 395: 396: { type => UNDEF } 397: 398: =cut 399: 400: sub load_corpus { โ401 โ 415 โ 0 401: my ($self, $path) = @_; 402: 403: croak 'path required' unless defined $path; 404: 405: my $json = _load_json_module(); 406: 407: open my $fh, '<', $path 408: or croak "Cannot read corpus from $path: $!"; 409: 410: my $data = $json->new->decode(do { local $/; <$fh> }); 411: close $fh; 412: 413: # Load corpus entries with empty coverage â coverage state from a 414: # previous process cannot be restored, only the inputs themselves 415: for my $entry (@{ $data->{corpus} // [] }) { 416: push @{ $self->{corpus} }, { 417: input => $entry->{input}, 418: coverage => {}, 419: }; 420: } 421: } 422: 423: # -------------------------------------------------- 424: # _load_json_module 425: # 426: # Find and load the first available JSON 427: # module from the preference list. 428: # 429: # Entry: None. 430: # Exit: Returns the name of the loaded module. 431: # Croaks if none are available. 432: # 433: # Side effects: Loads a JSON module into the process. 434: # 435: # Notes: Uses explicit require rather than string 436: # eval for safety. JSON::MaybeXS is 437: # preferred over JSON. 438: # -------------------------------------------------- 439: sub _load_json_module { โ440 โ 440 โ 448โ440 โ 440 โ 0 440: for my $mod (@JSON_MODULES) { 441: # Convert package name to file path â require $var does not 442: # do the :: -> / conversion that bareword require does 443: (my $file = $mod) =~ s{::}{/}g; 444: $file .= '.pm'; 445: my $ok = eval { require $file; 1 }; 446: return $mod if $ok;
Mutants (Total: 2, Killed: 2, Survived: 0)
447: } โ[NOT COVERED] 448 โ 448 โ 0 448: croak 'No JSON module available; install JSON or JSON::MaybeXS'; 449: } 450: 451: # -------------------------------------------------- 452: # _run_one 453: # 454: # Run the target sub with a single input, 455: # record coverage, detect bugs, and update 456: # the corpus if the input is interesting. 457: # 458: # Entry: $input - the value to pass to target_sub. 459: # 460: # Exit: Returns nothing. Updates $self->{corpus}, 461: # $self->{bugs}, and $self->{covered}. 462: # 463: # Side effects: Calls target_sub. May update corpus 464: # and covered hashes. 465: # 466: # Notes: When Devel::Cover is available, coverage 467: # is captured via _run_with_cover. 468: # Unexpected warnings are treated as soft 469: # bugs if they match known warning patterns. 470: # -------------------------------------------------- 471: sub _run_one { โ472 โ 476 โ 504โ472 โ 476 โ 0 472: my ($self, $input) = @_; 473: 474: my ($result, $error, $coverage); 475: 476: if($self->{_cover_available}) {
477: $coverage = $self->_run_with_cover($input, \$result, \$error); 478: } else { 479: $coverage = {}; 480: 481: # Include instance as invocant for method calls 482: my @call_args = defined($self->{instance}) 483: ? ($self->{instance}, $input) 484: : ($input); 485: 486: my @warnings; 487: eval { 488: local $SIG{__WARN__} = sub { push @warnings, @_ }; 489: local $SIG{__DIE__}; 490: $result = $self->{target_sub}->(@call_args); 491: }; 492: $error = $@ if $@; 493: 494: # Treat unexpected warnings matching known bad patterns as soft bugs 495: if(!defined($error) && @warnings) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_476_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes496: my $w = join '', @warnings; 497: $error = "warning: $w" 498: if $w =~ /uninitialized|undefined|blessed|invalid/i; 499: } 500: } 501: 502: # Record bugs â only when the input was valid per the schema. 503: # A die on invalid input is correct behaviour, not a bug. โ504 โ 504 โ 510โ504 โ 504 โ 0 504: if($error && $self->_input_is_valid($input)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_495_3: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes505: push @{ $self->{bugs} }, { input => $input, error => "$error" }; 506: $self->{stats}{bugs}++; 507: } 508: 509: # Keep the input in the corpus if it exercised new branches โ510 โ 510 โ 0 510: if($self->_is_interesting($coverage)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_504_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes511: push @{ $self->{corpus} }, { input => $input, coverage => $coverage }; 512: $self->_update_covered($coverage); 513: $self->{stats}{interesting}++; 514: } 515: } 516: 517: # -------------------------------------------------- 518: # _run_with_cover 519: # 520: # Purpose: Run the target sub with Devel::Cover 521: # active and return the set of newly hit 522: # branches as a hashref. 523: # 524: # Entry: $input - value to pass to target_sub. 525: # $result_ref - scalar ref to store result. 526: # $error_ref - scalar ref to store error. 527: # 528: # Exit: Returns a hashref of newly hit branch 529: # keys ("file:line:branch"). 530: # 531: # Side effects: Calls Devel::Cover::start/stop. 532: # Sets $$result_ref and $$error_ref. 533: # 534: # Notes: Snapshot comparison is imprecise for 535: # concurrent use but correct for single- 536: # threaded fuzzing. Instance is passed 537: # as invocant when set. 538: # -------------------------------------------------- 539: sub _run_with_cover { โ540 โ 562 โ 566โ540 โ 562 โ 0 540: my ($self, $input, $result_ref, $error_ref) = @_; 541: 542: Devel::Cover::start() if Devel::Cover->can('start'); 543: 544: my %before = $self->_snapshot_cover(); 545: 546: # Include instance as invocant for method calls 547: my @call_args = defined($self->{instance}) 548: ? ($self->{instance}, $input) 549: : ($input); 550: 551: eval { 552: local $SIG{__DIE__}; 553: $$result_ref = $self->{target_sub}->(@call_args); 554: }; 555: $$error_ref = $@ if $@; 556: 557: my %after = $self->_snapshot_cover(); 558: Devel::Cover::stop() if Devel::Cover->can('stop'); 559: 560: # Return only branches newly hit in this call 561: my %delta; 562: for my $key (keys %after) { 563: $delta{$key} = 1 unless exists $before{$key}; 564: } 565: โ566 โ 566 โ 0 566: return \%delta; 567: } 568: 569: # -------------------------------------------------- 570: # _snapshot_cover 571: # 572: # Purpose: Take a lightweight snapshot of the 573: # currently hit branches from Devel::Cover. 574: # 575: # Entry: None beyond $self. 576: # Exit: Returns a hash of "file:line:branch" keys. 577: # 578: # Side effects: Reads Devel::Cover internal state. 579: # 580: # Notes: Falls back to empty hash if the 581: # Devel::Cover API is not accessible. 582: # All errors are silently swallowed since 583: # coverage is best-effort. 584: # -------------------------------------------------- 585: sub _snapshot_cover { 586: my ($self) = @_; 587: my %snap; 588: 589: eval { 590: my $cover = Devel::Cover::get_coverage(); 591: return unless $cover; 592: 593: for my $file (keys %{$cover}) { 594: my $branch = $cover->{$file}{branch} or next; 595: for my $line (keys %{$branch}) { 596: for my $b (0 .. $#{ $branch->{$line} }) { 597: $snap{"$file:$line:$b"} = 1 598: if $branch->{$line}[$b]; 599: } 600: } 601: } 602: }; 603: 604: return %snap;Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_510_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes605: } 606: 607: # -------------------------------------------------- 608: # _is_interesting 609: # 610: # Purpose: Return true if the coverage hashref 611: # contains any branch not yet in the 612: # global covered set. 613: # 614: # Entry: $coverage - hashref of branch keys. 615: # Exit: Returns 1 if interesting, 0 otherwise. 616: # 617: # Side effects: None. 618: # 619: # Notes: When no coverage data is available, 620: # keeps a random sample of inputs at 621: # RANDOM_KEEP_RATIO so the corpus still 622: # grows even without branch feedback. 623: # -------------------------------------------------- 624: sub _is_interesting { โ625 โ 628 โ 633โ625 โ 628 โ 0 625: my ($self, $coverage) = @_; 626: 627: # Check for any newly covered branch 628: for my $key (keys %{$coverage}) { 629: return 1 unless $self->{covered}{$key};Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_604_2: 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_604_2: 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)
630: } 631: 632: # No coverage data â keep a random sample to grow the corpus โ633 โ 635 โ 0 633: return rand() < $RANDOM_KEEP_RATIO unless %{$coverage};
634: 635: return 0;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_633_16_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_633_16_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_633_16_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
636: } 637: 638: # -------------------------------------------------- 639: # _update_covered 640: # 641: # Purpose: Merge newly covered branches into the 642: # global covered set. 643: # 644: # Entry: $coverage - hashref of branch keys. 645: # Exit: Returns nothing. Updates $self->{covered}. 646: # Side effects: Modifies $self->{covered}. 647: # -------------------------------------------------- 648: sub _update_covered { 649: my ($self, $coverage) = @_; 650: $self->{covered}{$_} = 1 for keys %{$coverage}; 651: } 652: 653: # -------------------------------------------------- 654: # _generate_random 655: # 656: # Purpose: Generate a random input value from the 657: # top-level schema input specification. 658: # 659: # Entry: None beyond $self. 660: # Exit: Returns a randomly generated value. 661: # Side effects: None. 662: # -------------------------------------------------- 663: sub _generate_random { 664: my ($self) = @_; 665: return $self->_generate_for_schema($self->{schema}{input});
666: } 667: 668: # -------------------------------------------------- 669: # _generate_for_schema 670: # 671: # Purpose: Recursively generate a random value 672: # matching a schema specification hashref. 673: # 674: # Entry: $spec - schema spec hashref or scalar 675: # type hint. 676: # 677: # Exit: Returns a generated value appropriate 678: # for the spec type, or undef if spec is 679: # absent or 'undef'. 680: # 681: # Side effects: None. 682: # 683: # Notes: Edge cases declared in edge_case_array 684: # are selected at EDGE_CASE_RATIO frequency 685: # to bias toward known interesting values. 686: # -------------------------------------------------- 687: sub _generate_for_schema { โ688 โ 696 โ 702โ688 โ 696 โ 0 688: my ($self, $spec) = @_; 689: 690: return undef unless defined $spec;Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_665_2: 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_665_2: 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)
691: return undef if $spec eq 'undef';
Mutants (Total: 2, Killed: 2, Survived: 0)
692: 693: my $type = ref($spec) ? ($spec->{type} // $TYPE_STRING) : $TYPE_STRING; 694: 695: # Bias toward declared edge cases at EDGE_CASE_RATIO frequency 696: if(ref($spec) && $spec->{edge_case_array} && rand() < $EDGE_CASE_RATIO) {
697: my @ec = @{ $spec->{edge_case_array} }; 698: return $ec[ int(rand(@ec)) ]; 699: } 700: 701: # Dispatch to type-specific generator โ702 โ 702 โ 0 702: if ($type eq $TYPE_INTEGER) { return $self->_rand_int($spec) }Mutants (Total: 4, Killed: 1, Survived: 3)
- NUM_BOUNDARY_696_54_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_696_54_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_696_54_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 3, Killed: 3, Survived: 0)
703: elsif ($type eq $TYPE_NUMBER) { return $self->_rand_num($spec) }
704: elsif ($type eq $TYPE_BOOLEAN) { return int(rand(2)) } 705: elsif ($type eq $TYPE_ARRAY) { return $self->_rand_array($spec) }Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_703_35: 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_703_35: 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)
706: elsif ($type eq $TYPE_HASH) { return $self->_rand_hash($spec) }
Mutants (Total: 2, Killed: 2, Survived: 0)
707: else { return $self->_rand_string($spec) }
708: } 709: 710: # -------------------------------------------------- 711: # _rand_int 712: # 713: # Purpose: Generate a random integer within the 714: # spec's min/max range, biased toward 715: # boundary values at INT_BOUNDARY_RATIO. 716: # 717: # Entry: $spec - schema spec hashref. 718: # Exit: Returns an integer scalar. 719: # Side effects: None. 720: # -------------------------------------------------- 721: sub _rand_int { โ722 โ 728 โ 733โ722 โ 728 โ 0 722: my ($self, $spec) = @_; 723: 724: my $min = $spec->{min} // $INT32_MIN; 725: my $max = $spec->{max} // $INT32_MAX; 726: 727: # Bias toward boundary values to probe edge conditions 728: if(rand() < $INT_BOUNDARY_RATIO) {Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_707_35: 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_707_35: 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' );729: my @interesting = ($min, $min + 1, 0, -1, 1, $max - 1, $max); 730: return $interesting[ int(rand(@interesting)) ]; 731: } 732: โ733 โ 733 โ 0 733: return $min + int(rand($max - $min + 1));Mutants (Total: 4, Killed: 0, Survived: 4)
- NUM_BOUNDARY_728_12_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_728_12_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_728_12_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- COND_INV_728_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes734: } 735: 736: # -------------------------------------------------- 737: # _rand_num 738: # 739: # Purpose: Generate a random floating point number 740: # within the spec's min/max range. 741: # 742: # Entry: $spec - schema spec hashref. 743: # Exit: Returns a numeric scalar. 744: # Side effects: None. 745: # -------------------------------------------------- 746: sub _rand_num { 747: my ($self, $spec) = @_; 748: 749: my $min = $spec->{min} // -1e9; 750: my $max = $spec->{max} // 1e9; 751: 752: return $min + rand($max - $min);Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_733_2: 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_733_2: 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)
753: } 754: 755: # -------------------------------------------------- 756: # _rand_string 757: # 758: # Purpose: Generate a random string within the 759: # spec's min/max length range, biased 760: # toward boundary lengths. 761: # 762: # Entry: $spec - schema spec hashref. 763: # Exit: Returns a string scalar. 764: # Side effects: None. 765: # 766: # Notes: Character set includes control chars 767: # and NUL to probe boundary handling. 768: # -------------------------------------------------- 769: sub _rand_string { โ770 โ 777 โ 785โ770 โ 777 โ 0 770: my ($self, $spec) = @_; 771: 772: my $min_len = $spec->{min} // 0; 773: my $max_len = $spec->{max} // $DEFAULT_MAX_STR_LEN; 774: 775: # Bias toward boundary lengths at STR_BOUNDARY_RATIO frequency 776: my $len; 777: if(rand() < $STR_BOUNDARY_RATIO) {
778: my @boundary_lens = ($min_len, $min_len + 1, $max_len - 1, $max_len); 779: $len = $boundary_lens[ int(rand(@boundary_lens)) ]; 780: } else { 781: $len = $min_len + int(rand($max_len - $min_len + 1)); 782: } 783: 784: # Clamp to non-negative โ785 โ 788 โ 0 785: $len = 0 if $len < 0;Mutants (Total: 4, Killed: 0, Survived: 4)
- NUM_BOUNDARY_777_12_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_777_12_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_777_12_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- COND_INV_777_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes786: 787: my @chars = ('a'..'z', 'A'..'Z', '0'..'9', ' ', "\t", "\n", "\0"); 788: return join '', map { $chars[ int(rand(@chars)) ] } 1 .. $len; 789: } 790: 791: # -------------------------------------------------- 792: # _rand_array 793: # 794: # Purpose: Generate a random arrayref with 0 to 795: # DEFAULT_MAX_ARRAY elements, each 796: # generated from the items spec. 797: # 798: # Entry: $spec - schema spec hashref. 799: # Exit: Returns an arrayref. 800: # Side effects: None. 801: # -------------------------------------------------- 802: sub _rand_array { 803: my ($self, $spec) = @_; 804: 805: my $items = $spec->{items} // {}; 806: my $count = int(rand($DEFAULT_MAX_ARRAY + 1)); 807: 808: return [ map { $self->_generate_for_schema($items) } 1 .. $count ]; 809: } 810: 811: # -------------------------------------------------- 812: # _rand_hash 813: # 814: # Purpose: Generate a random hashref with values 815: # generated from the properties spec. 816: # 817: # Entry: $spec - schema spec hashref. 818: # Exit: Returns a hashref. 819: # Side effects: None. 820: # -------------------------------------------------- 821: sub _rand_hash { โ822 โ 827 โ 831โ822 โ 827 โ 0 822: my ($self, $spec) = @_; 823: 824: my $props = $spec->{properties} // {}; 825: my %h; 826: 827: for my $key (keys %{$props}) { 828: $h{$key} = $self->_generate_for_schema($props->{$key}); 829: } 830: โ831 โ 831 โ 0 831: return \%h; 832: } 833: 834: # -------------------------------------------------- 835: # _input_is_valid 836: # 837: # Purpose: Return true if the input satisfies all 838: # constraints in the schema. Used to 839: # distinguish real bugs (die on valid 840: # input) from expected failures (die on 841: # invalid input). 842: # 843: # Entry: $input - the value to validate. 844: # Exit: Returns 1 if valid, 0 if not. 845: # Returns 1 if no schema is available. 846: # Side effects: None. 847: # -------------------------------------------------- 848: sub _input_is_valid { โ849 โ 858 โ 862โ849 โ 858 โ 0 849: my ($self, $input) = @_; 850: 851: my $spec = $self->{schema}{input}; 852: 853: # No schema means we cannot judge validity 854: return 1 unless defined $spec && ref($spec);Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_785_19_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_785_19_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_785_19_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
855: 856: my $input_style = $self->{schema}{input_style} // ''; 857: 858: if($input_style eq 'hash' || ref($input) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
859: return $self->_validate_hash_input($input, $spec);
Mutants (Total: 2, Killed: 2, Survived: 0)
860: } 861: โ862 โ 862 โ 0 862: return $self->_validate_value($input, $spec);
Mutants (Total: 2, Killed: 2, Survived: 0)
863: } 864: 865: # -------------------------------------------------- 866: # _validate_hash_input 867: # 868: # Purpose: Validate a hash-style input against the 869: # schema spec, checking each named field. 870: # 871: # Entry: $input - hashref of named parameters. 872: # $spec - schema spec hashref. 873: # Exit: Returns 1 if valid, 0 if not. 874: # Side effects: None. 875: # -------------------------------------------------- 876: sub _validate_hash_input { โ877 โ 881 โ 900โ877 โ 881 โ 0 877: my ($self, $input, $spec) = @_; 878: 879: return 0 unless defined $input;
Mutants (Total: 2, Killed: 2, Survived: 0)
880: 881: for my $key (keys %{$spec}) { 882: # Skip internal metadata keys 883: next if $key =~ /^_/; 884: 885: my $field_spec = $spec->{$key}; 886: next unless ref($field_spec) eq 'HASH'; 887: 888: my $value = ref($input) eq 'HASH' ? $input->{$key} : undef; 889: 890: # Required field missing is always invalid 891: if(!defined($value) && !$field_spec->{optional}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
892: return 0;
Mutants (Total: 2, Killed: 2, Survived: 0)
893: } 894: 895: next unless defined $value; 896: 897: return 0 unless $self->_validate_value($value, $field_spec);
Mutants (Total: 2, Killed: 2, Survived: 0)
898: } 899: โ900 โ 900 โ 0 900: return 1;
Mutants (Total: 2, Killed: 2, Survived: 0)
901: } 902: 903: # -------------------------------------------------- 904: # _validate_value 905: # 906: # Purpose: Validate a single value against a schema 907: # type spec, checking type and constraints. 908: # 909: # Entry: $value - the value to validate. 910: # $spec - schema spec hashref. 911: # Exit: Returns 1 if valid, 0 if not. 912: # Side effects: None. 913: # 914: # Notes: Number validation accepts both integer 915: # and floating point forms including 916: # scientific notation. Type mismatch 917: # always returns 0. 918: # -------------------------------------------------- 919: sub _validate_value { โ920 โ 927 โ 957โ920 โ 927 โ 0 920: my ($self, $value, $spec) = @_; 921: 922: # Undef is never valid unless optional â caller already checked optional 923: return 0 unless defined $value;
Mutants (Total: 2, Killed: 2, Survived: 0)
924: 925: my $type = $spec->{type} // $TYPE_STRING; 926: 927: if($type eq $TYPE_INTEGER) {
Mutants (Total: 1, Killed: 1, Survived: 0)
928: return 0 unless $value =~ /^-?\d+$/;
Mutants (Total: 2, Killed: 2, Survived: 0)
929: return 0 if defined($spec->{min}) && $value < $spec->{min};
Mutants (Total: 5, Killed: 5, Survived: 0)
930: return 0 if defined($spec->{max}) && $value > $spec->{max};
Mutants (Total: 5, Killed: 5, Survived: 0)
931: } 932: elsif($type eq $TYPE_NUMBER) { 933: # Accept integers, decimals, and scientific notation 934: return 0 unless $value =~ /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
Mutants (Total: 2, Killed: 2, Survived: 0)
935: return 0 if defined($spec->{min}) && $value < $spec->{min};
936: return 0 if defined($spec->{max}) && $value > $spec->{max};Mutants (Total: 5, Killed: 0, Survived: 5)
- NUM_BOUNDARY_935_47_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_935_47_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_935_47_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- BOOL_NEGATE_935_3: 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_935_3: 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' );937: } 938: elsif($type eq $TYPE_STRING) { 939: my $len = length($value); 940: return 0 if defined($spec->{min}) && $len < $spec->{min};Mutants (Total: 5, Killed: 0, Survived: 5)
- NUM_BOUNDARY_936_47_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_936_47_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_936_47_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- BOOL_NEGATE_936_3: 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_936_3: 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: 5, Killed: 5, Survived: 0)
941: return 0 if defined($spec->{max}) && $len > $spec->{max};
Mutants (Total: 5, Killed: 5, Survived: 0)
942: if(defined($spec->{matches})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
943: (my $pat = $spec->{matches}) =~ s{^/(.+)/$}{$1}; 944: return 0 unless $value =~ /$pat/;
Mutants (Total: 2, Killed: 2, Survived: 0)
945: } 946: } 947: elsif($type eq $TYPE_BOOLEAN) { 948: return 0 unless $value =~ /^[01]$/;
Mutants (Total: 2, Killed: 2, Survived: 0)
949: } 950: elsif($type eq $TYPE_ARRAY || $type eq 'array') { 951: return 0 unless ref($value) eq 'ARRAY';
Mutants (Total: 2, Killed: 2, Survived: 0)
952: } 953: elsif($type eq $TYPE_HASH || $type eq 'hash') { 954: return 0 unless ref($value) eq 'HASH';
Mutants (Total: 2, Killed: 2, Survived: 0)
955: } 956: โ957 โ 957 โ 0 957: return 1;
Mutants (Total: 2, Killed: 2, Survived: 0)
958: } 959: 960: # -------------------------------------------------- 961: # _mutate 962: # 963: # Purpose: Apply a random mutation to an input 964: # value, dispatching on its type. 965: # 966: # Entry: $input - the value to mutate. 967: # Exit: Returns a mutated copy of the input. 968: # Side effects: None. 969: # 970: # Notes: Blessed references are passed through 971: # unchanged. Undef is replaced with a 972: # freshly generated random value. 973: # -------------------------------------------------- 974: sub _mutate { โ975 โ 979 โ 1001โ975 โ 979 โ 0 975: my ($self, $input) = @_; 976: 977: my $type = ref($input); 978: 979: if(!defined $input) {
Mutants (Total: 1, Killed: 1, Survived: 0)
980: # Replace undef with a fresh random value 981: return $self->_generate_random();
982: } 983: elsif(!$type) { 984: # Dispatch scalar mutation based on apparent type 985: if($input =~ /^-?\d+$/) {Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_981_3: 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_981_3: 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' );986: return $self->_mutate_int($input);Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_985_3: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes987: } elsif($input =~ /^-?[\d.]+$/) { 988: return $self->_mutate_num($input);Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_986_4: 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_986_4: 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' );989: } else { 990: return $self->_mutate_string($input);Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_988_4: 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_988_4: 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' );991: } 992: } 993: elsif($type eq 'ARRAY') { 994: return $self->_mutate_array($input);Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_990_4: 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_990_4: 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)
995: } 996: elsif($type eq 'HASH') { 997: return $self->_mutate_hash($input);
Mutants (Total: 2, Killed: 2, Survived: 0)
998: } 999: 1000: # Blessed refs and other types pass through unchanged โ1001 โ 1001 โ 0 1001: return $input;
Mutants (Total: 2, Killed: 2, Survived: 0)
1002: } 1003: 1004: # -------------------------------------------------- 1005: # _mutate_int 1006: # 1007: # Purpose: Apply a random arithmetic mutation to 1008: # an integer value. 1009: # 1010: # Entry: $n - the integer to mutate. 1011: # Exit: Returns a mutated integer. 1012: # Side effects: None. 1013: # -------------------------------------------------- 1014: sub _mutate_int { 1015: my ($self, $n) = @_; 1016: 1017: my @ops = ( 1018: sub { $n + 1 }, 1019: sub { $n - 1 }, 1020: sub { $n * 2 }, 1021: sub { $n == 0 ? 1 : int($n / 2) },
1022: sub { -$n }, 1023: sub { 0 }, 1024: sub { $INT32_MAX }, 1025: sub { $INT32_MIN }, 1026: ); 1027: 1028: return $ops[ int(rand(@ops)) ]->(); 1029: } 1030: 1031: # -------------------------------------------------- 1032: # _mutate_num 1033: # 1034: # Purpose: Apply a random arithmetic mutation to 1035: # a floating point value. 1036: # 1037: # Entry: $n - the number to mutate. 1038: # Exit: Returns a mutated number. 1039: # Side effects: None. 1040: # -------------------------------------------------- 1041: sub _mutate_num { 1042: my ($self, $n) = @_; 1043: 1044: my @ops = ( 1045: sub { $n + rand(10) }, 1046: sub { $n - rand(10) }, 1047: sub { $n * (1 + rand()) }, 1048: sub { 0 }, 1049: sub { -$n }, 1050: ); 1051: 1052: return $ops[ int(rand(@ops)) ]->(); 1053: } 1054: 1055: # -------------------------------------------------- 1056: # _mutate_string 1057: # 1058: # Purpose: Apply a random structural mutation to 1059: # a string value â bit flip, insert, 1060: # delete, truncate, repeat, or replace 1061: # with an interesting known value. 1062: # 1063: # Entry: $s - the string to mutate. 1064: # Exit: Returns a mutated string. 1065: # Side effects: None. 1066: # -------------------------------------------------- 1067: sub _mutate_string { 1068: my ($self, $s) = @_; 1069: 1070: my $len = length($s); 1071: 1072: my @ops = ( 1073: # Bit flip a random character 1074: sub { 1075: return $s unless $len;Mutants (Total: 1, Killed: 0, Survived: 1)
- NUM_BOUNDARY_1021_12_!=: Numeric boundary flip == to !=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );1076: my $pos = int(rand($len)); 1077: my $char = substr($s, $pos, 1); 1078: substr($s, $pos, 1) = chr(ord($char) ^ (1 << int(rand(8)))); 1079: $s 1080: }, 1081: # Insert a random byte 1082: sub { 1083: my $pos = int(rand($len + 1)); 1084: my $char = chr(int(rand(256))); 1085: substr($s, $pos, 0, $char); 1086: $s 1087: }, 1088: # Delete a random character 1089: sub { 1090: return $s unless $len;Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_1075_4: 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_1075_4: 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' );1091: substr($s, int(rand($len)), 1, ''); 1092: $s 1093: }, 1094: # Truncate at a random position 1095: sub { substr($s, 0, int(rand($len + 1))) }, 1096: # Double the string 1097: sub { $s x 2 }, 1098: # Replace with a known interesting string 1099: sub { 1100: my @interesting = ( 1101: '', ' ', "\0", "\n", "\t", 1102: 'a' x 256, 1103: 'null', 'undefined', 1104: "'; DROP TABLE foo; --", 1105: '<script>alert(1)</script>', 1106: ); 1107: $interesting[ int(rand(@interesting)) ] 1108: }, 1109: ); 1110: 1111: return $ops[ int(rand(@ops)) ]->(); 1112: } 1113: 1114: # -------------------------------------------------- 1115: # _mutate_array 1116: # 1117: # Purpose: Apply a random structural mutation to 1118: # an arrayref â mutate element, duplicate, 1119: # delete, or empty. 1120: # 1121: # Entry: $arr - the arrayref to mutate. 1122: # Exit: Returns a mutated arrayref copy. 1123: # Side effects: None. 1124: # -------------------------------------------------- 1125: sub _mutate_array { 1126: my ($self, $arr) = @_; 1127: 1128: my @copy = @{$arr}; 1129: 1130: my @ops = ( 1131: # Mutate a random element 1132: sub { 1133: return [] unless @copy; 1134: my $i = int(rand(@copy)); 1135: $copy[$i] = $self->_mutate($copy[$i]); 1136: \@copy 1137: }, 1138: # Duplicate a random element 1139: sub { 1140: return \@copy unless @copy; 1141: my $i = int(rand(@copy)); 1142: splice @copy, $i, 0, $copy[$i]; 1143: \@copy 1144: }, 1145: # Delete a random element 1146: sub { 1147: return \@copy unless @copy; 1148: splice @copy, int(rand(@copy)), 1; 1149: \@copy 1150: }, 1151: # Return empty array 1152: sub { [] }, 1153: ); 1154: 1155: return $ops[ int(rand(@ops)) ]->(); 1156: } 1157: 1158: # -------------------------------------------------- 1159: # _mutate_hash 1160: # 1161: # Purpose: Apply a random mutation to one value 1162: # in a hashref copy. 1163: # 1164: # Entry: $h - the hashref to mutate. 1165: # Exit: Returns a mutated hashref copy. 1166: # Side effects: None. 1167: # -------------------------------------------------- 1168: sub _mutate_hash { 1169: my ($self, $h) = @_; 1170: 1171: my %copy = %{$h}; 1172: my @keys = keys %copy; 1173: 1174: # Return unchanged if hash is empty 1175: return \%copy unless @keys; 1176: 1177: my $k = $keys[ int(rand(@keys)) ]; 1178: $copy{$k} = $self->_mutate($copy{$k}); 1179: 1180: return \%copy; 1181: } 1182: 1183: # -------------------------------------------------- 1184: # _seed_corpus 1185: # 1186: # Purpose: Pre-populate the corpus with a small 1187: # set of randomly generated inputs to 1188: # give the fuzzing loop a starting point. 1189: # 1190: # Entry: None beyond $self. 1191: # Exit: Returns nothing. Appends to $self->{corpus}. 1192: # Side effects: Modifies $self->{corpus}. 1193: # -------------------------------------------------- 1194: sub _seed_corpus { โ1195 โ 1197 โ 0 1195: my $self = $_[0]; 1196: 1197: for (1 .. $SEED_CORPUS_SIZE) { 1198: push @{ $self->{corpus} }, { 1199: input => $self->_generate_random(), 1200: coverage => {}, 1201: }; 1202: } 1203: } 1204: 1205: # -------------------------------------------------- 1206: # _build_report 1207: # 1208: # Purpose: Construct the summary report hashref 1209: # returned by run(). 1210: # 1211: # Entry: None beyond $self. 1212: # Exit: Returns a report hashref. 1213: # Side effects: None. 1214: # -------------------------------------------------- 1215: sub _build_report { 1216: my $self = $_[0]; 1217: 1218: return { 1219: total_iterations => $self->{stats}{total}, 1220: interesting_inputs => $self->{stats}{interesting}, 1221: corpus_size => scalar @{ $self->{corpus} }, 1222: branches_covered => $self->{stats}{coverage}, 1223: bugs_found => $self->{stats}{bugs}, 1224: bugs => $self->{bugs}, 1225: }; 1226: } 1227: 1228: =head1 AUTHOR 1229: 1230: Nigel Horne, C<< <njh at nigelhorne.com> >> 1231: 1232: Portions of this module's initial design and documentation were created 1233: with the assistance of AI. 1234: 1235: =head1 LICENCE AND COPYRIGHT 1236: 1237: Copyright 2026 Nigel Horne. 1238: 1239: Usage is subject to GPL2 licence terms. 1240: If you use it, 1241: please let me know. 1242: 1243: =cut 1244: 1245: 1;Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_1090_4: 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_1090_4: 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' );