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(croak);
    7: use Readonly;
    8: 
    9: Readonly my $HIGH_CONFIDENCE_THRESHOLD   => 40;
   10: Readonly my $MEDIUM_CONFIDENCE_THRESHOLD => 20;
   11: 
   12: our $VERSION = '0.41';
   13: 
   14: =head1 NAME
   15: 
   16: App::Test::Generator::Model::Method - Evidence-based model of a single method under test
   17: 
   18: =head1 VERSION
   19: 
   20: Version 0.41
   21: 
   22: =head1 DESCRIPTION
   23: 
   24: Accumulates weighted evidence about a single method's return behaviour,
   25: gathered independently by several analysers
   26: (L<App::Test::Generator::Analyzer::Return> and friends), then resolves
   27: that evidence into a best-guess return type, test classification, and
   28: confidence level. This lets multiple independent heuristics contribute
   29: to one final judgement instead of the first heuristic to run winning
   30: outright.
   31: 
   32: =head2 new
   33: 
   34: Construct a new Method model.
   35: 
   36:     my $method = App::Test::Generator::Model::Method->new(
   37:         name   => 'get_name',
   38:         source => 'sub get_name { return $_[0]->{name}; }',
   39:     );
   40: 
   41: =head3 Arguments
   42: 
   43: =over 4
   44: 

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

45: =item * C<name>

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

46: 47: The method's name. Required. 48: 49: =item * C<source> 50:

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

51: The method's raw Perl source text. Required.

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

52: 53: =back 54: 55: =head3 Returns 56:

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

57: A blessed hashref with C<evidence> initialised to an empty arrayref

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

