File Coverage

File:blib/lib/App/Test/Generator/TestStrategy.pm
Coverage:90.8%

linestmtbrancondsubtimecode
1package App::Test::Generator::TestStrategy;
2
3
9
9
9
65214
8
104
use strict;
4
9
9
9
14
6
139
use warnings;
5
9
9
9
201
1659
3053
use Readonly;
6
7# --------------------------------------------------
8# Accessor type strings from the schema
9# --------------------------------------------------
10Readonly my $ACCESSOR_GETTER   => 'getter';
11Readonly my $ACCESSOR_SETTER   => 'setter';
12Readonly my $ACCESSOR_GETSET   => 'getset';
13
14# --------------------------------------------------
15# Output type strings from the schema
16# --------------------------------------------------
17Readonly my $TYPE_BOOLEAN => 'boolean';
18Readonly my $TYPE_OBJECT  => 'object';
19Readonly my $TYPE_VOID    => 'void';
20
21# --------------------------------------------------
22# Default confidence threshold for plan generation
23# --------------------------------------------------
24Readonly my $DEFAULT_CONFIDENCE => 'medium';
25
26# --------------------------------------------------
27# Test plan flag keys written to the method plan
28# --------------------------------------------------
29Readonly my $TEST_CONTEXT         => 'context_tests';
30Readonly my $TEST_PREDICATE       => 'predicate_test';
31Readonly my $TEST_GETTER          => 'getter_test';
32Readonly my $TEST_SETTER          => 'setter_test';
33Readonly my $TEST_GETSET          => 'getset_test';
34Readonly my $TEST_OBJECT_INJECT   => 'object_injection_test';
35Readonly my $TEST_BOOLEAN_SET     => 'boolean_set_test';
36Readonly my $TEST_VOID            => 'void_context_test';
37Readonly my $TEST_ERROR_HANDLING  => 'error_handling_test';
38Readonly my $TEST_BOUNDARY        => 'boundary_tests';
39Readonly my $TEST_CHAINING        => 'chaining_test';
40Readonly my $TEST_BASIC           => 'basic_test';
41
42our $VERSION = '0.41';
43
44 - 102
=head1 VERSION

Version 0.41

=head1 DESCRIPTION

Generates a test strategy plan for all methods in a schema, determining
which test types should be produced for each method based on its
accessor classification, output type, and other metadata. Side-effect
and dependency-driven planning (mocking, isolation) is handled
separately by L<App::Test::Generator::Planner::Mock> and
L<App::Test::Generator::Planner::Isolation>.

=head2 new

Construct a new TestStrategy.

    my $strategy = App::Test::Generator::TestStrategy->new(
        schema     => \%schemas,
        thresholds => { confidence => 'high' },
    );

=head3 Arguments

=over 4

=item * C<schema>

A hashref of method name to schema hashref. Optional — defaults to
an empty hashref.

=item * C<thresholds>

A hashref of threshold configuration. Optional — defaults to
C<< { confidence => 'medium' } >>.

=back

=head3 Returns

A blessed hashref.

=head3 API specification

=head4 input

    {
        schema     => { type => HASHREF, optional => 1 },
        thresholds => { type => HASHREF, optional => 1 },
    }

=head4 output

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

=cut
103
104sub new {
105
26
113489
        my ($class, %args) = @_;
106        return bless {
107                schema     => $args{schema}     || {},
108
26
109
                thresholds => $args{thresholds} || { confidence => $DEFAULT_CONFIDENCE },
109                plans      => {},
110        }, $class;
111}
112
113 - 153
=head2 generate_plan

Generate a test plan for all methods in the schema and return it as
a hashref mapping method names to plan hashrefs.

    my $strategy = App::Test::Generator::TestStrategy->new(
        schema => \%schemas,
    );
    my $plan = $strategy->generate_plan;

    for my $method (keys %{$plan}) {
        print "$method: ", join(', ', keys %{ $plan->{$method} }), "\n";
    }

=head3 Arguments

None beyond C<$self>.

=head3 Returns

A hashref mapping method names to test plan hashrefs, each containing
boolean flags for the test types that should be generated.

=head3 API specification

=head4 input

    {
        self => { type => OBJECT, isa => 'App::Test::Generator::TestStrategy' },
    }

=head4 output

    {
        type => HASHREF,
        keys => {
            '*' => { type => HASHREF },
        },
    }

