File Coverage

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

linestmtbrancondsubtimecode
1package App::Test::Generator::Mutant;
2
3
20
20
20
199623
18
241
use strict;
4
20
20
20
32
11
346
use warnings;
5
20
20
20
31
15
3027
use Carp qw(croak);
6
7our $VERSION = '0.41';
8
9 - 117
=head1 VERSION

Version 0.41

=head1 DESCRIPTION

Represents a single mutant - a specific transformation of a source file
that a test suite should be able to detect. Each mutant carries the
metadata needed to identify it, locate it in the source, and apply the
transformation to a fresh L<PPI::Document> copy at test time.

=head2 new

Construct a new mutant object.

    my $mutant = App::Test::Generator::Mutant->new(
        id          => 'NUM_BOUNDARY_10_5_!=',
        description => 'Numeric boundary flip == to !=',
        original    => '==',
        line        => 10,
        type        => 'comparison',
        group       => 'NUM_BOUNDARY:10',
        transform   => sub {
            my ($doc) = @_;
            # ... modify $doc in place ...
        },
    );

=head3 Arguments

=over 4

=item * C<id>

A unique string identifying this mutant. Required.

=item * C<description>

A human-readable description of the mutation. Required.

=item * C<original>

The original source token or expression being mutated. Required.

=item * C<line>

The line number in the source file where the mutation occurs. Required.

=item * C<transform>

A CODE reference that accepts a L<PPI::Document> and applies the
mutation to it in place. Required.

=item * C<type>

An optional string classifying the mutation kind e.g. C<comparison>,
C<boolean>.

=item * C<group>

An optional string grouping related mutants together e.g. all mutations
on the same source line.

=item * C<context>

An optional string classifying the syntactic context the mutation was
taken from, e.g. C<conditional> when the mutated code sits inside an
C<if>/C<unless>/C<while>/C<until> condition, or C<statement>/C<expression>
otherwise. Used by L<App::Test::Generator::Mutator>'s fast-mode dedup to
recognise mutations that Perl's own boolean coercion already makes
redundant.

=item * C<line_content>

An optional string holding the raw source text of the line the mutation
targets. Used by L<App::Test::Generator::Mutator>'s fast-mode dedup to
skip mutations on comment-only lines.

=back

=head3 Returns

A blessed hashref representing the mutant. Croaks if any required
attribute is missing or if C<transform> is not a CODE reference.

=head3 API specification

=head4 input

    {
        id           => { type => SCALAR },
        description  => { type => 'string' },
        original     => { type => SCALAR },
        line         => { type => SCALAR },
        transform    => { type => CODEREF },
        type         => { type => SCALAR, optional => 1 },
        group        => { type => SCALAR, optional => 1 },
        context      => { type => SCALAR, optional => 1 },
        line_content => { type => SCALAR, optional => 1 },
    }

=head4 output

    {
        type => 'object',
        isa  => 'App::Test::Generator::Mutant',
    }

=cut
118
119sub new {
120
1000
379405
        my ($class, %args) = @_;
121
122        # Validate all required attributes are present
123
1000
929
        for my $required (qw(id description original line transform)) {
124                croak "Missing required attribute: $required"
125
4980
3926
                        unless exists $args{$required};
126        }
127
128        # Ensure transform is actually executable
129
990
1038
        croak 'transform must be a CODE reference' unless ref($args{transform}) eq 'CODE';
130
131
985
1368
        return bless \%args, $class;
132}
133
134 - 150
=head2 id

Return the unique identifier for this mutant.

    my $id = $mutant->id;

=head3 API specification

=head4 input

    { self => { type => 'object', isa => 'App::Test::Generator::Mutant' } }

=head4 output

    { type => SCALAR }

=cut
151
152sub id {
153
113
9760
        return $_[0]->{id};
154}
155
156 - 172
=head2 description

Return the human-readable description of the mutation.

    my $desc = $mutant->description;

=head3 API specification

=head4 input

    { self => { type => 'object', isa => 'App::Test::Generator::Mutant' } }

=head4 output

    { type => SCALAR }

=cut
173
174
181
8167
sub description { $_[0]->{description} }
175
176 - 192
=head2 original

Return the original source token or expression that is being mutated.

    my $orig = $mutant->original;

=head3 API specification

=head4 input

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

=head4 output

    { type => SCALAR }

=cut
193
194
304
943
sub original    { $_[0]->{original}    }
195
196 - 212
=head2 line

Return the line number in the source file where the mutation occurs.

    my $line = $mutant->line;

=head3 API specification

=head4 input

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

=head4 output

    { type => SCALAR }

=cut
213
214
808
3426
sub line        { $_[0]->{line}        }
215
216 - 234
=head2 transform

Return the CODE reference that applies the mutation to a
L<PPI::Document> copy.

    my $xform = $mutant->transform;
    $xform->($doc_copy);

=head3 API specification

=head4 input

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

=head4 output

    { type => CODEREF }

=cut
235
236
72
59525
sub transform   { $_[0]->{transform}   }
237
238 - 255
=head2 type

Return the optional mutation type classification string,
e.g. C<comparison> or C<boolean>.

    my $type = $mutant->type;

=head3 API specification

=head4 input

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

=head4 output

    { type => SCALAR, optional => 1 }

=cut
256
257
42
1730
sub type        { $_[0]->{type}        }
258
259 - 276
=head2 group

Return the optional group string that clusters related mutants,
e.g. all mutations targeting the same source line.

    my $group = $mutant->group;

=head3 API specification

=head4 input

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

=head4 output

    { type => SCALAR, optional => 1 }

=cut
277
278
27
759
sub group { $_[0]->{group} }
279
280 - 297
=head2 context

Return the optional syntactic-context classification string,
e.g. C<conditional> or C<statement>.

    my $context = $mutant->context;

=head3 API specification

=head4 input

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

=head4 output

    { type => SCALAR, optional => 1 }

=cut
298
299
260
5840
sub context { $_[0]->{context} }
300
301 - 317
=head2 line_content

Return the optional raw source text of the line this mutant targets.

    my $text = $mutant->line_content;

=head3 API specification

=head4 input

    { self => { type => 'object', isa => 'App::Test::Generator::Mutant' } }

=head4 output

    { type => 'string', optional => 1 }

=cut
318
319
271
8582
sub line_content { $_[0]->{line_content} }
320
321 - 336
=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

Portions of this module's initial design and documentation were created
with the assistance of AI.

=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
337
3381;