mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-06 00:44:22 +02:00
test: speed up (#1121)
Full suite 178s → 96s, and a run's log 12MB → 2KB. `act/runner` was 177s of the 178s, serialised behind one docker daemon. - Its fixtures now run in parallel, bounded by a slot count instead of `go test -parallel`, with a per-test container name prefix and a pinned `MaxParallel`. - Fixtures asserting a job failure leaked their container and network (`AutoRemove` was at the act-CLI default), filling the daemon's address pool over time. - Replaced sleeps used as synchronisation in the parallel-executor and cache-handler tests. - Dropped duplicate coverage: two files re-testing `NewParallelExecutor`, a test asserting on its own semaphore, and `TestDockerActionForcePullForceRebuild`, whose config `runTest` discarded. - `fmt-check`/`security-check` move from `make test` to a `checks` target; `security-check` no longer installs `xgo` and `gxz`. - Test flags follow gitea: `GOTEST_FLAGS ?= -race -timeout 20m -parallel 8`, with `-cover`/`-coverprofile` left in the target. Dropped `-v`, since a failing package still prints its full output without it. Coverage unchanged at 73.4%. Reviewed-on: https://gitea.com/gitea/runner/pulls/1121 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -5,11 +5,9 @@ package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
|
||||
@@ -42,18 +40,6 @@ func requireDocker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// requireNetwork skips the test unless github.com is reachable. A few tests exercise behaviour
|
||||
// that inherently needs the network (force-pulling an image, resolving a remote short-sha ref);
|
||||
// gating lets the rest of the suite run offline without these failing.
|
||||
func requireNetwork(t *testing.T) {
|
||||
t.Helper()
|
||||
conn, err := net.DialTimeout("tcp", "github.com:443", 3*time.Second)
|
||||
if err != nil {
|
||||
t.Skipf("skipping: network unavailable: %v", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
// requireHostTools skips the test unless every named executable is on PATH. Used by the
|
||||
// self-hosted (host environment) suite, which runs steps directly on the host.
|
||||
func requireHostTools(t *testing.T, tools ...string) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
)
|
||||
|
||||
func TestJobExecutor(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Dryrun only checks syntax/planning; all cases resolve locally, so this runs offline.
|
||||
tables := []TestJobFileInfo{
|
||||
{workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, secrets},
|
||||
@@ -46,6 +47,7 @@ func TestJobExecutor(t *testing.T) {
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
for _, table := range tables {
|
||||
t.Run(table.workflowPath, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
table.runTest(ctx, t, &Config{})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMaxParallelConfig tests that MaxParallel config is properly set
|
||||
func TestMaxParallelConfig(t *testing.T) {
|
||||
t.Run("MaxParallel set to 2", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Workdir: "testdata",
|
||||
MaxParallel: 2,
|
||||
}
|
||||
|
||||
runner, err := New(config)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, runner)
|
||||
|
||||
// Verify config is properly stored
|
||||
runnerImpl, ok := runner.(*runnerImpl)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 2, runnerImpl.config.MaxParallel)
|
||||
})
|
||||
|
||||
t.Run("MaxParallel set to 0 (no limit)", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Workdir: "testdata",
|
||||
MaxParallel: 0,
|
||||
}
|
||||
|
||||
runner, err := New(config)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, runner)
|
||||
|
||||
runnerImpl, ok := runner.(*runnerImpl)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 0, runnerImpl.config.MaxParallel)
|
||||
})
|
||||
|
||||
t.Run("MaxParallel not set (defaults to 0)", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Workdir: "testdata",
|
||||
}
|
||||
|
||||
runner, err := New(config)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, runner)
|
||||
|
||||
runnerImpl, ok := runner.(*runnerImpl)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 0, runnerImpl.config.MaxParallel)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMaxParallelConcurrencyTracking tests that max-parallel actually limits concurrent execution
|
||||
func TestMaxParallelConcurrencyTracking(t *testing.T) {
|
||||
// This is a unit test for the parallel executor logic
|
||||
// We test that when MaxParallel is set, it limits the number of workers
|
||||
|
||||
var mu sync.Mutex
|
||||
var maxConcurrent int
|
||||
var currentConcurrent int
|
||||
|
||||
// Create a function that tracks concurrent execution
|
||||
trackingFunc := func() {
|
||||
mu.Lock()
|
||||
currentConcurrent++
|
||||
if currentConcurrent > maxConcurrent {
|
||||
maxConcurrent = currentConcurrent
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Simulate work
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
currentConcurrent--
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
// Run multiple tasks with limited parallelism
|
||||
maxConcurrent = 0
|
||||
currentConcurrent = 0
|
||||
|
||||
// This simulates what NewParallelExecutor does with a semaphore
|
||||
var wg sync.WaitGroup
|
||||
semaphore := make(chan struct{}, 2) // Limit to 2 concurrent
|
||||
|
||||
for range 6 {
|
||||
wg.Go(func() {
|
||||
semaphore <- struct{}{} // Acquire
|
||||
defer func() { <-semaphore }() // Release
|
||||
trackingFunc()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// With a semaphore of 2, max concurrent should be <= 2
|
||||
assert.LessOrEqual(t, maxConcurrent, 2, "Maximum concurrent executions should not exceed limit")
|
||||
assert.GreaterOrEqual(t, maxConcurrent, 1, "Should have at least 1 concurrent execution")
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -163,6 +164,12 @@ func TestGraphEvent(t *testing.T) {
|
||||
assert.Empty(t, plan.Stages)
|
||||
}
|
||||
|
||||
// these two build the same action Dockerfiles into one image tag, so they cannot overlap
|
||||
var sharedImageWorkflows = []string{"local-action-dockerfile", "local-action-via-composite-dockerfile"}
|
||||
|
||||
// bounds concurrent plans: each job holds a network, and the daemon's address pool is finite
|
||||
var planSlots = make(chan struct{}, 4)
|
||||
|
||||
type TestJobFileInfo struct {
|
||||
workdir string
|
||||
workflowPath string
|
||||
@@ -182,12 +189,19 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
|
||||
fullWorkflowPath := filepath.Join(workdir, j.workflowPath)
|
||||
runnerConfig := &Config{
|
||||
Workdir: workdir,
|
||||
BindWorkdir: false,
|
||||
EventName: j.eventName,
|
||||
EventPath: cfg.EventPath,
|
||||
Platforms: j.platforms,
|
||||
ReuseContainers: false,
|
||||
Workdir: workdir,
|
||||
BindWorkdir: false,
|
||||
EventName: j.eventName,
|
||||
EventPath: cfg.EventPath,
|
||||
Platforms: j.platforms,
|
||||
// fixtures reuse workflow and job names, so parallel tests would collide without this
|
||||
ContainerNamePrefix: strings.ReplaceAll(t.Name(), "/", "-"),
|
||||
ReuseContainers: false,
|
||||
// as the shipped runner does, else a fixture asserting a job failure keeps its
|
||||
// container, and its network, on the daemon forever
|
||||
AutoRemove: true,
|
||||
// 0 would run jobs runtime.NumCPU()-wide, making the network peak machine-dependent
|
||||
MaxParallel: 2,
|
||||
ForceRebuild: true,
|
||||
Env: cfg.Env,
|
||||
Secrets: cfg.Secrets,
|
||||
@@ -210,7 +224,11 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
plan, err := planner.PlanEvent(j.eventName)
|
||||
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
if err == nil && plan != nil {
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
err = func() error {
|
||||
planSlots <- struct{}{}
|
||||
defer func() { <-planSlots }()
|
||||
return runner.NewPlanExecutor(plan)(ctx)
|
||||
}()
|
||||
if j.errorMessage == "" {
|
||||
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
} else {
|
||||
@@ -227,6 +245,7 @@ type TestConfig struct {
|
||||
|
||||
func TestRunEvent(t *testing.T) {
|
||||
requireDocker(t)
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -315,6 +334,9 @@ func TestRunEvent(t *testing.T) {
|
||||
// host /proc bind mounts are Linux-Docker-only
|
||||
requireLinuxDocker(t)
|
||||
}
|
||||
if !slices.Contains(sharedImageWorkflows, table.workflowPath) {
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Secrets: table.secrets,
|
||||
@@ -445,6 +467,7 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDryrunEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Dryrun plans without containers or network (shells and local actions only).
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
|
||||
@@ -464,6 +487,7 @@ func TestDryrunEvent(t *testing.T) {
|
||||
|
||||
for _, table := range tables {
|
||||
t.Run(table.workflowPath, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
table.runTest(ctx, t, &Config{})
|
||||
})
|
||||
}
|
||||
@@ -474,33 +498,11 @@ func TestDryrunEvent(t *testing.T) {
|
||||
// workflow's outputs via `needs`).
|
||||
func TestReusableWorkflowCaller(t *testing.T) {
|
||||
requireDocker(t)
|
||||
t.Parallel()
|
||||
table := TestJobFileInfo{workdir, "uses-workflow", "push", "", platforms, map[string]string{"secret": "keep_it_private"}}
|
||||
table.runTest(context.Background(), t, &Config{Secrets: table.secrets})
|
||||
}
|
||||
|
||||
func TestDockerActionForcePullForceRebuild(t *testing.T) {
|
||||
requireDocker(t)
|
||||
requireNetwork(t) // force-pulls a docker action image
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
config := &Config{
|
||||
ForcePull: true,
|
||||
ForceRebuild: true,
|
||||
}
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
{workdir, "local-action-dockerfile", "push", "", platforms, secrets},
|
||||
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
t.Run(table.workflowPath, func(t *testing.T) {
|
||||
table.runTest(ctx, t, config)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type maskJobLoggerFactory struct {
|
||||
Output bytes.Buffer
|
||||
}
|
||||
@@ -513,6 +515,7 @@ func (f *maskJobLoggerFactory) WithJobLogger() *log.Logger {
|
||||
}
|
||||
|
||||
func TestMaskValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertNoSecret := func(text, secret string) { //nolint:unparam // pre-existing issue from nektos/act
|
||||
found := strings.Contains(text, "composite secret")
|
||||
if found {
|
||||
@@ -543,6 +546,7 @@ func TestMaskValues(t *testing.T) {
|
||||
|
||||
func TestRunEventSecrets(t *testing.T) {
|
||||
requireDocker(t)
|
||||
t.Parallel()
|
||||
workflowPath := "secrets"
|
||||
|
||||
tjfi := TestJobFileInfo{
|
||||
@@ -598,6 +602,7 @@ func TestRunWithService(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunActionInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
requireDocker(t)
|
||||
workflowPath := "input-from-cli"
|
||||
|
||||
@@ -617,6 +622,7 @@ func TestRunActionInputs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunEventPullRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
requireDocker(t)
|
||||
|
||||
workflowPath := "pull-request"
|
||||
@@ -633,6 +639,7 @@ func TestRunEventPullRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunMatrixWithUserDefinedInclusions(t *testing.T) {
|
||||
t.Parallel()
|
||||
requireDocker(t)
|
||||
workflowPath := "matrix-with-user-inclusions"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user