mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-10-29 03:16:43 +08:00
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>
190 lines
4.6 KiB
Go
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):
|
|
}
|
|
}
|
|
}
|