mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-07-07 14:09:25 +00:00
test: Enhance Coverage + CI (#1055)
Reviewed-on: https://gitea.com/gitea/runner/pulls/1055 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
40
internal/app/cmd/daemon_test.go
Normal file
40
internal/app/cmd/daemon_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDockerSocketPathUsesConfigAndEnvironment(t *testing.T) {
|
||||
got, err := getDockerSocketPath("tcp://docker.example:2376")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "tcp://docker.example:2376", got)
|
||||
|
||||
t.Setenv("DOCKER_HOST", "unix:///tmp/docker.sock")
|
||||
got, err = getDockerSocketPath("-")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "unix:///tmp/docker.sock", got)
|
||||
}
|
||||
|
||||
func TestInitLoggingSetsLevelAndCaller(t *testing.T) {
|
||||
oldLevel := log.GetLevel()
|
||||
oldReportCaller := log.StandardLogger().ReportCaller
|
||||
t.Cleanup(func() {
|
||||
log.SetLevel(oldLevel)
|
||||
log.SetReportCaller(oldReportCaller)
|
||||
})
|
||||
|
||||
cfg := &config.Config{}
|
||||
cfg.Log.Level = "debug"
|
||||
initLogging(cfg)
|
||||
|
||||
require.Equal(t, log.DebugLevel, log.GetLevel())
|
||||
require.True(t, log.StandardLogger().ReportCaller)
|
||||
}
|
||||
220
internal/app/cmd/exec_test.go
Normal file
220
internal/app/cmd/exec_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func TestExecuteArgsResolve(t *testing.T) {
|
||||
workdir := t.TempDir()
|
||||
args := &executeArgs{workdir: workdir}
|
||||
|
||||
require.Empty(t, args.resolve(""))
|
||||
require.Equal(t, filepath.Join(workdir, "sub", "file"), args.resolve("sub/file"))
|
||||
|
||||
abs := filepath.Join(workdir, "abs")
|
||||
require.Equal(t, abs, args.resolve(abs))
|
||||
}
|
||||
|
||||
func TestExecuteArgsPaths(t *testing.T) {
|
||||
workdir := t.TempDir()
|
||||
args := &executeArgs{
|
||||
workdir: workdir,
|
||||
workflowsPath: ".gitea/workflows",
|
||||
envfile: ".env",
|
||||
}
|
||||
|
||||
require.Equal(t, filepath.Join(workdir, ".gitea/workflows"), args.WorkflowsPath())
|
||||
require.Equal(t, filepath.Join(workdir, ".env"), args.Envfile())
|
||||
require.Equal(t, workdir, args.Workdir())
|
||||
}
|
||||
|
||||
func TestExecuteArgsLoadVars(t *testing.T) {
|
||||
require.Empty(t, (&executeArgs{}).LoadVars())
|
||||
|
||||
args := &executeArgs{vars: []string{"FOO=bar", "EMPTY", "WITH=eq=sign"}}
|
||||
require.Equal(t, map[string]string{
|
||||
"FOO": "bar",
|
||||
"EMPTY": "",
|
||||
"WITH": "eq=sign",
|
||||
}, args.LoadVars())
|
||||
}
|
||||
|
||||
func TestExecuteArgsLoadSecrets(t *testing.T) {
|
||||
t.Setenv("FROMENV", "from-env-value")
|
||||
|
||||
args := &executeArgs{secrets: []string{"token=abc", "fromenv"}}
|
||||
require.Equal(t, map[string]string{
|
||||
"TOKEN": "abc",
|
||||
"FROMENV": "from-env-value",
|
||||
}, args.LoadSecrets())
|
||||
}
|
||||
|
||||
func TestReadEnvs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envFile := filepath.Join(dir, ".env")
|
||||
require.NoError(t, os.WriteFile(envFile, []byte("FOO=bar\nBAZ=qux\n"), 0o600))
|
||||
|
||||
envs := map[string]string{"EXISTING": "keep"}
|
||||
require.True(t, readEnvs(envFile, envs))
|
||||
require.Equal(t, map[string]string{
|
||||
"EXISTING": "keep",
|
||||
"FOO": "bar",
|
||||
"BAZ": "qux",
|
||||
}, envs)
|
||||
|
||||
missing := map[string]string{}
|
||||
require.False(t, readEnvs(filepath.Join(dir, "does-not-exist"), missing))
|
||||
require.Empty(t, missing)
|
||||
}
|
||||
|
||||
func TestRunExecListUsesJobEventAndAllPlans(t *testing.T) {
|
||||
planner := &fakeWorkflowPlanner{
|
||||
events: []string{"push", "pull_request"},
|
||||
plans: map[string]*model.Plan{
|
||||
"job:build": listPlan("build", "Build", "push"),
|
||||
"event:push": listPlan("test", "Test", "push"),
|
||||
"all": listPlan("lint", "Lint", "push"),
|
||||
},
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
require.NoError(t, runExecList(planner, &executeArgs{job: "build"}))
|
||||
require.NoError(t, runExecList(planner, &executeArgs{event: "push"}))
|
||||
require.NoError(t, runExecList(planner, &executeArgs{autodetectEvent: true}))
|
||||
require.NoError(t, runExecList(planner, &executeArgs{}))
|
||||
})
|
||||
|
||||
require.Contains(t, out, "Build")
|
||||
require.Contains(t, out, "Test")
|
||||
require.Contains(t, out, "Lint")
|
||||
require.Equal(t, []string{"job:build", "event:push", "event:push", "all"}, planner.calls)
|
||||
}
|
||||
|
||||
func TestPrintListReportsDuplicateJobIDs(t *testing.T) {
|
||||
workflowA := workflowForList("A", "a.yml", "push", "build", "Build A")
|
||||
workflowB := workflowForList("B", "b.yml", "pull_request", "build", "Build B")
|
||||
plan := &model.Plan{Stages: []*model.Stage{{
|
||||
Runs: []*model.Run{
|
||||
{Workflow: workflowA, JobID: "build"},
|
||||
{Workflow: workflowB, JobID: "build"},
|
||||
},
|
||||
}}}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
printList(plan)
|
||||
})
|
||||
|
||||
require.Contains(t, out, "Workflow file")
|
||||
require.Contains(t, out, "Build A")
|
||||
require.Contains(t, out, "Build B")
|
||||
require.Contains(t, out, "Detected multiple jobs with the same job name")
|
||||
}
|
||||
|
||||
func TestLoadExecCmdDefinesExpectedFlags(t *testing.T) {
|
||||
cmd := loadExecCmd(context.Background())
|
||||
|
||||
for _, name := range []string{
|
||||
"list",
|
||||
"job",
|
||||
"event",
|
||||
"workflows",
|
||||
"directory",
|
||||
"env",
|
||||
"secret",
|
||||
"var",
|
||||
"dryrun",
|
||||
"image",
|
||||
"gitea-instance",
|
||||
} {
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
t.Fatalf("expected flag %q to be registered", name)
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, "exec", cmd.Use)
|
||||
require.NoError(t, cmd.Args(cmd, strings.Split("a b c", " ")))
|
||||
require.Error(t, cmd.Args(cmd, strings.Fields(strings.Repeat("arg ", 21))))
|
||||
}
|
||||
|
||||
type fakeWorkflowPlanner struct {
|
||||
events []string
|
||||
plans map[string]*model.Plan
|
||||
calls []string
|
||||
}
|
||||
|
||||
func (p *fakeWorkflowPlanner) PlanEvent(eventName string) (*model.Plan, error) {
|
||||
p.calls = append(p.calls, "event:"+eventName)
|
||||
return p.plans["event:"+eventName], nil
|
||||
}
|
||||
|
||||
func (p *fakeWorkflowPlanner) PlanJob(jobName string) (*model.Plan, error) {
|
||||
p.calls = append(p.calls, "job:"+jobName)
|
||||
return p.plans["job:"+jobName], nil
|
||||
}
|
||||
|
||||
func (p *fakeWorkflowPlanner) PlanAll() (*model.Plan, error) {
|
||||
p.calls = append(p.calls, "all")
|
||||
return p.plans["all"], nil
|
||||
}
|
||||
|
||||
func (p *fakeWorkflowPlanner) GetEvents() []string {
|
||||
return p.events
|
||||
}
|
||||
|
||||
func listPlan(jobID, jobName, event string) *model.Plan {
|
||||
workflow := workflowForList("Workflow "+jobID, jobID+".yml", event, jobID, jobName)
|
||||
return &model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: jobID}}}}}
|
||||
}
|
||||
|
||||
func workflowForList(name, file, event, jobID, jobName string) *model.Workflow {
|
||||
return &model.Workflow{
|
||||
Name: name,
|
||||
File: file,
|
||||
RawOn: rawOnNode(event),
|
||||
Jobs: map[string]*model.Job{
|
||||
jobID: {Name: jobName},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func rawOnNode(event string) yaml.Node {
|
||||
var node yaml.Node
|
||||
if err := yaml.Unmarshal([]byte(event), &node); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *node.Content[0]
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
os.Stdout = w
|
||||
|
||||
fn()
|
||||
|
||||
require.NoError(t, w.Close())
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, r)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, r.Close())
|
||||
return buf.String()
|
||||
}
|
||||
@@ -4,8 +4,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -17,3 +21,136 @@ func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
|
||||
})
|
||||
assert.Error(t, err, "unsupported schema: invalid")
|
||||
}
|
||||
|
||||
func TestRegisterInputsValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs registerInputs
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty instance address",
|
||||
inputs: registerInputs{Token: "token"},
|
||||
wantErr: "instance address is empty",
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
inputs: registerInputs{InstanceAddr: "http://localhost:3000"},
|
||||
wantErr: "token is empty",
|
||||
},
|
||||
{
|
||||
name: "invalid label",
|
||||
inputs: registerInputs{InstanceAddr: "http://localhost:3000", Token: "token", Labels: []string{"ubuntu:vm:bad"}},
|
||||
wantErr: "unsupported schema: vm",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
inputs: registerInputs{InstanceAddr: "http://localhost:3000", Token: "token", Labels: []string{"ubuntu:host"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.inputs.validate()
|
||||
if tt.wantErr != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLabels(t *testing.T) {
|
||||
require.NoError(t, validateLabels([]string{"ubuntu:host", "ubuntu:docker://node:18"}))
|
||||
require.Error(t, validateLabels([]string{"ubuntu:host", "ubuntu:vm:bad"}))
|
||||
}
|
||||
|
||||
func TestRegisterInputsStageValue(t *testing.T) {
|
||||
inputs := ®isterInputs{
|
||||
InstanceAddr: "http://localhost:3000",
|
||||
Token: "token",
|
||||
RunnerName: "runner",
|
||||
Labels: []string{"ubuntu:host", "ubuntu:docker://node:18"},
|
||||
}
|
||||
require.Equal(t, "http://localhost:3000", inputs.stageValue(StageInputInstance))
|
||||
require.Equal(t, "token", inputs.stageValue(StageInputToken))
|
||||
require.Equal(t, "runner", inputs.stageValue(StageInputRunnerName))
|
||||
require.Equal(t, "ubuntu:host,ubuntu:docker://node:18", inputs.stageValue(StageInputLabels))
|
||||
require.Empty(t, (®isterInputs{}).stageValue(StageInputLabels))
|
||||
require.Empty(t, inputs.stageValue(StageWaitingForRegistration))
|
||||
}
|
||||
|
||||
func TestRegisterInputsAssignToNext(t *testing.T) {
|
||||
emptyCfg := &config.Config{}
|
||||
|
||||
t.Run("instance and token stay on empty value", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageInputInstance, inputs.assignToNext(StageInputInstance, "", emptyCfg))
|
||||
require.Equal(t, StageInputToken, inputs.assignToNext(StageInputToken, "", emptyCfg))
|
||||
})
|
||||
|
||||
t.Run("instance then token then runner name", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageInputToken, inputs.assignToNext(StageInputInstance, "http://localhost:3000", emptyCfg))
|
||||
require.Equal(t, "http://localhost:3000", inputs.InstanceAddr)
|
||||
require.Equal(t, StageInputRunnerName, inputs.assignToNext(StageInputToken, "token", emptyCfg))
|
||||
require.Equal(t, "token", inputs.Token)
|
||||
})
|
||||
|
||||
t.Run("empty runner name falls back to hostname", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageInputLabels, inputs.assignToNext(StageInputRunnerName, "", emptyCfg))
|
||||
hostname, _ := os.Hostname()
|
||||
require.Equal(t, hostname, inputs.RunnerName)
|
||||
})
|
||||
|
||||
t.Run("labels from config skip the labels stage", func(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.Runner.Labels = []string{"ubuntu:host", "ubuntu:vm:bad"}
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageWaitingForRegistration, inputs.assignToNext(StageInputRunnerName, "runner", cfg))
|
||||
// only the valid label survives
|
||||
require.Equal(t, []string{"ubuntu:host"}, inputs.Labels)
|
||||
})
|
||||
|
||||
t.Run("blank labels input uses defaults", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageWaitingForRegistration, inputs.assignToNext(StageInputLabels, "", emptyCfg))
|
||||
require.Equal(t, defaultLabels, inputs.Labels)
|
||||
})
|
||||
|
||||
t.Run("invalid labels input loops back", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageInputLabels, inputs.assignToNext(StageInputLabels, "ubuntu:vm:bad", emptyCfg))
|
||||
require.Nil(t, inputs.Labels)
|
||||
})
|
||||
|
||||
t.Run("overwrite local config", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageInputInstance, inputs.assignToNext(StageOverwriteLocalConfig, "Y", emptyCfg))
|
||||
require.Equal(t, StageInputInstance, inputs.assignToNext(StageOverwriteLocalConfig, "y", emptyCfg))
|
||||
require.Equal(t, StageExit, inputs.assignToNext(StageOverwriteLocalConfig, "n", emptyCfg))
|
||||
})
|
||||
|
||||
t.Run("unknown stage", func(t *testing.T) {
|
||||
inputs := ®isterInputs{}
|
||||
require.Equal(t, StageUnknown, inputs.assignToNext(StageWaitingForRegistration, "x", emptyCfg))
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitInputs(t *testing.T) {
|
||||
inputs := initInputs(®isterArgs{
|
||||
InstanceAddr: "http://localhost:3000",
|
||||
Token: "token",
|
||||
RunnerName: "runner",
|
||||
Ephemeral: true,
|
||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
||||
})
|
||||
require.Equal(t, "http://localhost:3000", inputs.InstanceAddr)
|
||||
require.Equal(t, "token", inputs.Token)
|
||||
require.Equal(t, "runner", inputs.RunnerName)
|
||||
require.True(t, inputs.Ephemeral)
|
||||
require.Equal(t, []string{"ubuntu:host ", " ubuntu:docker://node:18"}, inputs.Labels)
|
||||
|
||||
require.Nil(t, initInputs(®isterArgs{Labels: " "}).Labels)
|
||||
}
|
||||
|
||||
103
internal/app/run/runner_test.go
Normal file
103
internal/app/run/runner_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/ver"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestRunnerCapabilitiesAndDeclare(t *testing.T) {
|
||||
require.Equal(t, []string{CapabilityCancelling}, RunnerCapabilities())
|
||||
|
||||
cli := clientmocks.NewClient(t)
|
||||
cli.On("Declare", mock.Anything, mock.MatchedBy(func(req *connect.Request[runnerv1.DeclareRequest]) bool {
|
||||
return req.Msg.Version == ver.Version() &&
|
||||
len(req.Msg.Labels) == 1 &&
|
||||
req.Msg.Labels[0] == "ubuntu" &&
|
||||
len(req.Msg.Capabilities) == 1 &&
|
||||
req.Msg.Capabilities[0] == CapabilityCancelling
|
||||
})).Return(connect.NewResponse(&runnerv1.DeclareResponse{}), nil)
|
||||
|
||||
r := &Runner{client: cli}
|
||||
_, err := r.Declare(context.Background(), []string{"ubuntu"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRunnerSetCapabilitiesFromDeclare(t *testing.T) {
|
||||
r := &Runner{}
|
||||
r.SetCapabilitiesFromDeclare(nil)
|
||||
require.Empty(t, r.capabilities)
|
||||
|
||||
resp := connect.NewResponse(&runnerv1.DeclareResponse{})
|
||||
resp.Header().Set("X-Gitea-Actions-Capabilities", " cancelling,cache-v2 ")
|
||||
r.SetCapabilitiesFromDeclare(resp)
|
||||
require.Equal(t, "cancelling,cache-v2", r.capabilities)
|
||||
}
|
||||
|
||||
func TestRunnerDefaultActionsURLUsesMirrorOnlyForGithub(t *testing.T) {
|
||||
r := &Runner{cfg: &config.Config{}}
|
||||
r.cfg.Runner.GithubMirror = "https://mirror.example"
|
||||
|
||||
task := taskWithDefaultActionsURL("https://github.com")
|
||||
require.Equal(t, "https://mirror.example", r.getDefaultActionsURL(task))
|
||||
|
||||
task = taskWithDefaultActionsURL("https://gitea.example")
|
||||
require.Equal(t, "https://gitea.example", r.getDefaultActionsURL(task))
|
||||
}
|
||||
|
||||
func TestRunnerRunningCountAndNullLogger(t *testing.T) {
|
||||
r := &Runner{}
|
||||
require.Equal(t, int64(0), r.RunningCount())
|
||||
r.runningCount.Add(2)
|
||||
require.Equal(t, int64(2), r.RunningCount())
|
||||
|
||||
logger := NullLogger{}.WithJobLogger()
|
||||
require.NotNil(t, logger)
|
||||
require.NotNil(t, logger.Out)
|
||||
}
|
||||
|
||||
func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
|
||||
cacheEnabled := false
|
||||
cfg := &config.Config{}
|
||||
cfg.Cache.Enabled = &cacheEnabled
|
||||
cfg.Runner.Envs = map[string]string{"EXISTING": "value"}
|
||||
reg := &config.Registration{
|
||||
Name: "runner",
|
||||
Labels: []string{"ubuntu:host", "bad:vm:label"},
|
||||
}
|
||||
cli := clientmocks.NewClient(t)
|
||||
cli.On("Address").Return("https://gitea.example/").Maybe()
|
||||
|
||||
r := NewRunner(cfg, reg, cli)
|
||||
|
||||
require.Equal(t, "runner", r.name)
|
||||
require.Len(t, r.labels, 1)
|
||||
require.Equal(t, "value", r.envs["EXISTING"])
|
||||
require.Equal(t, "https://gitea.example/api/actions_pipeline/", r.envs["ACTIONS_RUNTIME_URL"])
|
||||
require.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
|
||||
require.Equal(t, "true", r.envs["GITEA_ACTIONS"])
|
||||
require.NotEmpty(t, r.envs["GITEA_ACTIONS_RUNNER_VERSION"])
|
||||
require.Nil(t, r.cacheHandler)
|
||||
}
|
||||
|
||||
func taskWithDefaultActionsURL(url string) *runnerv1.Task {
|
||||
return &runnerv1.Task{
|
||||
Context: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"gitea_default_actions_url": structpb.NewStringValue(url),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,12 @@ package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
pingv1 "gitea.dev/actions-proto-go/ping/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -25,3 +29,67 @@ func TestGetHTTPClientUsesProxyFromEnvironment(t *testing.T) {
|
||||
require.NotNil(t, proxyURL)
|
||||
require.Equal(t, "http://proxy.example.com:8080", proxyURL.String())
|
||||
}
|
||||
|
||||
func TestGetHTTPClientInsecureTLS(t *testing.T) {
|
||||
// insecure only takes effect for https endpoints
|
||||
httpsInsecure := getHTTPClient("https://gitea.example.com", true)
|
||||
transport, ok := httpsInsecure.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.TLSClientConfig)
|
||||
require.True(t, transport.TLSClientConfig.InsecureSkipVerify)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
endpoint string
|
||||
insecure bool
|
||||
}{
|
||||
{"https secure", "https://gitea.example.com", false},
|
||||
{"http insecure ignored", "http://gitea.example.com", true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := getHTTPClient(tc.endpoint, tc.insecure)
|
||||
tr, ok := c.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, tr.TLSClientConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSetsBaseURLAndHeaders(t *testing.T) {
|
||||
var gotPath string
|
||||
gotHeaders := make(http.Header)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// trailing slash must be trimmed before "/api/actions" is appended
|
||||
c := New(server.URL+"/", false, "the-uuid", "the-token")
|
||||
// Address returns the endpoint as supplied (untrimmed)
|
||||
require.Equal(t, server.URL+"/", c.Address())
|
||||
require.False(t, c.Insecure())
|
||||
|
||||
// the call is expected to fail (server returns 500), we only assert what was sent
|
||||
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
|
||||
|
||||
require.True(t, strings.HasPrefix(gotPath, "/api/actions/"), "unexpected path %q", gotPath)
|
||||
require.Equal(t, "the-uuid", gotHeaders.Get(UUIDHeader))
|
||||
require.Equal(t, "the-token", gotHeaders.Get(TokenHeader))
|
||||
}
|
||||
|
||||
func TestNewOmitsEmptyHeaders(t *testing.T) {
|
||||
gotHeaders := make(http.Header)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
c := New(server.URL, false, "", "")
|
||||
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
|
||||
|
||||
require.Empty(t, gotHeaders.Get(UUIDHeader))
|
||||
require.Empty(t, gotHeaders.Get(TokenHeader))
|
||||
}
|
||||
|
||||
51
internal/pkg/config/registration_test.go
Normal file
51
internal/pkg/config/registration_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSaveAndLoadRegistration(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), ".runner")
|
||||
|
||||
reg := &Registration{
|
||||
ID: 42,
|
||||
UUID: "the-uuid",
|
||||
Name: "runner",
|
||||
Token: "the-token",
|
||||
Address: "http://localhost:3000",
|
||||
Labels: []string{"ubuntu:host", "ubuntu:docker://node:18"},
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
require.NoError(t, SaveRegistration(file, reg))
|
||||
// SaveRegistration stamps the warning onto the in-memory struct
|
||||
require.Equal(t, registrationWarning, reg.Warning)
|
||||
|
||||
loaded, err := LoadRegistration(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the warning is intentionally cleared on load
|
||||
require.Empty(t, loaded.Warning)
|
||||
loaded.Warning = reg.Warning
|
||||
require.Equal(t, reg, loaded)
|
||||
}
|
||||
|
||||
func TestLoadRegistrationMissingFile(t *testing.T) {
|
||||
_, err := LoadRegistration(filepath.Join(t.TempDir(), "does-not-exist"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoadRegistrationInvalidJSON(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), ".runner")
|
||||
require.NoError(t, os.WriteFile(file, []byte("not json"), 0o600))
|
||||
|
||||
_, err := LoadRegistration(file)
|
||||
require.Error(t, err)
|
||||
}
|
||||
20
internal/pkg/envcheck/docker_test.go
Normal file
20
internal/pkg/envcheck/docker_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCheckIfDockerRunningReturnsPingError(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := CheckIfDockerRunning(ctx, "unix:///definitely/missing/docker.sock")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "cannot ping the docker daemon")
|
||||
}
|
||||
@@ -61,3 +61,75 @@ func TestParse(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mustParse parses the given label strings, failing the test on any error.
|
||||
func mustParse(t *testing.T, strs ...string) Labels {
|
||||
t.Helper()
|
||||
ls := make(Labels, 0, len(strs))
|
||||
for _, s := range strs {
|
||||
l, err := Parse(s)
|
||||
require.NoError(t, err)
|
||||
ls = append(ls, l)
|
||||
}
|
||||
return ls
|
||||
}
|
||||
|
||||
func TestRequireDocker(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strs []string
|
||||
want bool
|
||||
}{
|
||||
{"empty", nil, false},
|
||||
{"only host", []string{"ubuntu:host", "self-hosted"}, false},
|
||||
{"has docker", []string{"ubuntu:host", "ubuntu:docker://node:18"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, mustParse(t, tt.strs...).RequireDocker())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickPlatform(t *testing.T) {
|
||||
ls := mustParse(t,
|
||||
"ubuntu:docker://node:18",
|
||||
"self-hosted:host",
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
runsOn []string
|
||||
want string
|
||||
}{
|
||||
{"docker strips leading slashes", []string{"ubuntu"}, "node:18"},
|
||||
{"host maps to self-hosted marker", []string{"self-hosted"}, "-self-hosted"},
|
||||
{"first match wins", []string{"self-hosted", "ubuntu"}, "-self-hosted"},
|
||||
{"unknown falls back to default", []string{"windows"}, "docker.gitea.com/runner-images:ubuntu-latest"},
|
||||
{"no runsOn falls back to default", nil, "docker.gitea.com/runner-images:ubuntu-latest"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, ls.PickPlatform(tt.runsOn))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNames(t *testing.T) {
|
||||
ls := mustParse(t, "ubuntu:docker://node:18", "self-hosted:host")
|
||||
require.Equal(t, []string{"ubuntu", "self-hosted"}, ls.Names())
|
||||
require.Empty(t, Labels{}.Names())
|
||||
}
|
||||
|
||||
func TestToStrings(t *testing.T) {
|
||||
ls := mustParse(t,
|
||||
"ubuntu:docker://node:18",
|
||||
"self-hosted:host",
|
||||
"bare",
|
||||
)
|
||||
require.Equal(t, []string{
|
||||
"ubuntu:docker://node:18",
|
||||
"self-hosted:host",
|
||||
"bare:host",
|
||||
}, ls.ToStrings())
|
||||
}
|
||||
|
||||
95
internal/pkg/metrics/metrics_test.go
Normal file
95
internal/pkg/metrics/metrics_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResultToStatusLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
result runnerv1.Result
|
||||
want string
|
||||
}{
|
||||
{"success", runnerv1.Result_RESULT_SUCCESS, LabelStatusSuccess},
|
||||
{"failure", runnerv1.Result_RESULT_FAILURE, LabelStatusFailure},
|
||||
{"cancelled", runnerv1.Result_RESULT_CANCELLED, LabelStatusCancelled},
|
||||
{"skipped", runnerv1.Result_RESULT_SKIPPED, LabelStatusSkipped},
|
||||
{"unspecified", runnerv1.Result_RESULT_UNSPECIFIED, LabelStatusUnknown},
|
||||
{"out of range", runnerv1.Result(999), LabelStatusUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, ResultToStatusLabel(tt.result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAndDynamicMetricRegistration(t *testing.T) {
|
||||
oldRegistry := Registry
|
||||
t.Cleanup(func() {
|
||||
Registry = oldRegistry
|
||||
})
|
||||
|
||||
Registry = prometheus.NewRegistry()
|
||||
initOnce = sync.Once{}
|
||||
|
||||
Init()
|
||||
Init()
|
||||
RunnerInfo.WithLabelValues("test", "runner").Set(1)
|
||||
RegisterUptimeFunc(time.Now().Add(-time.Second))
|
||||
RegisterRunningJobsFunc(func() int64 { return 2 }, 4)
|
||||
|
||||
metrics, err := Registry.Gather()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, hasMetric(metrics, "gitea_runner_info"))
|
||||
require.True(t, hasMetric(metrics, "gitea_runner_uptime_seconds"))
|
||||
require.True(t, hasMetric(metrics, "gitea_runner_job_running"))
|
||||
require.True(t, hasMetric(metrics, "gitea_runner_job_capacity_utilization_ratio"))
|
||||
}
|
||||
|
||||
func TestRegisterRunningJobsFuncZeroCapacity(t *testing.T) {
|
||||
oldRegistry := Registry
|
||||
t.Cleanup(func() { Registry = oldRegistry })
|
||||
Registry = prometheus.NewRegistry()
|
||||
|
||||
RegisterRunningJobsFunc(func() int64 { return 3 }, 0)
|
||||
|
||||
metrics, err := Registry.Gather()
|
||||
require.NoError(t, err)
|
||||
for _, mf := range metrics {
|
||||
if mf.GetName() == "gitea_runner_job_capacity_utilization_ratio" {
|
||||
require.Len(t, mf.GetMetric(), 1)
|
||||
require.InDelta(t, 0, mf.GetMetric()[0].GetGauge().GetValue(), 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("capacity utilization metric not gathered")
|
||||
}
|
||||
|
||||
func TestStartServerCanBeCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
StartServer(ctx, "127.0.0.1:0")
|
||||
cancel()
|
||||
}
|
||||
|
||||
func hasMetric(metrics []*dto.MetricFamily, name string) bool {
|
||||
for _, mf := range metrics {
|
||||
if strings.EqualFold(mf.GetName(), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
23
internal/pkg/process/sysprocattr_unix_test.go
Normal file
23
internal/pkg/process/sysprocattr_unix_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows && !plan9
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSysProcAttrUnixModes(t *testing.T) {
|
||||
plain := SysProcAttr("", false)
|
||||
require.True(t, plain.Setpgid)
|
||||
require.False(t, plain.Setsid)
|
||||
|
||||
tty := SysProcAttr("", true)
|
||||
require.True(t, tty.Setsid)
|
||||
require.True(t, tty.Setctty)
|
||||
require.False(t, tty.Setpgid)
|
||||
}
|
||||
36
internal/pkg/process/treekill_test.go
Normal file
36
internal/pkg/process/treekill_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewTreeKillConfiguresCommand(t *testing.T) {
|
||||
cmd := exec.CommandContext(context.Background(), "sleep", "1")
|
||||
tk := NewTreeKill(cmd)
|
||||
|
||||
require.NotNil(t, tk)
|
||||
require.NotNil(t, cmd.Cancel)
|
||||
require.Equal(t, treeKillWaitDelay, cmd.WaitDelay)
|
||||
require.NoError(t, cmd.Cancel())
|
||||
}
|
||||
|
||||
func TestTreeKillCaptureStoresKiller(t *testing.T) {
|
||||
cmd := exec.CommandContext(context.Background(), "sleep", "10")
|
||||
cmd.SysProcAttr = SysProcAttr("", false)
|
||||
tk := NewTreeKill(cmd)
|
||||
require.NoError(t, cmd.Start())
|
||||
defer func() { _ = cmd.Wait() }()
|
||||
|
||||
killer, err := tk.Capture(cmd.Process)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, killer)
|
||||
require.NoError(t, cmd.Cancel())
|
||||
require.NoError(t, killer.Close())
|
||||
}
|
||||
@@ -983,3 +983,75 @@ func TestReporter_StopHeartbeats(t *testing.T) {
|
||||
assert.Greater(t, updateTaskCalls.Load(), beforeStop,
|
||||
"Close() must still send a final UpdateTask after StopHeartbeats")
|
||||
}
|
||||
|
||||
func TestAppendIfNotNil(t *testing.T) {
|
||||
var s []*int
|
||||
s = appendIfNotNil(s, nil)
|
||||
assert.Empty(t, s)
|
||||
|
||||
v := 7
|
||||
s = appendIfNotNil(s, &v)
|
||||
require.Len(t, s, 1)
|
||||
assert.Equal(t, &v, s[0])
|
||||
|
||||
s = appendIfNotNil(s, nil)
|
||||
require.Len(t, s, 1)
|
||||
}
|
||||
|
||||
func TestReporter_Levels(t *testing.T) {
|
||||
assert.Equal(t, log.AllLevels, (&Reporter{}).Levels())
|
||||
}
|
||||
|
||||
func TestReporter_Result(t *testing.T) {
|
||||
r := &Reporter{state: &runnerv1.TaskState{Result: runnerv1.Result_RESULT_SUCCESS}}
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, r.Result())
|
||||
}
|
||||
|
||||
func TestReporter_SetOutputs(t *testing.T) {
|
||||
r := &Reporter{state: &runnerv1.TaskState{}}
|
||||
|
||||
r.SetOutputs(map[string]string{"foo": "bar"})
|
||||
got, ok := r.outputs.Load("foo")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "bar", got)
|
||||
|
||||
// first value wins: a later write to the same key is ignored
|
||||
r.SetOutputs(map[string]string{"foo": "baz"})
|
||||
got, _ = r.outputs.Load("foo")
|
||||
assert.Equal(t, "bar", got)
|
||||
|
||||
// keys longer than 255 chars are dropped
|
||||
longKey := strings.Repeat("k", 256)
|
||||
r.SetOutputs(map[string]string{longKey: "v"})
|
||||
_, ok = r.outputs.Load(longKey)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestReporter_EffectiveCloseTimeout(t *testing.T) {
|
||||
assert.Equal(t, 10*time.Second, (&Reporter{}).effectiveCloseTimeout())
|
||||
assert.Equal(t, 5*time.Second, (&Reporter{closeTimeout: 5 * time.Second}).effectiveCloseTimeout())
|
||||
}
|
||||
|
||||
func TestReporter_ParseResult(t *testing.T) {
|
||||
r := &Reporter{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input any
|
||||
want runnerv1.Result
|
||||
wantOk bool
|
||||
}{
|
||||
{"job result string", "success", runnerv1.Result_RESULT_SUCCESS, true},
|
||||
{"failure string", "failure", runnerv1.Result_RESULT_FAILURE, true},
|
||||
{"step result stringer", runnerv1.Result_RESULT_SKIPPED, runnerv1.Result_RESULT_UNSPECIFIED, false},
|
||||
{"unknown string", "bogus", runnerv1.Result_RESULT_UNSPECIFIED, false},
|
||||
{"unsupported type", 123, runnerv1.Result_RESULT_UNSPECIFIED, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := r.parseResult(tt.input)
|
||||
assert.Equal(t, tt.wantOk, ok)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
13
internal/pkg/ver/version_test.go
Normal file
13
internal/pkg/ver/version_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
// version defaults to "dev" and is overridden at build time via -ldflags
|
||||
if got := Version(); got != version {
|
||||
t.Errorf("Version() = %q, want %q", got, version)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user