| File: | blib/lib/App/Test/Generator/Planner/Isolation.pm |
| Coverage: | 100.0% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package 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 | # -------------------------------------------------- | |||||
| 11 | Readonly my $PURITY_PURE => 'pure'; | |||||
| 12 | Readonly my $PURITY_SELF_MUTATING => 'self_mutating'; | |||||
| 13 | ||||||
| 14 | # -------------------------------------------------- | |||||
| 15 | # Fixture isolation modes written to the plan output | |||||
| 16 | # -------------------------------------------------- | |||||
| 17 | Readonly my $FIXTURE_SHARED => 'shared_fixture'; | |||||
| 18 | Readonly my $FIXTURE_FRESH => 'fresh_object'; | |||||
| 19 | Readonly my $FIXTURE_ISOLATED => 'isolated_block'; | |||||
| 20 | ||||||
| 21 | our $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 | ||||||
| 130 | sub 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 | ||||||
| 184 | 1; | |||||