a06f331eb8
install-script / powershell-syntax (push) Waiting to run
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
install-script / install (macos-14) (push) Waiting to run
install-script / install (ubuntu-latest) (push) Waiting to run
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
188 lines
6.0 KiB
Go
188 lines
6.0 KiB
Go
package indexer
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// IsTestFile returns true when the file's name or directory matches a
|
|
// recognised test convention from the table below. False positives
|
|
// here are downgraded downstream by the symbol-name filter
|
|
// (IsTestSymbol).
|
|
//
|
|
// Recognised conventions:
|
|
//
|
|
// *_test.go (Go)
|
|
// *.test.{ts,tsx,js,jsx,mts,cts} (TS/JS via Jest/Vitest convention)
|
|
// *.spec.{ts,tsx,js,jsx,mts,cts} (TS/JS spec convention)
|
|
// test_*.py / *_test.py (Python)
|
|
// *_test.dart (Dart)
|
|
// *_spec.rb / *_test.rb (Ruby)
|
|
// *Test.java / *Tests.java (JUnit / Spring)
|
|
// *Test.kt / *Tests.kt (Kotlin)
|
|
// *Tests.cs (C# xUnit/NUnit)
|
|
// *Tests.swift (Swift)
|
|
// *Test.php / *test.php (PHPUnit / Pest)
|
|
// files under __tests__/, tests/,
|
|
// test/, spec/ (any language using these dirs)
|
|
func IsTestFile(path string) bool {
|
|
if path == "" {
|
|
return false
|
|
}
|
|
// Directory-based hints first — covers projects that don't follow
|
|
// the per-file naming convention.
|
|
dir := filepath.ToSlash(path)
|
|
for _, marker := range []string{"/__tests__/", "/tests/", "/test/", "/spec/"} {
|
|
if strings.Contains(dir, marker) {
|
|
return true
|
|
}
|
|
}
|
|
if strings.HasPrefix(dir, "tests/") || strings.HasPrefix(dir, "test/") || strings.HasPrefix(dir, "spec/") {
|
|
return true
|
|
}
|
|
|
|
base := filepath.Base(path)
|
|
ext := strings.ToLower(filepath.Ext(base))
|
|
stem := strings.TrimSuffix(base, ext)
|
|
|
|
switch ext {
|
|
case ".go":
|
|
return strings.HasSuffix(stem, "_test")
|
|
case ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs":
|
|
return strings.HasSuffix(stem, ".test") || strings.HasSuffix(stem, ".spec")
|
|
case ".py":
|
|
return strings.HasPrefix(stem, "test_") || strings.HasSuffix(stem, "_test")
|
|
case ".dart":
|
|
return strings.HasSuffix(stem, "_test")
|
|
case ".rb":
|
|
return strings.HasSuffix(stem, "_spec") || strings.HasSuffix(stem, "_test")
|
|
case ".java", ".kt":
|
|
return strings.HasSuffix(stem, "Test") || strings.HasSuffix(stem, "Tests")
|
|
case ".cs":
|
|
return strings.HasSuffix(stem, "Tests") || strings.HasSuffix(stem, "Test")
|
|
case ".swift":
|
|
return strings.HasSuffix(stem, "Tests")
|
|
case ".php":
|
|
return strings.HasSuffix(stem, "Test") || strings.HasSuffix(stem, "test")
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestRole classifies a function/method name by its language's test
|
|
// convention and returns the specific role — "test", "benchmark",
|
|
// "fuzz", or "example" — or "" when the name matches no convention.
|
|
// For languages where test runners pick up by annotation (Java @Test,
|
|
// Rust #[test]) or by file membership alone (TS/JS), the name carries
|
|
// no role signal; callers fall back to IsTestFile and treat such
|
|
// symbols as a plain "test".
|
|
func TestRole(name, language string) string {
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
switch language {
|
|
case "go":
|
|
switch {
|
|
case hasTestPrefix(name, "Benchmark"):
|
|
return "benchmark"
|
|
case hasTestPrefix(name, "Fuzz"):
|
|
return "fuzz"
|
|
case hasTestPrefix(name, "Example"):
|
|
return "example"
|
|
case hasTestPrefix(name, "Test"):
|
|
return "test"
|
|
}
|
|
case "python":
|
|
if strings.HasPrefix(name, "test_") || strings.HasPrefix(name, "Test") {
|
|
return "test"
|
|
}
|
|
case "ruby":
|
|
if strings.HasPrefix(name, "test_") {
|
|
return "test"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsTestSymbol returns true when a function/method name looks like a
|
|
// test entry point per its language's convention. It is a back-compat
|
|
// wrapper over TestRole — callers that need the specific role should
|
|
// use TestRole directly.
|
|
func IsTestSymbol(name, language string) bool {
|
|
return TestRole(name, language) != ""
|
|
}
|
|
|
|
// AnnotationTestRole maps a (language, annotation-name) pair to a test
|
|
// role for the languages whose runners discover tests by attribute
|
|
// rather than by file location — Rust's #[test] / #[bench] family and
|
|
// the JVM JUnit / TestNG @Test family. Returns "" when the annotation
|
|
// does not denote a test.
|
|
//
|
|
// The name is the bare attribute path as captured by the extractor (no
|
|
// leading `@` / `#[`). Rust scoped attributes arrive as "tokio::test" /
|
|
// "async_std::test"; JVM annotations may be written fully qualified
|
|
// ("org.junit.jupiter.api.Test"), so the JVM branch matches on the
|
|
// last path segment. This is the signal that lets an inline #[test] fn
|
|
// in a production-path src/foo.rs — or a @Test method in a class whose
|
|
// file name carries no test suffix — classify as a test even though
|
|
// IsTestFile is false for its file.
|
|
func AnnotationTestRole(language, name string) string {
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
switch language {
|
|
case "rust":
|
|
switch {
|
|
case name == "bench":
|
|
return "benchmark"
|
|
case name == "test" || strings.HasSuffix(name, "::test"):
|
|
// #[test], #[tokio::test], #[async_std::test], #[actix_rt::test], …
|
|
return "test"
|
|
case name == "rstest" || name == "test_case" || name == "googletest":
|
|
return "test"
|
|
}
|
|
case "java", "kotlin":
|
|
short := name
|
|
if i := strings.LastIndexByte(short, '.'); i >= 0 {
|
|
short = short[i+1:]
|
|
}
|
|
switch short {
|
|
case "Test", "ParameterizedTest", "RepeatedTest", "TestFactory", "TestTemplate":
|
|
return "test"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// AnnotationTestRunner names the test runner for an annotation-discovered
|
|
// test that lives in a production-path file, where the file-name and
|
|
// import heuristics in detectTestRunnerForFile do not apply. Returns ""
|
|
// for languages without an attribute-driven runner.
|
|
func AnnotationTestRunner(language string) string {
|
|
switch language {
|
|
case "rust":
|
|
return "cargo-test"
|
|
case "java", "kotlin":
|
|
return "junit"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func hasTestPrefix(name string, prefixes ...string) bool {
|
|
for _, p := range prefixes {
|
|
if !strings.HasPrefix(name, p) {
|
|
continue
|
|
}
|
|
// Must be followed by an uppercase letter or end of name —
|
|
// "Testing" is not a Go test fn but "TestFoo" is. "Test" alone
|
|
// is not picked up by `go test` either; require a suffix.
|
|
if len(name) == len(p) {
|
|
return false
|
|
}
|
|
c := name[len(p)]
|
|
if c >= 'A' && c <= 'Z' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|