]> git.sur5r.net Git - i3/i3/blob - testcases/lib/i3test/XTEST.pm
9ec083b1aec9d54c04f434a07bdad64c595d9bf1
[i3/i3] / testcases / lib / i3test / XTEST.pm
1 package i3test::XTEST;
2 # vim:ts=4:sw=4:expandtab
3
4 use strict;
5 use warnings;
6 use v5.10;
7
8 use i3test i3_autostart => 0;
9 use AnyEvent::I3;
10 use ExtUtils::PkgConfig;
11
12 use Exporter ();
13 our @EXPORT = qw(
14     inlinec_connect
15     set_xkb_group
16     xtest_key_press
17     xtest_key_release
18     xtest_button_press
19     xtest_button_release
20     listen_for_binding
21     start_binding_capture
22     binding_events
23 );
24
25 =encoding utf-8
26
27 =head1 NAME
28
29 i3test::XTEST - Inline::C wrappers for xcb-xtest and xcb-xkb
30
31 =cut
32
33 # We need to use libxcb-xkb because xdotool cannot trigger ISO_Next_Group
34 # anymore: it contains code to set the XKB group to 1 and then restore the
35 # previous group, effectively rendering any keys that switch groups
36 # ineffective.
37 my %sn_config;
38 BEGIN {
39     %sn_config = ExtUtils::PkgConfig->find('xcb-xkb xcb-xtest');
40 }
41
42 use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags};
43 use Inline C => <<'END_OF_C_CODE';
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <string.h>
47 #include <unistd.h>
48 #include <stdbool.h>
49 #include <stdint.h>
50
51 #include <xcb/xcb.h>
52 #include <xcb/xkb.h>
53 #include <xcb/xtest.h>
54
55 static xcb_connection_t *conn = NULL;
56
57 bool inlinec_connect() {
58     int screen;
59
60     if ((conn = xcb_connect(NULL, &screen)) == NULL ||
61         xcb_connection_has_error(conn)) {
62         fprintf(stderr, "Could not connect to X11\n");
63         return false;
64     }
65
66     if (!xcb_get_extension_data(conn, &xcb_xkb_id)->present) {
67         fprintf(stderr, "XKB not present\n");
68         return false;
69     }
70
71     if (!xcb_get_extension_data(conn, &xcb_test_id)->present) {
72         fprintf(stderr, "XTEST not present\n");
73         return false;
74     }
75
76     xcb_generic_error_t *err = NULL;
77     xcb_xkb_use_extension_reply_t *usereply;
78     usereply = xcb_xkb_use_extension_reply(
79         conn, xcb_xkb_use_extension(conn, XCB_XKB_MAJOR_VERSION, XCB_XKB_MINOR_VERSION), &err);
80     if (err != NULL || usereply == NULL) {
81         fprintf(stderr, "xcb_xkb_use_extension() failed\n");
82         return false;
83     }
84     free(usereply);
85
86     return true;
87 }
88
89 // NOTE: while |group| should be a uint8_t, Inline::C will not define the
90 // function unless we use an int.
91 bool set_xkb_group(int group) {
92     xcb_generic_error_t *err = NULL;
93     // Needs libxcb ≥ 1.11 so that we have the following bug fix:
94     // http://cgit.freedesktop.org/xcb/proto/commit/src/xkb.xml?id=8d7ee5b6ba4cf343f7df70372a3e1f85b82aeed7
95     xcb_void_cookie_t cookie = xcb_xkb_latch_lock_state_checked(
96         conn,
97         XCB_XKB_ID_USE_CORE_KBD, /* deviceSpec */
98         0,                       /* affectModLocks */
99         0,                       /* modLocks */
100         1,                       /* lockGroup */
101         group,                   /* groupLock */
102         0,                       /* affectModLatches */
103         0,                       /* latchGroup */
104         0);                      /* groupLatch */
105     if ((err = xcb_request_check(conn, cookie)) != NULL) {
106         fprintf(stderr, "X error code %d\n", err->error_code);
107         return false;
108     }
109     return true;
110 }
111
112 bool xtest_input(int type, int detail, int x, int y) {
113     xcb_generic_error_t *err;
114     xcb_void_cookie_t cookie;
115
116     cookie = xcb_test_fake_input_checked(
117         conn,
118         type,             /* type */
119         detail,           /* detail */
120         XCB_CURRENT_TIME, /* time */
121         XCB_NONE,         /* root */
122         x,                /* rootX */
123         y,                /* rootY */
124         XCB_NONE);        /* deviceid */
125     if ((err = xcb_request_check(conn, cookie)) != NULL) {
126         fprintf(stderr, "X error code %d\n", err->error_code);
127         return false;
128     }
129
130     return true;
131 }
132
133 bool xtest_key(int type, int detail) {
134     return xtest_input(type, detail, 0, 0);
135 }
136
137 bool xtest_key_press(int detail) {
138     return xtest_key(XCB_KEY_PRESS, detail);
139 }
140
141 bool xtest_key_release(int detail) {
142     return xtest_key(XCB_KEY_RELEASE, detail);
143 }
144
145 bool xtest_button_press(int button, int x, int y) {
146     return xtest_input(XCB_BUTTON_PRESS, button, x, y);
147 }
148
149 bool xtest_button_release(int button, int x, int y) {
150     return xtest_input(XCB_BUTTON_RELEASE, button, x, y);
151 }
152
153 END_OF_C_CODE
154
155 sub import {
156     my ($class, %args) = @_;
157     ok(inlinec_connect(), 'Connect to X11, verify XKB and XTEST are present (via Inline::C)');
158     goto \&Exporter::import;
159 }
160
161 =head1 EXPORT
162
163 =cut
164
165 my $i3;
166 our @binding_events;
167
168 =head2 start_binding_capture()
169
170 Captures all binding events sent by i3 in the C<@binding_events> symbol, so
171 that you can verify the correct number of binding events was generated.
172
173   my $pid = launch_with_config($config);
174   start_binding_capture;
175   # …
176   sync_with_i3;
177   is(scalar @i3test::XTEST::binding_events, 2, 'Received exactly 2 binding events');
178
179 =cut
180
181 sub start_binding_capture {
182     # Store a copy of each binding event so that we can count the expected
183     # events in test cases.
184     $i3 = i3(get_socket_path());
185     $i3->connect()->recv;
186     $i3->subscribe({
187         binding => sub {
188             my ($event) = @_;
189             @binding_events = (@binding_events, $event);
190         },
191     })->recv;
192 }
193
194 =head2 listen_for_binding($cb)
195
196 Helper function to evaluate whether sending KeyPress/KeyRelease events via
197 XTEST triggers an i3 key binding or not (with a timeout of 0.5s). Expects key
198 bindings to be configured in the form “bindsym <binding> nop <binding>”, e.g.
199 “bindsym Mod4+Return nop Mod4+Return”.
200
201   is(listen_for_binding(
202       sub {
203           xtest_key_press(133); # Super_L
204           xtest_key_press(36); # Return
205           xtest_key_release(36); # Return
206           xtest_key_release(133); # Super_L
207       },
208       ),
209      'Mod4+Return',
210      'triggered the "Mod4+Return" keybinding');
211
212 =cut
213
214 sub listen_for_binding {
215     my ($cb) = @_;
216     my $triggered = AnyEvent->condvar;
217     my $i3 = i3(get_socket_path());
218     $i3->connect()->recv;
219     $i3->subscribe({
220         binding => sub {
221             my ($event) = @_;
222             return unless $event->{change} eq 'run';
223             # We look at the command (which is “nop <binding>”) because that is
224             # easier than re-assembling the string representation of
225             # $event->{binding}.
226             $triggered->send($event->{binding}->{command});
227         },
228     })->recv;
229
230     my $t;
231     $t = AnyEvent->timer(
232         after => 0.5,
233         cb => sub {
234             $triggered->send('timeout');
235         }
236     );
237
238     $cb->();
239
240     my $recv = $triggered->recv;
241     $recv =~ s/^nop //g;
242     return $recv;
243 }
244
245 =head2 set_xkb_group($group)
246
247 Changes the current XKB group from the default of 1 to C<$group>, which must be
248 one of 1, 2, 3, 4.
249
250 Returns false when there was an X11 error changing the group, true otherwise.
251
252 =head2 xtest_key_press($detail)
253
254 Sends a KeyPress event via XTEST, with the specified C<$detail>, i.e. key code.
255 Use C<xev(1)> to find key codes.
256
257 Returns false when there was an X11 error, true otherwise.
258
259 =head2 xtest_key_release($detail)
260
261 Sends a KeyRelease event via XTEST, with the specified C<$detail>, i.e. key code.
262 Use C<xev(1)> to find key codes.
263
264 Returns false when there was an X11 error, true otherwise.
265
266 =head2 xtest_button_press($button, $x, $y)
267
268 Sends a ButtonPress event via XTEST, with the specified C<$button>.
269
270 Returns false when there was an X11 error, true otherwise.
271
272 =head2 xtest_button_release($button, $x, $y)
273
274 Sends a ButtonRelease event via XTEST, with the specified C<$button>.
275
276 Returns false when there was an X11 error, true otherwise.
277
278 =head1 AUTHOR
279
280 Michael Stapelberg <michael@i3wm.org>
281
282 =cut
283
284 1