blob: c4f73876b62ad1eefbbeab7753b7fd79f03cc953 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// `go mod` ignores file names for the purpose of resolving
// dependencies, and zxwait doesn't build on not-Fuchsia.
//go:build fuchsia
package zxwait_test
import (
"context"
"fmt"
"runtime"
"sync"
"syscall/zx"
"syscall/zx/zxwait"
"testing"
)
func TestWaitContextPreexisting(t *testing.T) {
c0, c1, err := zx.NewChannel(0)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = c0.Close()
_ = c1.Close()
}()
for _, ch := range [...]zx.Channel{c0, c1} {
obs, err := zxwait.WaitContext(context.Background(), *ch.Handle(), zx.SignalChannelWritable)
if err != nil {
t.Fatal(err)
}
if want := zx.Signals(zx.SignalChannelWritable); obs != want {
t.Errorf("got obs = %x, want = %x", obs, want)
}
}
}
type waitResult struct {
obs zx.Signals
err error
}
func TestWaitContext(t *testing.T) {
var pairs [][2]zx.Channel
defer func() {
for _, pair := range pairs {
for _, ch := range pair {
_ = ch.Close()
}
}
}()
ch := make(chan waitResult, 100)
for i := 0; i < cap(ch); i++ {
ch1, ch2, err := zx.NewChannel(0)
if err != nil {
t.Fatal(err)
}
pair := [...]zx.Channel{ch1, ch2}
pairs = append(pairs, pair)
go func() {
obs, err := zxwait.WaitContext(context.Background(), *pair[0].Handle(), zx.SignalChannelReadable)
ch <- waitResult{obs: obs, err: err}
}()
}
b := []byte("hello")
for i, pair := range pairs {
if err := pair[1].Write(b, nil, 0); err != nil {
t.Fatalf("%d: %s", i, err)
}
}
for i := 0; i < cap(ch); i++ {
waitResult := <-ch
if err := waitResult.err; err != nil {
t.Fatal(err)
}
if obs, want := waitResult.obs, zx.Signals(zx.SignalChannelReadable|zx.SignalChannelWritable); obs != want {
t.Errorf("%d: got obs = %b, want = %b", i, obs, want)
}
}
}
func TestWaitContext_Cancel(t *testing.T) {
event, err := zx.NewEvent(0)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := event.Close(); err != nil {
t.Fatal(err)
}
}()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan waitResult, 100)
var wg sync.WaitGroup
for i := 0; i < cap(ch)-1; i++ {
wg.Add(1)
go func() {
wg.Done()
obs, err := zxwait.WaitContext(ctx, *event.Handle(), 0)
ch <- waitResult{obs: obs, err: err}
}()
}
// Wait for all goroutines to be scheduled.
wg.Wait()
cancel()
// Guarantee at least one result happens after cancel.
{
obs, err := zxwait.WaitContext(ctx, *event.Handle(), 0)
ch <- waitResult{obs: obs, err: err}
}
for i := 0; i < cap(ch); i++ {
waitResult := <-ch
if got, want := waitResult.obs, zx.Signals(0); got != want {
t.Errorf("%d: got obs = %b, want = %b", i, got, want)
}
if got, want := waitResult.err, context.Canceled; got != want {
t.Errorf("%d: got zxwait.WaitContext(<closed handle>) = (_, %s), want = (_, %s)", i, got, want)
}
}
}
func TestWaitContext_LocalClose(t *testing.T) {
// We repeatedly run this test in a loop because we noticed it frequently fails
// iff it is the first test run. In the fail case it was receiving exclusively
// bad handles. This is due to how WaitContext must wait for zxwait's waiter before
// it can actually listen for signals on a handle. By running multiple times, we guarantee
// initialization has time to complete.
attempt := 0
for {
event, err := zx.NewEvent(0)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = event.Close()
}()
ch := make(chan waitResult, 100)
var wg sync.WaitGroup
for i := 0; i < cap(ch)-1; i++ {
wg.Add(1)
go func() {
wg.Done()
obs, err := zxwait.WaitContext(context.Background(), *event.Handle(), 0)
ch <- waitResult{obs: obs, err: err}
}()
}
// Wait for all goroutines to be scheduled.
wg.Wait()
if err := event.Close(); err != nil {
t.Fatal(err)
}
// Guarantee at least one result happens after local close.
{
obs, err := zxwait.WaitContext(context.Background(), *event.Handle(), 0)
ch <- waitResult{obs: obs, err: err}
}
var badHandle, cancelled int
for i := 0; i < cap(ch); i++ {
waitResult := <-ch
err := waitResult.err
switch err := err.(type) {
case *zx.Error:
obs := waitResult.obs
switch err.Status {
case zx.ErrBadHandle:
// This goroutine didn't get scheduled in time.
if want := zx.Signals(0); obs != want {
t.Errorf("%d: got obs = %b, want = %b", i, obs, want)
}
badHandle++
continue
case zx.ErrCanceled:
if want := zx.Signals(zx.SignalHandleClosed); obs != want {
t.Errorf("%d: got obs = %b, want = %b", i, obs, want)
}
cancelled++
continue
}
}
t.Errorf("%d: got zxwait.WaitContext(<closed handle>) = (_, %s), want = (_, %s or %s)", i, err, zx.ErrBadHandle, zx.ErrCanceled)
}
if badHandle == 0 {
t.Errorf("failed to observe post-close condition")
}
if cancelled != 0 {
// We have observed the cancelled condition.
break
}
attempt++
if attempt >= 100 {
t.Errorf("failed to observe pre-close condition after %d tries", attempt)
}
}
}
func TestWaitContext_LocalCloseRace(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
// Start a wait, assert the signal, close the handle, and cancel the
// context. This is a regression test for two issues:
// 1) internal state was reused without canceling pending waits, and
// 2) internal state was not flagged as defunct after failing to
// cleanly cancel a pending wait,
// Both of which caused future waits to catch stale wakeups.
wg.Add(1)
go func() {
defer wg.Done()
if err := func() error {
var event zx.Handle
if status := zx.Sys_event_create(0, &event); status != zx.ErrOk {
return &zx.Error{Status: status, Text: "failed to create event"}
}
defer func() {
_ = event.Close()
}()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Buffer the channel to avoid leaking a goroutine in case of error
// below.
ch := make(chan error, 1)
// Capture the event by value to avoid racing against the Close call,
// which resets its receiver.
go func(event zx.Handle, ctx context.Context) {
ch <- func() error {
obs, err := zxwait.WaitContext(ctx, event, zx.SignalUser0)
if err != nil {
if err, ok := err.(*zx.Error); ok {
switch err.Status {
case zx.ErrCanceled:
if obs != zx.SignalHandleClosed {
return fmt.Errorf("got zxwait.WaitContext(..., %b, ...) = %b", zx.SignalUser0, obs)
}
fallthrough
case zx.ErrBadHandle:
// We lost the race against the Close call; it's fine.
return nil
}
}
return fmt.Errorf("failed to zxwait: %w", err)
}
if obs != zx.SignalUser0 {
return fmt.Errorf("got zxwait.WaitContext(..., %b, ...) = %b", zx.SignalUser0, obs)
}
return nil
}()
}(event, ctx)
// Yield to allow the goroutine to be scheduled, beginning the wait.
runtime.Gosched()
if err := event.Signal(0, zx.SignalUser0); err != nil {
return fmt.Errorf("failed to signal event: %w", err)
}
if err := event.Close(); err != nil {
return fmt.Errorf("failed to close event: %w", err)
}
cancel()
return <-ch
}(); err != nil {
t.Error(err)
}
}()
// Create a new event, assert a (different) signal, and wait for that
// signal; if internal state was incorrectly reused, the wait will return
// the previous wait's signal.
wg.Add(1)
go func() {
defer wg.Done()
if err := func() error {
var event zx.Handle
if status := zx.Sys_event_create(0, &event); status != zx.ErrOk {
return &zx.Error{Status: status, Text: "failed to create event"}
}
defer func() {
_ = event.Close()
}()
if err := event.Signal(0, zx.SignalUser1); err != nil {
return fmt.Errorf("failed to signal event: %w", err)
}
obs, err := zxwait.WaitContext(context.Background(), event, zx.SignalUser1)
if err != nil {
return fmt.Errorf("failed to zxwait: %w", err)
}
if obs != zx.SignalUser1 {
return fmt.Errorf("got zxwait.WaitContext(..., %b, ...) = %b", zx.SignalUser1, obs)
}
if err := event.Close(); err != nil {
return fmt.Errorf("failed to close event: %w", err)
}
return nil
}(); err != nil {
t.Error(err)
}
}()
}
wg.Wait()
}