1package drm
2
3import (
4 "context"
5 "errors"
6 "os"
7 "testing"
8 "time"
9
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/stretchr/testify/mock"
13)
14
15func TestProtectedModesetWaitsForClearFence(t *testing.T) {
16 o, k, commits, mu := testOutputMu(t)
17 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
18 r := portsmocks.NewMockRenderer(t)
19 f, w := protectionFence(t, false)
20 rendered := make(chan struct{})
21 r.EXPECT().UseTarget(0).Return().Once()
22 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
23 close(rendered)
24 return f, nil
25 }).Once()
26 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
27 defer cancel()
28 done := make(chan error, 1)
29 go func() { done <- o.protectedModeset(ctx, r) }()
30 <-rendered
31 // The fence remains unreadable; activation must not be queued behind it.
32 select {
33 case err := <-done:
34 t.Fatalf("returned before clear fence signalled: %v", err)
35 case <-time.After(20 * time.Millisecond):
36 }
37 mu.Lock()
38 n := len(*commits)
39 mu.Unlock()
40 if n != 1 {
41 t.Errorf("committed activation before fence signalled: %d commits", n)
42 }
43 if _, err := w.Write([]byte{1}); err != nil {
44 t.Fatal(err)
45 }
46 if err := <-done; err != nil {
47 t.Fatal(err)
48 }
49 if len(*commits) != 2 || o.back != 1 || o.off {
50 t.Fatal("signalled clear did not activate")
51 }
52}
53
54func TestProtectedModesetPreservesUncommittedGPUReads(t *testing.T) {
55 for _, fail := range []bool{false, true} {
56 t.Run(map[bool]string{false: "success", true: "clear-failure"}[fail], func(t *testing.T) {
57 o, k, _ := testOutput(t)
58 clientFence, _ := protectionFence(t, false)
59 o.holdRead(clientFence)
60 owned := o.readFences[0]
61 r := portsmocks.NewMockRenderer(t)
62 r.EXPECT().UseTarget(0).Return().Once()
63 clearFence, _ := protectionFence(t, true)
64 var renderErr error
65 if fail {
66 renderErr = errors.New("clear failed")
67 } else {
68 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
69 }
70 r.EXPECT().Render(mock.Anything, mock.Anything).Return(clearFence, renderErr).Once()
71 err := o.protectedModeset(context.Background(), r)
72 if (err != nil) != fail {
73 t.Fatalf("clear result: %v", err)
74 }
75 if o.readDone() || len(o.readFences) != 1 || o.readFences[0] != owned {
76 t.Fatal("KMS detachment released an uncommitted GPU read")
77 }
78 if _, err := owned.Stat(); err != nil {
79 t.Fatalf("owned client read fence closed: %v", err)
80 }
81 o.dropRead() // test cleanup only
82 })
83 }
84}
85
86func TestProtectedCloseCommitFailureDoesNotRestoreDesktop(t *testing.T) {
87 o, k, commits := testOutput(t, errors.New("no DRM master"))
88 o.protected = true
89 o.saved = modeCrtc{crtcID: tCrtc, fbID: 123, modeValid: 1, mode: o.mode}
90 k.EXPECT().rmFB(mock.Anything).Return(nil)
91 o.Close()
92 if len(*commits) != 1 || !o.protected {
93 t.Fatal("failed close dropped protection or retried restore")
94 }
95 checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor)
96}