File Coverage

File:blib/lib/App/Test/Generator/Analyzer/ReturnMeta.pm
Coverage:96.5%

linestmtbrancondsubtimecode
1package App::Test::Generator::Analyzer::ReturnMeta;
2
3
24
24
24
68155
22
284
use strict;
4
24
24
24
35
19
408
use warnings;
5
24
24
24
38
17
395
use Carp qw(croak);
6
24
24
24
34
19
4386
use Readonly;
7
8# --------------------------------------------------
9# Scoring penalties and bonuses applied to stability
10# and consistency based on detected return patterns.
11# Scores are clamped to [0, 100] after all adjustments.
12# --------------------------------------------------
13Readonly my $PENALTY_CONTEXT_SENSITIVE_STABILITY   => 25;
14Readonly my $PENALTY_CONTEXT_SENSITIVE_CONSISTENCY => 15;
15Readonly my $PENALTY_MIXED_RETURN_CONSISTENCY      => 30;
16Readonly my $PENALTY_IMPLICIT_UNDEF_STABILITY      => 20;
17Readonly my $PENALTY_EXPLICIT_UNDEF_STABILITY      => 10;
18Readonly my $PENALTY_EMPTY_LIST_CONSISTENCY        => 15;
19Readonly my $PENALTY_EXCEPTION_SWALLOW_STABILITY   => 20;
20Readonly my $BONUS_BOOLEAN_STABILITY               => 5;
21
22our $VERSION = '0.36';
23
24 - 62
=head1 VERSION

Version 0.36

=head1 DESCRIPTION

Analyses the return metadata of a schema's output section and produces
stability and consistency scores along with a list of risk flags. This
is used by L<App::Test::Generator> to assess how reliably a function's
return value can be tested.

=head2 new

Construct a new ReturnMeta analyser.

    my $analyser = App::Test::Generator::Analyzer::ReturnMeta->new;

=head3 Arguments

None.

=head3 Returns

A blessed hashref.

=head3 API specification

=head4 input

    {}

=head4 output

    {
        type => OBJECT,
        isa  => 'App::Test::Generator::Analyzer::ReturnMeta',
    }

=cut
63
64
294
111971
sub new { bless {}, shift }
65
66 - 148
=head2 analyze

Analyse the C<output> section of a schema hashref and return a scoring
report covering stability, consistency, and risk flags.

    my $analyser = App::Test::Generator::Analyzer::ReturnMeta->new;
    my $report   = $analyser->analyze($schema);

    printf "Stability:   %d\n", $report->{stability_score};
    printf "Consistency: %d\n", $report->{consistency_score};
    printf "Risks:       %s\n", join(', ', @{ $report->{risk_flags} });

=head3 Arguments

=over 4

=item * C<$schema>

A hashref with an optional C<output> key containing return metadata.
The C<output> hashref may include any of the following keys:

=over 4

=item C<_context_aware> — true if the function returns differently in
list vs scalar context.

=item C<_returns_self> — true if the function returns C<$self>.

=item C<type> — the declared return type string e.g. C<object>,
C<boolean>, C<string>.

=item C<_error_handling> — a hashref with boolean keys C<implicit_undef>,
C<empty_list>, and C<exception_handling>.

=item C<_error_return> — the value returned on error e.g. C<undef>.

=back

=back

=head3 Returns

A hashref with three keys:

=over 4

=item * C<stability_score> — integer in [0, 100]. Higher is more stable.

=item * C<consistency_score> — integer in [0, 100]. Higher is more
consistent.

=item * C<risk_flags> — arrayref of string risk identifiers detected
during analysis.

=back

=head3 Notes

Both scores start at 100 and are reduced by penalties for each detected
risk pattern. A small bonus is applied to stability for boolean return
types. All scores are clamped to [0, 100] after adjustments.

=head3 API specification

=head4 input

    {
        self   => { type => OBJECT, isa => 'App::Test::Generator::Analyzer::ReturnMeta' },
        schema => { type => HASHREF },
    }

