feat/data-dnd e05cb15

reacquire_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"sync"
7	"testing"
8	"testing/synctest"
9	"time"
10
11	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/stretchr/testify/mock"
14	"golang.org/x/sys/unix"
15)
16
17func TestMasterRetryBackoff(t *testing.T) {
18	var m masterRetry
19	now := time.Unix(0, 0)
20	if _, ok := m.next(now); ok {
21		t.Fatal("retry due before a refusal")
22	}
23	if m.refused(unix.EBUSY, false, now); m.pending {
24		t.Fatal("a busy refusal is retried by the busy path, not here")
25	}
26	if m.refused(unix.EACCES, true, now); m.pending {
27		t.Fatal("an enabled output is not retried")
28	}
29	m.refused(unix.EACCES, false, now)
30	// The deadline is fixed: asking later leaves less, never more.
31	if d, ok := m.next(now); !ok || d != masterRetryFirst {
32		t.Fatalf("first retry %v %v", d, ok)
33	}
34	if d, _ := m.next(now.Add(masterRetryFirst / 2)); d != masterRetryFirst/2 {
35		t.Fatalf("half-way %v", d)
36	}
37	if d, _ := m.next(now.Add(time.Hour)); d != 0 {
38		t.Fatalf("overdue %v", d)
39	}
40	m.fire()
41	if _, ok := m.next(now); ok {
42		t.Fatal("pending while an attempt runs")
43	}
44	m.attempts = 30
45	m.refused(unix.EPERM, false, now)
46	if d, _ := m.next(now); d != masterRetryMax {
47		t.Fatalf("capped retry %v", d)
48	}
49	m.attempts = masterRetryLimit
50	if !m.refused(unix.EACCES, false, now) || m.pending {
51		t.Fatal("retries past the limit")
52	}
53	m.done()
54	if _, ok := m.next(now); ok || m.attempts != 0 {
55		t.Fatal("retry after done")
56	}
57}
58
59// retryHarness runs an output whose enable modesets the kernel refuses
60// (lost master) until allow is called.
61type retryHarness struct {
62	o        *Output
63	active   chan bool
64	modesets func() int
65	allow    func()
66	stop     func()
67}
68
69func newRetryHarness(t *testing.T) retryHarness {
70	t.Helper()
71	var mu sync.Mutex
72	refuse := true
73	o, k, commits, commitMu := testOutputRules(t, func(_ *atomicReq, flags uint32) error {
74		mu.Lock()
75		defer mu.Unlock()
76		if refuse && flags&atomicAllowModes != 0 && flags&atomicTestOnly == 0 {
77			return unix.EACCES
78		}
79		return nil
80	})
81	o.cursor, o.tearing = nil, false
82	r := portsmocks.NewMockRenderer(t)
83	r.EXPECT().SetHDR(mock.Anything).Return().Maybe()
84	r.EXPECT().UseTarget(mock.Anything).Return().Maybe()
85	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
86	r.EXPECT().Close().Return().Once()
87	// The 10 s stats tick runs in long tests.
88	r.EXPECT().Trim(mock.Anything).Return(nil).Maybe()
89	r.EXPECT().TakeRedrawn().Return(0).Maybe()
90	k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe()
91	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
92	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
93	o.ping = make(chan chan struct{})
94	active := make(chan bool, 1)
95	active <- false
96	ctx, cancel := context.WithCancel(context.Background())
97	done := make(chan error, 1)
98	go func() {
99		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, discardSink(t), nil, nil)
100	}()
101	synctest.Wait()
102	return retryHarness{
103		o: o, active: active,
104		modesets: func() int {
105			commitMu.Lock()
106			defer commitMu.Unlock()
107			n := 0
108			for _, c := range *commits {
109				if c.flags&atomicAllowModes != 0 && c.flags&atomicTestOnly == 0 {
110					n++
111				}
112			}
113			return n
114		},
115		allow: func() { mu.Lock(); refuse = false; mu.Unlock() },
116		stop: func() {
117			cancel()
118			if err := <-done; err != nil && !errors.Is(err, context.Canceled) {
119				t.Fatal(err)
120			}
121		},
122	}
123}
124
125// The seat reports active before the kernel gives DRM master back: the
126// enable modeset is refused, then retried until it succeeds, without a
127// second seat change, even while other events (watchdog pings) keep
128// waking the loop faster than the backoff.
129func TestEnableRefusedForMasterIsRetried(t *testing.T) {
130	synctest.Test(t, func(t *testing.T) {
131		h := newRetryHarness(t)
132		h.active <- true
133		synctest.Wait()
134		if n := h.modesets(); n != 1 {
135			t.Fatalf("%d modesets after the refused enable", n)
136		}
137		// Let the backoff grow past the ping period.
138		pingFor := func(d time.Duration) {
139			for end := time.Now().Add(d); time.Now().Before(end); {
140				time.Sleep(100 * time.Millisecond)
141				h.o.Ping(context.Background())
142			}
143			synctest.Wait()
144		}
145		pingFor(3 * time.Second)
146		before := h.modesets()
147		if before < 6 {
148			t.Fatalf("%d modesets in 3 s of pings: the retry was pushed back", before)
149		}
150		h.allow()
151		pingFor(2 * masterRetryMax)
152		after := h.modesets()
153		if after != before+1 {
154			t.Fatalf("%d modesets before success, %d after", before, after)
155		}
156		// Once enabled, nothing retries any more.
157		pingFor(2 * masterRetryMax)
158		if n := h.modesets(); n != after {
159			t.Fatalf("%d modesets after success", n)
160		}
161		h.stop()
162	})
163}
164
165// A seat disable ends a pending retry: no modeset may follow the plane
166// reset before the next enable.
167func TestSeatDisableCancelsMasterRetry(t *testing.T) {
168	synctest.Test(t, func(t *testing.T) {
169		h := newRetryHarness(t)
170		h.active <- true
171		synctest.Wait()
172		ack := make(chan struct{})
173		h.o.seatDisable <- ack
174		<-ack
175		synctest.Wait()
176		n := h.modesets()
177		time.Sleep(10 * masterRetryMax)
178		synctest.Wait()
179		if got := h.modesets(); got != n {
180			t.Fatalf("%d modesets after the seat disable, want %d", got, n)
181		}
182		h.stop()
183	})
184}

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