1package main
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/bnema/go-wayland-bindings/spec"
11)
12
13const customXML = `<?xml version="1.0" encoding="UTF-8"?>
14<protocol name="custom_thing">
15 <copyright>Test copyright.</copyright>
16 <interface name="custom_thing" version="1">
17 <request name="attach">
18 <arg name="surface" type="object" interface="wl_surface"/>
19 </request>
20 <event name="done"/>
21 </interface>
22</protocol>
23`
24
25func writeXML(t *testing.T, body string) string {
26 t.Helper()
27 p := filepath.Join(t.TempDir(), "custom.xml")
28 if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
29 t.Fatal(err)
30 }
31 return p
32}
33
34func TestCustomResolvesFromManifest(t *testing.T) {
35 for side, want := range map[string]string{
36 "client": `"github.com/bnema/go-wayland-bindings/client/wayland"`,
37 "server": `wayland "github.com/bnema/go-wayland-bindings/server/wayland"`,
38 } {
39 t.Run(side, func(t *testing.T) {
40 out := filepath.Join(t.TempDir(), "x", "out.go")
41 args := []string{"-side", side, "-package", "custom", "-out", out, writeXML(t, customXML)}
42 if err := run(args, &bytes.Buffer{}); err != nil {
43 t.Fatal(err)
44 }
45 b, err := os.ReadFile(out)
46 if err != nil {
47 t.Fatal(err)
48 }
49 if !strings.Contains(string(b), want) {
50 t.Errorf("output lacks import %s:\n%s", want, b)
51 }
52 if !strings.Contains(string(b), "Code generated by wlbgen") {
53 t.Error("header does not name wlbgen")
54 }
55 if !strings.Contains(string(b), "// Test copyright.\n") || strings.Contains(string(b), "SPDX") {
56 t.Errorf("custom output must keep the XML copyright and add no SPDX line:\n%s", b[:300])
57 }
58 })
59 }
60}
61
62func TestCustomImportOverride(t *testing.T) {
63 out := filepath.Join(t.TempDir(), "out.go")
64 args := []string{"-side", "server", "-package", "custom", "-out", out,
65 "-import", "wl_surface=example.com/mine/surfaces", writeXML(t, customXML)}
66 if err := run(args, &bytes.Buffer{}); err != nil {
67 t.Fatal(err)
68 }
69 b, _ := os.ReadFile(out)
70 if !strings.Contains(string(b), `surfaces "example.com/mine/surfaces"`) {
71 t.Errorf("override not used:\n%s", b)
72 }
73}
74
75func TestCustomUnknownInterfaceErrors(t *testing.T) {
76 xml := strings.ReplaceAll(customXML, "wl_surface", "no_such_iface")
77 for _, side := range []string{"client", "server"} {
78 t.Run(side, func(t *testing.T) {
79 out := filepath.Join(t.TempDir(), "out.go")
80 err := run([]string{"-side", side, "-package", "custom", "-out", out, writeXML(t, xml)}, &bytes.Buffer{})
81 if err == nil || !strings.Contains(err.Error(), "no_such_iface") {
82 t.Fatalf("want unresolved-interface error, got %v", err)
83 }
84 if _, statErr := os.Stat(out); statErr == nil {
85 t.Error("output written despite error")
86 }
87 })
88 }
89}
90
91func TestCustomBadSide(t *testing.T) {
92 out := filepath.Join(t.TempDir(), "out.go")
93 if err := run([]string{"-side", "both", "-package", "p", "-out", out, writeXML(t, customXML)}, &bytes.Buffer{}); err == nil {
94 t.Fatal("want error for bad -side")
95 }
96}
97
98// smallManifest builds a one-package manifest backed by customXML-like input.
99func smallManifest() (*spec.Manifest, func(string) ([]byte, error)) {
100 xml := strings.ReplaceAll(customXML, `<arg name="surface" type="object" interface="wl_surface"/>`, ``)
101 m := &spec.Manifest{Protocols: []spec.ProtocolEntry{{
102 Package: "custom", XML: "custom.xml", Path: "dir/custom-v1.xml", License: "MIT",
103 Interfaces: []spec.InterfaceEntry{{Name: "custom_thing", Version: 1}},
104 }}}
105 return m, func(string) ([]byte, error) { return []byte(xml), nil }
106}
107
108func TestAllDeterministic(t *testing.T) {
109 root := t.TempDir()
110 if err := generateAll(mustLoad(t), spec.Open, root); err != nil {
111 t.Fatal(err)
112 }
113 first := snapshot(t, root)
114 if len(first) != 2*len(mustLoad(t).Protocols) {
115 t.Fatalf("got %d files, want 2 per protocol", len(first))
116 }
117 if err := generateAll(mustLoad(t), spec.Open, root); err != nil {
118 t.Fatal(err)
119 }
120 second := snapshot(t, root)
121 for name, b := range first {
122 if !bytes.Equal(b, second[name]) {
123 t.Errorf("%s differs between runs", name)
124 }
125 }
126 if len(first) != len(second) {
127 t.Errorf("file count changed: %d -> %d", len(first), len(second))
128 }
129}
130
131func mustLoad(t *testing.T) *spec.Manifest {
132 t.Helper()
133 m, err := spec.Load()
134 if err != nil {
135 t.Fatal(err)
136 }
137 return m
138}
139
140func snapshot(t *testing.T, root string) map[string][]byte {
141 t.Helper()
142 out := map[string][]byte{}
143 err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
144 if err != nil || d.IsDir() {
145 return err
146 }
147 b, err := os.ReadFile(p)
148 out[p] = b
149 return err
150 })
151 if err != nil {
152 t.Fatal(err)
153 }
154 return out
155}
156
157func TestAllSourceNameAndLicense(t *testing.T) {
158 root := t.TempDir()
159 m, open := smallManifest()
160 m.Protocols[0].License = "LGPL-2.1-or-later"
161 if err := generateAll(m, open, root); err != nil {
162 t.Fatal(err)
163 }
164 b, err := os.ReadFile(filepath.Join(root, "client", "custom", "custom_generated.go"))
165 if err != nil {
166 t.Fatal(err)
167 }
168 for _, want := range []string{"custom-v1.xml", "SPDX-License-Identifier: LGPL-2.1-or-later"} {
169 if !strings.Contains(string(b), want) {
170 t.Errorf("client header lacks %q", want)
171 }
172 }
173 sb, err := os.ReadFile(filepath.Join(root, "server", "custom", "custom_generated.go"))
174 if err != nil {
175 t.Fatal(err)
176 }
177 for _, want := range []string{"SPDX-License-Identifier: LGPL-2.1-or-later", "Upstream protocol copyright and license:", "// Test copyright."} {
178 if !strings.Contains(string(sb), want) {
179 t.Errorf("server header lacks %q", want)
180 }
181 }
182}
183
184func TestAllErrorLeavesTreeUntouched(t *testing.T) {
185 root := t.TempDir()
186 m, open := smallManifest()
187 m.Protocols = append(m.Protocols, spec.ProtocolEntry{Package: "broken", XML: "broken.xml", Path: "broken.xml"})
188 failing := func(pkg string) ([]byte, error) {
189 if pkg == "broken" {
190 return []byte(`<protocol name="broken"><interface name="b" version="1"><event name="e"><arg name="o" type="object" interface="nowhere"/></event></interface></protocol>`), nil
191 }
192 return open(pkg)
193 }
194 stale := filepath.Join(root, "client", "gone")
195 if err := os.MkdirAll(stale, 0o755); err != nil {
196 t.Fatal(err)
197 }
198 if err := os.WriteFile(filepath.Join(stale, "gone_generated.go"), []byte("package gone\n"), 0o600); err != nil {
199 t.Fatal(err)
200 }
201 if err := generateAll(m, failing, root); err == nil {
202 t.Fatal("expected error")
203 }
204 got := snapshot(t, root)
205 if len(got) != 1 {
206 t.Fatalf("tree changed on error: %v", got)
207 }
208}
209
210func TestStaleGeneratedRemoved(t *testing.T) {
211 root := t.TempDir()
212 for _, side := range []string{"client", "server"} {
213 dir := filepath.Join(root, side, "gone")
214 if err := os.MkdirAll(dir, 0o755); err != nil {
215 t.Fatal(err)
216 }
217 if err := os.WriteFile(filepath.Join(dir, "gone_generated.go"), []byte("package gone\n"), 0o600); err != nil {
218 t.Fatal(err)
219 }
220 }
221 // A directory without a generated file is not ours and stays.
222 keep := filepath.Join(root, "client", "handwritten")
223 if err := os.MkdirAll(keep, 0o755); err != nil {
224 t.Fatal(err)
225 }
226 m, open := smallManifest()
227 if err := generateAll(m, open, root); err != nil {
228 t.Fatal(err)
229 }
230 for _, side := range []string{"client", "server"} {
231 if _, err := os.Stat(filepath.Join(root, side, "gone")); !os.IsNotExist(err) {
232 t.Errorf("%s/gone not removed (err=%v)", side, err)
233 }
234 }
235 if _, err := os.Stat(keep); err != nil {
236 t.Errorf("unrelated dir removed: %v", err)
237 }
238}
239
240func TestStaleWithHandWrittenFileErrors(t *testing.T) {
241 root := t.TempDir()
242 dir := filepath.Join(root, "client", "gone")
243 if err := os.MkdirAll(dir, 0o755); err != nil {
244 t.Fatal(err)
245 }
246 for _, f := range []string{"gone_generated.go", "table.go"} {
247 if err := os.WriteFile(filepath.Join(dir, f), []byte("package gone\n"), 0o600); err != nil {
248 t.Fatal(err)
249 }
250 }
251 m, open := smallManifest()
252 err := generateAll(m, open, root)
253 if err == nil || !strings.Contains(err.Error(), filepath.Join("client", "gone", "table.go")) {
254 t.Fatalf("want error listing table.go, got %v", err)
255 }
256 if _, err := os.Stat(filepath.Join(dir, "gone_generated.go")); err != nil {
257 t.Error("generated file deleted despite error")
258 }
259}