TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (9/9)
Approximate LCSAJ segments: 13
● 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.
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 → 191●159 → 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 → 199●191 → 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 → 207●199 → 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 → 217●207 → 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;