File Coverage

File:blib/lib/App/Test/Generator/Planner/Isolation.pm
Coverage:100.0%

linestmtbrancondsubtimecode
1package App::Test::Generator::Planner::Isolation;
2
3
8
8
8
64351
8
128
use strict;
4
8
8
8
13
5
175
use warnings;
5
8
8
8
15
5
144
use Carp    qw(croak);
6
8
8
8
204
1707
1219
use Readonly;
7
8# --------------------------------------------------
9# Purity levels from Analyzer::SideEffect
10# --------------------------------------------------
11Readonly my $PURITY_PURE          => 'pure';
12Readonly my $PURITY_SELF_MUTATING => 'self_mutating';
13
14# --------------------------------------------------
15# Fixture isolation modes written to the plan output
16# --------------------------------------------------
17Readonly my $FIXTURE_SHARED   => 'shared_fixture';
18Readonly my $FIXTURE_FRESH    => 'fresh_object';
19Readonly my $FIXTURE_ISOLATED => 'isolated_block';
20
21our $VERSION = '0.36';
22
23 - 62
=head1 VERSION

Version 0.36

=head1 DESCRIPTION

Plans isolation strategy for each method under test, based on side
effect analysis and dependency metadata from the schema. Determines
whether each method needs a shared fixture, a fresh object per test,
or a fully isolated block, and records any environmental dependencies
that need mocking.

=head2 new

Construct a new Isolation planner.

    my $planner = App::Test::Generator::Planner::Isolation->new;

=head3 Arguments

None.

=head3 Returns

A blessed hashref.

=head3 API specification

=head4 input

    {}

=head4 output

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

=cut
63
64
37
121536
sub new { bless {}, shift }
65
66 - 128
=head2 plan

Produce an isolation plan for each method based on its side effect
analysis and dependency metadata.

    my $planner   = App::Test::Generator::Planner::Isolation->new;
    my $isolation = $planner->plan($schema, $strategy);

    for my $method (keys %{$isolation}) {
        printf "%s fixture: %s\n", $method, $isolation->{$method}{fixture};
    }

=head3 Arguments

=over 4

=item * C<$schema>

A hashref of method schemas, each optionally containing a C<_analysis>
key with C<side_effects> and C<dependencies> sub-keys.

=item * C<$strategy>

A hashref whose keys are the method names to plan isolation for.
Values are not used directly — the hashref is used only for its keys.

=back

=head3 Returns

A hashref mapping method names to isolation plan hashrefs. Each plan
has a C<fixture> key and optionally C<env>, C<filesystem>, C<time>,
and C<network> keys where relevant dependencies were detected.

=head3 API specification

=head4 input

    {
        self     => { type => OBJECT,  isa  => 'App::Test::Generator::Planner::Isolation' },
        schema   => { type => HASHREF },
        strategy => { type => HASHREF },
    }

=head4 output

    {
        type => HASHREF,
        keys => {
            '*' => {
                type => HASHREF,
                keys => {
                    fixture    => { type => SCALAR },
                    env        => { type => HASHREF,  optional => 1 },
                    filesystem => { type => HASHREF,  optional => 1 },
                    time       => { type => SCALAR,   optional => 1 },
                    network    => { type => SCALAR,   optional => 1 },
                },
            },
        },
    }

=cut
129
130sub plan {
131
35
1547
        my ($self, $schema, $strategy) = @_;
132
133        # Validate that strategy is a hashref before iterating its keys
134
35
64
        croak 'strategy must be a hashref' unless ref($strategy) eq 'HASH';
135
136
29
20
        my %isolation;
137
138
29
29
19
35
        for my $method (keys %{$strategy}) {
139                # Extract side effect and dependency analysis from schema
140                # if present — default to empty hashrefs if not available
141
27
36
                my $analysis = $schema->{$method}{_analysis} || {};
142
27
31
                my $effects  = $analysis->{side_effects}     || {};
143
27
31
                my $deps     = $analysis->{dependencies}     || {};
144
145
27
20
                my %plan;
146
147                # --------------------------------------------------
148                # Choose fixture isolation mode based on purity level
149                # as determined by Analyzer::SideEffect:
150                #   pure          -> shared fixture safe to reuse
151                #   self_mutating -> fresh object needed per test
152                #   impure        -> full isolation block required
153                # --------------------------------------------------
154
27
29
                my $purity = $effects->{purity_level} // '';
155                $plan{fixture} =
156
27
43
                        $purity eq $PURITY_PURE          ? $FIXTURE_SHARED   :
157                        $purity eq $PURITY_SELF_MUTATING ? $FIXTURE_FRESH     :
158                                                           $FIXTURE_ISOLATED;
159
160                # --------------------------------------------------
161                # Record dependency isolation requirements so the
162                # test emitter knows what to mock or stub out
163                # --------------------------------------------------
164
165                # Environment variable dependencies — pass through
166                # the full env hashref for the emitter to use
167
27
152
                $plan{env}        = $deps->{env}        if $deps->{env};
168
169                # Filesystem dependencies — pass through the full
170                # filesystem hashref for the emitter to use
171
27
20
                $plan{filesystem} = $deps->{filesystem} if $deps->{filesystem};
172
173                # Time and network are boolean flags — we only care
174                # whether they are present, not their value
175
27
22
                $plan{time}    = 1 if $deps->{time};
176
27
25
                $plan{network} = 1 if $deps->{network};
177
178
27
34
                $isolation{$method} = \%plan;
179        }
180
181
29
30
        return \%isolation;
182}
183
1841;