TER1 (Statement): 100.00%
TER2 (Branch): 71.95%
TER3 (LCSAJ): 100.0% (3/3)
Approximate LCSAJ segments: 165
● 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::GHGen::Detector; 2: 3: use v5.36; 4: use strict; 5: use warnings; 6: 7: use Path::Tiny; 8: 9: use Exporter 'import'; 10: our @EXPORT_OK = qw( 11: detect_project_type 12: get_project_indicators 13: ); 14: 15: our $VERSION = '0.10'; 16: 17: =head1 NAME 18: 19: App::GHGen::Detector - Detect project type from repository contents 20: 21: =head1 SYNOPSIS 22: 23: use App::GHGen::Detector qw(detect_project_type); 24: 25: my $type = detect_project_type(); 26: # Returns: 'perl', 'node', 'python', etc. 27: 28: =head1 FUNCTIONS 29: 30: =head2 detect_project_type() 31: 32: Detect the project type by examining indicator files in the current directory. 33: 34: =head3 Purpose 35: 36: Score the current working directory against ten known project types 37: (perl, node, python, rust, go, ruby, php, java, cpp, docker) and return 38: the highest-scoring type. 39: 40: =head3 Arguments 41: 42: None. 43: 44: =head3 Returns 45: 46: In scalar context: the type string (e.g. C<'perl'>) of the best match, or 47: C<undef> when no type scores above zero. 48: 49: In list context: a list of C<{ type => Str, score => Int, indicators => [] }> 50: hash references sorted by descending score. 51: 52: =head3 Side Effects 53: 54: Reads the filesystem; results depend on the current working directory. 55: 56: =head3 Usage Example 57: 58: use App::GHGen::Detector qw(detect_project_type); 59: my $type = detect_project_type(); 60: say $type // 'unknown'; 61: 62: =head3 API SPECIFICATION 63: 64: =head4 Input 65: 66: # No parameters. 67: 68: =head4 Output (scalar context) 69: 70: { type => 'scalar' } # may be undef 71: 72: =head4 Output (list context) 73: 74: { type => 'array', element => { type => 'hashref' } } 75: 76: =head3 FORMAL SPECIFICATION 77: 78: detect_project_type : â â¤* ⪠{ ⥠} | seq Detection 79: 80: Detection â { type: ProjectType, score: â, indicators: seq â¤* } 81: detections â [ { type: t, score: score(t) } ⣠t â Types, score(t) > 0 ] 82: ranked â sort desc-score detections 83: 84: scalar context: ranked = â â ⥠| ranked â â â ranked[0].type 85: list context: ranked 86: 87: =cut 88: 89: sub detect_project_type() { ●90 → 93 → 100 90: my @detections; 91: 92: # Check for each project type 93: for my $type (qw(perl node python rust go ruby php java cpp docker)) { 94: my $detector = "_detect_$type"; 95: my $score = __PACKAGE__->can($detector)->(); 96: push @detections, { type => $type, score => $score, indicators => [] } if $score > 0;Mutants (Total: 3, Killed: 3, Survived: 0)
97: } 98: 99: # Sort by score (highest first) 100: @detections = sort { $b->{score} <=> $a->{score} } @detections; 101: 102: # `return;` returns () in list context and undef in scalar context â correct for both. 103: # `return undef` would return (undef) in list context, i.e. a one-element list. 104: return unless @detections; 105: return wantarray ? @detections : $detections[0]->{type};
Mutants (Total: 2, Killed: 2, Survived: 0)
106: } 107: 108: =head2 get_project_indicators($type) 109: 110: Return the indicator file patterns associated with a given project type. 111: 112: =head3 Purpose 113: 114: Provide a human-readable or programmatic list of the files and patterns that 115: the detector uses to score each project type. 116: 117: =head3 Arguments 118: 119: =over 4 120: 121: =item C<$type> (Str, optional) 122: 123: A project type string such as C<'perl'>, C<'node'>, C<'python'>, etc. 124: When omitted or C<undef>, the full indicator table is returned. 125: 126: =back 127: 128: =head3 Returns 129: 130: When C<$type> is given and known: an array reference of strings (file names / 131: glob patterns). 132: 133: When C<$type> is C<undef> or omitted: a hash reference mapping every known 134: type to its indicator list. 135: 136: When C<$type> is given but unknown: C<undef>. 137: 138: =head3 Side Effects 139: 140: None. Pure lookup function. 141: 142: =head3 Usage Example 143: 144: my $indicators = get_project_indicators('perl'); 145: say for @$indicators; 146: 147: my $all = get_project_indicators(); 148: for my $type (sort keys %$all) { 149: say "$type: " . join(', ', @{$all->{$type}}); 150: } 151: 152: =head3 API SPECIFICATION 153: 154: =head4 Input 155: 156: { type => { type => 'scalar', optional => 1 } } 157: 158: =head4 Output 159: 160: known type â { type => 'arrayref' } 161: all types â { type => 'hashref' } 162: unknown type â undef 163: 164: =head3 FORMAL SPECIFICATION 165: 166: get_project_indicators : â¤* ⪠{ ⥠} â seq â¤* ⪠(â¤* â seq â¤*) ⪠{ ⥠} 167: 168: t given â§ t â KnownTypes â indicators[t] (ArrayRef) 169: t given â§ t â KnownTypes â ⥠(undef) 170: t absent â indicators (HashRef of all types) 171: 172: =cut 173: 174: sub get_project_indicators($type = undef) { 175: my %indicators = ( 176: perl => [ 177: 'cpanfile', 'dist.ini', 'Makefile.PL', 'Build.PL', 178: 'lib/*.pm', 't/*.t', 'META.json', 'META.yml' 179: ], 180: node => [ 181: 'package.json', 'package-lock.json', 'yarn.lock', 182: 'node_modules/', 'tsconfig.json', '.npmrc' 183: ], 184: python => [ 185: 'requirements.txt', 'setup.py', 'pyproject.toml', 186: 'Pipfile', 'poetry.lock', 'setup.cfg', 'tox.ini' 187: ], 188: rust => [ 189: 'Cargo.toml', 'Cargo.lock', 'src/main.rs', 190: 'src/lib.rs', 'rust-toolchain.toml' 191: ], 192: go => [ 193: 'go.mod', 'go.sum', 'main.go', '*.go' 194: ], 195: ruby => [ 196: 'Gemfile', 'Gemfile.lock', 'Rakefile', 197: '.ruby-version', 'config.ru' 198: ], 199: php => [ 200: 'composer.json', 'composer.lock', 'phpunit.xml', 201: 'phpunit.xml.dist', 'src/', 'tests/' 202: ], 203: java => [ 204: 'pom.xml', 'build.gradle', 'build.gradle.kts', 205: 'gradlew', 'mvnw', 'src/main/java/' 206: ], 207: cpp => [ 208: 'CMakeLists.txt', 'Makefile', 'configure.ac', 209: '*.cpp', '*.hpp', '*.cc', '*.h' 210: ], 211: c => [ 212: 'CMakeLists.txt', 'Makefile', 'configure.ac', 213: '*.c', '*.h', 214: ], 215: docker => [ 216: 'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 217: '.dockerignore' 218: ], 219: ); 220: 221: return $type ? $indicators{$type} : \%indicators;
Mutants (Total: 2, Killed: 2, Survived: 0)
222: } 223: 224: # Detection functions - return a score (0 = not detected, higher = more confident) 225: 226: sub _detect_perl() { ●227 → 244 → 250 227: my $score = 0; 228: 229: # Strong indicators 230: $score += 10 if path('cpanfile')->exists; 231: $score += 10 if path('dist.ini')->exists; 232: $score += 8 if path('Makefile.PL')->exists; 233: $score += 8 if path('Build.PL')->exists; 234: 235: # Medium indicators 236: $score += 5 if path('META.json')->exists; 237: $score += 5 if path('META.yml')->exists; 238: 239: # Weak indicators 240: $score += 3 if path('lib')->exists && path('lib')->is_dir; 241: $score += 2 if path('t')->exists && path('t')->is_dir; 242: 243: # Check for .pm files in lib/ 244: if (path('lib')->exists) {
Mutants (Total: 1, Killed: 1, Survived: 0)
245: my @pm_files = path('lib')->children(qr/\.pm$/); 246: $score += 2 if @pm_files > 0;
247: } 248: 249: # Check for .t files in t/ ●250 → 250 → 255 250: if (path('t')->exists) {Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_246_28_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_246_28_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_246_28_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 1, Killed: 1, Survived: 0)
251: my @t_files = path('t')->children(qr/\.t$/); 252: $score += 1 if @t_files > 0;
253: } 254: 255: return $score;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_252_27_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_252_27_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_252_27_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
256: } 257: 258: sub _detect_node() { 259: my $score = 0; 260: 261: # Strong indicators 262: $score += 15 if path('package.json')->exists; 263: $score += 8 if path('package-lock.json')->exists; 264: $score += 8 if path('yarn.lock')->exists; 265: $score += 7 if path('pnpm-lock.yaml')->exists; 266: 267: # Medium indicators 268: $score += 5 if path('node_modules')->exists && path('node_modules')->is_dir; 269: $score += 4 if path('tsconfig.json')->exists; 270: $score += 3 if path('.npmrc')->exists; 271: 272: # Weak indicators 273: $score += 2 if path('src')->exists && path('src')->is_dir; 274: 275: return $score;
Mutants (Total: 2, Killed: 2, Survived: 0)
276: } 277: 278: sub _detect_python() { 279: my $score = 0; 280: 281: # Strong indicators 282: $score += 12 if path('requirements.txt')->exists; 283: $score += 12 if path('setup.py')->exists; 284: $score += 12 if path('pyproject.toml')->exists; 285: $score += 10 if path('Pipfile')->exists; 286: $score += 8 if path('poetry.lock')->exists; 287: 288: # Medium indicators 289: $score += 5 if path('setup.cfg')->exists; 290: $score += 4 if path('tox.ini')->exists; 291: $score += 3 if path('.python-version')->exists; 292: 293: # Weak indicators 294: $score += 2 if path('venv')->exists || path('.venv')->exists; 295: 296: # Check for .py files 297: my @py_files = path('.')->children(qr/\.py$/); 298: $score += 2 if @py_files > 0;
299: 300: return $score;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_298_27_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_298_27_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_298_27_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
301: } 302: 303: sub _detect_rust() { 304: my $score = 0; 305: 306: # Strong indicators 307: $score += 15 if path('Cargo.toml')->exists; 308: $score += 10 if path('Cargo.lock')->exists; 309: 310: # Medium indicators 311: $score += 5 if path('src/main.rs')->exists; 312: $score += 5 if path('src/lib.rs')->exists; 313: $score += 3 if path('rust-toolchain.toml')->exists || path('rust-toolchain')->exists; 314: 315: # Weak indicators 316: $score += 2 if path('target')->exists && path('target')->is_dir; 317: 318: return $score;
Mutants (Total: 2, Killed: 2, Survived: 0)
319: } 320: 321: sub _detect_go() { 322: my $score = 0; 323: 324: # Strong indicators 325: $score += 15 if path('go.mod')->exists; 326: $score += 10 if path('go.sum')->exists; 327: 328: # Medium indicators 329: $score += 5 if path('main.go')->exists; 330: 331: # Check for .go files 332: my @go_files = path('.')->children(qr/\.go$/); 333: $score += 3 if @go_files > 0;
334: $score += 1 if @go_files > 3;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_333_27_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_333_27_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_333_27_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 3, Killed: 3, Survived: 0)
335: 336: return $score;
Mutants (Total: 2, Killed: 2, Survived: 0)
337: } 338: 339: sub _detect_ruby() { 340: my $score = 0; 341: 342: # Strong indicators 343: $score += 15 if path('Gemfile')->exists; 344: $score += 10 if path('Gemfile.lock')->exists; 345: 346: # Medium indicators 347: $score += 5 if path('Rakefile')->exists; 348: $score += 4 if path('.ruby-version')->exists; 349: $score += 3 if path('config.ru')->exists; 350: 351: # Check for .rb files 352: my @rb_files = path('.')->children(qr/\.rb$/); 353: $score += 2 if @rb_files > 0;
354: 355: return $score;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_353_27_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_353_27_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_353_27_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
356: } 357: 358: sub _detect_docker() { 359: my $score = 0; 360: 361: # Strong indicators 362: $score += 12 if path('Dockerfile')->exists; 363: $score += 8 if path('docker-compose.yml')->exists; 364: $score += 8 if path('docker-compose.yaml')->exists; 365: 366: # Medium indicators 367: $score += 3 if path('.dockerignore')->exists; 368: 369: return $score;
Mutants (Total: 2, Killed: 2, Survived: 0)
370: } 371: 372: sub _detect_php() { 373: my $score = 0; 374: 375: # Strong indicators 376: $score += 15 if path('composer.json')->exists; 377: $score += 10 if path('composer.lock')->exists; 378: 379: # Medium indicators 380: $score += 5 if path('phpunit.xml')->exists; 381: $score += 5 if path('phpunit.xml.dist')->exists; 382: $score += 3 if path('.php-version')->exists; 383: 384: # Weak indicators 385: $score += 2 if path('src')->exists && path('src')->is_dir; 386: $score += 2 if path('tests')->exists && path('tests')->is_dir; 387: 388: # Check for .php files 389: my @php_files = path('.')->children(qr/\.php$/); 390: $score += 2 if @php_files > 0;
391: 392: return $score;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_390_28_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_390_28_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_390_28_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
393: } 394: 395: sub _detect_java() { 396: my $score = 0; 397: 398: # Strong indicators 399: $score += 15 if path('pom.xml')->exists; 400: $score += 15 if path('build.gradle')->exists; 401: $score += 15 if path('build.gradle.kts')->exists; 402: 403: # Medium indicators 404: $score += 5 if path('gradlew')->exists; 405: $score += 5 if path('mvnw')->exists; 406: $score += 4 if path('settings.gradle')->exists; 407: $score += 4 if path('settings.gradle.kts')->exists; 408: 409: # Weak indicators 410: $score += 3 if path('src/main/java')->exists; 411: $score += 2 if path('src/test/java')->exists; 412: 413: # Check for .java files 414: my @java_files = path('.')->children(qr/\.java$/); 415: $score += 2 if @java_files > 0;
416: 417: return $score;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_415_29_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_415_29_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_415_29_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
418: } 419: 420: sub _detect_cpp() { 421: my $score = 0; 422: 423: # Strong indicators 424: $score += 12 if path('CMakeLists.txt')->exists; 425: $score += 8 if path('Makefile')->exists; 426: $score += 6 if path('configure.ac')->exists; 427: $score += 6 if path('configure')->exists; 428: 429: # Medium indicators 430: $score += 4 if path('meson.build')->exists; 431: $score += 3 if path('.clang-format')->exists; 432: 433: # Check for C++ files 434: my @cpp_files = path('.')->children(qr/\.(?:cpp|cc|cxx|hpp|hxx|h)$/); 435: $score += 3 if @cpp_files > 0;
436: $score += 2 if @cpp_files > 5;Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_435_28_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_435_28_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_435_28_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 3, Killed: 3, Survived: 0)
437: 438: # Check for include directory 439: $score += 2 if path('include')->exists && path('include')->is_dir; 440: 441: return $score;
Mutants (Total: 2, Killed: 2, Survived: 0)
442: } 443: 444: =head1 AUTHOR 445: 446: Nigel Horne E<lt>njh@nigelhorne.comE<gt> 447: 448: L<https://github.com/nigelhorne> 449: 450: =head1 LICENSE 451: 452: This is free software; you can redistribute it and/or modify it under 453: the same terms as the Perl 5 programming language system itself. 454: 455: =cut 456: 457: 1;