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

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 92.86%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 15

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::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;