A Go question: how do you test <code>select</code> based code?

A while back I wrote an entry about understanding reading all available things from a Go channel (with a timeout) , where the code used two select s to, well, let me quote myself:

The goal of waitReadAll() is to either receive (read) all currently available items from a channel (possibly a buffered one) or to time out if nothing shows up in time. This requires two nested selects, with the inner one in a for loop.

In a recent comment on that entry, Aristotle Pagaltzis proposed a code variation that only used a single select :

func waitReadAll[T any](c chan T, d time.Duration) ([]T, bool) {
    var out []T
    for {
        select {
        case v, ok := <-c:
            if !ok {
               return out, false
    	       }
            out = append(out, v)

        case <-time.After(d):
            if len(out) == 0 {
               return out, true
            }

        default:
            return out, true
        }
    }
}

Aristotle Pagaltzis wrote tests for this code in the Go playground , but despite passing those tests, this code has an intrinsic bug that means it can't work as designed. The bug is that if this code is entered with nothing in the channel, the default case is immediately triggered rather than it waiting for the length of the timeout. When I saw this code, I was convinced it had the bug and so I tried to modify the Go playground code to have a test that would expose the bug. However, I couldn't find an easy way to do so at the time, and even now my attempts have been somewhat awkward, so at the least I think it's not obvious how to do this.

In Go 1.25 (and later), the primary tool for testing synchronization and concurrency is the testing/synctest package ( also ). Running our hypothetical test with synctest.Test() do it in an environment where time won't advance arbitrarily on us, insuring that the timeout in waitReadAll() won't trigger before we can do other things, like send to the channel. To create ordering in our case, I believe we can use synctest.Wait() . Consider this sketched code inside a synctest.Test():

c := make(chan int)
// sending goroutine:
go func() {
    // Point 1
    synctest.Wait()
    // Point 2
    time.Sleep(1*time.Second)
    c <- 1
}

// Point 3 (receiving goroutine)
out, ok = waitReadAll(c, 2*time.Second)
// assert ok and len(out) == 1

The synctest.Wait() in the sending goroutine at point 1 will wait until everything is 'durably blocked'; the first durable block point is in theory a working select inside waitReadAll() , called at point 3 in a different goroutine. Then in our sending goroutine at point 2 we use time.Sleep() to wait less than the timeout, forcing ordering, and finally we send to the channel, which waitReadAll() should pick up before it times out. This (and a related test for a timeout) works properly with a working waitReadAll() , but it took a bunch of contortions to avoid having it panic in various ways with the buggy version of waitReadAll(). I'm also not convinced my testing code is completely correct.

(Some of the initial panics came from me learning that you often want to avoid using t.Fatal() inside a synctest bubble; instead you want to call t.Error() and arrange to have the rest of your code still work right.)

Effectively I'm using synctest to try to create an ordering of events between two goroutines without modifying any code to have explicit locking or synchronization. Synctest doesn't completely serialize execution but it does create predictable 'durable blocking' points where I know where everything is if things are working correctly. But it's awkward, and I can't directly wait and check for a blocked select at point 1.

Synctest also makes certain things that normally would be races into safer, probably race-free operations. Consider a version of this test with a bit more checking:

c := make(chan int)
readall := false
go func() {
    // Point 1
    synctest.Wait()
    // Point 2
    time.Sleep(1*time.Second)
    if readall {
       // failure!
    }
    c <- 1
}

// Point 3
out, ok = waitReadAll(c, 2*time.Second)
readall = true
// assert ok and len(out) == 1

Because of how synctest.Wait() and time work within synctest bubbles, I believe in theory the only way that the two goroutines can access readall at the same time is if waitReadAll() is delaying for the same amount of time as our sending goroutine (instead of the amount of time we told it to). But the whole area is alarmingly subtle and I'm not sure I'm right.

( One of the synctest examples uses an unguarded variable in broadly this way.)

It's entirely possible that there's an easier way to do this sort of testing of select expressions, and I'd certainly hope so. However, synctest itself is quite new, so perhaps there's no better way right now. Also, possibly this sort of low level testing isn't necessary very often in practice. Both Aristotle Pagaltzis and I are in a sort of artificial situation where we're narrowly focused on a single peculiar function.