TER1 (Statement): 100.00%
TER2 (Branch): 92.86%
TER3 (LCSAJ): 100.0% (7/7)
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.36'; 42: 43: =head1 VERSION 44: 45: Version 0.36 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: An L<App::Test::Generator::Model::Method> object. The method source is 103: read via C<source()>. 104: 105: =back 106: 107: =head3 Returns 108: 109: A hashref with the following keys: 110: 111: =over 4 112: 113: =item * C<cyclomatic_score> â integer starting at 1, incremented for 114: each branching point, logical operator, early return, and exception path. 115: 116: =item * C<branching_points> â count of branching keywords found. 117: 118: =item * C<early_returns> â number of C<return> statements beyond the 119: first (each additional return adds a path). 120: 121: =item * C<exception_paths> â count of exception-related keywords found. 122: 123: =item * C<nesting_depth> â maximum brace nesting depth observed. 124: 125: =item * C<complexity_level> â one of C<low>, C<moderate>, or C<high> 126: based on the cyclomatic score. 127: 128: =back 129: 130: =head3 Notes 131: 132: Nesting depth is computed by naive brace counting and will be 133: inaccurate if the source contains braces inside strings or regexes. 134: This is a known limitation and is acceptable for dashboard display 135: purposes. 136: 137: =head3 API specification 138: 139: =head4 input 140: 141: { 142: self => { type => OBJECT, isa => 'App::Test::Generator::Analyzer::Complexity' }, 143: method => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' }, 144: } 145: 146: =head4 output 147: 148: { 149: type => HASHREF, 150: keys => { 151: cyclomatic_score => { type => SCALAR }, 152: branching_points => { type => SCALAR }, 153: early_returns => { type => SCALAR }, 154: exception_paths => { type => SCALAR }, 155: nesting_depth => { type => SCALAR }, 156: complexity_level => { type => SCALAR }, 157: }, 158: } 159: 160: =cut 161: 162: sub analyze { ●163 → 181 → 188●163 → 181 → 0 163: my ($self, $method) = @_; 164: 165: # The method argument is a raw hashref from SchemaExtractor, 166: # not a Model::Method object â access the body key directly 167: my $body = $method->{body} // ''; 168: 169: my %result = ( 170: cyclomatic_score => $CYCLOMATIC_BASE, 171: branching_points => 0, 172: early_returns => 0, 173: exception_paths => 0, 174: nesting_depth => 0, 175: ); 176: 177: # -------------------------------------------------- 178: # Count branching keywords â each one introduces a 179: # new decision point that increases cyclomatic complexity 180: # -------------------------------------------------- 181: for my $token (@BRANCH_TOKENS) { 182: my $count = () = $body =~ /\b$token\b/g; 183: $result{branching_points} += $count; 184: $result{cyclomatic_score} += $count; 185: } 186: 187: # Logical operators also introduce implicit branches ●188 → 203 → 215●188 → 203 → 0 188: my $logic_count = () = $body =~ /&&|\|\||\?/g; 189: $result{cyclomatic_score} += $logic_count; 190: 191: # -------------------------------------------------- 192: # Early returns â each return beyond the first adds 193: # an additional exit path through the method 194: # -------------------------------------------------- 195: my $return_count = () = $body =~ /\breturn\b/g; 196: $result{early_returns} = $return_count > 1 ? $return_count - 1 : 0;Mutants (Total: 3, Killed: 3, Survived: 0)
197: $result{cyclomatic_score} += $result{early_returns}; 198: 199: # -------------------------------------------------- 200: # Exception paths â die/croak/eval etc. each introduce 201: # a path that must be tested separately 202: # -------------------------------------------------- 203: for my $token (@EXCEPTION_TOKENS) { 204: my $count = () = $body =~ /\b$token\b/g; 205: $result{exception_paths} += $count; 206: $result{cyclomatic_score} += $count; 207: } 208: 209: # -------------------------------------------------- 210: # Nesting depth â count brace depth by scanning chars. 211: # NOTE: this is naive and will overcount if braces 212: # appear inside strings or regexes. Acceptable for 213: # dashboard display purposes. 214: # -------------------------------------------------- ●215 → 217 → 225●215 → 217 → 0 215: my $depth = 0; 216: my $max_depth = 0; 217: for my $char (split //, $body) { 218: if($char eq '{') {
Mutants (Total: 1, Killed: 1, Survived: 0)
219: $depth++; 220: $max_depth = $depth if $depth > $max_depth;
Mutants (Total: 3, Killed: 3, Survived: 0)
221: } elsif($char eq '}') { 222: $depth-- if $depth > 0;
Mutants (Total: 3, Killed: 3, Survived: 0)
223: } 224: } ●225 → 236 → 0 225: $result{nesting_depth} = $max_depth; 226: 227: # -------------------------------------------------- 228: # Classify complexity level based on cyclomatic score 229: # -------------------------------------------------- 230: my $score = $result{cyclomatic_score}; 231: $result{complexity_level} = 232: $score <= $LOW_THRESHOLD ? $LEVEL_LOW :
Mutants (Total: 3, Killed: 3, Survived: 0)
233: $score <= $HIGH_THRESHOLD ? $LEVEL_MODERATE :
Mutants (Total: 3, Killed: 3, Survived: 0)
234: $LEVEL_HIGH; 235: 236: return \%result; 237: } 238: 239: 1;