1package client
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11const requestArgsXML = `<protocol name="argsfix">
12<interface name="args_obj" version="1">
13 <request name="mixed">
14 <arg name="serial" type="uint"/><arg name="dx" type="int"/><arg name="fx" type="fixed"/>
15 <arg name="name" type="string"/><arg name="blob" type="array"/>
16 <arg name="peer" type="object" interface="args_obj" allow-null="true"/>
17 </request>
18 <request name="make"><arg name="id" type="new_id" interface="args_obj"/><arg name="n" type="uint"/></request>
19 <request name="give"><arg name="tag" type="string"/><arg name="fd" type="fd"/><arg name="size" type="uint"/></request>
20 <request name="destroy" type="destructor"/>
21</interface></protocol>`
22
23func generateArgsFixture(t *testing.T) string {
24 t.Helper()
25 path := filepath.Join(t.TempDir(), "argsfix.xml")
26 if err := os.WriteFile(path, []byte(requestArgsXML), 0600); err != nil {
27 t.Fatal(err)
28 }
29 s := NewScanner()
30 if err := s.ParseXML(path); err != nil {
31 t.Fatal(err)
32 }
33 b, err := s.Generate("argsfix")
34 if err != nil {
35 t.Fatal(err)
36 }
37 return string(b)
38}
39
40// Generated requests must use typed arguments only: no interface{} boxing
41// path and no uintptr placeholder for descriptors.
42func TestGeneratedRequestsUseTypedArgs(t *testing.T) {
43 text := generateArgsFixture(t)
44 for _, want := range []string{
45 "RequestArgs(wl.Request{Proxy: o, Opcode: 0, Name: \"args_obj.mixed\"}, wl.ArgUint(serial), wl.ArgInt(dx), wl.ArgFixed(fx), wl.ArgString(name), wl.ArgArray(blob), wl.ArgObject(arg5))",
46 "Child: child}, wl.ArgObject(child), wl.ArgUint(n))",
47 "FDs: []int{fd}}, wl.ArgString(tag), wl.ArgFD(), wl.ArgUint(size))",
48 "Destructor: true})",
49 } {
50 if !strings.Contains(text, want) {
51 t.Errorf("generated code missing %q\n%s", want, text)
52 }
53 }
54 for _, bad := range []string{".Request(wl.Request", "uintptr("} {
55 if strings.Contains(text, bad) {
56 t.Errorf("generated code still contains %q", bad)
57 }
58 }
59}
60
61// The generated requests compile against the transport, put the exact wire
62// bytes on the socket, and do not allocate for realistic numeric values.
63func TestGeneratedRequestsWireAndAllocations(t *testing.T) {
64 goBin, err := exec.LookPath("go")
65 if err != nil {
66 t.Skip("go unavailable")
67 }
68 dir := t.TempDir()
69 write := func(name, content string) {
70 if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil {
71 t.Fatal(err)
72 }
73 }
74 write("args.go", generateArgsFixture(t))
75 writeTempModule(t, dir, "example.com/argsfix", false)
76 write("args_test.go", `package argsfix
77
78import (
79 "bytes"
80 "net"
81 "os"
82 "testing"
83 "time"
84
85 "github.com/bnema/wlturbo"
86 "golang.org/x/sys/unix"
87)
88
89type sink struct{ last []byte }
90
91func (c *sink) Read([]byte) (int, error) { return 0, net.ErrClosed }
92func (c *sink) Write(p []byte) (int, error) {
93 c.last = append(c.last[:0], p...)
94 return len(p), nil
95}
96func (c *sink) Close() error { return nil }
97func (c *sink) LocalAddr() net.Addr { return a{} }
98func (c *sink) RemoteAddr() net.Addr { return a{} }
99func (c *sink) SetDeadline(time.Time) error { return nil }
100func (c *sink) SetReadDeadline(time.Time) error { return nil }
101func (c *sink) SetWriteDeadline(time.Time) error { return nil }
102
103type a struct{}
104
105func (a) Network() string { return "t" }
106func (a) String() string { return "t" }
107
108func setup(t testing.TB) (*wlturbo.Display, *sink, *ArgsObj) {
109 c := &sink{}
110 d, err := wlturbo.ConnectFromConn(c)
111 if err != nil {
112 t.Fatal(err)
113 }
114 o := NewArgsObj(d.Context())
115 o.SetID(d.AllocateID())
116 d.Context().Register(o)
117 return d, c, o
118}
119
120func TestMixedWire(t *testing.T) {
121 _, c, o := setup(t)
122 peer := NewArgsObj(o.Context())
123 peer.SetID(0x1234)
124 if err := o.Mixed(1000, -2, wlturbo.Fixed(512), "ab", []byte{9, 8, 7}, peer); err != nil {
125 t.Fatal(err)
126 }
127 want := []byte{
128 byte(o.ID()), 0, 0, 0, 0, 0, 40, 0,
129 0xe8, 3, 0, 0,
130 0xfe, 0xff, 0xff, 0xff,
131 0, 2, 0, 0,
132 3, 0, 0, 0, 'a', 'b', 0, 0,
133 3, 0, 0, 0, 9, 8, 7, 0,
134 0x34, 0x12, 0, 0,
135 }
136 if !bytes.Equal(c.last, want) {
137 t.Fatalf("got %x want %x", c.last, want)
138 }
139 if err := o.Mixed(1, 2, 3, "", nil, nil); err != nil {
140 t.Fatal(err)
141 }
142 if got := c.last[len(c.last)-4:]; !bytes.Equal(got, []byte{0, 0, 0, 0}) {
143 t.Fatalf("nil object = %x", got)
144 }
145}
146
147func TestMixedAllocFree(t *testing.T) {
148 _, _, o := setup(t)
149 blob := []byte("blob")
150 n := testing.AllocsPerRun(200, func() {
151 if err := o.Mixed(123456, -70000, wlturbo.Fixed(1<<20), "hello", blob, nil); err != nil {
152 t.Fatal(err)
153 }
154 })
155 if n != 0 {
156 t.Fatalf("Mixed: %v allocs/op, want 0", n)
157 }
158}
159
160func TestChildAndDestructor(t *testing.T) {
161 d, c, o := setup(t)
162 kid, err := o.Make(77777)
163 if err != nil {
164 t.Fatal(err)
165 }
166 if kid.ID() == 0 || c.last[8] != byte(kid.ID()) {
167 t.Fatalf("child id %d not on the wire: %x", kid.ID(), c.last)
168 }
169 if err := o.Destroy(); err != nil {
170 t.Fatal(err)
171 }
172 n := len(c.last)
173 c.last = c.last[:0]
174 if err := o.Destroy(); err == nil {
175 t.Fatal("second destructor succeeded")
176 }
177 if len(c.last) != 0 || n == 0 {
178 t.Fatal("wrote after destructor")
179 }
180 _ = d
181}
182
183func TestFDRequest(t *testing.T) {
184 sp, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
185 if err != nil {
186 t.Fatal(err)
187 }
188 fa, fb := os.NewFile(uintptr(sp[0]), "a"), os.NewFile(uintptr(sp[1]), "b")
189 ca, err := net.FileConn(fa)
190 fa.Close()
191 if err != nil {
192 fb.Close()
193 t.Fatal(err)
194 }
195 defer ca.Close()
196 cb, err := net.FileConn(fb)
197 fb.Close()
198 if err != nil {
199 t.Fatal(err)
200 }
201 defer cb.Close()
202 d, err := wlturbo.ConnectFromConn(ca)
203 if err != nil {
204 t.Fatal(err)
205 }
206 defer d.Close()
207 o := NewArgsObj(d.Context())
208 o.SetID(d.AllocateID())
209 d.Context().Register(o)
210 fd, err := unix.Open(os.DevNull, unix.O_RDONLY|unix.O_CLOEXEC, 0)
211 if err != nil {
212 t.Fatal(err)
213 }
214 if err := o.Give("t", fd, 5); err != nil {
215 t.Fatal(err)
216 }
217 if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err == nil {
218 t.Fatal("fd not closed after send")
219 }
220 // The connection first sends wl_display.get_registry (12 bytes).
221 buf := make([]byte, 12+8+8+4)
222 oob := make([]byte, unix.CmsgSpace(4))
223 n, oobn, _, _, err := cb.(*net.UnixConn).ReadMsgUnix(buf, oob)
224 if err != nil || n != len(buf) || oobn == 0 {
225 t.Fatalf("n=%d oobn=%d err=%v", n, oobn, err)
226 }
227 // The descriptor has no body word: string(8) + uint(4) only.
228 if buf[12+4] != 2 || buf[12+8+8] != 5 || buf[12+6] != 20 {
229 t.Fatalf("message = %x", buf[12:])
230 }
231}
232`)
233 cmd := exec.Command(goBin, "test", "./...")
234 cmd.Dir = dir
235 cmd.Env = tempModuleEnv()
236 if out, err := cmd.CombinedOutput(); err != nil {
237 t.Fatalf("generated request test: %v\n%s", err, out)
238 }
239}