mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-07 15:50:28 +00:00
test(e2e): speed up and cover exec and Kubernetes modes (#1363)
Follow-up to https://gitea.com/gitea/runner/pulls/1362, whose crash e2e missed because it only drove the daemon on Docker. - `dispatch`, `failure_paths` and `reusable_workflow` run through the daemon and `exec`, each on Docker and one shared k3s cluster. - Fixed sleeps become gates the test releases, the job timeout uses a 15s runner deadline, k3s boots alongside Gitea, Gitea's data is on tmpfs. - Outside a container, `make test-e2e` runs `go test` in a Linux container on its own network like CI, so local runs take the same paths. Local wall time: | | Before | After | |---|---|---| | Suite | 84s | 27–35s | | `job_timeout` | 64s | 17s | | `reconnect` | 22s | 4.5s | | `capacity/serial` | 20s | 2s | | `cli_shutdown_drains` | 14s | 2.3s | | `kubernetes` | 31s | 21–23s | _Written by Claude (Opus 5.5)._ --------- Co-authored-by: bircni <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1363 Reviewed-by: bircni <[email protected]> Co-authored-by: silverwind <[email protected]>
This commit is contained in:
1 parent
2e72cf902a
commit
a46d3f3eda
23 files changed
+577
-340
No files matched your search
@@ -0,0 +1,227 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mode struct {
|
||||
exec, kubernetes bool
|
||||
}
|
||||
|
||||
var allModes = []mode{{}, {kubernetes: true}, {exec: true}, {exec: true, kubernetes: true}}
|
||||
|
||||
func (m mode) String() string {
|
||||
driver, backend := "daemon", "docker"
|
||||
if m.exec {
|
||||
driver = "exec"
|
||||
}
|
||||
if m.kubernetes {
|
||||
backend = "kubernetes"
|
||||
}
|
||||
return driver + "_" + backend
|
||||
}
|
||||
|
||||
func inAllModes(scenario func(*testing.T, mode)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, m := range allModes {
|
||||
t.Run(m.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
scenario(t, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type workflowSpec struct {
|
||||
files []string
|
||||
inputs map[string]string // dispatches files[0] instead of relying on push
|
||||
execArgs []string
|
||||
}
|
||||
|
||||
type workflowResult struct {
|
||||
conclusion string
|
||||
jobs map[string]string
|
||||
lines []string
|
||||
}
|
||||
|
||||
func (r *workflowResult) job(id string) string {
|
||||
return cmp.Or(r.jobs[id], "skipped") // exec reports nothing for a job it skipped
|
||||
}
|
||||
|
||||
func (r *workflowResult) hasLine(line string) bool {
|
||||
return slices.Contains(r.lines, line)
|
||||
}
|
||||
|
||||
func runWorkflow(t *testing.T, m mode, spec workflowSpec) *workflowResult {
|
||||
t.Helper()
|
||||
run := daemonWorkflow
|
||||
if m.exec {
|
||||
run = execWorkflow
|
||||
}
|
||||
result := run(t, m, spec)
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
t.Logf("%s concluded %q, jobs %v, logs:\n%s", m, result.conclusion, result.jobs, strings.Join(result.lines, "\n"))
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func daemonWorkflow(t *testing.T, m mode, spec workflowSpec) *workflowResult {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
|
||||
api, repo := newScenario(t)
|
||||
if m.kubernetes {
|
||||
cluster := requireCluster(t)
|
||||
startRunner(t, repo, "e2e-kubernetes", runnerOptions{image: cluster.jobImage, kubernetes: cluster.kubernetes, capacity: 16})
|
||||
}
|
||||
files := map[string]string{}
|
||||
for _, file := range spec.files {
|
||||
content, err := os.ReadFile(filepath.Join("testdata", "workflows", file))
|
||||
if err != nil {
|
||||
t.Fatalf("read workflow fixture %s: %v", file, err)
|
||||
}
|
||||
if m.kubernetes {
|
||||
content = bytes.ReplaceAll(content, []byte("runs-on: ubuntu-latest"), []byte("runs-on: e2e-kubernetes"))
|
||||
}
|
||||
files[".gitea/workflows/"+file] = string(content)
|
||||
}
|
||||
if err := api.CreateFiles(ctx, repo, files, "add workflows"); err != nil {
|
||||
t.Fatalf("push workflows: %v", err)
|
||||
}
|
||||
if spec.inputs != nil {
|
||||
branch, err := api.DefaultBranch(ctx, repo)
|
||||
if err != nil {
|
||||
t.Fatalf("get default branch: %v", err)
|
||||
}
|
||||
if err := waitForDispatch(ctx, api, repo, spec.files[0], branch, spec.inputs); err != nil {
|
||||
t.Fatalf("dispatch workflow: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
wfRun := waitForRun(t, api, repo)
|
||||
completed, err := api.WaitForRunConclusion(ctx, repo, wfRun.ID, runTimeout)
|
||||
if err != nil {
|
||||
dumpRunLogs(t, api, repo, wfRun.ID)
|
||||
t.Fatalf("wait for run: %v", err)
|
||||
}
|
||||
jobs, err := api.Jobs(ctx, repo, wfRun.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("list jobs: %v", err)
|
||||
}
|
||||
result := &workflowResult{conclusion: completed.Conclusion, jobs: map[string]string{}}
|
||||
for _, job := range jobs {
|
||||
result.jobs[job.Name] = job.Conclusion
|
||||
}
|
||||
for line := range strings.SplitSeq(runLogs(t, api, repo, wfRun.ID), "\n") {
|
||||
if _, payload, found := strings.Cut(line, "Z "); found {
|
||||
result.lines = append(result.lines, strings.TrimRight(payload, "\r"))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func execWorkflow(t *testing.T, m mode, spec workflowSpec) *workflowResult {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
|
||||
bin, err := runnerBinary()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
workdir := t.TempDir()
|
||||
workflows := filepath.Join(workdir, ".gitea", "workflows")
|
||||
if err := os.MkdirAll(workflows, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, file := range spec.files {
|
||||
content, err := os.ReadFile(filepath.Join("testdata", "workflows", file))
|
||||
if err != nil {
|
||||
t.Fatalf("read workflow fixture %s: %v", file, err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workflows, file), content, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, args := range [][]string{
|
||||
{"init", "-q"},
|
||||
{"remote", "add", "origin", "https://example.com/e2e/exec.git"},
|
||||
{"-c", "user.name=e2e", "-c", "[email protected]", "commit", "-q", "--allow-empty", "-m", "init"},
|
||||
} {
|
||||
if out, err := exec.CommandContext(ctx, "git", append([]string{"-C", workdir}, args...)...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"exec", "--json", "-C", workdir, "-W", workflows, "--artifact-server-path", t.TempDir(), "--artifact-server-port", "0"}
|
||||
env := slices.DeleteFunc(os.Environ(), func(kv string) bool { return strings.HasPrefix(kv, "ACTIONS_") })
|
||||
if m.kubernetes {
|
||||
cluster := requireCluster(t)
|
||||
args = append(args, "-i", "kubernetes://"+cluster.jobImage)
|
||||
env = append(env, "KUBECONFIG="+cluster.kubernetes.Kubeconfig)
|
||||
} else {
|
||||
args = append(args, "-i", os.Getenv("E2E_JOB_IMAGE"), "--network", fixture.network)
|
||||
}
|
||||
if spec.inputs != nil {
|
||||
args = append(args, "-E", "workflow_dispatch")
|
||||
for name, value := range spec.inputs {
|
||||
args = append(args, "--input", name+"="+value)
|
||||
}
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, bin, append(args, spec.execArgs...)...)
|
||||
cmd.Env = env
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
runErr := cmd.Run()
|
||||
var exitErr *exec.ExitError
|
||||
if runErr != nil && !errors.As(runErr, &exitErr) {
|
||||
t.Fatalf("exec: %v\n%s", runErr, stderr.String())
|
||||
}
|
||||
|
||||
result := &workflowResult{conclusion: "success", jobs: map[string]string{}}
|
||||
if runErr != nil {
|
||||
result.conclusion = "failure"
|
||||
}
|
||||
scanner := bufio.NewScanner(&stdout)
|
||||
scanner.Buffer(nil, 1<<20)
|
||||
for scanner.Scan() {
|
||||
var entry struct {
|
||||
JobID string `json:"jobID"`
|
||||
JobResult string `json:"jobResult"`
|
||||
Msg string `json:"msg"`
|
||||
RawOutput bool `json:"raw_output"`
|
||||
}
|
||||
if json.Unmarshal(scanner.Bytes(), &entry) != nil {
|
||||
continue
|
||||
}
|
||||
if entry.JobResult != "" {
|
||||
result.jobs[entry.JobID] = entry.JobResult
|
||||
}
|
||||
if entry.RawOutput {
|
||||
for line := range strings.SplitSeq(strings.TrimRight(entry.Msg, "\n"), "\n") {
|
||||
result.lines = append(result.lines, strings.TrimRight(line, "\r"))
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.conclusion != "success" && len(result.jobs) == 0 {
|
||||
t.Fatalf("exec ran no job: %v\n%s", runErr, stderr.String())
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user