Files

168 lines
3.5 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package lock
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestCond(t *testing.T) {
cancelled, cancel := context.WithCancel(context.Background())
cancel()
var (
noop = func(*Cond) {}
signal = (*Cond).Signal
broadcast = (*Cond).Broadcast
merge = func(ops ...func(*Cond)) func(*Cond) {
return func(c *Cond) {
for _, op := range ops {
op(c)
}
}
}
tests = []struct {
name string
ctx context.Context
expectedErrors []error
next []func(*Cond)
}{
{
name: "signal_once",
ctx: context.Background(),
expectedErrors: make([]error, 1),
next: []func(*Cond){
signal,
},
},
{
name: "signal_twice",
ctx: context.Background(),
expectedErrors: make([]error, 1),
next: []func(*Cond){
merge(
signal,
signal,
),
},
},
{
name: "signal_both_once",
ctx: context.Background(),
expectedErrors: make([]error, 2),
next: []func(*Cond){
signal,
signal,
},
},
{
name: "signal_both_once_atomically",
ctx: context.Background(),
expectedErrors: make([]error, 2),
next: []func(*Cond){
merge(
signal,
signal,
),
noop,
},
},
{
name: "broadcast_once",
ctx: context.Background(),
expectedErrors: make([]error, 2),
next: []func(*Cond){
broadcast,
noop,
},
},
{
name: "broadcast_twice",
ctx: context.Background(),
expectedErrors: make([]error, 2),
next: []func(*Cond){
broadcast,
broadcast,
},
},
{
name: "cancelled",
ctx: cancelled,
expectedErrors: []error{
context.Canceled,
context.Canceled,
},
next: []func(*Cond){
noop,
noop,
},
},
}
timeout = time.After(time.Second)
)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
wg sync.WaitGroup // WaitGroup to ensure all goroutines have exited before the test ends
require = require.New(t)
c = NewCond(&sync.Mutex{})
errs = make(chan error, len(test.expectedErrors))
)
wg.Add(len(test.expectedErrors))
defer wg.Wait()
for i := range test.expectedErrors {
// By grabbing the lock outside of the goroutine, we are ensured
// that all goroutines are waiting after this for loop exits
// once the lock is no longer held.
c.L.Lock()
go func() {
defer wg.Done()
t.Log("waiting", i)
errs <- c.Wait(test.ctx)
t.Log("waited", i)
c.L.Unlock()
}()
}
c.L.Lock()
t.Log("synchronized waiters")
c.L.Unlock()
// All goroutines are waiting on the condition variable.
for i, next := range test.next {
t.Log("calling next", i)
next(c)
t.Log("called next", i)
select {
case err := <-errs:
require.ErrorIs(err, test.expectedErrors[i])
t.Log("checked error", i)
// Timing out rather than depending on the test timeout allows
// the t.Logs to be printed rather than a stack trace.
case <-timeout:
t.Log("error checking timeout", i)
t.Fail()
}
}
// If the test has passed, this lock shouldn't be needed. But it is
// included to ensure the test doesn't panic if a timeout has fired.
c.m.Lock()
defer c.m.Unlock()
require.Empty(c.w)
})
}
}