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

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 92.86%
TER3 (LCSAJ): 100.0% (7/7)
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.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 → 188163 → 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 → 215188 → 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 → 225215 → 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;