| File: | blib/lib/App/Test/Generator/Analyzer/ReturnMeta.pm |
| Coverage: | 96.5% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package 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 | # -------------------------------------------------- | |||||
| 13 | Readonly my $PENALTY_CONTEXT_SENSITIVE_STABILITY => 25; | |||||
| 14 | Readonly my $PENALTY_CONTEXT_SENSITIVE_CONSISTENCY => 15; | |||||
| 15 | Readonly my $PENALTY_MIXED_RETURN_CONSISTENCY => 30; | |||||
| 16 | Readonly my $PENALTY_IMPLICIT_UNDEF_STABILITY => 20; | |||||
| 17 | Readonly my $PENALTY_EXPLICIT_UNDEF_STABILITY => 10; | |||||
| 18 | Readonly my $PENALTY_EMPTY_LIST_CONSISTENCY => 15; | |||||
| 19 | Readonly my $PENALTY_EXCEPTION_SWALLOW_STABILITY => 20; | |||||
| 20 | Readonly my $BONUS_BOOLEAN_STABILITY => 5; | |||||
| 21 | ||||||
| 22 | our $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 | ||||||
| 154 | sub 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 | ||||||
| 245 | 1; | |||||