Files
act_runner/e2e/main_test.go
Lunny Xiao b712c91c2d test(e2e): cover host mode, reusable workflows, scheduling, reconnect and CLI flags (#1347)
Extends the e2e suite with scenarios for host labels, local `workflow_call`, label routing, `runner.capacity`, a dropped and restored connection to Gitea, and the daemon's `--once`, `--labels` and `shutdown_timeout` behaviour.

Stacked on https://gitea.com/gitea/runner/pulls/1346, so its commit shows here until that merges. The new scenarios pass locally against gitea/gitea:latest and main-nightly.

Assisted-by: Codet:codet
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1347
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-10-28 04:32:50 +00:00

190 lines
4.6 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build e2e
package e2e
import (
"cmp"
"context"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"time"
"gitea.com/gitea/runner/internal/app/poll"
)
const runTimeout = 3 * time.Minute
var nonRepoChars = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
var fixture *GiteaFixture
var skipReason string
var binDir string
var runnerBinary = sync.OnceValues(func() (string, error) {
bin := filepath.Join(binDir, "gitea-runner")
if out, err := exec.Command(cmp.Or(os.Getenv("GO"), "go"), "build", "-o", bin, "..").CombinedOutput(); err != nil {
return "", fmt.Errorf("build runner: %w\n%s", err, out)
}
return bin, nil
})
func TestMain(m *testing.M) {
os.Exit(runSuite(m))
}
func runSuite(m *testing.M) int {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
cli, err := dockerClient(ctx)
if err != nil {
skipReason = fmt.Sprintf("docker unavailable: %v", err)
return m.Run()
}
f, err := StartGitea(ctx, cli)
if err != nil {
fmt.Fprintf(os.Stderr, "start gitea fixture: %v\n", err)
return 1
}
fixture = f
defer func() { _ = fixture.Close(context.Background()) }()
if binDir, err = os.MkdirTemp("", "gitea-runner-e2e-"); err != nil {
fmt.Fprintf(os.Stderr, "create binary dir: %v\n", err)
return 1
}
defer os.RemoveAll(binDir)
fmt.Fprintf(os.Stderr, "gitea fixture: image=%s version=%s\n", fixture.image, fixture.version)
return m.Run()
}
func newScenario(t *testing.T) (*GiteaAPI, string) {
t.Helper()
repo := strings.ToLower(nonRepoChars.ReplaceAllString(t.Name(), "-"))
api := &GiteaAPI{baseURL: fixture.baseURL, token: fixture.adminToken}
if err := api.CreateRepo(t.Context(), repo); err != nil {
t.Fatalf("create repo: %v", err)
}
return api, repo
}
func startIsolatedScenario(t *testing.T, workflow, label string, options runnerOptions) (*GiteaAPI, string, *poll.Poller) {
t.Helper()
api, repo := newScenario(t)
poller := startRunner(t, repo, label, options)
pushWorkflow(t, api, repo, workflow)
return api, repo, poller
}
func pushWorkflow(t *testing.T, api *GiteaAPI, repo, workflow string) {
t.Helper()
content, err := os.ReadFile("testdata/workflows/" + workflow)
if err != nil {
t.Fatalf("read workflow fixture %s: %v", workflow, err)
}
if err := api.CreateFile(t.Context(), repo, ".gitea/workflows/"+workflow, string(content), "add "+workflow); err != nil {
t.Fatalf("push workflow %s: %v", workflow, err)
}
}
func requireSuccess(t *testing.T, api *GiteaAPI, repo string, runID int64) {
t.Helper()
completed, err := api.WaitForRunConclusion(t.Context(), repo, runID, runTimeout)
if err != nil {
dumpRunLogs(t, api, repo, runID)
t.Fatalf("wait for run: %v", err)
}
if completed.Conclusion != "success" {
dumpRunLogs(t, api, repo, runID)
t.Fatalf("run concluded %q, want success", completed.Conclusion)
}
}
func runLogs(t *testing.T, api *GiteaAPI, repo string, runID int64) string {
t.Helper()
ctx := t.Context()
jobs, err := api.Jobs(ctx, repo, runID)
if err != nil {
t.Fatalf("list jobs: %v", err)
}
var all strings.Builder
for _, job := range jobs {
logs, err := api.JobLogs(ctx, repo, job.ID)
var response statusError
if errors.As(err, &response) && response.code == http.StatusNotFound {
continue // skipped jobs and reusable workflow callers never ran a task, so have no log
}
if err != nil {
t.Fatalf("job logs for %s: %v", job.Name, err)
}
all.WriteString(logs)
}
return all.String()
}
func dumpRunLogs(t *testing.T, api *GiteaAPI, repo string, runID int64) {
t.Helper()
ctx := t.Context()
jobs, err := api.Jobs(ctx, repo, runID)
if err != nil {
t.Logf("dump run %d: list jobs: %v", runID, err)
return
}
for _, job := range jobs {
logs, err := api.JobLogs(ctx, repo, job.ID)
if err != nil {
t.Logf("job %q (%s): logs unavailable: %v", job.Name, job.Conclusion, err)
continue
}
t.Logf("job %q concluded %q:\n%s", job.Name, job.Conclusion, logs)
}
}
func waitForRun(t *testing.T, api *GiteaAPI, repo string) *ActionRun {
t.Helper()
return waitForRunAfter(t, api, repo, 0)
}
func waitForRunAfter(t *testing.T, api *GiteaAPI, repo string, previousID int64) *ActionRun {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
for {
runs, err := api.Runs(ctx, repo)
if err != nil {
t.Fatalf("get latest run: %v", err)
}
if len(runs) > 0 && runs[0].ID > previousID {
return &runs[0]
}
select {
case <-ctx.Done():
t.Fatalf("no run appeared for %s within timeout", repo)
case <-time.After(pollInterval):
}
}
}