58: and C<return_type>, C<classification>, and C<confidence> initialised 59: to C<undef>. Croaks with C<"name required"> or C<"source required"> 60: if either argument is missing. 61: 62: =head3 API specification 63: 64: =head4 input 65: 66: { 67: name => { type => SCALAR }, 68: source => { type => SCALAR }, 69: } 70: 71: =head4 output 72: 73: { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } 74: 75: =cut 76: 77: sub new { 78: my ($class, %args) = @_; 79: croak 'name required' unless defined $args{name}; 80: croak 'source required' unless defined $args{source}; 81: 82: my $self = { 83: name => $args{name}, 84: source => $args{source}, 85: # parameters => [], 86: evidence => [], 87: return_type => undef, 88: classification => undef, 89: confidence => undef, 90: }; 91: 92: return bless $self, $class; 93: } 94: 95: =head2 name 96:

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

97: Return the method's name. 98: 99: my $name = $method->name; 100: 101: =head3 Arguments 102: 103: None beyond C<$self>. 104: 105: =head3 Returns

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

106: 107: The name string supplied to C<new>. Read-only — there is no setter; 108: C<name> ignores any extra arguments passed to it. 109: 110: =head3 API specification 111: 112: =head4 input 113: 114: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }

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

115: 116: =head4 output 117: 118: { type => SCALAR } 119: 120: =cut 121: 122: sub name { $_[0]->{name} } 123: 124: =head2 source 125: 126: Return the method's raw source text. 127: 128: my $source = $method->source; 129: 130: =head3 Arguments 131: 132: None beyond C<$self>.

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

133: 134: =head3 Returns 135: 136: The source string supplied to C<new>. Read-only — there is no setter; 137: C<source> ignores any extra arguments passed to it. 138: 139: =head3 API specification 140: 141: =head4 input

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

142: 143: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 144: 145: =head4 output

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

146: 147: { type => SCALAR } 148: 149: =cut 150: 151: sub source { $_[0]->{source} } 152: 153: =head2 return_type 154:

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

155: Read/write accessor for the resolved return type. 156: 157: $method->return_type('object'); 158: my $type = $method->return_type; 159: 160: =head3 Arguments 161: 162: =over 4 163: 164: =item * C<$val>

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

165: 166: Optional. If supplied (including C<undef>), stores it as the new 167: return type. 168: 169: =back 170: 171: =head3 Returns 172:

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

173: The current return type string, or C<undef> if not yet resolved (or 174: explicitly set back to C<undef>). 175: 176: =head3 Side effects 177: 178: Overwrites the stored return type when called with an argument. 179: 180: =head3 API specification 181:

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

182: =head4 input 183: 184: { 185: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 186: val => { type => SCALAR, optional => 1 }, 187: } 188: 189: =head4 output

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

190: 191: { type => SCALAR, optional => 1 } 192: 193: =cut 194: 195: sub return_type { 196: my ($self, $val) = @_; 197: $self->{return_type} = $val if @_ > 1;

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

198: return $self->{return_type}; 199: } 200: 201: =head2 classification 202: 203: Read/write accessor for the resolved test classification. 204: 205: $method->classification('getter'); 206: my $class = $method->classification; 207: 208: =head3 Arguments 209: 210: =over 4 211: 212: =item * C<$val> 213: 214: Optional. If supplied (including C<undef>), stores it as the new 215: classification. 216: 217: =back 218: 219: =head3 Returns 220: 221: The current classification string, or C<undef> if not yet resolved. 222: 223: =head3 Side effects 224: 225: Overwrites the stored classification when called with an argument. 226: 227: =head3 API specification 228: 229: =head4 input 230: 231: { 232: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 233: val => { type => SCALAR, optional => 1 }, 234: } 235: 236: =head4 output 237: 238: { type => SCALAR, optional => 1 } 239: 240: =cut 241: 242: sub classification { 243: my ($self, $val) = @_; 244: $self->{classification} = $val if @_ > 1; 245: return $self->{classification}; 246: } 247: 248: =head2 confidence 249: 250: Read/write accessor for the resolved confidence hashref. 251: 252: $method->confidence({ score => 45, level => 'medium' }); 253: my $conf = $method->confidence; 254: 255: =head3 Arguments 256: 257: =over 4 258: 259: =item * C<$val> 260: 261: Optional. If supplied (including C<undef>), stores it as the new 262: confidence value. 263: 264: =back 265: 266: =head3 Returns 267: 268: The current confidence hashref (with C<score> and C<level> keys), or 269: C<undef> if not yet resolved. 270: 271: =head3 Side effects 272: 273: Overwrites the stored confidence value when called with an argument. 274: 275: =head3 API specification 276: 277: =head4 input 278: 279: { 280: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 281: val => { type => HASHREF, optional => 1 }, 282: } 283: 284: =head4 output 285: 286: { type => HASHREF, optional => 1 } 287: 288: =cut 289: 290: sub confidence { 291: my ($self, $val) = @_; 292: $self->{confidence} = $val if @_ > 1; 293: return $self->{confidence}; 294: } 295: 296: =head2 add_evidence 297: 298: Record one piece of weighted evidence about the method's behaviour. 299: 300: $method->add_evidence( 301: category => 'return', 302: signal => 'returns_property', 303: value => 'name', 304: weight => 20, 305: ); 306: 307: =head3 Arguments 308: 309: =over 4 310: 311: =item * C<category> 312: 313: One of C<return>, C<input>, or C<effect>. Required. Croaks 314: C<"Invalid evidence category '...'"> for any other value, including a 315: missing category. 316: 317: =item * C<signal> 318: 319: A recognised signal name (see L</Notes>). Required. Croaks 320: C<"Invalid evidence signal '...'"> for any other value, including a 321: missing signal. 322: 323: =item * C<value> 324: 325: Optional. An arbitrary value associated with the signal (e.g. the 326: property name for C<returns_property>). 327: 328: =item * C<weight> 329: 330: Optional. A numeric weight. Defaults to 1. 331: 332: =back 333: 334: =head3 Returns 335: 336: Nothing (undef). 337: 338: =head3 Side effects 339: 340: Appends an evidence hashref (with keys C<category>, C<signal>, 341: C<value>, C<weight>) to the object's internal evidence list. 342: 343: =head3 Notes 344: 345: Recognised signals are C<returns_property>, C<returns_constant>, 346: C<returns_self>, C<legacy_type>, C<context_aware>, C<error_pattern> 347: (intended for category C<return>); C<input_validated>, C<input_typed>, 348: C<input_optional> (category C<input>); and C<has_side_effect>, 349: C<no_side_effect> (category C<effect>). Signal validity is checked 350: against the full set regardless of category — passing a return-only 351: signal with C<category =E<gt> 'input'> does not croak. 352: 353: =head3 API specification 354: 355: =head4 input 356: 357: { 358: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 359: category => { type => SCALAR }, 360: signal => { type => SCALAR }, 361: value => { type => SCALAR, optional => 1 }, 362: weight => { type => SCALAR, optional => 1 }, 363: } 364: 365: =head4 output 366: 367: { type => UNDEF } 368: 369: =cut 370: 371: sub add_evidence { 372: my ($self, %args) = @_; 373: 374: # Validate category — must be one of the three recognised kinds 375: my %valid_categories = map { $_ => 1 } qw(return input effect); 376: 377: my $cat = $args{category} // ''; 378: croak "Invalid evidence category '$cat'" unless $valid_categories{$cat}; 379: 380: # Validate signal — must be a known signal name to catch typos early. 381: # Signals are per-category; we validate the full set across all categories. 382: my %valid_signals = map { $_ => 1 } qw( 383: returns_property returns_constant returns_self 384: legacy_type context_aware error_pattern 385: input_validated input_typed input_optional 386: has_side_effect no_side_effect 387: ); 388: 389: my $sig = $args{signal} // ''; 390: croak "Invalid evidence signal '$sig'" unless $valid_signals{$sig}; 391: 392: push @{ $self->{evidence} }, { 393: category => $args{category}, 394: signal => $args{signal}, 395: value => $args{value}, 396: weight => defined $args{weight} ? $args{weight} : 1, 397: }; 398: 399: return; 400: } 401: 402: =head2 evidence 403: 404: Return all recorded evidence entries. 405: 406: my @evidence = $method->evidence; 407: for my $entry (@evidence) { 408: print "$entry->{category}/$entry->{signal}: $entry->{weight}\n"; 409: } 410: 411: =head3 Arguments 412: 413: None beyond C<$self>. 414: 415: =head3 Returns 416: 417: A list of evidence hashrefs (each with keys C<category>, C<signal>, 418: C<value>, C<weight>), in the order they were added via 419: C<add_evidence>. Empty list if no evidence has been recorded. Called 420: in scalar context, returns the count of evidence entries. 421: 422: =head3 API specification 423: 424: =head4 input 425: 426: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 427: 428: =head4 output 429: 430: { type => ARRAYREF, items => { type => HASHREF } } 431: 432: =cut 433: 434: sub evidence { 435: my $self = $_[0]; 436: return @{ $self->{evidence} }; 437: } 438: 439: =head2 evidence_ref 440: 441: Return all recorded evidence entries as an arrayref. 442: 443: my $ref = $method->evidence_ref; 444: print "count: ", scalar(@$ref), "\n"; 445: 446: =head3 Arguments 447: 448: None beyond C<$self>. 449: 450: =head3 Returns 451: 452: An arrayref of the same evidence hashrefs returned by C<evidence>. 453: This is the live internal arrayref, not a copy — modifying it 454: modifies the object's evidence list. 455: 456: =head3 API specification 457: 458: =head4 input 459: 460: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 461: 462: =head4 output 463: 464: { type => ARRAYREF, items => { type => HASHREF } } 465: 466: =cut 467: 468: sub evidence_ref { 469: my $self = $_[0]; 470: return $self->{evidence}; 471: } 472: 473: =head2 resolve_return_type 474: 475: Derive a return type from the accumulated C<return>-category evidence 476: and store it. 477: 478: $method->add_evidence(category => 'return', signal => 'returns_self', weight => 20); 479: my $type = $method->resolve_return_type; # 'object' 480: 481: =head3 Arguments 482: 483: None beyond C<$self>. 484: 485: =head3 Returns 486: 487: One of C<object>, C<property>, or C<constant>, chosen by summing the 488: weight of all C<return>-category evidence into three buckets 489: (C<returns_self> -> object; C<returns_property>, C<context_aware>, 490: C<error_pattern> -> property; C<returns_constant> -> constant; 491: C<legacy_type> -> object or property depending on its C<value>) and 492: picking the highest-scoring bucket. Ties are broken alphabetically 493: among the tied bucket names (C<constant> E<lt> C<object> E<lt> 494: C<property>). With no C<return>-category evidence at all, all three 495: buckets score 0 and C<constant> wins the alphabetical tie-break. 496: 497: =head3 Side effects 498: 499: Sets C<return_type> to the resolved value. 500: 501: =head3 Notes 502: 503: Evidence outside the C<return> category is ignored. Evidence with an 504: unrecognised signal name is also ignored (this can only happen if a 505: caller other than C<add_evidence> populated the evidence list 506: directly, since C<add_evidence> itself rejects unrecognised signals). 507: 508: =head3 API specification 509: 510: =head4 input 511: 512: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 513: 514: =head4 output 515: 516: { type => SCALAR } 517: 518: =cut 519: 520: sub resolve_return_type { 521 → 524 → 550 521: my $self = $_[0]; 522: my %score = (property => 0, constant => 0, object => 0); 523: 524: for my $ev (@{ $self->{evidence} }) { 525: next unless $ev->{category} eq 'return'; 526: if($ev->{signal} eq 'returns_property') { 527: $score{property} += $ev->{weight}; 528: } elsif($ev->{signal} eq 'returns_constant') { 529: $score{constant} += $ev->{weight}; 530: } elsif($ev->{signal} eq 'returns_self') { 531: $score{object} += $ev->{weight}; 532: } elsif($ev->{signal} eq 'legacy_type') { 533: # Legacy type hint — map to nearest score bucket if recognisable 534: my $t = $ev->{value} // ''; 535: if($t eq 'object') { $score{object} += $ev->{weight} } 536: elsif($t eq 'self') { $score{object} += $ev->{weight} } 537: else { $score{property} += $ev->{weight} } 538: } elsif($ev->{signal} eq 'context_aware') { 539: # Context-aware return suggests getter behaviour 540: $score{property} += $ev->{weight}; 541: } elsif($ev->{signal} eq 'error_pattern') { 542: # Error pattern return doesn't strongly imply a type — 543: # give a small nudge toward property (scalar return) 544: $score{property} += $ev->{weight}; 545: } 546: # Unknown signals are ignored — they may be used by external consumers 547: } 548: 549: # Tie-break alphabetically — deterministic but arbitrary 550: my ($winner) = sort { ($score{$b} || 0) <=> ($score{$a} || 0) || $a cmp $b } keys %score; 551: 552: $self->{return_type} = $winner || 'unknown'; 553: return $self->{return_type}; 554: } 555: 556: =head2 resolve_confidence 557: 558: Derive a confidence level from the total weight of all accumulated 559: evidence (every category, not just C<return>) and store it. 560: 561: $method->add_evidence(category => 'return', signal => 'returns_self', weight => 50); 562: my $conf = $method->resolve_confidence; # { score => 50, level => 'high' } 563: 564: =head3 Arguments 565: 566: None beyond C<$self>. 567: 568: =head3 Returns 569: 570: A hashref with keys C<score> (the sum of every evidence entry's 571: C<weight>) and C<level>, which is C<low> if C<score> is below 572: C<$MEDIUM_CONFIDENCE_THRESHOLD> (20), C<medium> if at least 20 but 573: below C<$HIGH_CONFIDENCE_THRESHOLD> (40), or C<high> if 40 or above. 574: With no evidence at all, C<score> is 0 and C<level> is C<low>. 575: 576: =head3 Side effects 577: 578: Sets C<confidence> to the resolved hashref. 579: 580: =head3 API specification 581: 582: =head4 input 583: 584: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 585: 586: =head4 output 587: 588: { 589: type => HASHREF, 590: keys => { 591: score => { type => SCALAR }, 592: level => { type => SCALAR }, 593: }, 594: } 595: 596: =cut 597: 598: sub resolve_confidence { 599: my $self = $_[0]; 600: 601: my $total = 0; 602: $total += $_->{weight} for @{ $self->{evidence} }; 603: 604: my $level = $total >= $HIGH_CONFIDENCE_THRESHOLD ? 'high' : $total >= $MEDIUM_CONFIDENCE_THRESHOLD ? 'medium' : 'low'; 605: 606: $self->{confidence} = { score => $total, level => $level }; 607: 608: return $self->{confidence}; 609: } 610: 611: =head2 resolve_classification 612: 613: Derive a test classification from the resolved return type and store 614: it. 615: 616: $method->add_evidence(category => 'return', signal => 'returns_self', weight => 20); 617: my $class = $method->resolve_classification; # 'chainable' 618: 619: =head3 Arguments 620: 621: None beyond C<$self>. 622: 623: =head3 Returns 624: 625: C<chainable> if C<return_type> is C<object>, C<getter> if 626: C<property>, C<constant> if C<constant>, or C<unknown> for any other 627: value. 628: 629: =head3 Side effects 630: 631: Calls C<resolve_return_type> first (and so also sets C<return_type>) 632: if C<return_type> has not already been resolved. Sets 633: C<classification> to the resolved value. 634: 635: =head3 API specification 636: 637: =head4 input 638: 639: { self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } } 640: 641: =head4 output 642: 643: { type => SCALAR } 644: 645: =cut 646: 647: sub resolve_classification { 648 → 653 → 663 648: my $self = $_[0]; 649: 650: # Return_type must be resolved before classification can be determined 651: $self->resolve_return_type() unless defined $self->{return_type}; 652: 653: if($self->{return_type} eq 'object') { 654: $self->{classification} = 'chainable'; 655: } elsif ($self->{return_type} eq 'property') { 656: $self->{classification} = 'getter'; 657: } elsif ($self->{return_type} eq 'constant') { 658: $self->{classification} = 'constant'; 659: } else { 660: $self->{classification} = 'unknown'; 661: } 662: 663: return $self->{classification}; 664: } 665: 666: =head2 absorb_legacy_output 667: 668: Convert a legacy schema output hashref (the pre-evidence-model output 669: descriptor format) into one or more C<return>-category evidence 670: entries. 671: 672: $method->absorb_legacy_output({ 673: type => 'object', 674: _returns_self => 1, 675: }); 676: 677: =head3 Arguments 678: 679: =over 4 680: 681: =item * C<$output> 682: 683: A hashref of legacy output hints, or C<undef>. 684: 685: =back 686: 687: =head3 Returns 688: 689: Nothing (undef). 690: 691: =head3 Side effects 692: 693: For each recognised key present and true in C<$output>, calls 694: C<add_evidence> once: 695: 696: =over 4 697: 698: =item * C<type> -> C<legacy_type> evidence, C<value> set to 699: C<$output-E<gt>{type}>, weight 20. 700: 701: =item * C<_returns_self> -> C<returns_self> evidence, weight 25. 702: 703: =item * C<_context_aware> -> C<context_aware> evidence, weight 15. 704: 705: =item * C<_error_return> -> C<error_pattern> evidence, C<value> set to 706: C<$output-E<gt>{_error_return}>, weight 15. 707: 708: =back 709: 710: =head3 Notes 711: 712: C<$output> being C<undef> or any non-hashref value is silently 713: ignored — no evidence is added and no exception is raised. A hashref 714: with none of the four recognised keys set to a true value also adds 715: no evidence. 716: 717: =head3 API specification 718: 719: =head4 input 720: 721: { 722: self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 723: output => { type => HASHREF, optional => 1 }, 724: } 725: 726: =head4 output 727: 728: { type => UNDEF } 729: 730: =cut 731: 732: sub absorb_legacy_output { 733 → 737 → 746 733: my ($self, $output) = @_; 734: 735: return unless $output && ref $output eq 'HASH'; 736: 737: if ($output->{type}) { 738: $self->add_evidence( 739: category => 'return', 740: signal => 'legacy_type', 741: value => $output->{type}, 742: weight => 20, 743: ); 744: } 745: 746 → 746 → 754 746: if ($output->{_returns_self}) { 747: $self->add_evidence( 748: category => 'return', 749: signal => 'returns_self', 750: weight => 25, 751: ); 752: } 753: 754 → 754 → 762 754: if ($output->{_context_aware}) { 755: $self->add_evidence( 756: category => 'return', 757: signal => 'context_aware', 758: weight => 15, 759: ); 760: } 761: 762 → 762 → 0 762: if ($output->{_error_return}) { 763: $self->add_evidence( 764: category => 'return', 765: signal => 'error_pattern', 766: value => $output->{_error_return}, 767: weight => 15, 768: ); 769: } 770: } 771: 772: 1;