Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions test/library/encryption/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -475,3 +476,35 @@ func WaitForCurrentKeyMigrated(t testing.TB, kubeClient kubernetes.Interface, pr
require.NoError(t, newErr)
}
}

// inParallel returns a single testStep that runs the given steps
// concurrently and waits for all to finish before returning.
// Panics are caught and reported via t.Errorf. Failures from
// t.FailNow/require (runtime.Goexit) are handled naturally since
// the testing framework already records the error on t.
func inParallel(steps ...testStep) testStep {
if len(steps) == 1 {
return steps[0]
}
names := make([]string, len(steps))
for i, s := range steps {
names[i] = s.name
}
return testStep{
name: strings.Join(names, " | "),
testFunc: func(t testing.TB) {
var wg sync.WaitGroup
for _, s := range steps {
wg.Go(func() {
defer func() {
if r := recover(); r != nil {
t.Errorf("step %q panicked: %v", s.name, r)
}
}()
s.testFunc(t)
})
}
wg.Wait()
},
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
136 changes: 136 additions & 0 deletions test/library/encryption/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package encryption

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this file ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. I would prefer having tests that ensure errors from individual runs will be reported and propagated to the caller.


import (
"runtime"
"sync/atomic"
"testing"
"time"
)

// recordingTB intercepts failure methods so we can check Failed()
// without actually failing the real test.
type recordingTB struct {
*testing.T
failed atomic.Bool
}

func (r *recordingTB) Errorf(format string, args ...interface{}) {
r.failed.Store(true)
r.T.Logf("recorded error: "+format, args...)
}

func (r *recordingTB) FailNow() {
r.failed.Store(true)
runtime.Goexit()
}

func (r *recordingTB) Failed() bool { return r.failed.Load() }

func countCompletions(steps []testStep, completed *atomic.Int32) []testStep {
wrapped := make([]testStep, len(steps))
for i, s := range steps {
wrapped[i] = testStep{name: s.name, testFunc: wrapWithCounter(s.testFunc, completed)}
}
return wrapped
}

func wrapWithCounter(fn func(testing.TB), completed *atomic.Int32) func(testing.TB) {
return func(t testing.TB) {
fn(t)
completed.Add(1)
}
}

func TestInParallel(t *testing.T) {
noop := func(t testing.TB) {}

tests := []struct {
name string
steps []testStep
expectName string
expectFailed bool
expectCompleted int32
}{
{
name: "single step passes through",
steps: []testStep{{name: "only", testFunc: noop}},
expectName: "only",
expectCompleted: 1,
},
{
name: "all succeed",
steps: []testStep{
{name: "a", testFunc: noop},
{name: "b", testFunc: noop},
},
expectName: "a | b",
expectCompleted: 2,
},
{
name: "panic fails test and other steps complete",
steps: []testStep{
{name: "ok-1", testFunc: noop},
{name: "panicker", testFunc: func(t testing.TB) { panic("boom") }},
{name: "ok-2", testFunc: noop},
},
expectName: "ok-1 | panicker | ok-2",
expectFailed: true,
expectCompleted: 2,
},
{
name: "FailNow fails test and other steps complete",
steps: []testStep{
{name: "ok", testFunc: noop},
{name: "failnow", testFunc: func(t testing.TB) { t.FailNow() }},
},
expectName: "ok | failnow",
expectFailed: true,
expectCompleted: 1,
},
{
name: "Errorf fails test and other steps complete",
steps: []testStep{
{name: "ok", testFunc: noop},
{name: "errorer", testFunc: func(t testing.TB) { t.Errorf("broke") }},
},
expectName: "ok | errorer",
expectFailed: true,
expectCompleted: 2,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var completed atomic.Int32
rec := &recordingTB{T: t}
combined := inParallel(countCompletions(tt.steps, &completed)...)
combined.testFunc(rec)

if combined.name != tt.expectName {
t.Errorf("expected name %q, actual %q", tt.expectName, combined.name)
}
if rec.Failed() != tt.expectFailed {
t.Errorf("expected Failed()=%v, actual %v", tt.expectFailed, rec.Failed())
}
if actual := completed.Load(); actual != tt.expectCompleted {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking completed.Load() immediately may not give goroutines enough time to complete.
can we add small grace time here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that wg.Wait() guarantees all goroutines will finish before inParallel returns.
Does it make sense ?

t.Errorf("expected %d completed, actual %d", tt.expectCompleted, actual)
}
})
}
}

func TestInParallelRunsConcurrently(t *testing.T) {
sleep := func(t testing.TB) { time.Sleep(500 * time.Millisecond) }

start := time.Now()
inParallel(
testStep{name: "a", testFunc: sleep},
testStep{name: "b", testFunc: sleep},
testStep{name: "c", testFunc: sleep},
).testFunc(t)
elapsed := time.Since(start)

if elapsed >= 1*time.Second {
t.Errorf("expected ~500ms (parallel), actual %v (sequential would be ~1.5s)", elapsed)
}
}