Skip to content

Commit 6b0e821

Browse files
committed
Add gotest adapter for standard Go tests
Similar to the Cypress adapter, this provides utilities for running standard Go tests (*_test.go) via OTE without modifying test code. Key features: - Operators compile test packages into binaries (go test -c) - Binaries are embedded in OTE extension binary (go:embed) - At runtime, binaries are extracted and executed - No hardcoded logic - operators provide all metadata Example usage: //go:embed compiled_tests/*.test var embeddedTestBinaries embed.FS metadata := []gotest.GoTestConfig{ { TestName: "[sig-api-machinery] TestOperatorNamespace [Serial]", BinaryName: "e2e.test", TestPattern: "TestOperatorNamespace", Tags: []string{"Serial"}, Lifecycle: "Informing", }, } specs, err := gotest.BuildExtensionTestSpecsFromGoTestMetadata( metadata, embeddedTestBinaries, "compiled_tests", )
1 parent 356b66a commit 6b0e821

File tree

5 files changed

+676
-0
lines changed

5 files changed

+676
-0
lines changed

pkg/gotest/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Custom Go Test Framework for OTE
2+
3+
**A custom test framework (like Ginkgo) that enables standard Go tests to run through OpenShift Tests Extension.**
4+
5+
## Key Features
6+
7+
-**Zero test code changes** - Use standard `*testing.T`
8+
-**Automatic discovery** - Parses source files to find tests
9+
-**No Ginkgo dependency** - Custom framework specifically for Go tests
10+
-**Full OTE integration** - Parallel/Serial, Blocking/Informing supported
11+
-**Metadata from comments** - `// Tags:`, `// Timeout:`, `// Lifecycle:`
12+
-**Runs from source** - No test compilation required
13+
14+
## Quick Start
15+
16+
### 1. Configure in Your Operator
17+
18+
```go
19+
package main
20+
21+
import (
22+
"github.com/openshift-eng/openshift-tests-extension/pkg/gotest"
23+
)
24+
25+
func main() {
26+
config := gotest.Config{
27+
TestPrefix: "[sig-api-machinery] my-operator",
28+
TestDirectories: []string{
29+
"test/e2e",
30+
"test/e2e-encryption",
31+
},
32+
}
33+
34+
specs, _ := gotest.BuildExtensionTestSpecs(config)
35+
extension.AddSpecs(specs)
36+
}
37+
```
38+
39+
### 2. Write Standard Go Tests
40+
41+
```go
42+
// test/e2e/my_test.go
43+
package e2e
44+
45+
import "testing"
46+
47+
// Tags: Serial
48+
// Timeout: 60m
49+
func TestMyFeature(t *testing.T) {
50+
// Standard Go test - no changes needed!
51+
}
52+
```
53+
54+
That's it! The framework automatically discovers and runs your tests through OTE.
55+
56+
## How It Works
57+
58+
```
59+
Source Files → AST Parser → Metadata → OTE Specs → go test → Results
60+
```
61+
62+
1. **Discovery** - Scans test directories, parses Go files using AST
63+
2. **Metadata** - Extracts Tags, Timeout, Lifecycle from comments
64+
3. **Integration** - Builds OTE ExtensionTestSpecs
65+
4. **Execution** - Runs tests via `go test -run TestName`
66+
5. **Results** - Converts to OTE format
67+
68+
## Metadata Tags
69+
70+
```go
71+
// Tags: Serial, Slow
72+
// Timeout: 120m
73+
// Lifecycle: Informing
74+
func TestExample(t *testing.T) { }
75+
```
76+
77+
- **Tags**: `Serial`, `Slow`, or custom tags
78+
- **Timeout**: Duration (e.g., `60m`, `2h`)
79+
- **Lifecycle**: `Blocking` (default) or `Informing`
80+
81+
## Benefits
82+
83+
**vs Compiled Binary Approach:**
84+
- Smaller binaries (~55 MB vs ~180 MB)
85+
- No build steps
86+
- Simpler architecture
87+
88+
**vs Ginkgo:**
89+
- Standard Go syntax
90+
- Better IDE support
91+
- Lower learning curve
92+
- No DSL to learn
93+
94+
## Complete Example
95+
96+
See [cluster-kube-apiserver-operator](https://github.com/openshift/cluster-kube-apiserver-operator) for a full implementation.
97+
98+
## Architecture
99+
100+
- **discovery.go** - AST-based test discovery
101+
- **executor.go** - Test execution via subprocess
102+
- **adapter.go** - OTE integration layer
103+
104+
## License
105+
106+
Apache 2.0

pkg/gotest/adapter.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package gotest
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
11+
"github.com/openshift-eng/openshift-tests-extension/pkg/util/sets"
12+
)
13+
14+
// Config holds configuration for the Go test framework
15+
type Config struct {
16+
// TestPrefix is prepended to all test names (e.g., "[sig-api-machinery] kube-apiserver operator")
17+
TestPrefix string
18+
19+
// TestDirectories are the directories to scan for tests (e.g., "test/e2e", "test/e2e-encryption")
20+
TestDirectories []string
21+
22+
// ModuleRoot is the root directory of the Go module (auto-detected if empty)
23+
ModuleRoot string
24+
}
25+
26+
// BuildExtensionTestSpecs discovers Go tests and converts them to OTE ExtensionTestSpecs
27+
func BuildExtensionTestSpecs(config Config) (et.ExtensionTestSpecs, error) {
28+
// Auto-detect module root if not provided
29+
if config.ModuleRoot == "" {
30+
moduleRoot, err := findModuleRoot()
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to find module root: %w", err)
33+
}
34+
config.ModuleRoot = moduleRoot
35+
}
36+
37+
// Build absolute paths for test directories
38+
var absoluteTestDirs []string
39+
testDirMap := make(map[string]string) // testName -> directory
40+
41+
for _, dir := range config.TestDirectories {
42+
absoluteDir := filepath.Join(config.ModuleRoot, dir)
43+
absoluteTestDirs = append(absoluteTestDirs, absoluteDir)
44+
45+
// Discover tests in this directory to build mapping
46+
tests, err := discoverTestsInDirectory(absoluteDir)
47+
if err != nil {
48+
// Directory might not exist, skip it
49+
continue
50+
}
51+
52+
for _, test := range tests {
53+
testDirMap[test.Name] = dir
54+
}
55+
}
56+
57+
// Discover all tests
58+
tests, err := DiscoverTests(absoluteTestDirs)
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to discover tests: %w", err)
61+
}
62+
63+
// Convert to ExtensionTestSpecs
64+
specs := make(et.ExtensionTestSpecs, 0, len(tests))
65+
for _, test := range tests {
66+
spec := buildTestSpec(config, test, testDirMap[test.Name])
67+
specs = append(specs, spec)
68+
}
69+
70+
return specs, nil
71+
}
72+
73+
func buildTestSpec(config Config, test TestMetadata, testDir string) *et.ExtensionTestSpec {
74+
// Build test name with prefix and tags
75+
testName := config.TestPrefix + " " + test.Name
76+
77+
// Add tags to name (for suite routing)
78+
for _, tag := range test.Tags {
79+
testName += fmt.Sprintf(" [%s]", tag)
80+
}
81+
82+
// Add timeout to name if specified
83+
if test.Timeout > 0 {
84+
testName += fmt.Sprintf(" [Timeout:%s]", test.Timeout)
85+
}
86+
87+
// Determine lifecycle
88+
lifecycle := et.LifecycleBlocking
89+
if strings.EqualFold(test.Lifecycle, "Informing") {
90+
lifecycle = et.LifecycleInforming
91+
}
92+
93+
// Determine parallelism (Serial tag means no parallelism)
94+
isSerial := false
95+
for _, tag := range test.Tags {
96+
if tag == "Serial" {
97+
isSerial = true
98+
break
99+
}
100+
}
101+
102+
// Capture testDir and testName in closure
103+
capturedTestDir := filepath.Join(config.ModuleRoot, testDir)
104+
capturedTestName := test.Name
105+
capturedTimeout := test.Timeout
106+
107+
// Build Labels set from tags (for OTE filtering)
108+
labels := sets.New[string](test.Tags...)
109+
110+
spec := &et.ExtensionTestSpec{
111+
Name: testName,
112+
Labels: labels,
113+
Lifecycle: lifecycle,
114+
Run: func(ctx context.Context) *et.ExtensionTestResult {
115+
// Execute test
116+
result := ExecuteTest(ctx, capturedTestDir, capturedTestName, capturedTimeout)
117+
118+
// Convert to ExtensionTestResult
119+
oteResult := et.ResultPassed
120+
if !result.Passed {
121+
oteResult = et.ResultFailed
122+
}
123+
124+
return &et.ExtensionTestResult{
125+
Result: oteResult,
126+
Output: result.Output,
127+
}
128+
},
129+
}
130+
131+
// Apply timeout tag if specified
132+
if test.Timeout > 0 {
133+
if spec.Tags == nil {
134+
spec.Tags = make(map[string]string)
135+
}
136+
spec.Tags["timeout"] = test.Timeout.String()
137+
}
138+
139+
// Apply isolation (Serial tests need isolation)
140+
if isSerial {
141+
spec.Resources = et.Resources{
142+
Isolation: et.Isolation{},
143+
}
144+
}
145+
146+
return spec
147+
}
148+
149+
// findModuleRoot walks up from current directory to find go.mod
150+
func findModuleRoot() (string, error) {
151+
dir, err := os.Getwd()
152+
if err != nil {
153+
return "", err
154+
}
155+
156+
for {
157+
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
158+
return dir, nil
159+
}
160+
parent := filepath.Dir(dir)
161+
if parent == dir {
162+
return "", fmt.Errorf("could not find go.mod")
163+
}
164+
dir = parent
165+
}
166+
}

0 commit comments

Comments
 (0)