File Coverage

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

linestmtbrancondsubtimecode
1package App::Test::Generator::TestStrategy;
2
3
9
9
9
65264
10
85
use strict;
4
9
9
9
14
26
137
use warnings;
5
9
9
9
222
1582
3340
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.46';
43
44 - 106
=head1 VERSION

Version 0.46

=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>.

This way a package's API can be automatically tested.

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

This module is provided as-is without any warranty.

=head1 LICENCE AND COPYRIGHT

Copyright 2025-2026 Nigel Horne.

Usage is subject to the terms of GPL2.
If you use it,
please let me know.

=cut
306
3071;