feat(server): return from Display.Do once the display loop stops
- commit
- 2717424478690f2d3df92653983672c36d70ab56
- author
- brice · · 2026-09-23 12:16 UTC
- parent
- 90ef96b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +129 -16
M internal/e2e/args_test.go +4 -2
| @@ -139,7 +139,7 @@ func TestArgumentRoundtripAndPostError(t *testing.T) { | |||
| 139 | 139 | socket, d, _ := startServer(t) | |
| 140 | 140 | requests := make(chan string, 1) | |
| 141 | 141 | stringsReceived := make(chan string, 1) | |
| 142 | - | d.Do(func() { | |
| 142 | + | if !d.Do(func() { | |
| 143 | 143 | if err := wayland.NewDataDeviceManagerGlobal(d, 1, func(c server.Client, v, id uint32) { | |
| 144 | 144 | if _, e := wayland.NewDataDeviceManager(c, int32(v), id, managerHandler{stringsReceived, t}); e != nil { | |
| 145 | 145 | t.Error(e) | |
| @@ -164,7 +164,9 @@ func TestArgumentRoundtripAndPostError(t *testing.T) { | |||
| 164 | 164 | }); err != nil { | |
| 165 | 165 | t.Error(err) | |
| 166 | 166 | } | |
| 167 | - | }) | |
| 167 | + | }) { | |
| 168 | + | t.Fatal("display stopped") | |
| 169 | + | } | |
| 168 | 170 | c, err := wlturbo.Connect(socket) | |
| 169 | 171 | must(t, err) | |
| 170 | 172 | defer c.Close() | |
| @@ -139,7 +139,7 @@ func TestArgumentRoundtripAndPostError(t *testing.T) { | |||||
| 139 | socket, d, _ := startServer(t) | 139 | socket, d, _ := startServer(t) | ||
| 140 | requests := make(chan string, 1) | 140 | requests := make(chan string, 1) | ||
| 141 | stringsReceived := make(chan string, 1) | 141 | stringsReceived := make(chan string, 1) | ||
| 142 | - | d.Do(func() { | 142 | + | if !d.Do(func() { |
| 143 | if err := wayland.NewDataDeviceManagerGlobal(d, 1, func(c server.Client, v, id uint32) { | 143 | if err := wayland.NewDataDeviceManagerGlobal(d, 1, func(c server.Client, v, id uint32) { | ||
| 144 | if _, e := wayland.NewDataDeviceManager(c, int32(v), id, managerHandler{stringsReceived, t}); e != nil { | 144 | if _, e := wayland.NewDataDeviceManager(c, int32(v), id, managerHandler{stringsReceived, t}); e != nil { | ||
| 145 | t.Error(e) | 145 | t.Error(e) | ||
| @@ -164,7 +164,9 @@ func TestArgumentRoundtripAndPostError(t *testing.T) { | |||||
| 164 | }); err != nil { | 164 | }); err != nil { | ||
| 165 | t.Error(err) | 165 | t.Error(err) | ||
| 166 | } | 166 | } | ||
| 167 | - | }) | 167 | + | }) { |
| 168 | + | t.Fatal("display stopped") | |||
| 169 | + | } | |||
| 168 | c, err := wlturbo.Connect(socket) | 170 | c, err := wlturbo.Connect(socket) | ||
| 169 | must(t, err) | 171 | must(t, err) | ||
| 170 | defer c.Close() | 172 | defer c.Close() | ||
M internal/e2e/feasibility_test.go +10 -4
| @@ -144,7 +144,9 @@ func TestRequestEventRoundtripAndDisconnect(t *testing.T) { | |||
| 144 | 144 | deadline := time.Now().Add(2 * time.Second) | |
| 145 | 145 | for { | |
| 146 | 146 | var n int | |
| 147 | - | d.Do(func() { n = server.LiveResources() }) | |
| 147 | + | if !d.Do(func() { n = server.LiveResources() }) { | |
| 148 | + | t.Fatal("display stopped") | |
| 149 | + | } | |
| 148 | 150 | if n == 0 { | |
| 149 | 151 | break | |
| 150 | 152 | } | |
| @@ -186,7 +188,9 @@ func TestPostEventAfterDisconnect(t *testing.T) { | |||
| 186 | 188 | deadline := time.Now().Add(2 * time.Second) | |
| 187 | 189 | for { | |
| 188 | 190 | var n int | |
| 189 | - | d.Do(func() { n = server.LiveResources() }) | |
| 191 | + | if !d.Do(func() { n = server.LiveResources() }) { | |
| 192 | + | t.Fatal("display stopped") | |
| 193 | + | } | |
| 190 | 194 | if n == 0 { | |
| 191 | 195 | break | |
| 192 | 196 | } | |
| @@ -195,7 +199,7 @@ func TestPostEventAfterDisconnect(t *testing.T) { | |||
| 195 | 199 | } | |
| 196 | 200 | time.Sleep(5 * time.Millisecond) | |
| 197 | 201 | } | |
| 198 | - | d.Do(func() { | |
| 202 | + | if !d.Do(func() { | |
| 199 | 203 | if stale == nil { | |
| 200 | 204 | t.Error("resource was not created") | |
| 201 | 205 | return | |
| @@ -208,7 +212,9 @@ func TestPostEventAfterDisconnect(t *testing.T) { | |||
| 208 | 212 | if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) { | |
| 209 | 213 | t.Error("destroyed resource returned nonzero metadata") | |
| 210 | 214 | } | |
| 211 | - | }) | |
| 215 | + | }) { | |
| 216 | + | t.Fatal("display stopped") | |
| 217 | + | } | |
| 212 | 218 | } | |
| 213 | 219 | ||
| 214 | 220 | // doneProxy records wl_callback.done. | |
| @@ -144,7 +144,9 @@ func TestRequestEventRoundtripAndDisconnect(t *testing.T) { | |||||
| 144 | deadline := time.Now().Add(2 * time.Second) | 144 | deadline := time.Now().Add(2 * time.Second) | ||
| 145 | for { | 145 | for { | ||
| 146 | var n int | 146 | var n int | ||
| 147 | - | d.Do(func() { n = server.LiveResources() }) | 147 | + | if !d.Do(func() { n = server.LiveResources() }) { |
| 148 | + | t.Fatal("display stopped") | |||
| 149 | + | } | |||
| 148 | if n == 0 { | 150 | if n == 0 { | ||
| 149 | break | 151 | break | ||
| 150 | } | 152 | } | ||
| @@ -186,7 +188,9 @@ func TestPostEventAfterDisconnect(t *testing.T) { | |||||
| 186 | deadline := time.Now().Add(2 * time.Second) | 188 | deadline := time.Now().Add(2 * time.Second) | ||
| 187 | for { | 189 | for { | ||
| 188 | var n int | 190 | var n int | ||
| 189 | - | d.Do(func() { n = server.LiveResources() }) | 191 | + | if !d.Do(func() { n = server.LiveResources() }) { |
| 192 | + | t.Fatal("display stopped") | |||
| 193 | + | } | |||
| 190 | if n == 0 { | 194 | if n == 0 { | ||
| 191 | break | 195 | break | ||
| 192 | } | 196 | } | ||
| @@ -195,7 +199,7 @@ func TestPostEventAfterDisconnect(t *testing.T) { | |||||
| 195 | } | 199 | } | ||
| 196 | time.Sleep(5 * time.Millisecond) | 200 | time.Sleep(5 * time.Millisecond) | ||
| 197 | } | 201 | } | ||
| 198 | - | d.Do(func() { | 202 | + | if !d.Do(func() { |
| 199 | if stale == nil { | 203 | if stale == nil { | ||
| 200 | t.Error("resource was not created") | 204 | t.Error("resource was not created") | ||
| 201 | return | 205 | return | ||
| @@ -208,7 +212,9 @@ func TestPostEventAfterDisconnect(t *testing.T) { | |||||
| 208 | if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) { | 212 | if stale.ID() != 0 || stale.Version() != 0 || stale.Client() != (server.Client{}) { | ||
| 209 | t.Error("destroyed resource returned nonzero metadata") | 213 | t.Error("destroyed resource returned nonzero metadata") | ||
| 210 | } | 214 | } | ||
| 211 | - | }) | 215 | + | }) { |
| 216 | + | t.Fatal("display stopped") | |||
| 217 | + | } | |||
| 212 | } | 218 | } | ||
| 213 | 219 | ||||
| 214 | // doneProxy records wl_callback.done. | 220 | // doneProxy records wl_callback.done. | ||
M server/display.go +34 -10
| @@ -43,9 +43,10 @@ var live = struct { | |||
| 43 | 43 | // Display owns a wl_display. All methods except Do must run on the goroutine | |
| 44 | 44 | // that calls Run, or before Run starts. | |
| 45 | 45 | type Display struct { | |
| 46 | - | c uintptr | |
| 47 | - | loop uintptr | |
| 48 | - | calls chan func() | |
| 46 | + | c uintptr | |
| 47 | + | loop uintptr | |
| 48 | + | calls chan func() | |
| 49 | + | stopped chan struct{} | |
| 49 | 50 | } | |
| 50 | 51 | ||
| 51 | 52 | func NewDisplay() (*Display, error) { | |
| @@ -56,7 +57,7 @@ func NewDisplay() (*Display, error) { | |||
| 56 | 57 | if c == 0 { | |
| 57 | 58 | return nil, errors.New("purego-libwayland: wl_display_create failed") | |
| 58 | 59 | } | |
| 59 | - | return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func())}, nil | |
| 60 | + | return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func()), stopped: make(chan struct{})}, nil | |
| 60 | 61 | } | |
| 61 | 62 | ||
| 62 | 63 | // AddSocketFD serves clients on an already bound, listening unix socket. | |
| @@ -82,11 +83,14 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) | |||
| 82 | 83 | func (d *Display) Run(ctx context.Context) error { | |
| 83 | 84 | runtime.LockOSThread() | |
| 84 | 85 | defer runtime.UnlockOSThread() | |
| 86 | + | defer close(d.stopped) | |
| 87 | + | defer func() { | |
| 88 | + | wlDisplayDestroyClients(d.c) | |
| 89 | + | wlDisplayDestroy(d.c) | |
| 90 | + | }() | |
| 85 | 91 | for { | |
| 86 | 92 | select { | |
| 87 | 93 | case <-ctx.Done(): | |
| 88 | - | wlDisplayDestroyClients(d.c) | |
| 89 | - | wlDisplayDestroy(d.c) | |
| 90 | 94 | return nil | |
| 91 | 95 | case fn := <-d.calls: | |
| 92 | 96 | fn() | |
| @@ -99,13 +103,33 @@ func (d *Display) Run(ctx context.Context) error { | |||
| 99 | 103 | } | |
| 100 | 104 | } | |
| 101 | 105 | ||
| 102 | - | // Do runs fn on the display goroutine and waits for it. | |
| 103 | - | func (d *Display) Do(fn func()) { | |
| 106 | + | // Do runs fn on the display goroutine and waits for it. It returns false if | |
| 107 | + | // the display loop has stopped (fn was not run, or its run was not confirmed). | |
| 108 | + | // Do must not be called from the display goroutine: it would deadlock. | |
| 109 | + | func (d *Display) Do(fn func()) bool { | |
| 104 | 110 | done := make(chan struct{}) | |
| 105 | - | d.calls <- func() { fn(); close(done) } | |
| 106 | - | <-done | |
| 111 | + | select { | |
| 112 | + | case d.calls <- func() { fn(); close(done) }: | |
| 113 | + | case <-d.stopped: | |
| 114 | + | return false | |
| 115 | + | } | |
| 116 | + | select { | |
| 117 | + | case <-done: | |
| 118 | + | return true | |
| 119 | + | case <-d.stopped: | |
| 120 | + | // If both are ready, prefer confirmation of a completed call. | |
| 121 | + | select { | |
| 122 | + | case <-done: | |
| 123 | + | return true | |
| 124 | + | default: | |
| 125 | + | return false | |
| 126 | + | } | |
| 127 | + | } | |
| 107 | 128 | } | |
| 108 | 129 | ||
| 130 | + | // Stopped is closed after Run returns and the display is destroyed. | |
| 131 | + | func (d *Display) Stopped() <-chan struct{} { return d.stopped } | |
| 132 | + | ||
| 109 | 133 | // LiveResources reports resources not yet destroyed. Display goroutine only. | |
| 110 | 134 | func LiveResources() int { return len(live.resources) } | |
| 111 | 135 | ||
| @@ -43,9 +43,10 @@ var live = struct { | |||||
| 43 | // Display owns a wl_display. All methods except Do must run on the goroutine | 43 | // Display owns a wl_display. All methods except Do must run on the goroutine | ||
| 44 | // that calls Run, or before Run starts. | 44 | // that calls Run, or before Run starts. | ||
| 45 | type Display struct { | 45 | type Display struct { | ||
| 46 | - | c uintptr | 46 | + | c uintptr |
| 47 | - | loop uintptr | 47 | + | loop uintptr |
| 48 | - | calls chan func() | 48 | + | calls chan func() |
| 49 | + | stopped chan struct{} | |||
| 49 | } | 50 | } | ||
| 50 | 51 | ||||
| 51 | func NewDisplay() (*Display, error) { | 52 | func NewDisplay() (*Display, error) { | ||
| @@ -56,7 +57,7 @@ func NewDisplay() (*Display, error) { | |||||
| 56 | if c == 0 { | 57 | if c == 0 { | ||
| 57 | return nil, errors.New("purego-libwayland: wl_display_create failed") | 58 | return nil, errors.New("purego-libwayland: wl_display_create failed") | ||
| 58 | } | 59 | } | ||
| 59 | - | return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func())}, nil | 60 | + | return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func()), stopped: make(chan struct{})}, nil |
| 60 | } | 61 | } | ||
| 61 | 62 | ||||
| 62 | // AddSocketFD serves clients on an already bound, listening unix socket. | 63 | // AddSocketFD serves clients on an already bound, listening unix socket. | ||
| @@ -82,11 +83,14 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) | |||||
| 82 | func (d *Display) Run(ctx context.Context) error { | 83 | func (d *Display) Run(ctx context.Context) error { | ||
| 83 | runtime.LockOSThread() | 84 | runtime.LockOSThread() | ||
| 84 | defer runtime.UnlockOSThread() | 85 | defer runtime.UnlockOSThread() | ||
| 86 | + | defer close(d.stopped) | |||
| 87 | + | defer func() { | |||
| 88 | + | wlDisplayDestroyClients(d.c) | |||
| 89 | + | wlDisplayDestroy(d.c) | |||
| 90 | + | }() | |||
| 85 | for { | 91 | for { | ||
| 86 | select { | 92 | select { | ||
| 87 | case <-ctx.Done(): | 93 | case <-ctx.Done(): | ||
| 88 | - | wlDisplayDestroyClients(d.c) | |||
| 89 | - | wlDisplayDestroy(d.c) | |||
| 90 | return nil | 94 | return nil | ||
| 91 | case fn := <-d.calls: | 95 | case fn := <-d.calls: | ||
| 92 | fn() | 96 | fn() | ||
| @@ -99,13 +103,33 @@ func (d *Display) Run(ctx context.Context) error { | |||||
| 99 | } | 103 | } | ||
| 100 | } | 104 | } | ||
| 101 | 105 | ||||
| 102 | - | // Do runs fn on the display goroutine and waits for it. | 106 | + | // Do runs fn on the display goroutine and waits for it. It returns false if |
| 103 | - | func (d *Display) Do(fn func()) { | 107 | + | // the display loop has stopped (fn was not run, or its run was not confirmed). |
| 108 | + | // Do must not be called from the display goroutine: it would deadlock. | |||
| 109 | + | func (d *Display) Do(fn func()) bool { | |||
| 104 | done := make(chan struct{}) | 110 | done := make(chan struct{}) | ||
| 105 | - | d.calls <- func() { fn(); close(done) } | 111 | + | select { |
| 106 | - | <-done | 112 | + | case d.calls <- func() { fn(); close(done) }: |
| 113 | + | case <-d.stopped: | |||
| 114 | + | return false | |||
| 115 | + | } | |||
| 116 | + | select { | |||
| 117 | + | case <-done: | |||
| 118 | + | return true | |||
| 119 | + | case <-d.stopped: | |||
| 120 | + | // If both are ready, prefer confirmation of a completed call. | |||
| 121 | + | select { | |||
| 122 | + | case <-done: | |||
| 123 | + | return true | |||
| 124 | + | default: | |||
| 125 | + | return false | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 107 | } | 128 | } | ||
| 108 | 129 | ||||
| 130 | + | // Stopped is closed after Run returns and the display is destroyed. | |||
| 131 | + | func (d *Display) Stopped() <-chan struct{} { return d.stopped } | |||
| 132 | + | ||||
| 109 | // LiveResources reports resources not yet destroyed. Display goroutine only. | 133 | // LiveResources reports resources not yet destroyed. Display goroutine only. | ||
| 110 | func LiveResources() int { return len(live.resources) } | 134 | func LiveResources() int { return len(live.resources) } | ||
| 111 | 135 | ||||
M server/display_test.go +81 -0
| @@ -1,8 +1,10 @@ | |||
| 1 | 1 | package server | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "golang.org/x/sys/unix" | |
| 5 | 6 | "testing" | |
| 7 | + | "time" | |
| 6 | 8 | "unsafe" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| @@ -30,3 +32,82 @@ func TestNoHandlerClosesFDs(t *testing.T) { | |||
| 30 | 32 | } | |
| 31 | 33 | } | |
| 32 | 34 | } | |
| 35 | + | ||
| 36 | + | func TestDoStopped(t *testing.T) { | |
| 37 | + | d, err := NewDisplay() | |
| 38 | + | if err != nil { | |
| 39 | + | t.Fatal(err) | |
| 40 | + | } | |
| 41 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 42 | + | returned := make(chan error, 1) | |
| 43 | + | go func() { returned <- d.Run(ctx) }() | |
| 44 | + | ran := false | |
| 45 | + | if !d.Do(func() { ran = true }) || !ran { | |
| 46 | + | t.Fatal("call did not run") | |
| 47 | + | } | |
| 48 | + | cancel() | |
| 49 | + | select { | |
| 50 | + | case err := <-returned: | |
| 51 | + | if err != nil { | |
| 52 | + | t.Fatal(err) | |
| 53 | + | } | |
| 54 | + | case <-time.After(3 * time.Second): | |
| 55 | + | t.Fatal("Run did not return") | |
| 56 | + | } | |
| 57 | + | select { | |
| 58 | + | case <-d.Stopped(): | |
| 59 | + | default: | |
| 60 | + | t.Fatal("Stopped not closed") | |
| 61 | + | } | |
| 62 | + | result := make(chan bool, 1) | |
| 63 | + | go func() { result <- d.Do(func() { t.Error("call ran after stop") }) }() | |
| 64 | + | select { | |
| 65 | + | case ok := <-result: | |
| 66 | + | if ok { | |
| 67 | + | t.Fatal("call confirmed after stop") | |
| 68 | + | } | |
| 69 | + | case <-time.After(100 * time.Millisecond): | |
| 70 | + | t.Fatal("Do blocked after stop") | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func TestDoBlockedDuringCancellation(t *testing.T) { | |
| 75 | + | d, err := NewDisplay() | |
| 76 | + | if err != nil { | |
| 77 | + | t.Fatal(err) | |
| 78 | + | } | |
| 79 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 80 | + | returned := make(chan error, 1) | |
| 81 | + | go func() { returned <- d.Run(ctx) }() | |
| 82 | + | entered := make(chan struct{}) | |
| 83 | + | release := make(chan struct{}) | |
| 84 | + | first := make(chan bool, 1) | |
| 85 | + | go func() { first <- d.Do(func() { close(entered); <-release }) }() | |
| 86 | + | select { | |
| 87 | + | case <-entered: | |
| 88 | + | case <-time.After(3 * time.Second): | |
| 89 | + | t.Fatal("first call not started") | |
| 90 | + | } | |
| 91 | + | result := make(chan bool, 1) | |
| 92 | + | go func() { result <- d.Do(func() {}) }() | |
| 93 | + | cancel() | |
| 94 | + | close(release) | |
| 95 | + | select { | |
| 96 | + | case <-first: | |
| 97 | + | case <-time.After(3 * time.Second): | |
| 98 | + | t.Fatal("first call blocked") | |
| 99 | + | } | |
| 100 | + | select { | |
| 101 | + | case err := <-returned: | |
| 102 | + | if err != nil { | |
| 103 | + | t.Fatal(err) | |
| 104 | + | } | |
| 105 | + | case <-time.After(3 * time.Second): | |
| 106 | + | t.Fatal("Run blocked") | |
| 107 | + | } | |
| 108 | + | select { | |
| 109 | + | case <-result: | |
| 110 | + | case <-time.After(100 * time.Millisecond): | |
| 111 | + | t.Fatal("blocked Do leaked") | |
| 112 | + | } | |
| 113 | + | } | |
| @@ -1,8 +1,10 @@ | |||||
| 1 | package server | 1 | package server | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "golang.org/x/sys/unix" | 5 | "golang.org/x/sys/unix" | ||
| 5 | "testing" | 6 | "testing" | ||
| 7 | + | "time" | |||
| 6 | "unsafe" | 8 | "unsafe" | ||
| 7 | ) | 9 | ) | ||
| 8 | 10 | ||||
| @@ -30,3 +32,82 @@ func TestNoHandlerClosesFDs(t *testing.T) { | |||||
| 30 | } | 32 | } | ||
| 31 | } | 33 | } | ||
| 32 | } | 34 | } | ||
| 35 | + | ||||
| 36 | + | func TestDoStopped(t *testing.T) { | |||
| 37 | + | d, err := NewDisplay() | |||
| 38 | + | if err != nil { | |||
| 39 | + | t.Fatal(err) | |||
| 40 | + | } | |||
| 41 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 42 | + | returned := make(chan error, 1) | |||
| 43 | + | go func() { returned <- d.Run(ctx) }() | |||
| 44 | + | ran := false | |||
| 45 | + | if !d.Do(func() { ran = true }) || !ran { | |||
| 46 | + | t.Fatal("call did not run") | |||
| 47 | + | } | |||
| 48 | + | cancel() | |||
| 49 | + | select { | |||
| 50 | + | case err := <-returned: | |||
| 51 | + | if err != nil { | |||
| 52 | + | t.Fatal(err) | |||
| 53 | + | } | |||
| 54 | + | case <-time.After(3 * time.Second): | |||
| 55 | + | t.Fatal("Run did not return") | |||
| 56 | + | } | |||
| 57 | + | select { | |||
| 58 | + | case <-d.Stopped(): | |||
| 59 | + | default: | |||
| 60 | + | t.Fatal("Stopped not closed") | |||
| 61 | + | } | |||
| 62 | + | result := make(chan bool, 1) | |||
| 63 | + | go func() { result <- d.Do(func() { t.Error("call ran after stop") }) }() | |||
| 64 | + | select { | |||
| 65 | + | case ok := <-result: | |||
| 66 | + | if ok { | |||
| 67 | + | t.Fatal("call confirmed after stop") | |||
| 68 | + | } | |||
| 69 | + | case <-time.After(100 * time.Millisecond): | |||
| 70 | + | t.Fatal("Do blocked after stop") | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func TestDoBlockedDuringCancellation(t *testing.T) { | |||
| 75 | + | d, err := NewDisplay() | |||
| 76 | + | if err != nil { | |||
| 77 | + | t.Fatal(err) | |||
| 78 | + | } | |||
| 79 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 80 | + | returned := make(chan error, 1) | |||
| 81 | + | go func() { returned <- d.Run(ctx) }() | |||
| 82 | + | entered := make(chan struct{}) | |||
| 83 | + | release := make(chan struct{}) | |||
| 84 | + | first := make(chan bool, 1) | |||
| 85 | + | go func() { first <- d.Do(func() { close(entered); <-release }) }() | |||
| 86 | + | select { | |||
| 87 | + | case <-entered: | |||
| 88 | + | case <-time.After(3 * time.Second): | |||
| 89 | + | t.Fatal("first call not started") | |||
| 90 | + | } | |||
| 91 | + | result := make(chan bool, 1) | |||
| 92 | + | go func() { result <- d.Do(func() {}) }() | |||
| 93 | + | cancel() | |||
| 94 | + | close(release) | |||
| 95 | + | select { | |||
| 96 | + | case <-first: | |||
| 97 | + | case <-time.After(3 * time.Second): | |||
| 98 | + | t.Fatal("first call blocked") | |||
| 99 | + | } | |||
| 100 | + | select { | |||
| 101 | + | case err := <-returned: | |||
| 102 | + | if err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | case <-time.After(3 * time.Second): | |||
| 106 | + | t.Fatal("Run blocked") | |||
| 107 | + | } | |||
| 108 | + | select { | |||
| 109 | + | case <-result: | |||
| 110 | + | case <-time.After(100 * time.Millisecond): | |||
| 111 | + | t.Fatal("blocked Do leaked") | |||
| 112 | + | } | |||
| 113 | + | } | |||