1package wayland
2
3import (
4 "testing"
5 "time"
6
7 ext "github.com/bnema/go-wayland-bindings/server/extworkspace"
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/bnema/wlturbo"
10)
11
12// protectedWorkspaceEvents is a real wire client recorder. Child inventory
13// proxies decode names as well as opcodes so unlock refresh is verified.
14type protectedWorkspaceEvents struct {
15 wlturbo.BaseProxy
16 client *wlturbo.Display
17 handles []uint32
18 names []string
19 events []uint32
20}
21
22func (p *protectedWorkspaceEvents) Dispatch(e *wlturbo.Event) {
23 p.events = append(p.events, uint32(e.Opcode))
24 if e.Opcode != uint16(ext.ExtWorkspaceManagerV1EventWorkspaceGroup) && e.Opcode != uint16(ext.ExtWorkspaceManagerV1EventWorkspace) {
25 return
26 }
27 id := e.Uint32()
28 group := e.Opcode == uint16(ext.ExtWorkspaceManagerV1EventWorkspaceGroup)
29 child := &protectedWorkspaceChildEvents{owner: p, group: group}
30 child.SetID(id)
31 registerWireProxy(p.client, child)
32 if !group {
33 p.handles = append(p.handles, id)
34 }
35}
36
37type protectedWorkspaceChildEvents struct {
38 wlturbo.BaseProxy
39 owner *protectedWorkspaceEvents
40 group bool
41}
42
43func (p *protectedWorkspaceChildEvents) Dispatch(e *wlturbo.Event) {
44 kind := uint32(2)
45 if p.group {
46 kind = 1
47 }
48 p.owner.events = append(p.owner.events, kind<<8|uint32(e.Opcode))
49 if !p.group && e.Opcode == uint16(ext.ExtWorkspaceHandleV1EventName) {
50 p.owner.names = append(p.owner.names, e.String())
51 }
52}
53
54func bindProtectedWorkspaceRecorder(t *testing.T, c *wlturbo.Display) *protectedWorkspaceEvents {
55 t.Helper()
56 p := &protectedWorkspaceEvents{client: c}
57 p.SetID(bindProtocol(t, c, "ext_workspace_manager_v1"))
58 registerWireProxy(c, p)
59 roundtrip(t, c)
60 return p
61}
62
63func TestProtectedWorkspaceInventoryAndActivation(t *testing.T) {
64 s, events, _, dir := lifecycleServer(t)
65 state := installSecurity(t, s)
66 snapshot := ports.Workspaces{Outputs: []ports.WorkspaceOutput{{Name: "HEADLESS-1", Workspaces: []ports.WorkspaceInfo{{ID: 42, Name: "workspace-before", Active: true}}}}}
67 if !s.display.Do(func() { s.updateWorkspaces(snapshot) }) {
68 t.Fatal("display stopped")
69 }
70 existingClient := protocolClient(t, s, dir)
71 existing := bindProtectedWorkspaceRecorder(t, existingClient)
72 if len(existing.handles) != 1 || len(existing.names) != 1 || existing.names[0] != "workspace-before" {
73 t.Fatalf("unlocked inventory: %+v", existing)
74 }
75 handle := existing.handles[0]
76 existing.events, existing.names = nil, nil
77 // A pre-protection uncommitted activation is discarded by transition refresh.
78 requestProtocol(t, existingClient, handle, ext.ExtWorkspaceHandleV1RequestActivate)
79 roundtrip(t, existingClient)
80 protect(t, s, state, 1, true)
81 if !s.display.Do(func() { s.updateWorkspaceManagers(s.workspaceSnapshot) }) {
82 t.Fatal("display stopped")
83 }
84 snapshot.Outputs[0].Workspaces[0].Name = "workspace-current"
85 snapshot.Outputs[0].Workspaces[0].Active = false
86 snapshot.Outputs[0].Workspaces[0].Hidden = true
87 if !s.display.Do(func() { s.updateWorkspaces(snapshot) }) {
88 t.Fatal("display stopped")
89 }
90 lateClient := protocolClient(t, s, dir)
91 late := bindProtectedWorkspaceRecorder(t, lateClient)
92 roundtrip(t, existingClient)
93 if len(existing.events) != 0 || len(late.events) != 0 || len(late.handles) != 0 {
94 t.Fatalf("protected inventory: existing=%v late=%v", existing.events, late.events)
95 }
96 // Commit cannot admit pre-lock pending requests; requests made while locked
97 // must not linger and execute after unlock either.
98 requestProtocol(t, existingClient, existing.ID(), ext.ExtWorkspaceManagerV1RequestCommit)
99 requestProtocol(t, existingClient, handle, ext.ExtWorkspaceHandleV1RequestActivate)
100 roundtrip(t, existingClient)
101 select {
102 case ev := <-events:
103 t.Fatalf("protected activation emitted: %v", ev)
104 default:
105 }
106 protect(t, s, state, 2, false)
107 if !s.display.Do(func() { s.updateWorkspaceManagers(s.workspaceSnapshot) }) {
108 t.Fatal("display stopped")
109 }
110 roundtrip(t, existingClient, lateClient)
111 for _, p := range []*protectedWorkspaceEvents{existing, late} {
112 if len(p.names) != 1 || p.names[0] != "workspace-current" {
113 t.Fatalf("unlock names %v", p.names)
114 }
115 }
116 requestProtocol(t, existingClient, existing.ID(), ext.ExtWorkspaceManagerV1RequestCommit)
117 roundtrip(t, existingClient)
118 select {
119 case ev := <-events:
120 t.Fatalf("protected activation survived unlock: %v", ev)
121 default:
122 }
123 requestProtocol(t, existingClient, handle, ext.ExtWorkspaceHandleV1RequestActivate)
124 requestProtocol(t, existingClient, existing.ID(), ext.ExtWorkspaceManagerV1RequestCommit)
125 roundtrip(t, existingClient)
126 select {
127 case ev := <-events:
128 activation, ok := ev.(ports.WorkspaceActivate)
129 if !ok || len(activation.IDs) != 1 || activation.IDs[0] != 42 {
130 t.Fatalf("unlocked activation: %v", ev)
131 }
132 case <-time.After(2 * time.Second):
133 t.Fatal("unlocked activation not emitted")
134 }
135}