lib/App/Test/Generator/Model/Method.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 96.55%
TER3 (LCSAJ): 100.0% (6/6)
Approximate LCSAJ segments: 59

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::Model::Method;
    2: 
    3: use strict;
    4: use warnings;
    5: 
    6: use Carp qw(confess croak);
    7: use Readonly;
    8: 
    9: Readonly my $HIGH_CONFIDENCE_THRESHOLD   => 40;
   10: Readonly my $MEDIUM_CONFIDENCE_THRESHOLD => 20;
   11: 
   12: Readonly my %VALID_CATEGORIES => map { $_ => 1 } qw(return input effect);
   13: Readonly my %VALID_SIGNALS    => map { $_ => 1 } qw(
   14: 	returns_property returns_constant returns_self
   15: 	legacy_type context_aware error_pattern
   16: 	input_validated input_typed input_optional
   17: 	has_side_effect no_side_effect
   18: );
   19: 
   20: our $VERSION = '0.46';
   21: 
   22: =head1 NAME
   23: 
   24: App::Test::Generator::Model::Method - Evidence-based model of a single method under test
   25: 
   26: =head1 VERSION
   27: 
   28: Version 0.46
   29: 
   30: =head1 DESCRIPTION
   31: 
   32: Accumulates weighted evidence about a single method's return behaviour,
   33: gathered independently by several analysers
   34: (L<App::Test::Generator::Analyzer::Return> and friends), then resolves
   35: that evidence into a best-guess return type, test classification, and
   36: confidence level. This lets multiple independent heuristics contribute
   37: to one final judgement instead of the first heuristic to run winning
   38: outright.
   39: 
   40: =head2 new
   41: 
   42: Construct a new Method model.
   43: 
   44:     my $method = App::Test::Generator::Model::Method->new(
   45:         name   => 'get_name',
   46:         source => 'sub get_name { return $_[0]->{name}; }',
   47:     );
   48: 
   49: =head3 Arguments
   50: 
   51: =over 4
   52: 
   53: =item * C<name>
   54: 
   55: The method's name. Required.
   56: 
   57: =item * C<source>
   58: 
   59: The method's raw Perl source text. Required.
   60: 
   61: =back
   62: 
   63: =head3 Returns
   64: 
   65: A blessed hashref with C<evidence> initialised to an empty arrayref
   66: and C<return_type>, C<classification>, and C<confidence> initialised
   67: to C<undef>. Croaks with C<"name required"> or C<"source required">
   68: if either argument is missing.
   69: 
   70: =head3 API specification
   71: 
   72: =head4 input
   73: 
   74:     {
   75:         name   => { type => SCALAR },
   76:         source => { type => SCALAR },
   77:     }
   78: 
   79: =head4 output
   80: 
   81:     { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }
   82: 
   83: =cut
   84: 
   85: sub new {
   86: 	my ($class, %args) = @_;
   87: 	croak 'name required'   unless defined $args{name};
   88: 	croak 'source required' unless defined $args{source};
   89: 
   90: 	my $self = {
   91: 		name          => $args{name},
   92: 		source        => $args{source},

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

93: # parameters => [], 94: evidence => [], 95: return_type => undef, 96: classification => undef, 97: confidence => undef, 98: }; 99: 100: return bless $self, $class; 101: } 102: 103: =head2 name 104: 105: Return the method's name. 106: 107: my $name = $method->name; 108: 109: =head3 Arguments 110: 111: None beyond C<$self>. 112: 113: =head3 Returns 114: 115: The name string supplied to C<new>. Read-only — there is no setter; 116: C<name> ignores any extra arguments passed to it. 117: 118: =head3 API specification 119: 120: =head4 input 121: 122: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 123: 124: =head4 output 125: 126: { type => SCALAR } 127: 128: =cut 129: 130: sub name { $_[0]->{name} } 131: 132: =head2 source 133: 134: Return the method's raw source text. 135: 136: my $source = $method->source; 137: 138: =head3 Arguments 139: 140: None beyond C<$self>. 141: 142: =head3 Returns 143: 144: The source string supplied to C<new>. Read-only — there is no setter; 145: C<source> ignores any extra arguments passed to it. 146: 147: =head3 API specification 148: 149: =head4 input 150: 151: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 152: 153: =head4 output 154: 155: { type => SCALAR } 156: 157: =cut 158: 159: sub source { $_[0]->{source} } 160: 161: =head2 return_type 162: 163: Read/write accessor for the resolved return type. 164: 165: $method->return_type('object'); 166: my $type = $method->return_type; 167: 168: =head3 Arguments 169: 170: =over 4 171: 172: =item * C<$val> 173: 174: Optional. If supplied (including C<undef>), stores it as the new 175: return type. 176: 177: =back 178: 179: =head3 Returns 180: 181: The current return type string, or C<undef> if not yet resolved (or 182: explicitly set back to C<undef>). 183: 184: =head3 Side effects 185: 186: Overwrites the stored return type when called with an argument. 187: 188: =head3 API specification 189: 190: =head4 input 191: 192: { 193: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 194: val => { type => SCALAR, optional => 1 }, 195: } 196: 197: =head4 output

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

198:

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

199: { type => SCALAR, optional => 1 } 200: 201: =cut 202: 203: sub return_type { 204: my ($self, $val) = @_; 205: $self->{return_type} = $val if @_ > 1; 206: return $self->{return_type}; 207: } 208: 209: =head2 classification 210: 211: Read/write accessor for the resolved test classification. 212: 213: $method->classification('getter'); 214: my $class = $method->classification; 215: 216: =head3 Arguments 217: 218: =over 4 219: 220: =item * C<$val> 221: 222: Optional. If supplied (including C<undef>), stores it as the new 223: classification. 224: 225: =back 226: 227: =head3 Returns 228: 229: The current classification string, or C<undef> if not yet resolved. 230: 231: =head3 Side effects 232: 233: Overwrites the stored classification when called with an argument. 234: 235: =head3 API specification 236: 237: =head4 input 238: 239: { 240: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 241: val => { type => SCALAR, optional => 1 }, 242: } 243: 244: =head4 output

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

245:

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

246: { type => SCALAR, optional => 1 } 247: 248: =cut 249: 250: sub classification { 251: my ($self, $val) = @_; 252: $self->{classification} = $val if @_ > 1; 253: return $self->{classification}; 254: } 255: 256: =head2 confidence 257: 258: Read/write accessor for the resolved confidence hashref. 259: 260: $method->confidence({ score => 45, level => 'medium' }); 261: my $conf = $method->confidence; 262: 263: =head3 Arguments 264: 265: =over 4 266: 267: =item * C<$val> 268: 269: Optional. If supplied (including C<undef>), stores it as the new 270: confidence value. 271: 272: =back 273: 274: =head3 Returns 275: 276: The current confidence hashref (with C<score> and C<level> keys), or 277: C<undef> if not yet resolved. 278: 279: =head3 Side effects 280: 281: Overwrites the stored confidence value when called with an argument. 282: 283: =head3 API specification 284: 285: =head4 input 286: 287: { 288: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 289: val => { type => HASHREF, optional => 1 }, 290: } 291: 292: =head4 output

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

293:

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

294: { type => HASHREF, optional => 1 } 295: 296: =cut 297: 298: sub confidence { 299: my ($self, $val) = @_; 300: $self->{confidence} = $val if @_ > 1; 301: return $self->{confidence}; 302: } 303: 304: =head2 add_evidence 305: 306: Record one piece of weighted evidence about the method's behaviour. 307: 308: $method->add_evidence( 309: category => 'return', 310: signal => 'returns_property', 311: value => 'name', 312: weight => 20, 313: ); 314: 315: =head3 Arguments 316: 317: =over 4 318: 319: =item * C<category> 320: 321: One of C<return>, C<input>, or C<effect>. Required. Croaks 322: C<"Invalid evidence category '...'"> for any other value, including a 323: missing category. 324: 325: =item * C<signal> 326: 327: A recognised signal name (see L</Notes>). Required. Croaks 328: C<"Invalid evidence signal '...'"> for any other value, including a 329: missing signal. 330: 331: =item * C<value> 332: 333: Optional. An arbitrary value associated with the signal (e.g. the 334: property name for C<returns_property>). 335: 336: =item * C<weight> 337: 338: Optional. A numeric weight. Defaults to 1. 339: 340: =back 341: 342: =head3 Returns 343: 344: Nothing (undef). 345: 346: =head3 Side effects 347: 348: Appends an evidence hashref (with keys C<category>, C<signal>, 349: C<value>, C<weight>) to the object's internal evidence list. 350: 351: =head3 Notes 352: 353: Recognised signals are C<returns_property>, C<returns_constant>, 354: C<returns_self>, C<legacy_type>, C<context_aware>, C<error_pattern> 355: (intended for category C<return>); C<input_validated>, C<input_typed>, 356: C<input_optional> (category C<input>); and C<has_side_effect>, 357: C<no_side_effect> (category C<effect>). Signal validity is checked 358: against the full set regardless of category — passing a return-only 359: signal with C<category =E<gt> 'input'> does not croak. 360: 361: =head3 API specification 362: 363: =head4 input 364: 365: { 366: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 367: category => { type => SCALAR }, 368: signal => { type => SCALAR }, 369: value => { type => SCALAR, optional => 1 }, 370: weight => { type => SCALAR, optional => 1 }, 371: } 372: 373: =head4 output 374: 375: { type => UNDEF } 376: 377: =cut 378: 379: sub add_evidence { 380: my ($self, %args) = @_; 381: 382: my $cat = $args{category} // ''; 383: croak "Invalid evidence category '$cat'" unless $VALID_CATEGORIES{$cat}; 384: 385: my $sig = $args{signal} // ''; 386: croak "Invalid evidence signal '$sig'" unless $VALID_SIGNALS{$sig}; 387: 388: push @{ $self->{evidence} }, { 389: category => $args{category}, 390: signal => $args{signal}, 391: value => $args{value}, 392: weight => defined $args{weight} ? $args{weight} : 1, 393: }; 394: 395: return; 396: } 397: 398: =head2 evidence 399: 400: Return all recorded evidence entries. 401: 402: my @evidence = $method->evidence; 403: for my $entry (@evidence) { 404: print "$entry->{category}/$entry->{signal}: $entry->{weight}\n"; 405: } 406: 407: =head3 Arguments 408: 409: None beyond C<$self>. 410: 411: =head3 Returns 412: 413: A list of evidence hashrefs (each with keys C<category>, C<signal>, 414: C<value>, C<weight>), in the order they were added via 415: C<add_evidence>. Empty list if no evidence has been recorded. Called 416: in scalar context, returns the count of evidence entries. 417: 418: =head3 API specification 419: 420: =head4 input 421: 422: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 423: 424: =head4 output 425: 426: { type => ARRAYREF, items => { type => HASHREF } } 427: 428: =cut 429: 430: sub evidence { 431: my $self = $_[0]; 432: return @{ $self->{evidence} }; 433: } 434: 435: =head2 evidence_ref 436:

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

437: Return all recorded evidence entries as an arrayref. 438: 439: my $ref = $method->evidence_ref; 440: print "count: ", scalar(@$ref), "\n"; 441: 442: =head3 Arguments 443: 444: None beyond C<$self>. 445: 446: =head3 Returns 447: 448: An arrayref of the same evidence hashrefs returned by C<evidence>. 449: This is the live internal arrayref, not a copy — modifying it 450: modifies the object's evidence list. 451: 452: =head3 API specification 453: 454: =head4 input 455: 456: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 457: 458: =head4 output 459: 460: { type => ARRAYREF, items => { type => HASHREF } } 461: 462: =cut 463: 464: sub evidence_ref { 465: my $self = $_[0]; 466: return $self->{evidence}; 467: } 468: 469: =head2 resolve_return_type 470:

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

471: Derive a return type from the accumulated C<return>-category evidence 472: and store it. 473: 474: $method->add_evidence(category => 'return', signal => 'returns_self', weight => 20); 475: my $type = $method->resolve_return_type; # 'object' 476: 477: =head3 Arguments 478: 479: None beyond C<$self>. 480: 481: =head3 Returns 482: 483: One of C<object>, C<property>, or C<constant>, chosen by summing the 484: weight of all C<return>-category evidence into three buckets 485: (C<returns_self> -> object; C<returns_property>, C<context_aware>, 486: C<error_pattern> -> property; C<returns_constant> -> constant; 487: C<legacy_type> -> object or property depending on its C<value>) and 488: picking the highest-scoring bucket. Ties are broken alphabetically 489: among the tied bucket names (C<constant> E<lt> C<object> E<lt> 490: C<property>). With no C<return>-category evidence at all, all three 491: buckets score 0 and C<constant> wins the alphabetical tie-break. 492: 493: =head3 Side effects 494: 495: Sets C<return_type> to the resolved value. 496: 497: =head3 Notes 498: 499: Evidence outside the C<return> category is ignored. Evidence with an 500: unrecognised signal name is also ignored (this can only happen if a 501: caller other than C<add_evidence> populated the evidence list 502: directly, since C<add_evidence> itself rejects unrecognised signals). 503: 504: =head3 API specification 505: 506: =head4 input 507: 508: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 509: 510: =head4 output 511: 512: { type => SCALAR } 513: 514: =cut 515: 516: sub resolve_return_type { 517 → 520 → 547 517: my $self = $_[0]; 518: my %score = (property => 0, constant => 0, object => 0); 519: 520: for my $ev (@{ $self->{evidence} }) { 521: next unless $ev->{category} eq 'return'; 522: if($ev->{signal} eq 'returns_property') { 523: $score{property} += $ev->{weight}; 524: } elsif($ev->{signal} eq 'returns_constant') { 525: $score{constant} += $ev->{weight}; 526: } elsif($ev->{signal} eq 'returns_self') {

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

527: $score{object} += $ev->{weight}; 528: } elsif($ev->{signal} eq 'legacy_type') { 529: # Legacy type hint — map to nearest score bucket if recognisable 530: my $t = $ev->{value} // ''; 531: if($t eq 'object') { $score{object} += $ev->{weight} } 532: elsif($t eq 'self') { $score{object} += $ev->{weight} } 533: else { $score{property} += $ev->{weight} } 534: } elsif($ev->{signal} eq 'context_aware') { 535: # Context-aware return suggests getter behaviour

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

536: $score{property} += $ev->{weight}; 537: } elsif($ev->{signal} eq 'error_pattern') { 538: # Error pattern return doesn't strongly imply a type — 539: # give a small nudge toward property (scalar return) 540: $score{property} += $ev->{weight}; 541: } 542: # Unknown signals are ignored — they may be used by external consumers 543: } 544: 545: # Tie-break alphabetically — deterministic but arbitrary. 546: # %score is always initialised with all three keys, so the || 0 guard is dead. 547: my ($winner) = sort { $score{$b} <=> $score{$a} || $a cmp $b } keys %score; 548: 549: return $self->{return_type} = $winner; 550: } 551: 552: =head2 resolve_confidence

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

553: 554: Derive a confidence level from the total weight of all accumulated 555: evidence (every category, not just C<return>) and store it. 556: 557: $method->add_evidence(category => 'return', signal => 'returns_self', weight => 50); 558: my $conf = $method->resolve_confidence; # { score => 50, level => 'high' } 559: 560: =head3 Arguments 561: 562: None beyond C<$self>. 563: 564: =head3 Returns 565: 566: A hashref with keys C<score> (the sum of every evidence entry's 567: C<weight>) and C<level>, which is C<low> if C<score> is below 568: C<$MEDIUM_CONFIDENCE_THRESHOLD> (20), C<medium> if at least 20 but 569: below C<$HIGH_CONFIDENCE_THRESHOLD> (40), or C<high> if 40 or above. 570: With no evidence at all, C<score> is 0 and C<level> is C<low>. 571: 572: =head3 Side effects 573: 574: Sets C<confidence> to the resolved hashref. 575: 576: =head3 API specification 577: 578: =head4 input 579: 580: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 581: 582: =head4 output 583: 584: { 585: type => HASHREF, 586: keys => { 587: score => { type => SCALAR }, 588: level => { type => SCALAR }, 589: }, 590: } 591: 592: =cut 593: 594: sub resolve_confidence { 595: my $self = $_[0]; 596: 597: my $total = 0; 598: $total += $_->{weight} for @{ $self->{evidence} }; 599: 600: my $level = $total >= $HIGH_CONFIDENCE_THRESHOLD ? 'high' : $total >= $MEDIUM_CONFIDENCE_THRESHOLD ? 'medium' : 'low'; 601: 602: $self->{confidence} = { score => $total, level => $level }; 603:

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

604: return $self->{confidence}; 605: } 606: 607: =head2 resolve_classification

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

608: 609: Derive a test classification from the resolved return type and store 610: it. 611: 612: $method->add_evidence(category => 'return', signal => 'returns_self', weight => 20); 613: my $class = $method->resolve_classification; # 'chainable' 614: 615: =head3 Arguments 616: 617: None beyond C<$self>. 618: 619: =head3 Returns 620: 621: C<chainable> if C<return_type> is C<object>, C<getter> if 622: C<property>, C<constant> if C<constant>, or C<unknown> for any other 623: value. 624: 625: =head3 Side effects 626: 627: Calls C<resolve_return_type> first (and so also sets C<return_type>) 628: if C<return_type> has not already been resolved. Sets 629: C<classification> to the resolved value. 630: 631: =head3 API specification 632: 633: =head4 input 634: 635: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 636: 637: =head4 output 638: 639: { type => SCALAR } 640: 641: =cut 642: 643: sub resolve_classification { 644 → 649 → 660 644: my $self = $_[0]; 645: 646: # Return_type must be resolved before classification can be determined 647: $self->resolve_return_type() unless defined $self->{return_type}; 648: 649: if($self->{return_type} eq 'object') { 650: $self->{classification} = 'chainable'; 651: } elsif ($self->{return_type} eq 'property') { 652: $self->{classification} = 'getter';

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

653: } elsif ($self->{return_type} eq 'constant') { 654: $self->{classification} = 'constant'; 655: } else { 656: # Unreachable: resolve_return_type always returns object/property/constant 657: confess "invariant violation: unexpected return_type '$self->{return_type}'"; 658: } 659: 660: return $self->{classification}; 661: } 662:

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

663: =head2 absorb_legacy_output 664: 665: Convert a legacy schema output hashref (the pre-evidence-model output 666: descriptor format) into one or more C<return>-category evidence 667: entries. 668: 669: $method->absorb_legacy_output({ 670: type => 'object', 671: _returns_self => 1, 672: }); 673: 674: =head3 Arguments 675: 676: =over 4 677: 678: =item * C<$output> 679: 680: A hashref of legacy output hints, or C<undef>. 681: 682: =back 683: 684: =head3 Returns 685: 686: Nothing (undef). 687: 688: =head3 Side effects 689: 690: For each recognised key present and true in C<$output>, calls 691: C<add_evidence> once: 692: 693: =over 4 694: 695: =item * C<type> -> C<legacy_type> evidence, C<value> set to 696: C<$output-E<gt>{type}>, weight 20. 697: 698: =item * C<_returns_self> -> C<returns_self> evidence, weight 25. 699: 700: =item * C<_context_aware> -> C<context_aware> evidence, weight 15. 701: 702: =item * C<_error_return> -> C<error_pattern> evidence, C<value> set to 703: C<$output-E<gt>{_error_return}>, weight 15. 704: 705: =back 706: 707: =head3 Notes 708: 709: C<$output> being C<undef> or any non-hashref value is silently 710: ignored — no evidence is added and no exception is raised. A hashref 711: with none of the four recognised keys set to a true value also adds 712: no evidence. 713: 714: =head3 API specification 715: 716: =head4 input 717: 718: { 719: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 720: output => { type => HASHREF, optional => 1 }, 721: } 722: 723: =head4 output 724: 725: { type => UNDEF } 726: 727: =cut 728: 729: sub absorb_legacy_output { 730 → 734 → 743 730: my ($self, $output) = @_; 731: 732: return unless $output && ref $output eq 'HASH'; 733: 734: if ($output->{type}) { 735: $self->add_evidence( 736: category => 'return',

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

737: signal => 'legacy_type', 738: value => $output->{type}, 739: weight => 20, 740: ); 741: } 742: 743 → 743 → 751 743: if ($output->{_returns_self}) { 744: $self->add_evidence( 745: category => 'return',

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

746: signal => 'returns_self', 747: weight => 25, 748: ); 749: } 750: 751 → 751 → 759 751: if ($output->{_context_aware}) { 752: $self->add_evidence( 753: category => 'return',

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

754: signal => 'context_aware', 755: weight => 15, 756: ); 757: } 758: 759 → 759 → 0 759: if ($output->{_error_return}) { 760: $self->add_evidence( 761: category => 'return',

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

762: signal => 'error_pattern', 763: value => $output->{_error_return}, 764: weight => 15, 765: ); 766: } 767: } 768: 769: 1;