feat: parser automático de test files Go/Python/Bash
Extrae test cases individuales con su código desde archivos _test. Go detecta func TestXxx, Python detecta def test_xxx, Bash soporta tres convenciones: test_xxx(){}, secciones === nombre ===, y comentarios # Test:.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// testCase represents a single test extracted from a test file.
|
||||
type testCase struct {
|
||||
Name string
|
||||
Code string
|
||||
}
|
||||
|
||||
// testPos marks the start of a test within a file.
|
||||
type testPos struct {
|
||||
name string
|
||||
startLine int
|
||||
}
|
||||
|
||||
// parseTestFile reads a test file and extracts individual test cases.
|
||||
// Supports Go, Python, and Bash test formats.
|
||||
func parseTestFile(path, lang string) ([]testCase, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading test file %s: %w", path, err)
|
||||
}
|
||||
content := string(data)
|
||||
|
||||
switch lang {
|
||||
case "go":
|
||||
return parseGoTests(content), nil
|
||||
case "py":
|
||||
return parsePythonTests(content), nil
|
||||
case "bash":
|
||||
return parseBashTests(content), nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseGoTests extracts Go test functions (func TestXxx).
|
||||
var goTestFuncRe = regexp.MustCompile(`(?m)^func\s+(Test\w+)\s*\(`)
|
||||
|
||||
func parseGoTests(content string) []testCase {
|
||||
lines := strings.Split(content, "\n")
|
||||
var positions []testPos
|
||||
|
||||
for i, line := range lines {
|
||||
if m := goTestFuncRe.FindStringSubmatch(line); m != nil {
|
||||
positions = append(positions, testPos{name: m[1], startLine: i})
|
||||
}
|
||||
}
|
||||
|
||||
return extractBlocks(lines, positions)
|
||||
}
|
||||
|
||||
// parsePythonTests extracts Python test functions (def test_xxx).
|
||||
var pyTestFuncRe = regexp.MustCompile(`(?m)^def\s+(test_\w+)\s*\(`)
|
||||
|
||||
func parsePythonTests(content string) []testCase {
|
||||
lines := strings.Split(content, "\n")
|
||||
var positions []testPos
|
||||
|
||||
for i, line := range lines {
|
||||
if m := pyTestFuncRe.FindStringSubmatch(line); m != nil {
|
||||
positions = append(positions, testPos{name: m[1], startLine: i})
|
||||
}
|
||||
}
|
||||
|
||||
return extractBlocks(lines, positions)
|
||||
}
|
||||
|
||||
// parseBashTests extracts Bash test blocks.
|
||||
// Tries three conventions in order:
|
||||
// 1. test_xxx() { ... } — function-based tests
|
||||
// 2. === section === — section headers (echo "=== name ===")
|
||||
// 3. # Test: ... — comment-based test blocks
|
||||
var bashTestFuncRe = regexp.MustCompile(`(?m)^(test_\w+)\s*\(\)\s*\{`)
|
||||
var bashTestCommentRe = regexp.MustCompile(`(?m)^#\s*[Tt]est:\s*(.+)`)
|
||||
var bashSectionRe = regexp.MustCompile(`(?i)^(?:echo\s+["'])?===\s*(\w[\w\s]*\w)\s*===["']?\s*$`)
|
||||
|
||||
func parseBashTests(content string) []testCase {
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
// Strategy 1: test_xxx() { ... }
|
||||
var positions []testPos
|
||||
for i, line := range lines {
|
||||
if m := bashTestFuncRe.FindStringSubmatch(line); m != nil {
|
||||
positions = append(positions, testPos{name: m[1], startLine: i})
|
||||
}
|
||||
}
|
||||
if len(positions) > 0 {
|
||||
return extractBlocks(lines, positions)
|
||||
}
|
||||
|
||||
// Strategy 2: === section === headers
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if m := bashSectionRe.FindStringSubmatch(trimmed); m != nil {
|
||||
positions = append(positions, testPos{name: m[1], startLine: i})
|
||||
}
|
||||
}
|
||||
if len(positions) > 0 {
|
||||
return extractBlocks(lines, positions)
|
||||
}
|
||||
|
||||
// Strategy 3: # Test: ... comments
|
||||
for i, line := range lines {
|
||||
if m := bashTestCommentRe.FindStringSubmatch(line); m != nil {
|
||||
positions = append(positions, testPos{name: strings.TrimSpace(m[1]), startLine: i})
|
||||
}
|
||||
}
|
||||
return extractBlocks(lines, positions)
|
||||
}
|
||||
|
||||
// extractBlocks splits lines into code blocks based on test positions.
|
||||
func extractBlocks(lines []string, positions []testPos) []testCase {
|
||||
var tests []testCase
|
||||
for i, pos := range positions {
|
||||
endLine := len(lines)
|
||||
if i+1 < len(positions) {
|
||||
endLine = positions[i+1].startLine
|
||||
}
|
||||
for endLine > pos.startLine && strings.TrimSpace(lines[endLine-1]) == "" {
|
||||
endLine--
|
||||
}
|
||||
code := strings.Join(lines[pos.startLine:endLine], "\n")
|
||||
tests = append(tests, testCase{Name: pos.name, Code: code})
|
||||
}
|
||||
return tests
|
||||
}
|
||||
Reference in New Issue
Block a user