Files
act_runner/e2e/runner_harness.go
silverwind a46d3f3eda 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 <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1363
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-31 10:47:35 +00:00

132 lines
4.0 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build e2e
package e2e
import (
"cmp"
"context"
"errors"
"os"
"testing"
"time"
"gitea.com/gitea/runner/internal/app/poll"
"gitea.com/gitea/runner/internal/app/run"
"gitea.com/gitea/runner/internal/pkg/client"
"gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/labels"
pingv1 "gitea.dev/actionslib/ping/v1"
runnerv1 "gitea.dev/actionslib/runner/v1"
)
type runnerOptions struct {
capacity int
ephemeral bool
cacheV2 *bool
cacheHost string
image string
kubernetes *config.Kubernetes
host bool // register a host-mode label instead of a docker one
name string // suffix that keeps runner names unique when a test starts several
address string // Gitea address the runner talks to, default the fixture
timeout time.Duration
}
func startRunner(t *testing.T, repo, labelName string, options runnerOptions) *poll.Poller {
t.Helper()
ctx := t.Context()
address := cmp.Or(options.address, fixture.baseURL)
token, err := fixture.RegistrationToken(ctx, repo)
if err != nil {
t.Fatalf("get registration token: %v", err)
}
cfg, err := config.LoadDefault("")
if err != nil {
t.Fatalf("load default config: %v", err)
}
if options.cacheV2 != nil {
cfg.Cache.V2 = options.cacheV2
}
cfg.Container.DockerHost = cmp.Or(os.Getenv("DOCKER_HOST"), "unix:///var/run/docker.sock")
cfg.Cache.Dir = t.TempDir() + "/cache"
cfg.Cache.Host = options.cacheHost
cfg.Runner.Insecure = true
cfg.Runner.FetchInterval = 250 * time.Millisecond // faster than prod defaults for local fixture
cfg.Runner.FetchIntervalMax = 250 * time.Millisecond
cfg.Runner.StateReportInterval = 500 * time.Millisecond // so cancel reaches the job quickly
cfg.Runner.LogReportInterval = 500 * time.Millisecond
cfg.Runner.Capacity = max(options.capacity, 1)
cfg.Runner.Timeout = cmp.Or(options.timeout, cfg.Runner.Timeout)
cfg.Container.Network = fixture.network
scheme := labels.SchemeDocker
if options.kubernetes != nil {
scheme, cfg.Kubernetes = labels.SchemeKubernetes, *options.kubernetes
}
rawLabel := labelName + ":" + scheme + "://" + cmp.Or(options.image, os.Getenv("E2E_JOB_IMAGE"))
if options.host {
rawLabel = labelName + ":" + labels.SchemeHost
cfg.Host.WorkdirParent = t.TempDir()
}
label, err := labels.Parse(rawLabel)
if err != nil {
t.Fatalf("parse label %q: %v", rawLabel, err)
}
labelNames := []string{label.Name}
pingCli := client.New(address, cfg.Runner.TLSConfig(), "", "", config.RequestTimeout, cfg.Runner.ExtraHeaders)
if _, err := pingCli.Ping(ctx, &pingv1.PingRequest{Data: t.Name() + options.name}); err != nil {
t.Fatalf("ping %s: %v", address, err)
}
regResp, err := pingCli.Register(ctx, &runnerv1.RegisterRequest{
Name: t.Name() + options.name,
Token: token,
Version: "e2e",
Labels: labelNames,
Ephemeral: options.ephemeral,
Capabilities: run.RunnerCapabilities(),
})
if err != nil {
t.Fatalf("register runner: %v", err)
}
if options.ephemeral && !regResp.Runner.Ephemeral {
t.Fatal("gitea did not grant ephemeral registration")
}
reg := &config.Registration{
ID: regResp.Runner.Id,
UUID: regResp.Runner.Uuid,
Name: regResp.Runner.Name,
Token: regResp.Runner.Token,
Address: address,
Labels: []string{rawLabel},
Ephemeral: regResp.Runner.Ephemeral,
}
cli := client.New(address, cfg.Runner.TLSConfig(), reg.UUID, reg.Token, config.RequestTimeout, cfg.Runner.ExtraHeaders)
runner := run.NewRunner(cfg, reg, cli)
if _, err := runner.Declare(ctx, labelNames); err != nil {
_ = runner.Close()
t.Fatalf("declare runner: %v", err)
}
poller := poll.New(cfg, cli, runner)
go poller.Poll()
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := errors.Join(poller.Shutdown(ctx), runner.Close()); err != nil {
t.Logf("runner shutdown: %v", err)
}
})
return poller
}