Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions pkg/gotest/adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package gotest

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
"github.com/openshift-eng/openshift-tests-extension/pkg/util/sets"
)

// Config holds configuration for the Go test framework
type Config struct {
// TestPrefix is prepended to all test names (e.g., "[sig-api-machinery] kube-apiserver operator")
TestPrefix string

// TestDirectories are the directories to scan for tests (e.g., "test/e2e", "test/e2e-encryption")
TestDirectories []string

// ModuleRoot is the root directory of the Go module (auto-detected if empty)
ModuleRoot string
}

// BuildExtensionTestSpecs discovers Go tests and converts them to OTE ExtensionTestSpecs
func BuildExtensionTestSpecs(config Config) (et.ExtensionTestSpecs, error) {
// Auto-detect module root if not provided
if config.ModuleRoot == "" {
moduleRoot, err := findModuleRoot()
if err != nil {
return nil, fmt.Errorf("failed to find module root: %w", err)
}
config.ModuleRoot = moduleRoot
}

// Build absolute paths for test directories
var absoluteTestDirs []string
testDirMap := make(map[string]string) // testName -> directory

for _, dir := range config.TestDirectories {
absoluteDir := filepath.Join(config.ModuleRoot, dir)
absoluteTestDirs = append(absoluteTestDirs, absoluteDir)

// Discover tests in this directory to build mapping
tests, err := discoverTestsInDirectory(absoluteDir)
if err != nil {
// Directory might not exist, skip it
continue
}

for _, test := range tests {
testDirMap[test.Name] = dir
}
}

// Discover all tests
tests, err := DiscoverTests(absoluteTestDirs)
if err != nil {
return nil, fmt.Errorf("failed to discover tests: %w", err)
}

// Convert to ExtensionTestSpecs
specs := make(et.ExtensionTestSpecs, 0, len(tests))
for _, test := range tests {
spec := buildTestSpec(config, test, testDirMap[test.Name])
specs = append(specs, spec)
}

return specs, nil
}

func buildTestSpec(config Config, test TestMetadata, testDir string) *et.ExtensionTestSpec {
// Build test name with prefix and tags
testName := config.TestPrefix + " " + test.Name

// Add tags to name (for suite routing)
for _, tag := range test.Tags {
testName += fmt.Sprintf(" [%s]", tag)
}

// Add timeout to name if specified
if test.Timeout > 0 {
testName += fmt.Sprintf(" [Timeout:%s]", test.Timeout)
}

// Determine lifecycle
lifecycle := et.LifecycleBlocking
if strings.EqualFold(test.Lifecycle, "Informing") {
lifecycle = et.LifecycleInforming
}

// Determine parallelism (Serial tag means no parallelism)
isSerial := false
for _, tag := range test.Tags {
if tag == "Serial" {
isSerial = true
break
}
}

// Capture testDir and testName in closure
capturedTestDir := filepath.Join(config.ModuleRoot, testDir)
capturedTestName := test.Name
capturedTimeout := test.Timeout

// Build Labels set from tags (for OTE filtering)
labels := sets.New[string](test.Tags...)

spec := &et.ExtensionTestSpec{
Name: testName,
Labels: labels,
Lifecycle: lifecycle,
Run: func(ctx context.Context) *et.ExtensionTestResult {
// Execute test
result := ExecuteTest(ctx, capturedTestDir, capturedTestName, capturedTimeout)

// Convert to ExtensionTestResult
oteResult := et.ResultPassed
if !result.Passed {
oteResult = et.ResultFailed
}

return &et.ExtensionTestResult{
Result: oteResult,
Output: result.Output,
}
},
}

// Apply timeout tag if specified
if test.Timeout > 0 {
if spec.Tags == nil {
spec.Tags = make(map[string]string)
}
spec.Tags["timeout"] = test.Timeout.String()
}

// Apply isolation (Serial tests need isolation)
if isSerial {
spec.Resources = et.Resources{
Isolation: et.Isolation{},
}
}

return spec
}

