| File: | blib/lib/App/Test/Generator/Model/Method.pm |
| Coverage: | 97.8% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::Test::Generator::Model::Method; | |||||
| 2 | ||||||
| 3 | 32 32 32 | 132597 27 446 | use strict; | |||
| 4 | 32 32 32 | 51 25 614 | use warnings; | |||
| 5 | ||||||
| 6 | 32 32 32 | 50 22 613 | use Carp qw(croak); | |||
| 7 | 32 32 32 | 5099 42162 17109 | 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 - 75 | =head1 NAME
App::Test::Generator::Model::Method - Evidence-based model of a single method under test
=head1 VERSION
Version 0.41
=head1 DESCRIPTION
Accumulates weighted evidence about a single method's return behaviour,
gathered independently by several analysers
(L<App::Test::Generator::Analyzer::Return> and friends), then resolves
that evidence into a best-guess return type, test classification, and
confidence level. This lets multiple independent heuristics contribute
to one final judgement instead of the first heuristic to run winning
outright.
=head2 new
Construct a new Method model.
my $method = App::Test::Generator::Model::Method->new(
name => 'get_name',
source => 'sub get_name { return $_[0]->{name}; }',
);
=head3 Arguments
=over 4
=item * C<name>
The method's name. Required.
=item * C<source>
The method's raw Perl source text. Required.
=back
=head3 Returns
A blessed hashref with C<evidence> initialised to an empty arrayref
and C<return_type>, C<classification>, and C<confidence> initialised
to C<undef>. Croaks with C<"name required"> or C<"source required">
if either argument is missing.
=head3 API specification
=head4 input
{
name => { type => SCALAR },
source => { type => SCALAR },
}
=head4 output
{ type => OBJECT, isa => 'App::Test::Generator::Model::Method' }
=cut | |||||
| 76 | ||||||
| 77 | sub new { | |||||
| 78 | 410 | 281779 | my ($class, %args) = @_; | |||
| 79 | 410 | 523 | croak 'name required' unless defined $args{name}; | |||
| 80 | 408 | 382 | croak 'source required' unless defined $args{source}; | |||
| 81 | ||||||
| 82 | my $self = { | |||||
| 83 | name => $args{name}, | |||||
| 84 | source => $args{source}, | |||||
| 85 | # parameters => [], | |||||
| 86 | 406 | 932 | evidence => [], | |||
| 87 | return_type => undef, | |||||
| 88 | classification => undef, | |||||
| 89 | confidence => undef, | |||||
| 90 | }; | |||||
| 91 | ||||||
| 92 | 406 | 636 | return bless $self, $class; | |||
| 93 | } | |||||
| 94 | ||||||
| 95 - 120 | =head2 name
Return the method's name.
my $name = $method->name;
=head3 Arguments
None beyond C<$self>.
=head3 Returns
The name string supplied to C<new>. Read-only â there is no setter;
C<name> ignores any extra arguments passed to it.
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{ type => SCALAR }
=cut | |||||
| 121 | ||||||
| 122 | 9 | 346 | sub name { $_[0]->{name} } | |||
| 123 | ||||||
| 124 - 149 | =head2 source
Return the method's raw source text.
my $source = $method->source;
=head3 Arguments
None beyond C<$self>.
=head3 Returns
The source string supplied to C<new>. Read-only â there is no setter;
C<source> ignores any extra arguments passed to it.
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{ type => SCALAR }
=cut | |||||
| 150 | ||||||
| 151 | 294 | 360 | sub source { $_[0]->{source} } | |||
| 152 | ||||||
| 153 - 193 | =head2 return_type
Read/write accessor for the resolved return type.
$method->return_type('object');
my $type = $method->return_type;
=head3 Arguments
=over 4
=item * C<$val>
Optional. If supplied (including C<undef>), stores it as the new
return type.
=back
=head3 Returns
The current return type string, or C<undef> if not yet resolved (or
explicitly set back to C<undef>).
=head3 Side effects
Overwrites the stored return type when called with an argument.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' },
val => { type => SCALAR, optional => 1 },
}
=head4 output
{ type => SCALAR, optional => 1 }
=cut | |||||
| 194 | ||||||
| 195 | sub return_type { | |||||
| 196 | 27 | 41 | my ($self, $val) = @_; | |||
| 197 | 27 | 35 | $self->{return_type} = $val if @_ > 1; | |||
| 198 | 27 | 38 | return $self->{return_type}; | |||
| 199 | } | |||||
| 200 | ||||||
| 201 - 240 | =head2 classification
Read/write accessor for the resolved test classification.
$method->classification('getter');
my $class = $method->classification;
=head3 Arguments
=over 4
=item * C<$val>
Optional. If supplied (including C<undef>), stores it as the new
classification.
=back
=head3 Returns
The current classification string, or C<undef> if not yet resolved.
=head3 Side effects
Overwrites the stored classification when called with an argument.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' },
val => { type => SCALAR, optional => 1 },
}
=head4 output
{ type => SCALAR, optional => 1 }
=cut | |||||
| 241 | ||||||
| 242 | sub classification { | |||||
| 243 | 300 | 284 | my ($self, $val) = @_; | |||
| 244 | 300 | 295 | $self->{classification} = $val if @_ > 1; | |||
| 245 | 300 | 358 | return $self->{classification}; | |||
| 246 | } | |||||
| 247 | ||||||
| 248 - 288 | =head2 confidence
Read/write accessor for the resolved confidence hashref.
$method->confidence({ score => 45, level => 'medium' });
my $conf = $method->confidence;
=head3 Arguments
=over 4
=item * C<$val>
Optional. If supplied (including C<undef>), stores it as the new
confidence value.
=back
=head3 Returns
The current confidence hashref (with C<score> and C<level> keys), or
C<undef> if not yet resolved.
=head3 Side effects
Overwrites the stored confidence value when called with an argument.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' },
val => { type => HASHREF, optional => 1 },
}
=head4 output
{ type => HASHREF, optional => 1 }
=cut | |||||
| 289 | ||||||
| 290 | sub confidence { | |||||
| 291 | 299 | 265 | my ($self, $val) = @_; | |||
| 292 | 299 | 266 | $self->{confidence} = $val if @_ > 1; | |||
| 293 | 299 | 523 | return $self->{confidence}; | |||
| 294 | } | |||||
| 295 | ||||||
| 296 - 369 | =head2 add_evidence
Record one piece of weighted evidence about the method's behaviour.
$method->add_evidence(
category => 'return',
signal => 'returns_property',
value => 'name',
weight => 20,
);
=head3 Arguments
=over 4
=item * C<category>
One of C<return>, C<input>, or C<effect>. Required. Croaks
C<"Invalid evidence category '...'"> for any other value, including a
missing category.
=item * C<signal>
A recognised signal name (see L</Notes>). Required. Croaks
C<"Invalid evidence signal '...'"> for any other value, including a
missing signal.
=item * C<value>
Optional. An arbitrary value associated with the signal (e.g. the
property name for C<returns_property>).
=item * C<weight>
Optional. A numeric weight. Defaults to 1.
=back
=head3 Returns
Nothing (undef).
=head3 Side effects
Appends an evidence hashref (with keys C<category>, C<signal>,
C<value>, C<weight>) to the object's internal evidence list.
=head3 Notes
Recognised signals are C<returns_property>, C<returns_constant>,
C<returns_self>, C<legacy_type>, C<context_aware>, C<error_pattern>
(intended for category C<return>); C<input_validated>, C<input_typed>,
C<input_optional> (category C<input>); and C<has_side_effect>,
C<no_side_effect> (category C<effect>). Signal validity is checked
against the full set regardless of category â passing a return-only
signal with C<category =E<gt> 'input'> does not croak.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' },
category => { type => SCALAR },
signal => { type => SCALAR },
value => { type => SCALAR, optional => 1 },
weight => { type => SCALAR, optional => 1 },
}
=head4 output
{ type => UNDEF }
=cut | |||||
| 370 | ||||||
| 371 | sub add_evidence { | |||||
| 372 | 584 | 3596 | my ($self, %args) = @_; | |||
| 373 | ||||||
| 374 | # Validate category â must be one of the three recognised kinds | |||||
| 375 | 584 1752 | 882 1676 | my %valid_categories = map { $_ => 1 } qw(return input effect); | |||
| 376 | ||||||
| 377 | 584 | 662 | my $cat = $args{category} // ''; | |||
| 378 | 584 | 563 | 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 | 580 6380 | 504 4912 | 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 | 580 | 651 | my $sig = $args{signal} // ''; | |||
| 390 | 580 | 523 | croak "Invalid evidence signal '$sig'" unless $valid_signals{$sig}; | |||
| 391 | ||||||
| 392 | 576 | 1193 | push @{ $self->{evidence} }, { | |||
| 393 | category => $args{category}, | |||||
| 394 | signal => $args{signal}, | |||||
| 395 | value => $args{value}, | |||||
| 396 | 576 | 357 | weight => defined $args{weight} ? $args{weight} : 1, | |||
| 397 | }; | |||||
| 398 | ||||||
| 399 | 576 | 1184 | return; | |||
| 400 | } | |||||
| 401 | ||||||
| 402 - 432 | =head2 evidence
Return all recorded evidence entries.
my @evidence = $method->evidence;
for my $entry (@evidence) {
print "$entry->{category}/$entry->{signal}: $entry->{weight}\n";
}
=head3 Arguments
None beyond C<$self>.
=head3 Returns
A list of evidence hashrefs (each with keys C<category>, C<signal>,
C<value>, C<weight>), in the order they were added via
C<add_evidence>. Empty list if no evidence has been recorded. Called
in scalar context, returns the count of evidence entries.
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{ type => ARRAYREF, items => { type => HASHREF } }
=cut | |||||
| 433 | ||||||
| 434 | sub evidence { | |||||
| 435 | 36 | 617 | my $self = $_[0]; | |||
| 436 | 36 36 | 30 55 | return @{ $self->{evidence} }; | |||
| 437 | } | |||||
| 438 | ||||||
| 439 - 466 | =head2 evidence_ref
Return all recorded evidence entries as an arrayref.
my $ref = $method->evidence_ref;
print "count: ", scalar(@$ref), "\n";
=head3 Arguments
None beyond C<$self>.
=head3 Returns
An arrayref of the same evidence hashrefs returned by C<evidence>.
This is the live internal arrayref, not a copy â modifying it
modifies the object's evidence list.
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{ type => ARRAYREF, items => { type => HASHREF } }
=cut | |||||
| 467 | ||||||
| 468 | sub evidence_ref { | |||||
| 469 | 6 | 1389 | my $self = $_[0]; | |||
| 470 | 6 | 9 | return $self->{evidence}; | |||
| 471 | } | |||||
| 472 | ||||||
| 473 - 518 | =head2 resolve_return_type
Derive a return type from the accumulated C<return>-category evidence
and store it.
$method->add_evidence(category => 'return', signal => 'returns_self', weight => 20);
my $type = $method->resolve_return_type; # 'object'
=head3 Arguments
None beyond C<$self>.
=head3 Returns
One of C<object>, C<property>, or C<constant>, chosen by summing the
weight of all C<return>-category evidence into three buckets
(C<returns_self> -> object; C<returns_property>, C<context_aware>,
C<error_pattern> -> property; C<returns_constant> -> constant;
C<legacy_type> -> object or property depending on its C<value>) and
picking the highest-scoring bucket. Ties are broken alphabetically
among the tied bucket names (C<constant> E<lt> C<object> E<lt>
C<property>). With no C<return>-category evidence at all, all three
buckets score 0 and C<constant> wins the alphabetical tie-break.
=head3 Side effects
Sets C<return_type> to the resolved value.
=head3 Notes
Evidence outside the C<return> category is ignored. Evidence with an
unrecognised signal name is also ignored (this can only happen if a
caller other than C<add_evidence> populated the evidence list
directly, since C<add_evidence> itself rejects unrecognised signals).
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{ type => SCALAR }
=cut | |||||
| 519 | ||||||
| 520 | sub resolve_return_type { | |||||
| 521 | 317 | 235 | my $self = $_[0]; | |||
| 522 | 317 | 417 | my %score = (property => 0, constant => 0, object => 0); | |||
| 523 | ||||||
| 524 | 317 317 | 209 318 | for my $ev (@{ $self->{evidence} }) { | |||
| 525 | 518 | 466 | next unless $ev->{category} eq 'return'; | |||
| 526 | 514 | 671 | if($ev->{signal} eq 'returns_property') { | |||
| 527 | 37 | 56 | $score{property} += $ev->{weight}; | |||
| 528 | } elsif($ev->{signal} eq 'returns_constant') { | |||||
| 529 | 163 | 145 | $score{constant} += $ev->{weight}; | |||
| 530 | } elsif($ev->{signal} eq 'returns_self') { | |||||
| 531 | 21 | 22 | $score{object} += $ev->{weight}; | |||
| 532 | } elsif($ev->{signal} eq 'legacy_type') { | |||||
| 533 | # Legacy type hint â map to nearest score bucket if recognisable | |||||
| 534 | 271 | 264 | my $t = $ev->{value} // ''; | |||
| 535 | 271 25 | 279 30 | if($t eq 'object') { $score{object} += $ev->{weight} } | |||
| 536 | 1 | 2 | elsif($t eq 'self') { $score{object} += $ev->{weight} } | |||
| 537 | 245 | 278 | else { $score{property} += $ev->{weight} } | |||
| 538 | } elsif($ev->{signal} eq 'context_aware') { | |||||
| 539 | # Context-aware return suggests getter behaviour | |||||
| 540 | 7 | 9 | $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 | 15 | 14 | $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 | 317 851 | 514 1540 | my ($winner) = sort { ($score{$b} || 0) <=> ($score{$a} || 0) || $a cmp $b } keys %score; | |||
| 551 | ||||||
| 552 | 317 | 428 | $self->{return_type} = $winner || 'unknown'; | |||
| 553 | 317 | 393 | return $self->{return_type}; | |||
| 554 | } | |||||
| 555 | ||||||
| 556 - 596 | =head2 resolve_confidence
Derive a confidence level from the total weight of all accumulated
evidence (every category, not just C<return>) and store it.
$method->add_evidence(category => 'return', signal => 'returns_self', weight => 50);
my $conf = $method->resolve_confidence; # { score => 50, level => 'high' }
=head3 Arguments
None beyond C<$self>.
=head3 Returns
A hashref with keys C<score> (the sum of every evidence entry's
C<weight>) and C<level>, which is C<low> if C<score> is below
C<$MEDIUM_CONFIDENCE_THRESHOLD> (20), C<medium> if at least 20 but
below C<$HIGH_CONFIDENCE_THRESHOLD> (40), or C<high> if 40 or above.
With no evidence at all, C<score> is 0 and C<level> is C<low>.
=head3 Side effects
Sets C<confidence> to the resolved hashref.
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{
type => HASHREF,
keys => {
score => { type => SCALAR },
level => { type => SCALAR },
},
}
=cut | |||||
| 597 | ||||||
| 598 | sub resolve_confidence { | |||||
| 599 | 306 | 515 | my $self = $_[0]; | |||
| 600 | ||||||
| 601 | 306 | 208 | my $total = 0; | |||
| 602 | 306 306 | 194 399 | $total += $_->{weight} for @{ $self->{evidence} }; | |||
| 603 | ||||||
| 604 | 306 | 405 | my $level = $total >= $HIGH_CONFIDENCE_THRESHOLD ? 'high' : $total >= $MEDIUM_CONFIDENCE_THRESHOLD ? 'medium' : 'low'; | |||
| 605 | ||||||
| 606 | 306 | 1314 | $self->{confidence} = { score => $total, level => $level }; | |||
| 607 | ||||||
| 608 | 306 | 312 | return $self->{confidence}; | |||
| 609 | } | |||||
| 610 | ||||||
| 611 - 645 | =head2 resolve_classification
Derive a test classification from the resolved return type and store
it.
$method->add_evidence(category => 'return', signal => 'returns_self', weight => 20);
my $class = $method->resolve_classification; # 'chainable'
=head3 Arguments
None beyond C<$self>.
=head3 Returns
C<chainable> if C<return_type> is C<object>, C<getter> if
C<property>, C<constant> if C<constant>, or C<unknown> for any other
value.
=head3 Side effects
Calls C<resolve_return_type> first (and so also sets C<return_type>)
if C<return_type> has not already been resolved. Sets
C<classification> to the resolved value.
=head3 API specification
=head4 input
{ self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' } }
=head4 output
{ type => SCALAR }
=cut | |||||
| 646 | ||||||
| 647 | sub resolve_classification { | |||||
| 648 | 304 | 688 | my $self = $_[0]; | |||
| 649 | ||||||
| 650 | # Return_type must be resolved before classification can be determined | |||||
| 651 | 304 | 329 | $self->resolve_return_type() unless defined $self->{return_type}; | |||
| 652 | ||||||
| 653 | 304 | 398 | if($self->{return_type} eq 'object') { | |||
| 654 | 30 | 32 | $self->{classification} = 'chainable'; | |||
| 655 | } elsif ($self->{return_type} eq 'property') { | |||||
| 656 | 250 | 202 | $self->{classification} = 'getter'; | |||
| 657 | } elsif ($self->{return_type} eq 'constant') { | |||||
| 658 | 21 | 23 | $self->{classification} = 'constant'; | |||
| 659 | } else { | |||||
| 660 | 3 | 4 | $self->{classification} = 'unknown'; | |||
| 661 | } | |||||
| 662 | ||||||
| 663 | 304 | 263 | return $self->{classification}; | |||
| 664 | } | |||||
| 665 | ||||||
| 666 - 730 | =head2 absorb_legacy_output
Convert a legacy schema output hashref (the pre-evidence-model output
descriptor format) into one or more C<return>-category evidence
entries.
$method->absorb_legacy_output({
type => 'object',
_returns_self => 1,
});
=head3 Arguments
=over 4
=item * C<$output>
A hashref of legacy output hints, or C<undef>.
=back
=head3 Returns
Nothing (undef).
=head3 Side effects
For each recognised key present and true in C<$output>, calls
C<add_evidence> once:
=over 4
=item * C<type> -> C<legacy_type> evidence, C<value> set to
C<$output-E<gt>{type}>, weight 20.
=item * C<_returns_self> -> C<returns_self> evidence, weight 25.
=item * C<_context_aware> -> C<context_aware> evidence, weight 15.
=item * C<_error_return> -> C<error_pattern> evidence, C<value> set to
C<$output-E<gt>{_error_return}>, weight 15.
=back
=head3 Notes
C<$output> being C<undef> or any non-hashref value is silently
ignored â no evidence is added and no exception is raised. A hashref
with none of the four recognised keys set to a true value also adds
no evidence.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' },
output => { type => HASHREF, optional => 1 },
}
=head4 output
{ type => UNDEF }
=cut | |||||
| 731 | ||||||
| 732 | sub absorb_legacy_output { | |||||
| 733 | 305 | 344 | my ($self, $output) = @_; | |||
| 734 | ||||||
| 735 | 305 | 577 | return unless $output && ref $output eq 'HASH'; | |||
| 736 | ||||||
| 737 | 301 | 292 | if ($output->{type}) { | |||
| 738 | $self->add_evidence( | |||||
| 739 | category => 'return', | |||||
| 740 | signal => 'legacy_type', | |||||
| 741 | value => $output->{type}, | |||||
| 742 | 272 | 319 | weight => 20, | |||
| 743 | ); | |||||
| 744 | } | |||||
| 745 | ||||||
| 746 | 301 | 321 | if ($output->{_returns_self}) { | |||
| 747 | 9 | 12 | $self->add_evidence( | |||
| 748 | category => 'return', | |||||
| 749 | signal => 'returns_self', | |||||
| 750 | weight => 25, | |||||
| 751 | ); | |||||
| 752 | } | |||||
| 753 | ||||||
| 754 | 301 | 272 | if ($output->{_context_aware}) { | |||
| 755 | 10 | 11 | $self->add_evidence( | |||
| 756 | category => 'return', | |||||
| 757 | signal => 'context_aware', | |||||
| 758 | weight => 15, | |||||
| 759 | ); | |||||
| 760 | } | |||||
| 761 | ||||||
| 762 | 301 | 348 | if ($output->{_error_return}) { | |||
| 763 | $self->add_evidence( | |||||
| 764 | category => 'return', | |||||
| 765 | signal => 'error_pattern', | |||||
| 766 | value => $output->{_error_return}, | |||||
| 767 | 18 | 26 | weight => 15, | |||
| 768 | ); | |||||
| 769 | } | |||||
| 770 | } | |||||
| 771 | ||||||
| 772 | 1; | |||||