From 384a87f8a7d56c308641cf33fab9c2ad28e37842 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 5 Apr 2026 18:19:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20parser=20autom=C3=A1tico=20de=20test=20?= =?UTF-8?q?files=20Go/Python/Bash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:. --- registry/test_parser.go | 133 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 registry/test_parser.go diff --git a/registry/test_parser.go b/registry/test_parser.go new file mode 100644 index 00000000..3abf75d0 --- /dev/null +++ b/registry/test_parser.go @@ -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 +}