mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-10-29 03:16:43 +08:00
test(e2e): cover the register/daemon CLI and failing jobs (#1346)
The e2e suite started the runner in-process and never exercised failing jobs. This adds a scenario that builds the binary and drives `register` then `daemon` against the Gitea fixture, and one for failed, skipped, continue-on-error and timed-out jobs. Verified locally against gitea/gitea:main-nightly. Assisted-by: Codet:codet Reviewed-on: https://gitea.com/gitea/runner/pulls/1346 Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
This commit is contained in:
@@ -59,5 +59,8 @@ Each lives in `internal/pkg/action/<name>` and is registered in `builtinActions`
|
||||
It requires Docker and is excluded from `make test`. CI runs stable and nightly variants in
|
||||
parallel.
|
||||
|
||||
The `cli_register_daemon` scenario builds the binary and drives `register` and `daemon` as a user would. The
|
||||
`failure_paths` scenario covers failed, skipped, tolerated and timed-out jobs.
|
||||
|
||||
The suite shares one Gitea and regular runner. Cache and ephemeral scenarios use isolated
|
||||
repository runners. A run path is `<workflow>@<ref>` and a log row is `<timestamp>Z <payload>`.
|
||||
|
||||
121
e2e/cli_test.go
Normal file
121
e2e/cli_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type syncBuffer struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *syncBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *syncBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buf.String()
|
||||
}
|
||||
|
||||
// testCLIRegisterDaemon drives the built binary the way a user does: register, then daemon.
|
||||
func testCLIRegisterDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
api, repo := newScenario(t)
|
||||
token, err := fixture.RegistrationToken(ctx, repo)
|
||||
if err != nil {
|
||||
t.Fatalf("get registration token: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "gitea-runner")
|
||||
if out, err := exec.CommandContext(ctx, "go", "build", "-o", bin, "..").CombinedOutput(); err != nil {
|
||||
t.Fatalf("build runner: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
cfgFile := filepath.Join(dir, "config.yaml")
|
||||
cfg := fmt.Sprintf(`runner:
|
||||
file: %s
|
||||
fetch_interval: 250ms
|
||||
fetch_interval_max: 250ms
|
||||
state_report_interval: 500ms
|
||||
log_report_interval: 500ms
|
||||
cache:
|
||||
dir: %s
|
||||
`, filepath.Join(dir, ".runner"), filepath.Join(dir, "cache"))
|
||||
if fixture.network != "" {
|
||||
cfg += "container:\n network: " + fixture.network + "\n"
|
||||
}
|
||||
if err := os.WriteFile(cfgFile, []byte(cfg), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
label := "e2e-cli:docker://" + os.Getenv("E2E_JOB_IMAGE")
|
||||
out, err := exec.CommandContext(ctx, bin, "register", "--no-interactive", "-c", cfgFile,
|
||||
"--instance", fixture.baseURL, "--token", token, "--name", repo, "--labels", label).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("register: %v\n%s", err, out)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, ".runner")); err != nil {
|
||||
t.Fatalf("register did not write the runner file: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
daemonLog := &syncBuffer{}
|
||||
daemon := exec.Command(bin, "daemon", "-c", cfgFile)
|
||||
daemon.Stdout, daemon.Stderr = daemonLog, daemonLog
|
||||
if err := daemon.Start(); err != nil {
|
||||
t.Fatalf("start daemon: %v", err)
|
||||
}
|
||||
exited := make(chan error, 1)
|
||||
go func() { exited <- daemon.Wait() }()
|
||||
t.Cleanup(func() {
|
||||
_ = daemon.Process.Kill() // no-op once the daemon has exited
|
||||
})
|
||||
|
||||
pushWorkflow(t, api, repo, "cli.yml")
|
||||
wfRun := waitForRun(t, api, repo)
|
||||
requireSuccessOrDumpDaemon(t, api, repo, wfRun.ID, daemonLog)
|
||||
if logs := runLogs(t, api, repo, wfRun.ID); commandRow(logs, "e2e-cli-job-ran") == "" {
|
||||
t.Errorf("job logs are missing the marker:\n%s", logs)
|
||||
}
|
||||
|
||||
if err := daemon.Process.Signal(syscall.SIGTERM); err != nil {
|
||||
t.Fatalf("signal daemon: %v", err)
|
||||
}
|
||||
select {
|
||||
case err := <-exited:
|
||||
if err != nil {
|
||||
t.Errorf("daemon did not exit cleanly on SIGTERM: %v\n%s", err, daemonLog)
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Errorf("daemon did not exit within 30s of SIGTERM\n%s", daemonLog)
|
||||
}
|
||||
}
|
||||
|
||||
func requireSuccessOrDumpDaemon(t *testing.T, api *GiteaAPI, repo string, runID int64, daemonLog *syncBuffer) {
|
||||
t.Helper()
|
||||
completed, err := api.WaitForRunConclusion(context.WithoutCancel(t.Context()), repo, runID, runTimeout)
|
||||
if err != nil || completed.Conclusion != "success" {
|
||||
t.Logf("daemon output:\n%s", daemonLog)
|
||||
requireSuccess(t, api, repo, runID)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ func TestCompatibility(t *testing.T) {
|
||||
t.Run("builtin_checkout", testBuiltinCheckout)
|
||||
t.Run("moving_action_ref", testMovingActionRef)
|
||||
t.Run("ephemeral", testEphemeralRunner)
|
||||
t.Run("failure_paths", testFailurePaths)
|
||||
t.Run("cli_register_daemon", testCLIRegisterDaemon)
|
||||
t.Run("kubernetes", testKubernetes)
|
||||
}
|
||||
|
||||
|
||||
73
e2e/failure_test.go
Normal file
73
e2e/failure_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testFailurePaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
|
||||
api, repo := newScenario(t)
|
||||
pushWorkflow(t, api, repo, "failure.yml")
|
||||
|
||||
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)
|
||||
}
|
||||
if completed.Conclusion != "failure" {
|
||||
t.Errorf("run concluded %q, want failure", completed.Conclusion)
|
||||
}
|
||||
|
||||
jobs, err := api.Jobs(ctx, repo, wfRun.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("list jobs: %v", err)
|
||||
}
|
||||
want := map[string]string{
|
||||
"fail": "failure",
|
||||
"tolerated": "success",
|
||||
"dependent": "skipped",
|
||||
"timeout": "failure",
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if w, ok := want[job.Name]; ok && job.Conclusion != w {
|
||||
t.Errorf("job %q concluded %q, want %q", job.Name, job.Conclusion, w)
|
||||
}
|
||||
delete(want, job.Name)
|
||||
}
|
||||
for name := range want {
|
||||
t.Errorf("job %q not found", name)
|
||||
}
|
||||
|
||||
var all strings.Builder
|
||||
for _, job := range jobs {
|
||||
if job.Conclusion == "skipped" { // a skipped job never started, so it has no log
|
||||
continue
|
||||
}
|
||||
jobLogs, err := api.JobLogs(ctx, repo, job.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("job logs for %s: %v", job.Name, err)
|
||||
}
|
||||
all.WriteString(jobLogs)
|
||||
}
|
||||
logs := all.String()
|
||||
for _, marker := range []string{"e2e-before-fail", "e2e-failure-cleanup", "e2e-tolerated-next", "e2e-timeout-started"} {
|
||||
if commandRow(logs, marker) != marker {
|
||||
t.Errorf("logs are missing %q", marker)
|
||||
}
|
||||
}
|
||||
if commandRow(logs, "e2e-skipped-step") != "" {
|
||||
t.Error("a success() step ran after a failed step")
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Logf("full stored logs:\n%s", logs)
|
||||
}
|
||||
}
|
||||
7
e2e/testdata/workflows/cli.yml
vendored
Normal file
7
e2e/testdata/workflows/cli.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
name: cli
|
||||
on: push
|
||||
jobs:
|
||||
hello:
|
||||
runs-on: e2e-cli
|
||||
steps:
|
||||
- run: echo e2e-cli-job-ran
|
||||
30
e2e/testdata/workflows/failure.yml
vendored
Normal file
30
e2e/testdata/workflows/failure.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: failure
|
||||
on: push
|
||||
jobs:
|
||||
fail:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo e2e-before-fail
|
||||
- run: exit 3
|
||||
- if: success()
|
||||
run: echo e2e-skipped-step
|
||||
- if: failure()
|
||||
run: echo e2e-failure-cleanup
|
||||
tolerated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- continue-on-error: true
|
||||
run: exit 1
|
||||
- run: echo e2e-tolerated-next
|
||||
dependent:
|
||||
needs: fail
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo dependent
|
||||
timeout:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- run: |
|
||||
echo e2e-timeout-started
|
||||
sleep 300
|
||||
Reference in New Issue
Block a user