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:
@@ -1,89 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Simple fast test that verifies max-parallel: 2 limits concurrency
|
||||
func TestMaxParallel2Quick(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var currentRunning atomic.Int32
|
||||
var maxSimultaneous atomic.Int32
|
||||
|
||||
executors := make([]Executor, 4)
|
||||
for i := range 4 {
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
current := currentRunning.Add(1)
|
||||
|
||||
// Update max if needed
|
||||
for {
|
||||
maxValue := maxSimultaneous.Load()
|
||||
if current <= maxValue || maxSimultaneous.CompareAndSwap(maxValue, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
currentRunning.Add(-1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := NewParallelExecutor(2, executors...)(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.LessOrEqual(t, maxSimultaneous.Load(), int32(2),
|
||||
"Should not exceed max-parallel: 2")
|
||||
}
|
||||
|
||||
// Test that verifies max-parallel: 1 enforces sequential execution
|
||||
func TestMaxParallel1Sequential(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var currentRunning atomic.Int32
|
||||
var maxSimultaneous atomic.Int32
|
||||
var executionOrder []int
|
||||
var orderMutex sync.Mutex
|
||||
|
||||
executors := make([]Executor, 5)
|
||||
for i := range 5 {
|
||||
taskID := i
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
current := currentRunning.Add(1)
|
||||
|
||||
// Track execution order
|
||||
orderMutex.Lock()
|
||||
executionOrder = append(executionOrder, taskID)
|
||||
orderMutex.Unlock()
|
||||
|
||||
// Update max if needed
|
||||
for {
|
||||
maxValue := maxSimultaneous.Load()
|
||||
if current <= maxValue || maxSimultaneous.CompareAndSwap(maxValue, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
currentRunning.Add(-1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := NewParallelExecutor(1, executors...)(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, int32(1), maxSimultaneous.Load(),
|
||||
"max-parallel: 1 should only run 1 task at a time")
|
||||
assert.Len(t, executionOrder, 5, "All 5 tasks should have executed")
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMaxParallelJobExecution tests actual job execution with max-parallel
|
||||
func TestMaxParallelJobExecution(t *testing.T) {
|
||||
t.Run("MaxParallel=1 Sequential", func(t *testing.T) {
|
||||
var currentRunning atomic.Int32
|
||||
var maxConcurrent int32
|
||||
var executionOrder []int
|
||||
var mu sync.Mutex
|
||||
|
||||
executors := make([]Executor, 5)
|
||||
for i := range 5 {
|
||||
taskID := i
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
current := currentRunning.Add(1)
|
||||
|
||||
// Track max concurrent
|
||||
for {
|
||||
maxValue := atomic.LoadInt32(&maxConcurrent)
|
||||
if current <= maxValue || atomic.CompareAndSwapInt32(&maxConcurrent, maxValue, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
executionOrder = append(executionOrder, taskID)
|
||||
mu.Unlock()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
currentRunning.Add(-1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := NewParallelExecutor(1, executors...)(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, int32(1), maxConcurrent, "Should never exceed 1 concurrent execution")
|
||||
assert.Len(t, executionOrder, 5, "All tasks should execute")
|
||||
})
|
||||
|
||||
t.Run("MaxParallel=3 Limited", func(t *testing.T) {
|
||||
var currentRunning atomic.Int32
|
||||
var maxConcurrent int32
|
||||
|
||||
executors := make([]Executor, 10)
|
||||
for i := range 10 {
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
current := currentRunning.Add(1)
|
||||
|
||||
for {
|
||||
maxValue := atomic.LoadInt32(&maxConcurrent)
|
||||
if current <= maxValue || atomic.CompareAndSwapInt32(&maxConcurrent, maxValue, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
currentRunning.Add(-1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := NewParallelExecutor(3, executors...)(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.LessOrEqual(t, int(maxConcurrent), 3, "Should never exceed 3 concurrent executions")
|
||||
assert.GreaterOrEqual(t, int(maxConcurrent), 1, "Should have at least 1 concurrent execution")
|
||||
})
|
||||
|
||||
t.Run("MaxParallel=0 Uses1Worker", func(t *testing.T) {
|
||||
var maxConcurrent int32
|
||||
var currentRunning atomic.Int32
|
||||
|
||||
executors := make([]Executor, 5)
|
||||
for i := range 5 {
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
current := currentRunning.Add(1)
|
||||
|
||||
for {
|
||||
maxValue := atomic.LoadInt32(&maxConcurrent)
|
||||
if current <= maxValue || atomic.CompareAndSwapInt32(&maxConcurrent, maxValue, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
currentRunning.Add(-1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// When maxParallel is 0 or negative, it defaults to 1
|
||||
err := NewParallelExecutor(0, executors...)(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, int32(1), maxConcurrent, "Should use 1 worker when max-parallel is 0")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMaxParallelWithErrors tests error handling with max-parallel
|
||||
func TestMaxParallelWithErrors(t *testing.T) {
|
||||
t.Run("OneTaskFailsOthersContinue", func(t *testing.T) {
|
||||
var successCount int32
|
||||
|
||||
executors := make([]Executor, 5)
|
||||
for i := range 5 {
|
||||
taskID := i
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
if taskID == 2 {
|
||||
return assert.AnError
|
||||
}
|
||||
atomic.AddInt32(&successCount, 1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := NewParallelExecutor(2, executors...)(ctx)
|
||||
|
||||
// Should return the error from task 2
|
||||
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// Other tasks should still execute
|
||||
assert.Equal(t, int32(4), successCount, "4 tasks should succeed")
|
||||
})
|
||||
|
||||
t.Run("ContextCancellation", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var startedCount int32
|
||||
executors := make([]Executor, 10)
|
||||
for i := range 10 {
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
atomic.AddInt32(&startedCount, 1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel after a short delay
|
||||
go func() {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := NewParallelExecutor(3, executors...)(ctx)
|
||||
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.ErrorIs(t, err, context.Canceled) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// Not all tasks should start due to cancellation (but timing may vary)
|
||||
// Just verify cancellation occurred
|
||||
t.Logf("Started %d tasks before cancellation", startedCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMaxParallelResourceSharing tests resource sharing scenarios
|
||||
func TestMaxParallelResourceSharing(t *testing.T) {
|
||||
t.Run("SharedResourceWithMutex", func(t *testing.T) {
|
||||
var sharedCounter int
|
||||
var mu sync.Mutex
|
||||
|
||||
executors := make([]Executor, 100)
|
||||
for i := range 100 {
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
mu.Lock()
|
||||
sharedCounter++
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := NewParallelExecutor(10, executors...)(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, 100, sharedCounter, "All tasks should increment counter")
|
||||
})
|
||||
|
||||
t.Run("ChannelCommunication", func(t *testing.T) {
|
||||
resultChan := make(chan int, 50)
|
||||
|
||||
executors := make([]Executor, 50)
|
||||
for i := range 50 {
|
||||
taskID := i
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
resultChan <- taskID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := NewParallelExecutor(5, executors...)(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
close(resultChan)
|
||||
|
||||
results := make(map[int]bool)
|
||||
for result := range resultChan {
|
||||
results[result] = true
|
||||
}
|
||||
|
||||
assert.Len(t, results, 50, "All task IDs should be received")
|
||||
})
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -82,44 +82,45 @@ func TestNewConditionalExecutor(t *testing.T) {
|
||||
assert.Equal(1, falseCount)
|
||||
}
|
||||
|
||||
func TestNewParallelExecutor(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// concurrencyProbe returns an executor recording the peak number of concurrent copies. Copies
|
||||
// block until wantActive are in flight so the peak is exact without sleeping, and later copies
|
||||
// find the gate already open so the last one still finishes with no partner left.
|
||||
func concurrencyProbe(wantActive int32) (exec Executor, count, maxActive *atomic.Int32) {
|
||||
var counted, active, peak atomic.Int32
|
||||
var once sync.Once
|
||||
reached := make(chan struct{})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var count, activeCount, maxCount atomic.Int32
|
||||
emptyWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
|
||||
count.Add(1)
|
||||
|
||||
active := activeCount.Add(1)
|
||||
return func(ctx context.Context) error {
|
||||
counted.Add(1)
|
||||
running := active.Add(1)
|
||||
for {
|
||||
m := maxCount.Load()
|
||||
if active <= m || maxCount.CompareAndSwap(m, active) {
|
||||
seen := peak.Load()
|
||||
if running <= seen || peak.CompareAndSwap(seen, running) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
activeCount.Add(-1)
|
||||
|
||||
if running >= wantActive {
|
||||
once.Do(func() { close(reached) })
|
||||
}
|
||||
<-reached
|
||||
active.Add(-1)
|
||||
return nil
|
||||
})
|
||||
}, &counted, &peak
|
||||
}
|
||||
|
||||
err := NewParallelExecutor(2, emptyWorkflow, emptyWorkflow, emptyWorkflow)(ctx)
|
||||
func TestNewParallelExecutor(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
|
||||
assert.Equal(int32(2), maxCount.Load(), "should run at most 2 executors in parallel")
|
||||
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
exec, count, maxActive := concurrencyProbe(2)
|
||||
require.NoError(t, NewParallelExecutor(2, exec, exec, exec)(ctx))
|
||||
assert.Equal(t, int32(3), count.Load(), "should run all 3 executors")
|
||||
assert.Equal(t, int32(2), maxActive.Load(), "should run at most 2 executors in parallel")
|
||||
|
||||
// Reset to test running the executor with 0 parallelism
|
||||
count.Store(0)
|
||||
activeCount.Store(0)
|
||||
maxCount.Store(0)
|
||||
|
||||
errSingle := NewParallelExecutor(0, emptyWorkflow, emptyWorkflow, emptyWorkflow)(ctx)
|
||||
|
||||
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
|
||||
assert.Equal(int32(1), maxCount.Load(), "should run at most 1 executors in parallel")
|
||||
assert.NoError(errSingle)
|
||||
// parallelism below 1 falls back to a single worker
|
||||
exec, count, maxActive = concurrencyProbe(1)
|
||||
require.NoError(t, NewParallelExecutor(0, exec, exec, exec)(ctx))
|
||||
assert.Equal(t, int32(3), count.Load(), "should run all 3 executors")
|
||||
assert.Equal(t, int32(1), maxActive.Load(), "should run at most 1 executor in parallel")
|
||||
}
|
||||
|
||||
func TestNewParallelExecutorEmpty(t *testing.T) {
|
||||
@@ -173,6 +174,23 @@ func TestNewParallelExecutorCanceled(t *testing.T) {
|
||||
assert.Error(errExpected, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
func TestNewParallelExecutorRunsRemainingAfterFailure(t *testing.T) {
|
||||
var successCount atomic.Int32
|
||||
executors := make([]Executor, 5)
|
||||
for i := range executors {
|
||||
executors[i] = func(ctx context.Context) error {
|
||||
if i == 2 {
|
||||
return errors.New("fake error")
|
||||
}
|
||||
successCount.Add(1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
require.Error(t, NewParallelExecutor(2, executors...)(context.Background()))
|
||||
assert.Equal(t, int32(4), successCount.Load(), "a failing executor must not stop the others")
|
||||
}
|
||||
|
||||
func TestExecutorConditionalsAndFinally(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var calls []string
|
||||
|
||||
Reference in New Issue
Block a user