Skip to content
This repository has been archived by the owner on Apr 18, 2023. It is now read-only.

Commit

Permalink
Implement the tick event
Browse files Browse the repository at this point in the history
This makes our tests less flaky, shorter, and more readable.

fixes #2988
  • Loading branch information
stapelberg committed Sep 30, 2017
1 parent 14c8cf8 commit ce21de8
Show file tree
Hide file tree
Showing 29 changed files with 463 additions and 740 deletions.
16 changes: 15 additions & 1 deletion AnyEvent-I3/lib/AnyEvent/I3.pm
Expand Up @@ -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} } );
Expand 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,
);

Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions docs/ipc
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:*
--------------------------------------------------------------------
Expand Down Expand Up @@ -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]]
Expand Down
4 changes: 3 additions & 1 deletion i3-msg/main.c
Expand Up @@ -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') {
Expand Down
7 changes: 7 additions & 0 deletions include/i3/ipc.h
Expand Up @@ -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
*
Expand All @@ -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.
Expand Down Expand Up @@ -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)
4 changes: 4 additions & 0 deletions include/ipc.h
Expand Up @@ -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;
Expand Down
51 changes: 49 additions & 2 deletions src/ipc.c
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/*
Expand All @@ -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,
Expand All @@ -1135,6 +1181,7 @@ handler_t handlers[10] = {
handle_get_version,
handle_get_binding_modes,
handle_get_config,
handle_send_tick,
};

/*
Expand Down
82 changes: 82 additions & 0 deletions testcases/lib/i3test.pm.in
Expand Up @@ -47,6 +47,8 @@ our @EXPORT = qw(
wait_for_unmap
$x
kill_all_windows
events_for
listen_for_binding
);

=head1 NAME
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit ce21de8

Please sign in to comment.