=cut
154
155sub generate_plan {
156
22
298
        my $self = $_[0];
157
158
22
22
16
41
        for my $method (keys %{ $self->{schema} }) {
159
22
21
                my $schema = $self->{schema}{$method};
160
161                # Generate and store the plan for this method
162
22
33
                $self->{plans}{$method} = $self->_plan_for_method($schema);
163        }
164
165
21
25
        return $self->{plans};
166}
167
168# --------------------------------------------------
169# _plan_for_method
170#
171# Determine which test types should be
172#     generated for a single method based on
173#     its schema metadata.
174#
175# Entry:      $schema - the per-method schema hashref
176#
177# Exit:       Returns a hashref of test type flags.
178#             Always contains at least basic_test => 1.
179#
180# Side effects: None.
181#
182# Notes:      All string comparisons use // '' guards
183#             to avoid uninitialized value warnings
184#             when schema fields are absent.
185# --------------------------------------------------
186sub _plan_for_method {
187
23
29
        my ($self, $schema) = @_;
188
189
23
14
        my %plan;
190
191        # --------------------------------------------------
192        # Context-aware returns need both scalar and list
193        # context tests to verify correct behaviour in each
194        # --------------------------------------------------
195
23
37
        if($schema->{output}{_context_aware}) {
196
1
1
                $plan{$TEST_CONTEXT} = 1;
197        }
198
199        # --------------------------------------------------
200        # Accessor detection — choose test types based on
201        # whether the method is a getter, setter, or both
202        # --------------------------------------------------
203
22
10
44
19
        if($schema->{accessor} && scalar keys %{ $schema->{accessor} }) {
204
10
16
                my $acc_type = $schema->{accessor}{type} // '';
205
206
10
17
                if($acc_type eq $ACCESSOR_GETTER) {
207                        # Boolean getters are predicates and need
208                        # truthy/falsy tests in addition to getter tests
209
3
18
                        if(($schema->{output}{type} // '') eq $TYPE_BOOLEAN) {
210
1
3
                                $plan{$TEST_PREDICATE} = 1;
211                        }
212
3
14
                        $plan{$TEST_GETTER} = 1;
213
214                } elsif($acc_type eq $ACCESSOR_SETTER) {
215
1
6
                        $plan{$TEST_SETTER} = 1;
216
217                } elsif($acc_type eq $ACCESSOR_GETSET) {
218                        # For getset accessors, check the input parameter
219                        # type to determine if object injection or boolean
220                        # set tests are more appropriate. Sort by position (keys
221                        # %hash has no defined order) so the choice is deterministic
222                        # if more than one candidate is present.
223
6
42
                        my $input = $schema->{input} || {};
224                        my ($param) = sort {
225
2
7
                                ($input->{$a}{position} // 9999) <=> ($input->{$b}{position} // 9999)
226                                || $a cmp $b
227
6
9
6
4
16
6
                        } grep { !/^_/ } keys %{ $input };
228
6
15
                        my $param_type = ($param && $input->{$param}{type}) // '';
229
230
6
8
                        if($param_type eq $TYPE_OBJECT) {
231
1
2
                                $plan{$TEST_OBJECT_INJECT} = 1;
232                        } elsif($param_type eq $TYPE_BOOLEAN) {
233
4
19
                                $plan{$TEST_BOOLEAN_SET} = 1;
234                        }
235
6
20
                        $plan{$TEST_GETSET} = 1;
236                }
237        }
238
239        # --------------------------------------------------
240        # Void return type — verify the method returns nothing
241        # and does not accidentally return a useful value
242        # --------------------------------------------------
243
22
61
        if(($schema->{output}{type} // '') eq $TYPE_VOID) {
244
2
8
                $plan{$TEST_VOID} = 1;
245        }
246
247        # --------------------------------------------------
248        # Error handling — verify error return conventions
249        # are tested explicitly
250        # --------------------------------------------------
251
22
88
        if($schema->{output}{_error_return}
252        || $schema->{output}{success_failure_pattern}) {
253
2
2
                $plan{$TEST_ERROR_HANDLING} = 1;
254        }
255
256        # --------------------------------------------------
257        # Boundary hints from YAML test configuration —
258        # generate boundary/equivalence class tests
259        # --------------------------------------------------
260
22
1
30
8
        if($schema->{_yamltest_hints} && keys %{ $schema->{_yamltest_hints} }) {
261
1
2
                $plan{$TEST_BOUNDARY} = 1;
262        }
263
264        # --------------------------------------------------
265        # Method chaining — verify that $self is returned
266        # and that calls can be chained
267        # --------------------------------------------------
268
22
28
        if($schema->{output}{_returns_self}) {
269
1
1
                $plan{$TEST_CHAINING} = 1;
270        }
271
272        # --------------------------------------------------
273        # Boolean output — needs predicate tests regardless
274        # of whether an accessor was detected
275        # --------------------------------------------------
276
22
42
        if(($schema->{output}{type} // '') eq $TYPE_BOOLEAN) {
277
2
5
                $plan{$TEST_PREDICATE} = 1;
278        }
279
280        # --------------------------------------------------
281        # Always generate at least a basic call test even
282        # if no other test types were identified
283        # --------------------------------------------------
284
22
61
        $plan{$TEST_BASIC} = 1 unless %plan;
285
286
22
41
        return \%plan;
287}
288
2891;