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:
bircni
2026-07-01 03:26:42 +00:00
committed by Lunny Xiao
parent 745b0ab6e4
commit 3396021e0f
27 changed files with 1980 additions and 1 deletions

View 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)
}

View 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()
}

View File

@@ -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 := &registerInputs{
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, (&registerInputs{}).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 := &registerInputs{}
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 := &registerInputs{}
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 := &registerInputs{}
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 := &registerInputs{}
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 := &registerInputs{}
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 := &registerInputs{}
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 := &registerInputs{}
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 := &registerInputs{}
require.Equal(t, StageUnknown, inputs.assignToNext(StageWaitingForRegistration, "x", emptyCfg))
})
}
func TestInitInputs(t *testing.T) {
inputs := initInputs(&registerArgs{
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(&registerArgs{Labels: " "}).Labels)
}