=head4 output

    {
        type    => HASHREF,
        keys    => {
            stability_score   => { type => SCALAR },
            consistency_score => { type => SCALAR },
            risk_flags        => { type => ARRAYREF },
        },
    }

=cut
149
150sub analyze {
151
296
2106
        my ($self, $schema) = @_;
152
153
296
350
        my $output      = $schema->{output} || {};
154
296
199
        my @risk;
155
296
209
        my $stability   = 100;
156
296
224
        my $consistency = 100;
157
158        # --------------------------------------------------
159        # Context sensitivity — function returns differently
160        # in list vs scalar context, making it harder to test
161        # predictably
162        # --------------------------------------------------
163
296
292
        if($output->{_context_aware}) {
164
9
8
                push @risk, 'context_sensitive';
165
9
12
                $stability   -= $PENALTY_CONTEXT_SENSITIVE_STABILITY;
166
9
44
                $consistency -= $PENALTY_CONTEXT_SENSITIVE_CONSISTENCY;
167        }
168
169        # --------------------------------------------------
170        # Mixed return types — function claims to return self
171        # but is not typed as object, suggesting inconsistent
172        # return paths
173        # --------------------------------------------------
174
296
349
        if($output->{_returns_self} && ($output->{type} // '') ne 'object') {
175
4
4
                push @risk, 'mixed_return_types';
176
4
6
                $consistency -= $PENALTY_MIXED_RETURN_CONSISTENCY;
177        }
178
179        # --------------------------------------------------
180        # Implicit undef returns — function falls off the end
181        # without an explicit return, making error paths hard
182        # to distinguish from successful empty returns
183        # --------------------------------------------------
184
296
371
        if($output->{_error_handling}{implicit_undef}) {
185
8
7
                push @risk, 'implicit_error_return';
186
8
11
                $stability -= $PENALTY_IMPLICIT_UNDEF_STABILITY;
187        }
188
189        # --------------------------------------------------
190        # Explicit undef on error — function explicitly returns
191        # undef on failure; lower penalty than implicit since
192        # the intent is at least documented in the code
193        # --------------------------------------------------
194
296
437
        if($output->{_error_return} && $output->{_error_return} eq 'undef') {
195
13
13
                push @risk, 'undef_on_error';
196
13
15
                $stability -= $PENALTY_EXPLICIT_UNDEF_STABILITY;
197        }
198
199        # --------------------------------------------------
200        # Empty list error pattern — function returns () on
201        # error, which is indistinguishable from a successful
202        # call that found no results
203        # --------------------------------------------------
204
296
293
        if($output->{_error_handling}{empty_list}) {
205
5
5
                push @risk, 'empty_list_error';
206
5
7
                $consistency -= $PENALTY_EMPTY_LIST_CONSISTENCY;
207        }
208
209        # --------------------------------------------------
210        # Exception swallowing — function catches exceptions
211        # without rethrowing, hiding failures from the caller
212        # --------------------------------------------------
213
296
277
        if($output->{_error_handling}{exception_handling}) {
214
4
4
                push @risk, 'exception_swallowing';
215
4
5
                $stability -= $PENALTY_EXCEPTION_SWALLOW_STABILITY;
216        }
217
218        # --------------------------------------------------
219        # Boolean return bonus — boolean returns are the most
220        # predictable and easiest to assert, so a small boost
221        # is applied. Only has effect if stability was already
222        # reduced below 95 by earlier penalties.
223        # --------------------------------------------------
224
296
461
        if($output->{type} && $output->{type} eq 'boolean') {
225
32
93
                $stability += $BONUS_BOOLEAN_STABILITY;
226        }
227
228        # Clamp both scores to the valid [0, 100] range
229
296
349
        $stability   = 0   if $stability   < 0;
230
296
261
        $stability   = 100 if $stability   > 100;
231
296
244
        $consistency = 0   if $consistency < 0;
232
296
266
        $consistency = 100 if $consistency > 100;
233
234        return {
235
296
601
                stability_score   => $stability,
236                consistency_score => $consistency,
237                risk_flags        => \@risk,
238        };
239}
240
2411;