detached f655a67

test(surface): restore strict FeedbackDone expectation and guard popup events

Revert TestHeadlessReconfigureWhilePending to its original expectation
(the loose form stays in the headlessLayer helper) and add
TestAllocPopupEvents for popup configure and popup_done.
commit
f655a676b561a031b569bb76fa2d1a6e4ddc0a4d
author
brice · · 2026-10-09 03:55 UTC
parent
2671432
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +51 -1
M alloc_test.go +50 -0
@@ -18,6 +18,7 @@ type countingHandler struct {
1818 neferclient.NopHandler
1919 fdReady, errs, frames int
2020 keys, pointers int
21+ configures, popupDone int
2122 lastText string // set only by the allocation-free comparison below
2223 }
2324
@@ -25,6 +26,9 @@ func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
2526 func (h *countingHandler) Error(error) { h.errs++ }
2627 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
2728
29+func (h *countingHandler) Configure(neferclient.SurfaceID, int32, int32) { h.configures++ }
30+func (h *countingHandler) PopupDone(neferclient.SurfaceID) { h.popupDone++ }
31+
2832 func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }
2933
3034 // Key records the text through a comparison that does not allocate: the
@@ -160,3 +164,49 @@ func TestAllocPresent(t *testing.T) {
160164 require.Positive(t, h.frames)
161165 require.Zero(t, h.errs)
162166 }
167+
168+// TestAllocPopupEvents measures the popup events in steady state: the
169+// xdg_popup configure (with its xdg_surface configure and the ack request it
170+// triggers) and popup_done.
171+func TestAllocPopupEvents(t *testing.T) {
172+ skipUnderRace(t)
173+ e := newPopupEnv(t, popupGlobals)
174+ p, err := e.c.NewPopup(neferclient.PopupConfig{Parent: e.layer, Place: place, Grab: true, Serial: 3})
175+ require.NoError(t, err)
176+ require.NoError(t, e.c.Roundtrip())
177+ xdg := e.xdgSurfaceOf(t, p.SurfaceObjectID())
178+ _, gp := e.getPopup(t, xdg)
179+ popup := gp.u32(0)
180+ e.srv.mu.Lock()
181+ e.srv.hook = func(uint32, uint16, []byte) {} // parse and discard, as the real peer's requests would be
182+ e.srv.mu.Unlock()
183+
184+ const rounds = 40
185+ var batch []byte
186+ for i := range rounds {
187+ batch = append(batch, frame(popup, 0, appendU32(appendU32(appendU32(appendU32(nil, 0), 0), 60), 40))...)
188+ batch = append(batch, frame(xdg, 0, appendU32(nil, uint32(i+1)))...)
189+ batch = append(batch, frame(popup, 1, nil)...) // popup_done
190+ }
191+ h := &countingHandler{}
192+ step := func() {
193+ if _, err := e.srv.conn.Write(batch); err != nil {
194+ t.Fatal(err)
195+ }
196+ for e.c.QueueLen() < 3*rounds {
197+ runtime.Gosched()
198+ }
199+ if err := e.c.Dispatch(h); err != nil {
200+ t.Fatal(err)
201+ }
202+ }
203+ step()
204+ step()
205+ before := h.popupDone
206+ allocs := testing.AllocsPerRun(20, step)
207+ t.Logf("allocs per Dispatch of %d popup events: %v", 3*rounds, allocs)
208+ require.Zero(t, allocs)
209+ require.Greater(t, h.popupDone, before)
210+ require.Positive(t, h.configures)
211+ require.Zero(t, h.errs)
212+}
@@ -18,6 +18,7 @@ type countingHandler struct {
18 neferclient.NopHandler18 neferclient.NopHandler
19 fdReady, errs, frames int19 fdReady, errs, frames int
20 keys, pointers int20 keys, pointers int
21+ configures, popupDone int
21 lastText string // set only by the allocation-free comparison below22 lastText string // set only by the allocation-free comparison below
22 }23 }
23 24
@@ -25,6 +26,9 @@ func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
25 func (h *countingHandler) Error(error) { h.errs++ }26 func (h *countingHandler) Error(error) { h.errs++ }
26 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }27 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
27 28
29+func (h *countingHandler) Configure(neferclient.SurfaceID, int32, int32) { h.configures++ }
30+func (h *countingHandler) PopupDone(neferclient.SurfaceID) { h.popupDone++ }
31+
28 func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }32 func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }
29 33
30 // Key records the text through a comparison that does not allocate: the34 // Key records the text through a comparison that does not allocate: the
@@ -160,3 +164,49 @@ func TestAllocPresent(t *testing.T) {
160 require.Positive(t, h.frames)164 require.Positive(t, h.frames)
161 require.Zero(t, h.errs)165 require.Zero(t, h.errs)
162 }166 }
167+
168+// TestAllocPopupEvents measures the popup events in steady state: the
169+// xdg_popup configure (with its xdg_surface configure and the ack request it
170+// triggers) and popup_done.
171+func TestAllocPopupEvents(t *testing.T) {
172+ skipUnderRace(t)
173+ e := newPopupEnv(t, popupGlobals)
174+ p, err := e.c.NewPopup(neferclient.PopupConfig{Parent: e.layer, Place: place, Grab: true, Serial: 3})
175+ require.NoError(t, err)
176+ require.NoError(t, e.c.Roundtrip())
177+ xdg := e.xdgSurfaceOf(t, p.SurfaceObjectID())
178+ _, gp := e.getPopup(t, xdg)
179+ popup := gp.u32(0)
180+ e.srv.mu.Lock()
181+ e.srv.hook = func(uint32, uint16, []byte) {} // parse and discard, as the real peer's requests would be
182+ e.srv.mu.Unlock()
183+
184+ const rounds = 40
185+ var batch []byte
186+ for i := range rounds {
187+ batch = append(batch, frame(popup, 0, appendU32(appendU32(appendU32(appendU32(nil, 0), 0), 60), 40))...)
188+ batch = append(batch, frame(xdg, 0, appendU32(nil, uint32(i+1)))...)
189+ batch = append(batch, frame(popup, 1, nil)...) // popup_done
190+ }
191+ h := &countingHandler{}
192+ step := func() {
193+ if _, err := e.srv.conn.Write(batch); err != nil {
194+ t.Fatal(err)
195+ }
196+ for e.c.QueueLen() < 3*rounds {
197+ runtime.Gosched()
198+ }
199+ if err := e.c.Dispatch(h); err != nil {
200+ t.Fatal(err)
201+ }
202+ }
203+ step()
204+ step()
205+ before := h.popupDone
206+ allocs := testing.AllocsPerRun(20, step)
207+ t.Logf("allocs per Dispatch of %d popup events: %v", 3*rounds, allocs)
208+ require.Zero(t, allocs)
209+ require.Greater(t, h.popupDone, before)
210+ require.Positive(t, h.configures)
211+ require.Zero(t, h.errs)
212+}
M headless_surface_test.go +1 -1
@@ -228,7 +228,7 @@ func TestHeadlessReconfigureWhilePending(t *testing.T) {
228228 var frames int
229229 hd := neferclientmocks.NewMockHandler(t)
230230 hd.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
231- hd.EXPECT().FeedbackDone(mock.Anything).Run(func(id neferclient.SurfaceID) { feedback = feedback || id == s.ID() }).Return().Maybe()
231+ hd.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { feedback = true }).Return().Maybe()
232232 hd.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()
233233 hd.EXPECT().Frame(s.ID()).Run(func(neferclient.SurfaceID) { frames++ }).Return().Maybe()
234234 dispatchUntil(t, c, hd, func() bool { return configured && feedback })
@@ -228,7 +228,7 @@ func TestHeadlessReconfigureWhilePending(t *testing.T) {
228 var frames int228 var frames int
229 hd := neferclientmocks.NewMockHandler(t)229 hd := neferclientmocks.NewMockHandler(t)
230 hd.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()230 hd.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
231- hd.EXPECT().FeedbackDone(mock.Anything).Run(func(id neferclient.SurfaceID) { feedback = feedback || id == s.ID() }).Return().Maybe()231+ hd.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { feedback = true }).Return().Maybe()
232 hd.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()232 hd.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()
233 hd.EXPECT().Frame(s.ID()).Run(func(neferclient.SurfaceID) { frames++ }).Return().Maybe()233 hd.EXPECT().Frame(s.ID()).Run(func(neferclient.SurfaceID) { frames++ }).Return().Maybe()
234 dispatchUntil(t, c, hd, func() bool { return configured && feedback })234 dispatchUntil(t, c, hd, func() bool { return configured && feedback })

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 1

Tags 4

↑↓ moveenter openesc close