go.go
package testexec
// Go-specific execution support for testmark.
// This file provides intelligent Go program execution with package resolution.
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/warpfork/go-testmark/v2"
)
// TestGo runs Go code from a testmark entry with intelligent package resolution.
// This is a convenience function that handles common Go testing scenarios.
func TestGo(t *testing.T, entry *testmark.DirEntry) {
TestGoWithTimeout(t, entry, 30*time.Second)
}
// TestGoWithTimeout runs Go code with a custom timeout.
func TestGoWithTimeout(t *testing.T, entry *testmark.DirEntry, timeout time.Duration) {
config := DefaultConfig()
config.Timeout = timeout
config.ExecFn = SmartGoExecFunc()
Test(t, entry, config)
}
// SmartGoExecFunc creates an ExecFunc that intelligently handles Go packages.
// It supports:
// - "." for current package
// - "./path" for relative packages
// - "package" for packages in current module
// - "module/package" for full module paths
func SmartGoExecFunc() ExecFunc {
return func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
if len(args) == 0 {
return 1, fmt.Errorf("no command specified")
}
// Handle Go-specific commands
if args[0] == "go" || strings.HasSuffix(args[0], ".go") || isGoPackageReference(args[0]) {
return runGoCommand(ctx, args, stdin, stdout, stderr)
}
// Fall back to regular execution
return DefaultExecFunc(ctx, args, stdin, stdout, stderr)
}
}
// isGoPackageReference checks if the argument looks like a Go package reference
func isGoPackageReference(arg string) bool {
// Explicit Go references
if arg == "." || strings.HasPrefix(arg, "./") || strings.HasPrefix(arg, "../") {
return true
}
// Module paths (contain slash)
if strings.Contains(arg, "/") && !strings.Contains(arg, " ") {
return true
}
// Avoid common system commands
commonCommands := []string{"echo", "ls", "cat", "grep", "awk", "sed", "sort", "uniq", "wc", "head", "tail", "curl", "wget"}
for _, cmd := range commonCommands {
if arg == cmd {
return false
}
}
// .go files are Go references
if strings.HasSuffix(arg, ".go") {
return true
}
// Simple names that look like Go packages (no spaces, no common extensions)
return !strings.Contains(arg, " ") && !strings.HasSuffix(arg, ".exe") &&
!strings.HasSuffix(arg, ".sh") && !strings.HasSuffix(arg, ".py") &&
!strings.HasSuffix(arg, ".js") && len(arg) > 0 &&
!strings.Contains(arg, "=") // avoid env vars
}
// runGoCommand handles Go-specific command execution with smart package resolution
func runGoCommand(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
// If it's already a "go" command, run it as-is
if args[0] == "go" {
return DefaultExecFunc(ctx, args, stdin, stdout, stderr)
}
// Handle .go files
if strings.HasSuffix(args[0], ".go") {
goArgs := []string{"go", "run"}
goArgs = append(goArgs, args...)
return DefaultExecFunc(ctx, goArgs, stdin, stdout, stderr)
}
// Handle package references
pkgRef := args[0]
resolvedPkg, err := resolvePackageReference(pkgRef)
if err != nil {
return 1, fmt.Errorf("failed to resolve package %q: %w", pkgRef, err)
}
goArgs := []string{"go", "run", resolvedPkg}
if len(args) > 1 {
goArgs = append(goArgs, args[1:]...) // Add remaining args as program arguments
}
return DefaultExecFunc(ctx, goArgs, stdin, stdout, stderr)
}
// resolvePackageReference converts various package reference formats to something `go run` can use
func resolvePackageReference(pkgRef string) (string, error) {
switch {
case pkgRef == ".":
// Current package
return ".", nil
case strings.HasPrefix(pkgRef, "./") || strings.HasPrefix(pkgRef, "../"):
// Relative path - use as-is
return pkgRef, nil
case strings.Contains(pkgRef, "/"):
// Already a module/package path - use as-is
return pkgRef, nil
default:
// Simple package name - try to resolve within current module
return resolveLocalPackage(pkgRef)
}
}
// resolveLocalPackage tries to find a package within the current module
func resolveLocalPackage(pkgName string) (string, error) {
// Try to find the module root
moduleRoot, err := findModuleRoot()
if err != nil {
// If we can't find module root, assume it's a relative package
return fmt.Sprintf("./%s", pkgName), nil
}
// Look for the package directory
pkgPath := filepath.Join(moduleRoot, pkgName)
if _, err := os.Stat(pkgPath); err == nil {
return fmt.Sprintf("./%s", pkgName), nil
}
// If not found, assume it's an external package
return pkgName, nil
}
// findModuleRoot walks up the directory tree looking for go.mod
func findModuleRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
// Reached root directory
break
}
dir = parent
}
return "", fmt.Errorf("go.mod not found")
}
// Convenience functions for common testing scenarios
v2/testexec/go.go · 5.0 KiB · 61d171ddc5