1package scanner
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 root, err := filepath.Abs("../..")
69 if err != nil {
70 t.Fatal(err)
71 }
72 dir := t.TempDir()
73 write := func(name, content string) {
74 if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil {
75 t.Fatal(err)
76 }
77 }
78 write("args.go", generateArgsFixture(t))
79 write("go.mod", "module example.com/argsfix\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => "+root+"\n")
80 write("args_test.go", `package argsfix
81
82import (
83 "bytes"
84 "net"
85 "os"
86 "testing"
87 "time"
88
89 "github.com/bnema/wlturbo"
90 "golang.org/x/sys/unix"
91)
92
93type sink struct{ last []byte }
94
95func (c *sink) Read([]byte) (int, error) { return 0, net.ErrClosed }
96func (c *sink) Write(p []byte) (int, error) {
97 c.last = append(c.last[:0], p...)
98 return len(p), nil
99}
100func (c *sink) Close() error { return nil }
101func (c *sink) LocalAddr() net.Addr { return a{} }
102func (c *sink) RemoteAddr() net.Addr { return a{} }
103func (c *sink) SetDeadline(time.Time) error { return nil }
104func (c *sink) SetReadDeadline(time.Time) error { return nil }
105func (c *sink) SetWriteDeadline(time.Time) error { return nil }
106
107type a struct{}
108
109func (a) Network() string { return "t" }
110func (a) String() string { return "t" }
111
112func setup(t testing.TB) (*wlturbo.Display, *sink, *ArgsObj) {
113 c := &sink{}
114 d, err := wlturbo.ConnectFromConn(c)
115 if err != nil {
116 t.Fatal(err)
117 }
118 o := NewArgsObj(d.Context())
119 o.SetID(d.AllocateID())
120 d.Context().Register(o)
121 return d, c, o
122}
123
124func TestMixedWire(t *testing.T) {
125 _, c, o := setup(t)
126 peer := NewArgsObj(o.Context())
127 peer.SetID(0x1234)
128 if err := o.Mixed(1000, -2, wlturbo.Fixed(512), "ab", []byte{9, 8, 7}, peer); err != nil {
129 t.Fatal(err)
130 }
131 want := []byte{
132 byte(o.ID()), 0, 0, 0, 0, 0, 40, 0,
133 0xe8, 3, 0, 0,
134 0xfe, 0xff, 0xff, 0xff,
135 0, 2, 0, 0,
136 3, 0, 0, 0, 'a', 'b', 0, 0,
137 3, 0, 0, 0, 9, 8, 7, 0,
138 0x34, 0x12, 0, 0,
139 }
140 if !bytes.Equal(c.last, want) {
141 t.Fatalf("got %x want %x", c.last, want)
142 }
143 if err := o.Mixed(1, 2, 3, "", nil, nil); err != nil {
144 t.Fatal(err)
145 }
146 if got := c.last[len(c.last)-4:]; !bytes.Equal(got, []byte{0, 0, 0, 0}) {
147 t.Fatalf("nil object = %x", got)
148 }
149}
150
151func TestMixedAllocFree(t *testing.T) {
152 _, _, o := setup(t)
153 blob := []byte("blob")
154 n := testing.AllocsPerRun(200, func() {
155 if err := o.Mixed(123456, -70000, wlturbo.Fixed(1<<20), "hello", blob, nil); err != nil {
156 t.Fatal(err)
157 }
158 })
159 if n != 0 {
160 t.Fatalf("Mixed: %v allocs/op, want 0", n)
161 }
162}
163
164func TestChildAndDestructor(t *testing.T) {
165 d, c, o := setup(t)
166 kid, err := o.Make(77777)
167 if err != nil {
168 t.Fatal(err)
169 }
170 if kid.ID() == 0 || c.last[8] != byte(kid.ID()) {
171 t.Fatalf("child id %d not on the wire: %x", kid.ID(), c.last)
172 }
173 if err := o.Destroy(); err != nil {
174 t.Fatal(err)
175 }
176 n := len(c.last)
177 c.last = c.last[:0]
178 if err := o.Destroy(); err == nil {
179 t.Fatal("second destructor succeeded")
180 }
181 if len(c.last) != 0 || n == 0 {
182 t.Fatal("wrote after destructor")
183 }
184 _ = d
185}
186
187func TestFDRequest(t *testing.T) {
188 sp, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
189 if err != nil {
190 t.Fatal(err)
191 }
192 fa, fb := os.NewFile(uintptr(sp[0]), "a"), os.NewFile(uintptr(sp[1]), "b")
193 ca, err := net.FileConn(fa)
194 fa.Close()
195 if err != nil {
196 fb.Close()
197 t.Fatal(err)
198 }
199 defer ca.Close()
200 cb, err := net.FileConn(fb)
201 fb.Close()
202 if err != nil {
203 t.Fatal(err)
204 }
205 defer cb.Close()
206 d, err := wlturbo.ConnectFromConn(ca)
207 if err != nil {
208 t.Fatal(err)
209 }
210 defer d.Close()
211 o := NewArgsObj(d.Context())
212 o.SetID(d.AllocateID())
213 d.Context().Register(o)
214 fd, err := unix.Open(os.DevNull, unix.O_RDONLY|unix.O_CLOEXEC, 0)
215 if err != nil {
216 t.Fatal(err)
217 }
218 if err := o.Give("t", fd, 5); err != nil {
219 t.Fatal(err)
220 }
221 if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err == nil {
222 t.Fatal("fd not closed after send")
223 }
224 // The connection first sends wl_display.get_registry (12 bytes).
225 buf := make([]byte, 12+8+8+4)
226 oob := make([]byte, unix.CmsgSpace(4))
227 n, oobn, _, _, err := cb.(*net.UnixConn).ReadMsgUnix(buf, oob)
228 if err != nil || n != len(buf) || oobn == 0 {
229 t.Fatalf("n=%d oobn=%d err=%v", n, oobn, err)
230 }
231 // The descriptor has no body word: string(8) + uint(4) only.
232 if buf[12+4] != 2 || buf[12+8+8] != 5 || buf[12+6] != 20 {
233 t.Fatalf("message = %x", buf[12:])
234 }
235}
236`)
237 cmd := exec.Command(goBin, "test", "./...")
238 cmd.Dir = dir
239 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
240 if out, err := cmd.CombinedOutput(); err != nil {
241 t.Fatalf("generated request test: %v\n%s", err, out)
242 }
243}