| 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 | 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 | # -------------------------------------------------- | |||||
| 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.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 | ||||||
| 150 | sub 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 | ||||||
| 241 | 1; | |||||