// findModuleRoot walks up from current directory to find 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 {
return "", fmt.Errorf("could not find go.mod")
}
dir = parent
}
}
140 changes: 140 additions & 0 deletions pkg/gotest/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package gotest

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"time"
)

// TestMetadata represents metadata extracted from test source code
type TestMetadata struct {
Name string
Tags []string
Timeout time.Duration
Lifecycle string
}

// DiscoverTests scans directories and discovers all Test* functions
func DiscoverTests(testDirs []string) ([]TestMetadata, error) {
var allTests []TestMetadata

for _, dir := range testDirs {
tests, err := discoverTestsInDirectory(dir)
if err != nil {
return nil, fmt.Errorf("failed to discover tests in %s: %w", dir, err)
}
allTests = append(allTests, tests...)
}

return allTests, nil
}

func discoverTestsInDirectory(dir string) ([]TestMetadata, error) {
var tests []TestMetadata

err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Skip subdirectories
if info.IsDir() && path != dir {
return filepath.SkipDir
}

// Only process Go files
if !strings.HasSuffix(path, ".go") {
return nil
}

fileTests, err := parseTestFile(path)
if err != nil {
return fmt.Errorf("error parsing %s: %w", path, err)
}

tests = append(tests, fileTests...)
return nil
})

return tests, err
}

func parseTestFile(filename string) ([]TestMetadata, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, err
}

var tests []TestMetadata

for _, decl := range node.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || !strings.HasPrefix(fn.Name.Name, "Test") {
continue
}

// Skip TestMain
if fn.Name.Name == "TestMain" {
continue
}

// Check if it's a test function
if fn.Type.Params == nil || len(fn.Type.Params.List) == 0 {
continue
}

// Extract metadata from comments
metadata := extractMetadataFromComments(fn.Doc, fn.Name.Name)
tests = append(tests, metadata)
}

return tests, nil
}

func extractMetadataFromComments(doc *ast.CommentGroup, testName string) TestMetadata {
metadata := TestMetadata{
Name: testName,
Tags: []string{},
Lifecycle: "Blocking", // Default
}

if doc == nil {
return metadata
}

for _, comment := range doc.List {
text := strings.TrimPrefix(comment.Text, "//")
text = strings.TrimSpace(text)

if strings.HasPrefix(text, "Tags:") {
tagStr := strings.TrimPrefix(text, "Tags:")
tagStr = strings.TrimSpace(tagStr)
tags := strings.Split(tagStr, ",")
for _, tag := range tags {
tag = strings.TrimSpace(tag)
if tag != "" {
metadata.Tags = append(metadata.Tags, tag)
}
}
}

if strings.HasPrefix(text, "Timeout:") {
timeoutStr := strings.TrimSpace(strings.TrimPrefix(text, "Timeout:"))
if timeout, err := time.ParseDuration(timeoutStr); err == nil {
metadata.Timeout = timeout
}
}

if strings.HasPrefix(text, "Lifecycle:") {
metadata.Lifecycle = strings.TrimSpace(strings.TrimPrefix(text, "Lifecycle:"))
}
}

return metadata
}
54 changes: 54 additions & 0 deletions pkg/gotest/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package gotest

import (
"bytes"
"context"
"fmt"
"os/exec"
"time"
)

// TestResult represents the result of running a test
type TestResult struct {
TestName string
Passed bool
Output string
Duration time.Duration
Error error
}

// ExecuteTest runs a single test via go test subprocess
func ExecuteTest(ctx context.Context, testDir string, testName string, timeout time.Duration) *TestResult {
start := time.Now()

// Apply timeout if specified
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}

// Build go test command
cmd := exec.CommandContext(ctx, "go", "test", "-v", "-run", fmt.Sprintf("^%s$", testName), testDir)

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

// Execute test
err := cmd.Run()
duration := time.Since(start)

// Combine output
output := stdout.String() + "\n" + stderr.String()

result := &TestResult{
TestName: testName,
Passed: err == nil,
Output: output,
Duration: duration,
Error: err,
}

return result
}