detached b5629dc

test(wayland): release stalled displays before server shutdown on failure

The command ring tests stall the display goroutine; a failing assertion left
it blocked while the cleanup joined Server.Run, so a regression hung until
the package timeout instead of failing.
commit
b5629dc35c9f58f992997f25da10a171aae676fa
author
brice · · 2026-10-18 05:08 UTC
parent
90639c0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +27 -6
M internal/adapters/wayland/commands_test.go +27 -6
@@ -3,6 +3,7 @@ package wayland
33 import (
44 "context"
55 "reflect"
6+ "sync"
67 "testing"
78 "time"
89
@@ -78,9 +79,9 @@ func TestCommandRingMergesWhenFull(t *testing.T) {
7879 s, _, _, _, _, _ := presentServer(t)
7980 var got []ports.ClientCommand
8081 r := newCommandRing(s.display, func(q queuedCommand) { got = append(got, q.command()) })
81- release := make(chan struct{})
82+ release := releaseOnce(t)
8283 stalled := make(chan struct{})
83- s.display.Post(func() { close(stalled); <-release })
84+ s.display.Post(func() { close(stalled); <-release.ch })
8485 <-stalled
8586 for i := range commandRingSize - 1 {
8687 r.Push(queue(ports.PointerFocus{ID: ports.WindowID(i)}))
@@ -97,7 +98,7 @@ func TestCommandRingMergesWhenFull(t *testing.T) {
9798 case <-time.After(2 * time.Second):
9899 t.Fatal("motion push blocked on a full ring")
99100 }
100- close(release)
101+ release.do()
101102 var last ports.ClientCommand
102103 s.display.Do(func() { last = got[len(got)-1] })
103104 if m, ok := last.(ports.PointerMotionTo); !ok || m.DX != 1000 {
@@ -119,9 +120,12 @@ func TestForwarderDrainsWhileDisplayBusy(t *testing.T) {
119120 go func() { done <- s.Run(ctx) }()
120121 defer func() { cancel(); <-done }()
121122
122- release := make(chan struct{})
123+ // Deferred after the shutdown above, so it runs first: a failing
124+ // assertion never leaves the display stalled while Run is joined.
125+ release := releaseOnce(t)
126+ defer release.do()
123127 stalled := make(chan struct{})
124- s.display.Post(func() { close(stalled); <-release })
128+ s.display.Post(func() { close(stalled); <-release.ch })
125129 <-stalled
126130
127131 // Not mergeable: every command takes a slot.
@@ -155,7 +159,7 @@ func TestForwarderDrainsWhileDisplayBusy(t *testing.T) {
155159 t.Fatal("sender finished although the ring is full")
156160 default:
157161 }
158- close(release)
162+ release.do()
159163 select {
160164 case <-sent:
161165 case <-time.After(5 * time.Second):
@@ -184,3 +188,20 @@ func TestCommandThroughput(t *testing.T) {
184188 t.Fatalf("2000 motion commands took %v (%.0f/s); want well above 1000/s", d, 2000/d.Seconds())
185189 }
186190 }
191+
192+// release unblocks a stalled display goroutine once, from the test or its
193+// cleanup, so a failed assertion never hangs the server shutdown.
194+type release struct {
195+ ch chan struct{}
196+ once sync.Once
197+}
198+
199+func (r *release) do() { r.once.Do(func() { close(r.ch) }) }
200+
201+// releaseOnce returns a release that the test cleanup also fires, before
202+// the cleanups registered earlier (the server's).
203+func releaseOnce(t *testing.T) *release {
204+ r := &release{ch: make(chan struct{})}
205+ t.Cleanup(r.do)
206+ return r
207+}
@@ -3,6 +3,7 @@ package wayland
3 import (3 import (
4 "context"4 "context"
5 "reflect"5 "reflect"
6+ "sync"
6 "testing"7 "testing"
7 "time"8 "time"
8 9
@@ -78,9 +79,9 @@ func TestCommandRingMergesWhenFull(t *testing.T) {
78 s, _, _, _, _, _ := presentServer(t)79 s, _, _, _, _, _ := presentServer(t)
79 var got []ports.ClientCommand80 var got []ports.ClientCommand
80 r := newCommandRing(s.display, func(q queuedCommand) { got = append(got, q.command()) })81 r := newCommandRing(s.display, func(q queuedCommand) { got = append(got, q.command()) })
81- release := make(chan struct{})82+ release := releaseOnce(t)
82 stalled := make(chan struct{})83 stalled := make(chan struct{})
83- s.display.Post(func() { close(stalled); <-release })84+ s.display.Post(func() { close(stalled); <-release.ch })
84 <-stalled85 <-stalled
85 for i := range commandRingSize - 1 {86 for i := range commandRingSize - 1 {
86 r.Push(queue(ports.PointerFocus{ID: ports.WindowID(i)}))87 r.Push(queue(ports.PointerFocus{ID: ports.WindowID(i)}))
@@ -97,7 +98,7 @@ func TestCommandRingMergesWhenFull(t *testing.T) {
97 case <-time.After(2 * time.Second):98 case <-time.After(2 * time.Second):
98 t.Fatal("motion push blocked on a full ring")99 t.Fatal("motion push blocked on a full ring")
99 }100 }
100- close(release)101+ release.do()
101 var last ports.ClientCommand102 var last ports.ClientCommand
102 s.display.Do(func() { last = got[len(got)-1] })103 s.display.Do(func() { last = got[len(got)-1] })
103 if m, ok := last.(ports.PointerMotionTo); !ok || m.DX != 1000 {104 if m, ok := last.(ports.PointerMotionTo); !ok || m.DX != 1000 {
@@ -119,9 +120,12 @@ func TestForwarderDrainsWhileDisplayBusy(t *testing.T) {
119 go func() { done <- s.Run(ctx) }()120 go func() { done <- s.Run(ctx) }()
120 defer func() { cancel(); <-done }()121 defer func() { cancel(); <-done }()
121 122
122- release := make(chan struct{})123+ // Deferred after the shutdown above, so it runs first: a failing
124+ // assertion never leaves the display stalled while Run is joined.
125+ release := releaseOnce(t)
126+ defer release.do()
123 stalled := make(chan struct{})127 stalled := make(chan struct{})
124- s.display.Post(func() { close(stalled); <-release })128+ s.display.Post(func() { close(stalled); <-release.ch })
125 <-stalled129 <-stalled
126 130
127 // Not mergeable: every command takes a slot.131 // Not mergeable: every command takes a slot.
@@ -155,7 +159,7 @@ func TestForwarderDrainsWhileDisplayBusy(t *testing.T) {
155 t.Fatal("sender finished although the ring is full")159 t.Fatal("sender finished although the ring is full")
156 default:160 default:
157 }161 }
158- close(release)162+ release.do()
159 select {163 select {
160 case <-sent:164 case <-sent:
161 case <-time.After(5 * time.Second):165 case <-time.After(5 * time.Second):
@@ -184,3 +188,20 @@ func TestCommandThroughput(t *testing.T) {
184 t.Fatalf("2000 motion commands took %v (%.0f/s); want well above 1000/s", d, 2000/d.Seconds())188 t.Fatalf("2000 motion commands took %v (%.0f/s); want well above 1000/s", d, 2000/d.Seconds())
185 }189 }
186 }190 }
191+
192+// release unblocks a stalled display goroutine once, from the test or its
193+// cleanup, so a failed assertion never hangs the server shutdown.
194+type release struct {
195+ ch chan struct{}
196+ once sync.Once
197+}
198+
199+func (r *release) do() { r.once.Do(func() { close(r.ch) }) }
200+
201+// releaseOnce returns a release that the test cleanup also fires, before
202+// the cleanups registered earlier (the server's).
203+func releaseOnce(t *testing.T) *release {
204+ r := &release{ch: make(chan struct{})}
205+ t.Cleanup(r.do)
206+ return r
207+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close