Files
fn_registry/registry/test_parser.go
T
egutierrez 131f860a94 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:.
2026-04-05 18:19:17 +02:00

134 lines
3.6 KiB
Go

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
}