| // 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 |
| // +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) { |
| 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 { |
| t.Error("failed to observe pre-close condition") |
| } |
| } |
| |
| func TestWaitContext_LocalCloseRace(t *testing.T) { |
| var wg sync.WaitGroup |
| |
| for i := 0; i < 100; i++ { |
| // Start a wait, assert the signal, and close the handle. This is a |
| // regression test for an issue where internal state was reused without |
| // cancelling pending waits, causing 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() |
| }() |
| |
| // 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) { |
| ch <- func() error { |
| obs, err := zxwait.WaitContext(context.Background(), 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) |
| |
| // 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) |
| } |
| |
| 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() |
| } |