File Coverage

File:blib/lib/App/Test/Generator/Model/Method.pm
Coverage:97.8%

linestmtbrancondsubtimecode
1package 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
9Readonly my $HIGH_CONFIDENCE_THRESHOLD   => 40;
10Readonly my $MEDIUM_CONFIDENCE_THRESHOLD => 20;
11
12our $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
77sub 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
195sub 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
242sub 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
290sub 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
371sub 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
434sub 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
468sub 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
520sub 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
598sub 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
647sub 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
732sub 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
7721;