1package xkbcommon
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "os"
8 "runtime"
9 "strings"
10 "testing"
11
12 "github.com/bnema/purego-xkbcommon/raw"
13)
14
15// fdKeymap compiles a pinned Wayland-format keymap from testdata.
16func fdKeymap(t *testing.T, c *Context, layout string) *Keymap {
17 t.Helper()
18 text, e := os.ReadFile("testdata/keymaps/" + layout + ".xkb")
19 if e != nil {
20 t.Fatal(e)
21 }
22 f := keymapFile(t, append(text, 0))
23 k, e := c.NewKeymapFD(int(f.Fd()), len(text)+1)
24 if e != nil {
25 t.Fatal(e)
26 }
27 t.Cleanup(func() { k.Close() })
28 return k
29}
30
31func assertNoAllocs(t *testing.T, name string, fn func()) {
32 t.Helper()
33 fn() // warm up lazy runtime state
34 if n := testing.AllocsPerRun(200, fn); n != 0 {
35 t.Errorf("%s: %v allocs/op, want 0", name, n)
36 } else {
37 t.Logf("%s: 0 allocs/op", name)
38 }
39}
40
41func TestHotPathAllocs(t *testing.T) {
42 c := live(t)
43 k := fdKeymap(t, c, "fr")
44 s := state(t, k)
45 code := WaylandKeycode(16)
46 var buf [MaxUTF8Buffer]byte
47 var fail error
48 check := func(e error) {
49 if e != nil {
50 fail = e
51 }
52 }
53
54 table, e := c.NewComposeTable("en_US.UTF-8")
55 if e != nil {
56 t.Fatal(e)
57 }
58 defer table.Close()
59 compose, e := table.NewState()
60 if e != nil {
61 t.Fatal(e)
62 }
63 defer compose.Close()
64
65 assertNoAllocs(t, "State.KeySym", func() { _, e := s.KeySym(code); check(e) })
66 assertNoAllocs(t, "State.UTF8Into", func() { _, e := s.UTF8Into(code, buf[:]); check(e) })
67 assertNoAllocs(t, "State.UpdateMask", func() { _, e := s.UpdateMask(1, 0, 2, 0, 0, 0); check(e) })
68 assertNoAllocs(t, "State.Layout", func() { _, e := s.Layout(); check(e) })
69 assertNoAllocs(t, "State.Mods", func() { _, e := s.Mods(); check(e) })
70 assertNoAllocs(t, "State.ModIndexActive", func() { _, e := s.ModIndexActive(0); check(e) })
71 assertNoAllocs(t, "State.ModNameActive", func() { _, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); check(e) })
72 assertNoAllocs(t, "Keymap.KeyRepeats", func() { _, e := k.KeyRepeats(code); check(e) })
73 assertNoAllocs(t, "KeysymNameInto", func() { _, e := KeysymNameInto(raw.XKB_KEY_a, buf[:]); check(e) })
74 assertNoAllocs(t, "compose key sequence", func() {
75 _, e := compose.Feed(raw.XKB_KEY_dead_circumflex)
76 check(e)
77 _, e = compose.Feed(raw.XKB_KEY_e)
78 check(e)
79 _, e = compose.Status()
80 check(e)
81 _, e = compose.KeySym()
82 check(e)
83 _, e = compose.UTF8Into(buf[:])
84 check(e)
85 check(compose.Reset())
86 })
87 if fail != nil {
88 t.Fatal(fail)
89 }
90}
91
92// TestHotPathValues guards the fixed-arity dispatch against argument/return
93// conversion mistakes.
94func TestHotPathValues(t *testing.T) {
95 c := live(t)
96 k := fdKeymap(t, c, "us")
97 s := state(t, k)
98 if r, e := k.KeyRepeats(WaylandKeycode(30)); e != nil || !r {
99 t.Fatalf("KeyRepeats(a) = %v, %v", r, e)
100 }
101 ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL)
102 if e != nil {
103 t.Fatal(e)
104 }
105 if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || a {
106 t.Fatalf("ctrl active before update: %v %v", a, e)
107 }
108 if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil {
109 t.Fatal(e)
110 }
111 if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a {
112 t.Fatalf("ctrl name active: %v %v", a, e)
113 }
114 if a, e := s.ModIndexActive(ctrl); e != nil || !a {
115 t.Fatalf("ctrl index active: %v %v", a, e)
116 }
117 if a, e := s.ModNameActive("NoSuchModifier"); e != nil || a {
118 t.Fatalf("unknown modifier active: %v %v", a, e)
119 }
120 var buf [16]byte
121 n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf[:])
122 if e != nil || string(buf[:n]) != "EuroSign" {
123 t.Fatalf("KeysymNameInto = %q %v", buf[:n], e)
124 }
125}
126
127func TestModNameActiveLongName(t *testing.T) {
128 c := live(t)
129 k := fdKeymap(t, c, "us")
130 s := state(t, k)
131 for _, n := range []int{62, 63, 64, 65, 200} {
132 name := strings.Repeat("x", n)
133 if a, e := s.ModNameActive(name); e != nil || a {
134 t.Fatalf("len %d: %v %v", n, a, e)
135 }
136 }
137 // A short name after a long one must not see stale scratch bytes.
138 ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL)
139 if e != nil {
140 t.Fatal(e)
141 }
142 if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil {
143 t.Fatal(e)
144 }
145 if a, e := s.ModNameActive(strings.Repeat("y", 63)); e != nil || a {
146 t.Fatal(a, e)
147 }
148 if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a {
149 t.Fatal(a, e)
150 }
151 if _, e := s.ModNameActive("a\x00b"); e == nil {
152 t.Fatal("embedded NUL accepted")
153 }
154}
155
156func TestKeysymNameIntoShortBuffer(t *testing.T) {
157 live(t)
158 buf := []byte{0xff, 0xff, 0xff}
159 n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf)
160 if !errors.Is(e, io.ErrShortBuffer) || n != len("EuroSign") || !bytes.Equal(buf, make([]byte, 3)) {
161 t.Fatalf("n=%d err=%v buf=%v", n, e, buf)
162 }
163 if _, e = KeysymNameInto(raw.XKB_KEY_a, nil); !errors.Is(e, ErrUTF8Buffer) {
164 t.Fatal(e)
165 }
166}
167
168func TestModIndexActiveInvalid(t *testing.T) {
169 c := live(t)
170 s := state(t, fdKeymap(t, c, "us"))
171 for _, idx := range []uint32{64, 1 << 20, ^uint32(0)} {
172 if a, e := s.ModIndexActive(idx); e != nil || a {
173 t.Fatalf("idx %d: %v %v", idx, a, e)
174 }
175 }
176 s.Close()
177 if _, e := s.ModIndexActive(0); !errors.Is(e, ErrClosed) {
178 t.Fatal(e)
179 }
180 if _, e := s.ModNameActive("Shift"); !errors.Is(e, ErrClosed) {
181 t.Fatal(e)
182 }
183}
184
185func TestNewKeymapRulesNames(t *testing.T) {
186 c := live(t)
187 // Every RMLVO component is exercised so each pointer in RuleNames must stay valid.
188 k, e := c.NewKeymapRules("evdev", "pc105", "us,fr", "dvorak,", "grp:alt_shift_toggle")
189 if e != nil {
190 t.Fatal(e)
191 }
192 defer k.Close()
193 if n, e := k.LayoutCount(); e != nil || n != 2 {
194 t.Fatalf("layouts %d %v", n, e)
195 }
196 s := state(t, k)
197 runtime.GC()
198 // Dvorak puts apostrophe on the evdev KEY_Q position.
199 if sym, e := s.KeySym(WaylandKeycode(16)); e != nil || sym != raw.XKB_KEY_apostrophe {
200 t.Fatalf("dvorak sym %#x %v", sym, e)
201 }
202 // Defaults (all empty) still work, and an unknown layout is rejected.
203 d, e := c.NewKeymapRules("", "", "", "", "")
204 if e != nil {
205 t.Fatal(e)
206 }
207 d.Close()
208 if _, e = c.NewKeymapRules("evdev", "pc105", "nosuchlayout", "", ""); e == nil {
209 t.Fatal("unknown layout accepted")
210 }
211}
212
213func TestZeroFuncsPanics(t *testing.T) {
214 defer func() {
215 if recover() == nil {
216 t.Fatal("zero raw.Funcs must panic, not call address 0")
217 }
218 }()
219 var f raw.Funcs
220 f.Xkb_keymap_num_layouts(0)
221}