lib/App/Test/Generator/Analyzer/SideEffect.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (9/9)
Approximate LCSAJ segments: 13

LCSAJ Legend

Covered — this LCSAJ path was executed during testing.

Not covered — this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start → end → jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package App::Test::Generator::Analyzer::SideEffect;
    2: 
    3: use strict;
    4: use warnings;
    5: use Readonly;
    6: 
    7: # --------------------------------------------------
    8: # Purity classification labels
    9: # --------------------------------------------------
   10: Readonly my $PURITY_PURE         => 'pure';
   11: Readonly my $PURITY_SELF_MUTATING => 'self_mutating';
   12: Readonly my $PURITY_IMPURE       => 'impure';
   13: 
   14: # --------------------------------------------------
   15: # IO operation keywords — print/say/warn/open etc.
   16: # NOTE: this list is not exhaustive; low-level sysread
   17: # and syswrite are included but higher-level abstractions
   18: # like Log::Any calls are not detected.
   19: # --------------------------------------------------
   20: use constant IO_PATTERN => qr/\b(?:print|say|printf|warn|open|close|syswrite|sysread|readline|read|write)\b/;
   21: 
   22: # --------------------------------------------------
   23: # External execution patterns — system calls and
   24: # backtick/qx operators
   25: # --------------------------------------------------
   26: use constant EXEC_PATTERN => qr/\b(?:system|exec)\b|qx\(|`/;
   27: 
   28: # --------------------------------------------------
   29: # Global variable patterns — %ENV, %SIG, @ARGV and
   30: # common Perl special variables.
   31: # NOTE: does not detect all possible globals; mutation
   32: # of $_, $/, $! etc. would require deeper analysis.
   33: # --------------------------------------------------
   34: use constant GLOBAL_PATTERN => qr/\$(?:GLOBAL|ENV|SIG|ARGV|_|!|0)\b|\$\/|%ENV\b|%SIG\b|\@ARGV\b/;
   35: 
   36: our $VERSION = '0.36';
   37: 
   38: =head1 VERSION
   39: 
   40: Version 0.36
   41: 
   42: =head1 DESCRIPTION
   43: 
   44: Analyses the source body of a method and produces a side effect report
   45: describing whether the method mutates C<$self>, mutates global state,
   46: performs IO, or calls external commands. Used by
   47: L<App::Test::Generator> to classify methods by purity and guide test
   48: generation strategy.
   49: 
   50: =head2 new
   51: 
   52: Construct a new SideEffect analyser.
   53: 
   54:     my $analyser = App::Test::Generator::Analyzer::SideEffect->new;
   55: 
   56: =head3 Arguments
   57: 
   58: None.
   59: 
   60: =head3 Returns
   61: 
   62: A blessed hashref.
   63: 
   64: =head3 API specification
   65: 
   66: =head4 input
   67: 
   68:     {}
   69: 
   70: =head4 output
   71: 
   72:     {
   73:         type => OBJECT,
   74:         isa  => 'App::Test::Generator::Analyzer::SideEffect',
   75:     }
   76: 
   77: =cut
   78: 
   79: sub new { bless {}, shift }
   80: 
   81: =head2 analyze
   82: 
   83: Analyse the source body of a method and return a side effect report
   84: hashref.
   85: 
   86:     my $analyser = App::Test::Generator::Analyzer::SideEffect->new;
   87:     my $report   = $analyser->analyze($method);
   88: 
   89:     if ($report->{purity_level} eq 'pure') {
   90:         print "Method is side-effect free\n";
   91:     }
   92: 
   93: =head3 Arguments
   94: 
   95: =over 4
   96: 
   97: =item * C<$method>
   98: 
   99: A hashref with a C<body> key containing the raw source text of the
  100: method to analyse.
  101: 
  102: =back
  103: 
  104: =head3 Returns
  105: 
  106: A hashref with the following keys:
  107: 
  108: =over 4
  109: 
  110: =item * C<mutates_self> — 1 if the method assigns to C<$self-E<gt>{field}>.
  111: 
  112: =item * C<mutates_globals> — 1 if the method modifies global variables.
  113: 
  114: =item * C<performs_io> — 1 if the method performs IO operations.
  115: 
  116: =item * C<calls_external> — 1 if the method calls external commands.
  117: 
  118: =item * C<mutation_fields> — arrayref of C<$self> field names assigned
  119: to (deduplicated).
  120: 
  121: =item * C<purity_level> — one of C<pure>, C<self_mutating>, or
  122: C<impure>.
  123: 
  124: =back
  125: 
  126: =head3 Notes
  127: 
  128: Detection is based on regex pattern matching against the raw source
  129: text and will not catch dynamically constructed calls or aliased
  130: operations. The global variable pattern covers common Perl specials
  131: but is not exhaustive.
  132: 
  133: =head3 API specification
  134: 
  135: =head4 input
  136: 
  137:     {
  138:         self   => { type => OBJECT, isa => 'App::Test::Generator::Analyzer::SideEffect' },
  139:         method => { type => HASHREF },
  140:     }
  141: 
  142: =head4 output
  143: 
  144:     {
  145:         type => HASHREF,
  146:         keys => {
  147:             mutates_self    => { type => SCALAR },
  148:             mutates_globals => { type => SCALAR },
  149:             performs_io     => { type => SCALAR },
  150:             calls_external  => { type => SCALAR },
  151:             mutation_fields => { type => ARRAYREF },
  152:             purity_level    => { type => SCALAR },
  153:         },
  154:     }
  155: 
  156: =cut
  157: 
  158: sub analyze {
159 → 177 → 191159 → 177 → 0  159: 	my ($self, $method) = @_;
  160: 
  161: 	# Method argument is a raw hashref from SchemaExtractor
  162: 	my $body = $method->{body} // '';
  163: 
  164: 	my %result = (
  165: 		mutates_self    => 0,
  166: 		mutates_globals => 0,
  167: 		performs_io     => 0,
  168: 		calls_external  => 0,
  169: 		mutation_fields => [],
  170: 	);
  171: 
  172: 	# --------------------------------------------------
  173: 	# Detect assignment to $self->{field} — any such
  174: 	# assignment means the method mutates its own state
  175: 	# --------------------------------------------------
  176: 	my %seen_fields;
  177: 	while($body =~ /\$self->\{(\w+)\}\s*=/g) {
  178: 		$result{mutates_self} = 1;
  179: 
  180: 		# Deduplicate field names in case the same field
  181: 		# is assigned more than once in the method body
  182: 		push @{ $result{mutation_fields} }, $1
  183: 			unless $seen_fields{$1}++;
  184: 	}
  185: 
  186: 	# --------------------------------------------------
  187: 	# Detect mutation of global variables — %ENV, %SIG,
  188: 	# @ARGV and common Perl special variables.
  189: 	# NOTE: does not catch all possible globals.
  190: 	# --------------------------------------------------
191 → 191 → 199191 → 191 → 0  191: 	if($body =~ GLOBAL_PATTERN) {

Mutants (Total: 1, Killed: 1, Survived: 0)

192: $result{mutates_globals} = 1; 193: } 194: 195: # -------------------------------------------------- 196: # Detect IO operations — print, say, warn, open etc. 197: # Higher-level logging abstractions are not detected. 198: # -------------------------------------------------- 199 → 199 → 207199 → 199 → 0 199: if($body =~ IO_PATTERN) {

Mutants (Total: 1, Killed: 1, Survived: 0)

200: $result{performs_io} = 1; 201: } 202: 203: # -------------------------------------------------- 204: # Detect external command execution via system(), 205: # exec(), qx() or backtick operators 206: # -------------------------------------------------- 207 → 207 → 217207 → 207 → 0 207: if($body =~ EXEC_PATTERN) {

Mutants (Total: 1, Killed: 1, Survived: 0)

208: $result{calls_external} = 1; 209: } 210: 211: # -------------------------------------------------- 212: # Classify purity level based on detected side effects. 213: # pure — no side effects of any kind 214: # self_mutating — only mutates own state, no external effects 215: # impure — any external side effect present 216: # -------------------------------------------------- 217 → 226 → 0 217: my $has_external = $result{mutates_globals} 218: || $result{performs_io} 219: || $result{calls_external}; 220: 221: $result{purity_level} = 222: !$result{mutates_self} && !$has_external ? $PURITY_PURE : 223: $result{mutates_self} && !$has_external ? $PURITY_SELF_MUTATING : 224: $PURITY_IMPURE; 225: 226: return \%result; 227: } 228: 229: 1;