TER1 (Statement): 100.00%
TER2 (Branch): 92.86%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 15
● 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::Complexity; 2: 3: use strict; 4: use warnings; 5: use Readonly; 6: 7: # -------------------------------------------------- 8: # Base cyclomatic complexity score before any analysis 9: # -------------------------------------------------- 10: Readonly my $CYCLOMATIC_BASE => 1; 11: 12: # -------------------------------------------------- 13: # Complexity level thresholds â scores at or below 14: # LOW_THRESHOLD are low, at or below HIGH_THRESHOLD 15: # are moderate, above HIGH_THRESHOLD are high 16: # -------------------------------------------------- 17: Readonly my $LOW_THRESHOLD => 3; 18: Readonly my $HIGH_THRESHOLD => 7; 19: 20: # -------------------------------------------------- 21: # Complexity level labels 22: # -------------------------------------------------- 23: Readonly my $LEVEL_LOW => 'low'; 24: Readonly my $LEVEL_MODERATE => 'moderate'; 25: Readonly my $LEVEL_HIGH => 'high'; 26: 27: # -------------------------------------------------- 28: # Keywords that introduce branching decision points 29: # -------------------------------------------------- 30: Readonly my @BRANCH_TOKENS => qw( 31: if elsif unless for foreach while until given when 32: ); 33: 34: # -------------------------------------------------- 35: # Keywords that introduce exception or error paths 36: # -------------------------------------------------- 37: Readonly my @EXCEPTION_TOKENS => qw( 38: die croak confess try catch eval 39: ); 40: 41: our $VERSION = '0.41'; 42: 43: =head1 VERSION 44: 45: Version 0.41 46: 47: =head1 DESCRIPTION 48: 49: Analyses the source body of a method and produces a complexity report 50: including cyclomatic score, branching points, early returns, exception 51: paths, and nesting depth. Used by L<App::Test::Generator> to guide test 52: planning â higher complexity methods are prioritised for more thorough 53: test generation. 54: 55: =head2 new 56: 57: Construct a new Complexity analyser. 58: 59: my $analyser = App::Test::Generator::Analyzer::Complexity->new; 60: 61: =head3 Arguments 62: 63: None. 64: 65: =head3 Returns 66: 67: A blessed hashref. 68: 69: =head3 API specification 70: 71: =head4 input 72: 73: {} 74: 75: =head4 output 76: 77: { 78: type => OBJECT, 79: isa => 'App::Test::Generator::Analyzer::Complexity', 80: } 81: 82: =cut 83: 84: sub new { bless {}, shift } 85: 86: =head2 analyze 87: 88: Analyse the source of a method and return a complexity report hashref. 89: 90: my $analyser = App::Test::Generator::Analyzer::Complexity->new; 91: my $report = $analyser->analyze($method); 92: 93: printf "Cyclomatic score: %d\n", $report->{cyclomatic_score}; 94: printf "Complexity level: %s\n", $report->{complexity_level}; 95: 96: =head3 Arguments 97: 98: =over 4 99: 100: =item * C<$method> 101: 102: A hashref describing the method, as built internally by 103: L<App::Test::Generator::SchemaExtractor>. The method source is read 104: from its C<body> key (a plain string of Perl source); this is I<not> 105: an L<App::Test::Generator::Model::Method> object. 106: 107: =back 108: 109: =head3 Returns 110: 111: A hashref with the following keys: 112: 113: =over 4 114: 115: =item * C<cyclomatic_score> â integer starting at 1, incremented for 116: each branching point, logical operator, early return, and exception path. 117: 118: =item * C<branching_points> â count of branching keywords found. 119: 120: =item * C<early_returns> â number of C<return> statements beyond the 121: first (each additional return adds a path). 122: 123: =item * C<exception_paths> â count of exception-related keywords found. 124: 125: =item * C<nesting_depth> â maximum brace nesting depth observed. 126: 127: =item * C<complexity_level> â one of C<low>, C<moderate>, or C<high> 128: based on the cyclomatic score. 129: 130: =back 131: 132: =head3 Notes 133: 134: Nesting depth is computed by naive brace counting and will be 135: inaccurate if the source contains braces inside strings or regexes. 136: This is a known limitation and is acceptable for dashboard display 137: purposes. 138: 139: =head3 API specification 140: 141: =head4 input 142: 143: { 144: self => { type => OBJECT, isa => 'App::Test::Generator::Analyzer::Complexity' }, 145: method => { type => HASHREF, keys => { body => { type => SCALAR, optional => 1 } } }, 146: } 147: 148: =head4 output 149: 150: { 151: type => HASHREF, 152: keys => { 153: cyclomatic_score => { type => SCALAR }, 154: branching_points => { type => SCALAR }, 155: early_returns => { type => SCALAR }, 156: exception_paths => { type => SCALAR }, 157: nesting_depth => { type => SCALAR }, 158: complexity_level => { type => SCALAR }, 159: }, 160: } 161: 162: =cut 163: 164: sub analyze { ●165 → 189 → 196 165: my ($self, $method) = @_; 166: 167: # The method argument is a raw hashref from SchemaExtractor, 168: # not a Model::Method object â access the body key directly 169: my $body = $method->{body} // ''; 170: 171: # Branch/logic/exception keywords and the ?/&&/|| operators are 172: # only real decision points as actual code; the same characters 173: # inside a string literal (e.g. "Are you sure?") or a comment 174: # must not inflate the cyclomatic score 175: my $code_only = _strip_strings_and_comments($body); 176: 177: my %result = ( 178: cyclomatic_score => $CYCLOMATIC_BASE, 179: branching_points => 0, 180: early_returns => 0, 181: exception_paths => 0, 182: nesting_depth => 0, 183: ); 184: 185: # -------------------------------------------------- 186: # Count branching keywords â each one introduces a 187: # new decision point that increases cyclomatic complexity 188: # -------------------------------------------------- 189: for my $token (@BRANCH_TOKENS) { 190: my $count = () = $code_only =~ /\b$token\b/g; 191: $result{branching_points} += $count; 192: $result{cyclomatic_score} += $count; 193: } 194: 195: # Logical operators also introduce implicit branches ●196 → 211 → 223 196: my $logic_count = () = $code_only =~ /&&|\|\||\?/g;Mutants (Total: 3, Killed: 3, Survived: 0)
197: $result{cyclomatic_score} += $logic_count; 198: 199: # -------------------------------------------------- 200: # Early returns â each return beyond the first adds 201: # an additional exit path through the method 202: # -------------------------------------------------- 203: my $return_count = () = $code_only =~ /\breturn\b/g; 204: $result{early_returns} = $return_count > 1 ? $return_count - 1 : 0; 205: $result{cyclomatic_score} += $result{early_returns}; 206: 207: # -------------------------------------------------- 208: # Exception paths â die/croak/eval etc. each introduce 209: # a path that must be tested separately 210: # -------------------------------------------------- 211: for my $token (@EXCEPTION_TOKENS) { 212: my $count = () = $code_only =~ /\b$token\b/g; 213: $result{exception_paths} += $count; 214: $result{cyclomatic_score} += $count; 215: } 216: 217: # -------------------------------------------------- 218: # Nesting depth â count brace depth by scanning chars.
Mutants (Total: 1, Killed: 1, Survived: 0)
219: # NOTE: this is naive and will overcount if braces 220: # appear inside strings or regexes. Acceptable for
Mutants (Total: 3, Killed: 3, Survived: 0)
221: # dashboard display purposes. 222: # --------------------------------------------------
Mutants (Total: 3, Killed: 3, Survived: 0)
●223 → 225 → 233 223: my $depth = 0; 224: my $max_depth = 0; 225: for my $char (split //, $body) { 226: if($char eq '{') { 227: $depth++; 228: $max_depth = $depth if $depth > $max_depth; 229: } elsif($char eq '}') { 230: $depth-- if $depth > 0; 231: } 232: }
Mutants (Total: 3, Killed: 3, Survived: 0)
233: $result{nesting_depth} = $max_depth;
Mutants (Total: 3, Killed: 3, Survived: 0)
234: 235: # -------------------------------------------------- 236: # Classify complexity level based on cyclomatic score 237: # -------------------------------------------------- 238: my $score = $result{cyclomatic_score}; 239: $result{complexity_level} = 240: $score <= $LOW_THRESHOLD ? $LEVEL_LOW : 241: $score <= $HIGH_THRESHOLD ? $LEVEL_MODERATE : 242: $LEVEL_HIGH; 243: 244: return \%result; 245: } 246: 247: # -------------------------------------------------- 248: # Purpose: blank out the contents of '...' and "..." string 249: # literals and # line comments so that the keyword 250: # and operator counts above only see real code, not 251: # words/punctuation that merely appear inside a 252: # message or comment. 253: # Entry: a raw source body string. 254: # Exit: the same string with string-literal contents and 255: # comment text removed. 256: # Side effects: none. Best-effort only â does not handle q//, 257: # qq//, heredocs, or quote-like operators with custom 258: # delimiters. 259: # -------------------------------------------------- 260: sub _strip_strings_and_comments { 261: my ($body) = @_; 262: 263: $body =~ s/"(?:[^"\\]|\\.)*"//g; 264: $body =~ s/'(?:[^'\\]|\\.)*'//g; 265: $body =~ s/#.*$//mg; 266: 267: return $body; 268: } 269: 270: 1;