File Coverage

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

linestmtbrancondsubtimecode
1package App::Test::Generator::Analyzer::ReturnMeta;
2
3
31
31
31
67294
27
369
use strict;
4
31
31
31
78
22
583
use warnings;
5
31
31
31
52
23
581
use Carp qw(croak);
6
31
31
31
51
20
5504
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.41';
23
24 - 62
=head1 VERSION

Version 0.41

=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
314
109471
sub new { bless {}, shift }
65
66 - 152
=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 that may include an 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, but since C<stability_score> starts at 100 and is clamped to
[0, 100] after all adjustments, this bonus is a no-op unless an earlier
penalty has already reduced the score below 100 — for a boolean-returning
function with no other detected risk, C<stability_score> is unaffected
by C<$BONUS_BOOLEAN_STABILITY> and remains 100.

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