1package drm
2
3import (
4 "context"
5 "errors"
6 "maps"
7 "os"
8 "testing"
9
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/stretchr/testify/mock"
13 "golang.org/x/sys/unix"
14)
15
16func TestProtectedModesetPartialPlaneMetadataFailsClosed(t *testing.T) {
17 for _, role := range []string{"primary", "cursor", "overlay", "stray"} {
18 for _, missing := range []string{"FB_ID", "CRTC_ID"} {
19 t.Run(role+"/"+missing, func(t *testing.T) {
20 o, k, commits := testOutput(t)
21 props := maps.Clone(planeProps)
22 props[missing] = 0
23 p := &plane{id: tOverlay, props: props, crtc: tCrtc}
24 switch role {
25 case "primary":
26 o.primary.props = props
27 case "cursor":
28 o.cursor.plane.props = props
29 case "overlay":
30 o.overlay = p
31 o.stray = []*plane{p} // pickOverlay keeps the plane in stray.
32 case "stray":
33 o.stray = []*plane{p} // old master's overlay may still be here.
34 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
35 }
36 r := portsmocks.NewMockRenderer(t) // no GPU calls permitted
37 if err := o.protectedModeset(context.Background(), r); err == nil {
38 t.Fatal("partial metadata reported protection success")
39 }
40 if !o.protected || len(*commits) != 0 || o.off || o.back != 0 {
41 t.Fatal("unverified detachment committed, flipped or claimed off")
42 }
43 })
44 }
45 }
46}
47
48func TestProtectedModesetSkipsOwnedStrayDuplicates(t *testing.T) {
49 o, k, commits := testOutput(t)
50 o.overlay = &plane{id: tOverlay, props: planeProps}
51 o.stray = []*plane{o.overlay, o.primary, o.cursor.plane}
52 // Even if a fresh query would fail, cached owned-plane IDs suffice.
53 // No owned plane may be queried, during disable or activation.
54 k.EXPECT().objProps(mock.Anything, uint32(objPlane)).Return(nil, unix.EACCES).Maybe()
55 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
56 r := portsmocks.NewMockRenderer(t)
57 r.EXPECT().UseTarget(0).Return().Once()
58 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
59 if err := o.protectedModeset(context.Background(), r); err != nil {
60 t.Fatal(err)
61 }
62 k.AssertNotCalled(t, "objProps", mock.Anything, mock.Anything)
63 if len(*commits) != 2 {
64 t.Fatalf("commits %d", len(*commits))
65 }
66 checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor, tOverlay)
67}
68
69func TestProtectedModesetDisableSendsInactiveFormats(t *testing.T) {
70 for _, stage := range []string{"disable-failure", "clear-failure", "cancel", "off", "activate"} {
71 t.Run(stage, func(t *testing.T) {
72 var failures []error
73 if stage == "disable-failure" {
74 failures = []error{unix.EACCES}
75 }
76 o, k, _ := testOutput(t, failures...)
77 o.device, o.hdr.on = 42, true
78 feedback := make(chan ports.OutputFormats, 2)
79 o.formats = feedback
80 r := portsmocks.NewMockRenderer(t)
81 ctx, cancel := context.WithCancel(context.Background())
82 defer cancel()
83 if stage != "disable-failure" {
84 r.EXPECT().UseTarget(0).Return().Once()
85 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
86 // Disable feedback must precede GPU writes, not just return.
87 select {
88 case f := <-feedback:
89 if f.Output != o.conn.name || f.Device != 42 || len(f.Formats) != 0 || f.HDR != nil || !o.off {
90 t.Fatalf("not inactive feedback: %+v off=%v", f, o.off)
91 }
92 default:
93 t.Fatal("successful disable sent no inactive feedback")
94 }
95 if stage == "clear-failure" {
96 return nil, errors.New("clear failed")
97 }
98 if stage == "cancel" {
99 cancel()
100 }
101 return nil, nil
102 }).Once()
103 }
104 if stage == "off" {
105 o.wantOff = true
106 }
107 if stage == "activate" {
108 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
109 }
110 err := o.protectedModeset(ctx, r)
111 wantErr := stage == "disable-failure" || stage == "clear-failure" || stage == "cancel"
112 if (err != nil) != wantErr {
113 t.Fatalf("result %v", err)
114 }
115 if stage == "activate" {
116 select {
117 case f := <-feedback:
118 if f.HDR == nil || o.off {
119 t.Fatalf("activation did not send active feedback: %+v", f)
120 }
121 default:
122 t.Fatal("missing active feedback after activation")
123 }
124 }
125 if len(feedback) != 0 {
126 t.Fatal("unexpected feedback without a successful state transition")
127 }
128 })
129 }
130}