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}