Permalink
Browse files

Implement the tick event

This makes our tests less flaky, shorter, and more readable.

fixes #2988
  • Loading branch information...
stapelberg committed Sep 24, 2017
1 parent 14c8cf8 commit ce21de8ddea4468ffcaf966da763d02d4dbd1e8d
@@ -99,11 +99,12 @@ use constant TYPE_GET_BAR_CONFIG => 6;
use constant TYPE_GET_VERSION => 7;
use constant TYPE_GET_BINDING_MODES => 8;
use constant TYPE_GET_CONFIG => 9;
use constant TYPE_SEND_TICK => 10;
our %EXPORT_TAGS = ( 'all' => [
qw(i3 TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS
TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION
TYPE_GET_BINDING_MODES TYPE_GET_CONFIG)
TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK)
] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
@@ -120,6 +121,7 @@ my %events = (
barconfig_update => ($event_mask | 4),
binding => ($event_mask | 5),
shutdown => ($event_mask | 6),
tick => ($event_mask | 7),
_error => 0xFFFFFFFF,
);
@@ -519,6 +521,18 @@ sub get_config {
$self->message(TYPE_GET_CONFIG);
}
=head2 send_tick
Sends a tick event. Requires i3 >= 4.15
=cut
sub send_tick {
my ($self, $payload) = @_;
$self->_ensure_connection;
$self->message(TYPE_SEND_TICK, $payload);
}
=head2 command($content)
View
@@ -64,6 +64,7 @@ to do that).
| 7 | +GET_VERSION+ | <<_version_reply,VERSION>> | Gets the i3 version.
| 8 | +GET_BINDING_MODES+ | <<_binding_modes_reply,BINDING_MODES>> | Gets the names of all currently configured binding modes.
| 9 | +GET_CONFIG+ | <<_config_reply,CONFIG>> | Returns the last loaded i3 config.
| 10 | +SEND_TICK+ | <<_tick_reply,TICK>> | Sends a tick event with the specified payload.
|======================================================
So, a typical message could look like this:
@@ -126,6 +127,8 @@ BINDING_MODES (8)::
Reply to the GET_BINDING_MODES message.
GET_CONFIG (9)::
Reply to the GET_CONFIG message.
TICK (10)::
Reply to the SEND_TICK message.
[[_command_reply]]
=== COMMAND reply
@@ -637,6 +640,19 @@ which is a string containing the config file as loaded by i3 most recently.
{ "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" }
-------------------
[[_tick_reply]]
=== TICK reply
The reply is a map containing the "success" member. After the reply was
received, the tick event has been written to all IPC connections which subscribe
to tick events. UNIX sockets are usually buffered, but you can be certain that
once you receive the tick event you just triggered, you must have received all
events generated prior to the +SEND_TICK+ message (happened-before relation).
*Example:*
-------------------
{ "success": true }
-------------------
== Events
@@ -694,6 +710,10 @@ binding (5)::
mouse
shutdown (6)::
Sent when the ipc shuts down because of a restart or exit by user command
tick (7)::
Sent when the ipc client subscribes to the tick event (with +"first":
true+) or when any ipc client sends a SEND_TICK message (with +"first":
false+).
*Example:*
--------------------------------------------------------------------
@@ -866,6 +886,27 @@ because of a user action such as a +restart+ or +exit+ command. The +change
}
---------------------------
=== tick event
This event is triggered by a subscription to tick events or by a +SEND_TICK+
message.
*Example (upon subscription):*
--------------------------------------------------------------------------------
{
"first": true,
"payload": ""
}
--------------------------------------------------------------------------------
*Example (upon +SEND_TICK+ with a payload of +arbitrary string+):*
--------------------------------------------------------------------------------
{
"first": false,
"payload": "arbitrary string"
}
--------------------------------------------------------------------------------
== See also (existing libraries)
[[libraries]]
View
@@ -207,9 +207,11 @@ int main(int argc, char *argv[]) {
message_type = I3_IPC_MESSAGE_TYPE_GET_VERSION;
} else if (strcasecmp(optarg, "get_config") == 0) {
message_type = I3_IPC_MESSAGE_TYPE_GET_CONFIG;
} else if (strcasecmp(optarg, "send_tick") == 0) {
message_type = I3_IPC_MESSAGE_TYPE_SEND_TICK;
} else {
printf("Unknown message type\n");
printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config\n");
printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config, send_tick\n");
exit(EXIT_FAILURE);
}
} else if (o == 'q') {
View
@@ -60,6 +60,9 @@ typedef struct i3_ipc_header {
/** Request the raw last loaded i3 config. */
#define I3_IPC_MESSAGE_TYPE_GET_CONFIG 9
/** Send a tick event to all subscribers. */
#define I3_IPC_MESSAGE_TYPE_SEND_TICK 10
/*
* Messages from i3 to clients
*
@@ -74,6 +77,7 @@ typedef struct i3_ipc_header {
#define I3_IPC_REPLY_TYPE_VERSION 7
#define I3_IPC_REPLY_TYPE_BINDING_MODES 8
#define I3_IPC_REPLY_TYPE_CONFIG 9
#define I3_IPC_REPLY_TYPE_TICK 10
/*
* Events from i3 to clients. Events have the first bit set high.
@@ -101,3 +105,6 @@ typedef struct i3_ipc_header {
/** The shutdown event will be triggered when the ipc shuts down */
#define I3_IPC_EVENT_SHUTDOWN (I3_IPC_EVENT_MASK | 6)
/** The tick event will be sent upon a tick IPC message */
#define I3_IPC_EVENT_TICK (I3_IPC_EVENT_MASK | 7)
View
@@ -31,6 +31,10 @@ typedef struct ipc_client {
int num_events;
char **events;
/* For clients which subscribe to the tick event: whether the first tick
* event has been sent by i3. */
bool first_tick_sent;
TAILQ_ENTRY(ipc_client)
clients;
} ipc_client;
View
@@ -1046,8 +1046,9 @@ static int add_subscription(void *extra, const unsigned char *s,
memcpy(client->events[event], s, len);
DLOG("client is now subscribed to:\n");
for (int i = 0; i < client->num_events; i++)
for (int i = 0; i < client->num_events; i++) {
DLOG("event %s\n", client->events[i]);
}
DLOG("(done)\n");
return 1;
@@ -1099,6 +1100,25 @@ IPC_HANDLER(subscribe) {
yajl_free(p);
const char *reply = "{\"success\":true}";
ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply);
if (client->first_tick_sent) {
return;
}
bool is_tick = false;
for (int i = 0; i < client->num_events; i++) {
if (strcmp(client->events[i], "tick") == 0) {
is_tick = true;
break;
}
}
if (!is_tick) {
return;
}
client->first_tick_sent = true;
const char *payload = "{\"first\":true,\"payload\":\"\"}";
ipc_send_message(client->fd, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload);
}
/*
@@ -1122,9 +1142,35 @@ IPC_HANDLER(get_config) {
y(free);
}
/*
* Sends the tick event from the message payload to subscribers. Establishes a
* synchronization point in event-related tests.
*/
IPC_HANDLER(send_tick) {
yajl_gen gen = ygenalloc();
y(map_open);
ystr("payload");
yajl_gen_string(gen, (unsigned char *)message, message_size);
y(map_close);
const unsigned char *payload;
ylength length;
y(get_buf, &payload, &length);
ipc_send_event("tick", I3_IPC_EVENT_TICK, (const char *)payload);
y(free);
const char *reply = "{\"success\":true}";
ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply);
DLOG("Sent tick event\n");
}
/* The index of each callback function corresponds to the numeric
* value of the message type (see include/i3/ipc.h) */
handler_t handlers[10] = {
handler_t handlers[11] = {
handle_run_command,
handle_get_workspaces,
handle_subscribe,
@@ -1135,6 +1181,7 @@ handler_t handlers[10] = {
handle_get_version,
handle_get_binding_modes,
handle_get_config,
handle_send_tick,
};
/*
View
@@ -47,6 +47,8 @@ our @EXPORT = qw(
wait_for_unmap
$x
kill_all_windows
events_for
listen_for_binding
);
=head1 NAME
@@ -900,6 +902,86 @@ sub kill_all_windows {
cmd '[title=".*"] kill';
}
=head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ])
Helper function which returns an array containing all events of type $rettype
which were generated by i3 while $subscribecb was running.
Set $eventcbs to subscribe to multiple event types and/or perform your own event
aggregation.
=cut
sub events_for {
my ($subscribecb, $rettype, $eventcbs) = @_;
my @events;
$eventcbs //= {};
if (defined($rettype)) {
$eventcbs->{$rettype} = sub { push @events, shift };
}
my $subscribed = AnyEvent->condvar;
my $flushed = AnyEvent->condvar;
$eventcbs->{tick} = sub {
my ($event) = @_;
if ($event->{first}) {
$subscribed->send($event);
} else {
$flushed->send($event);
}
};
my $i3 = i3(get_socket_path(0));
$i3->connect->recv;
$i3->subscribe($eventcbs)->recv;
$subscribed->recv;
# Subscription established, run the callback.
$subscribecb->();
# Now generate a tick event, which we know we’ll receive (and at which point
# all other events have been received).
my $nonce = int(rand(255)) + 1;
$i3->send_tick($nonce);
my $tick = $flushed->recv;
$tester->is_eq($tick->{payload}, $nonce, 'tick nonce received');
return @events;
}
=head2 listen_for_binding($cb)
Helper function to evaluate whether sending KeyPress/KeyRelease events via XTEST
triggers an i3 key binding or not. Expects key bindings to be configured in the
form “bindsym <binding> nop <binding>”, e.g. “bindsym Mod4+Return nop
Mod4+Return”.
is(listen_for_binding(
sub {
xtest_key_press(133); # Super_L
xtest_key_press(36); # Return
xtest_key_release(36); # Return
xtest_key_release(133); # Super_L
xtest_sync_with_i3;
},
),
'Mod4+Return',
'triggered the "Mod4+Return" keybinding');
=cut
sub listen_for_binding {
my ($cb) = @_;
my $triggered = AnyEvent->condvar;
my @events = events_for(
$cb,
'binding');
$tester->is_eq(scalar @events, 1, 'Received precisely one event');
$tester->is_eq($events[0]->{change}, 'run', 'change is "run"');
# We look at the command (which is “nop <binding>”) because that is easier
# than re-assembling the string representation of $event->{binding}.
my $command = $events[0]->{binding}->{command};
$command =~ s/^nop //g;
return $command;
}
=head1 AUTHOR
Michael Stapelberg <michael@i3wm.org>
Oops, something went wrong.

0 comments on commit ce21de8

Please sign in to comment.