This is driving me OCD-crazy. Suppose I have the following function:
func Map(quit <-chan struct{}, dst chan<- interface{}, src <-chan interface{}, f func(interface{}) interface{} {
for {
select {
case v, ok := <- src:
if !ok {
return
}
select {
case dst <- f(v):
case <-quit:
return
}
case <-quit:
return
}
}
}
It sends f(v) on dst for each value v received from src until either src or quit is closed and empty or a value is received from quit.
Now, suppose I want to write a test which demonstrates that it can be cancelled:
func TestMapCancel(t *testing.T) {
var wg sync.WaitGroup
quit := make(chan struct{})
success := make(chan struct{})
wg.Add(3)
src := // channel providing arbitrary values until quit is closed
dst := make(chan interface{})
// mapper
go func() {
defer wg.Done()
defer close(dst)
Map(quit, dst, src, double)
}()
// provide a sink to consume values from dst until quit is closed
timeout(quit, 10*time.Millisecond)
wait(success, &wg)
select {
case <-success:
case <-time.After(100 * time.Millisecond):
t.Error("cancellation timed out")
}
}
The undefined functions here aren't terribly important. Please assume they work. timeout
closes its channel parameter after the specified amount of time, and wait
closes its channel parameter after wg.Wait()
.
The problem is that this won't offer 100% coverage because a select case is chosen uniformly at (pseudo-)random if both are ready to send/receive. The following version of Map
doesn't have this problem, but suffers from potential indefinite blocking if the upstream channel (src) isn't closed:
func Map(quit <-chan struct{}, dst chan<- interface{}, src <-chan interface{}, f func(interface{}) interface{}) {
for v := range src {
select {
case dst <- f(v):
case <-quit:
return
}
}
}
I can sort-of solve this by rewriting the test to repeat several times in a loop, so that each branch has a chance to be chosen at random. I've tried as few as 10 iterations and reached 100% coverage with all tests passing (there are other tests besides this one). But it bugs the crap out of me that I can't seem to write a best-of-both-worlds version that won't block if the upstream channel isn't closed and is guaranteed to give 100% test coverage (not just likely).
Any inspiration for me?
P.S., if you're curious why "doesn't block if upstream channel isn't closed" is important, it's just another bit of OCD. This function is exported, meaning that if client code is misbehaving, my code misbehaves. I'd like it to be more resilient than that, which the first version is.
Aucun commentaire:
Enregistrer un commentaire