Introduce cmp_tree test function

Related to #3503
next
Orestis Floros 2018-11-06 15:03:06 +02:00
parent 32188d7b2c
commit 90c08a52f0
No known key found for this signature in database
GPG Key ID: E9AD9F32E401E38F
2 changed files with 318 additions and 0 deletions

View File

@ -53,6 +53,7 @@ our @EXPORT = qw(
events_for
listen_for_binding
is_net_wm_state_focused
cmp_tree
);
=head1 NAME
@ -1084,6 +1085,229 @@ sub is_net_wm_state_focused {
return 0;
}
=head2 cmp_tree([ $args ])
Compares the tree layout before and after an operation inside a subtest.
The following arguments can be passed:
=over 4
=item layout_before
Required argument. The initial layout to be created. For example,
'H[ V[ a* S[ b c ] d ] e ]' or 'V[a b] T[c d*]'.
The layout will be converted to a JSON file which will be passed to i3's
append_layout command.
The syntax's rules, assertions and limitations are:
=over 8
=item 1.
Upper case letters H, V, S, T mean horizontal, vertical, stacked and tabbed
layout respectively. They must be followed by an opening square bracket and must
be closed with a closing square bracket.
Each of the non-leaf containers is marked with their corresponding letter
followed by a number indicating the position of the container relative to other
containers of the same type. For example, 'H[V[xxx] V[xxx] H[xxx]]' will mark
the non-leaf containers as H1, V1, V2, H2.
=item 2.
Spaces are ignored.
=item 3.
Other alphanumeric characters mean a new window which uses the provided
character for its class and name. Eg 'H[a b]' will open windows with classes 'a'
and 'b' inside a horizontal split. Windows use a single character for their
class, eg 'H[xxx]' will open 3 windows with class 'x'.
=item 4.
Asterisks after a window mean that the window must be focused after the layout
is loaded. Currently, focusing non-leaf containers must be done manually, in the
callback (C<cb>) function.
=back
=item cb
Subroutine to be called after the layout provided by C<layout_before> is created
but before the resulting layout (C<layout_after>) is checked.
=item layout_after
Required argument. The final layout in which the tree is expected to be after
the callback is called. Uses the same syntax with C<layout_before>.
For non-leaf containers, their layout (horizontal, vertical, stacked, tabbed)
is compared with the corresponding letter (H, V, S, T).
For leaf containers, their name is compared with the provided alphanumeric.
=item ws
The workspace in which the layout will be created. Will switch focus to it. If
not provided, a new one is created.
=item msg
Message to prepend to the subtest's name. If not empty, it will be followed by ': '.
=item dont_kill
By default, all windows are killed before the C<layout_before> layout is loaded.
Set to 1 to avoid this.
=back
=cut
sub cmp_tree {
local $Test::Builder::Level = $Test::Builder::Level + 1;
my %args = @_;
my $ws = $args{ws};
if (defined($ws)) {
cmd "workspace $ws";
} else {
$ws = fresh_workspace;
}
my $msg = '';
if ($args{msg}) {
$msg = $args{msg} . ': ';
}
die unless $args{layout_before};
die unless $args{layout_after};
kill_all_windows unless $args{dont_kill};
my @windows = create_layout($args{layout_before});
Test::More::subtest $msg . $args{layout_before} . ' -> ' . $args{layout_after} => sub {
$args{cb}->(\@windows) if $args{cb};
verify_layout($args{layout_after}, $ws);
};
return @windows;
}
sub create_layout {
my $layout = shift;
my $focus;
my @windows = ();
my $r = '';
my $depth = 0;
my %layout_counts = (H => 0, V => 0, S => 0, T => 0);
foreach my $char (split('', $layout)) {
if ($char eq 'H') {
$r = $r . '{"layout": "splith",';
$r = $r . '"marks": ["H' . ++$layout_counts{H} . '"],';
} elsif ($char eq 'V') {
$r = $r . '{"layout": "splitv",';
$r = $r . '"marks": ["V' . ++$layout_counts{V} . '"],';
} elsif ($char eq 'S') {
$r = $r . '{"layout": "stacked",';
$r = $r . '"marks": ["S' . ++$layout_counts{S} . '"],';
} elsif ($char eq 'T') {
$r = $r . '{"layout": "tabbed",';
$r = $r . '"marks": ["T' . ++$layout_counts{T} . '"],';
} elsif ($char eq '[') {
$depth++;
$r = $r . '"nodes": [';
} elsif ($char eq ']') {
# End of nodes array: delete trailing comma.
chop $r;
# When we are at depth 0 we need to split using newlines, making
# multiple "JSON texts".
$depth--;
$r = $r . ']}' . ($depth == 0 ? "\n" : ',');
} elsif ($char eq ' ') {
} elsif ($char eq '*') {
$focus = $windows[$#windows];
} elsif ($char =~ /[[:alnum:]]/) {
push @windows, $char;
$r = $r . '{"swallows": [{';
$r = $r . '"class": "^' . "$char" . '$"';
$r = $r . '}]},';
} else {
die "Could not understand $char";
}
}
die "Invalid layout, depth is $depth > 0" unless $depth == 0;
Test::More::diag($r);
my ($fh, $tmpfile) = tempfile("layout-XXXXXX", UNLINK => 1);
print $fh "$r\n";
close($fh);
my $return = cmd "append_layout $tmpfile";
die 'Could not parse layout json file' unless $return->[0]->{success};
my @result_windows;
push @result_windows, open_window(wm_class => "$_", name => "$_") foreach @windows;
cmd '[class=' . $focus . '] focus' if $focus;
return @result_windows;
}
sub verify_layout {
my ($layout, $ws) = @_;
my $nodes = get_ws_content($ws);
my %counters;
my $depth = 0;
my $node;
foreach my $char (split('', $layout)) {
my $node_name;
my $node_layout;
if ($char eq 'H') {
$node_layout = 'splith';
} elsif ($char eq 'V') {
$node_layout = 'splitv';
} elsif ($char eq 'S') {
$node_layout = 'stacked';
} elsif ($char eq 'T') {
$node_layout = 'tabbed';
} elsif ($char eq '[') {
$depth++;
delete $counters{$depth};
} elsif ($char eq ']') {
$depth--;
} elsif ($char eq ' ') {
} elsif ($char eq '*') {
$tester->is_eq($node->{focused}, 1, 'Correct node focused');
} elsif ($char =~ /[[:alnum:]]/) {
$node_name = $char;
} else {
die "Could not understand $char";
}
if ($node_layout || $node_name) {
if (exists($counters{$depth})) {
$counters{$depth} = $counters{$depth} + 1;
} else {
$counters{$depth} = 0;
}
$node = $nodes->[$counters{0}];
for my $i (1 .. $depth) {
$node = $node->{nodes}->[$counters{$i}];
}
if ($node_layout) {
$tester->is_eq($node->{layout}, $node_layout, "Layouts match in depth $depth, node number " . $counters{$depth});
} else {
$tester->is_eq($node->{name}, $node_name, "Names match in depth $depth, node number " . $counters{$depth});
}
}
}
}
=head1 AUTHOR

94
testcases/t/302-tree.t Normal file
View File

@ -0,0 +1,94 @@
#!perl
# vim:ts=4:sw=4:expandtab
#
# Please read the following documents before working on tests:
# • https://build.i3wm.org/docs/testsuite.html
# (or docs/testsuite)
#
# • https://build.i3wm.org/docs/lib-i3test.html
# (alternatively: perldoc ./testcases/lib/i3test.pm)
#
# • https://build.i3wm.org/docs/ipc.html
# (or docs/ipc)
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
#
# Contains various tests that use the cmp_tree subroutine.
# Ticket: #3503
use i3test;
sub sanity_check {
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($layout, $focus_idx) = @_;
my @windows = cmp_tree(
msg => 'Sanity check',
layout_before => $layout,
layout_after => $layout);
is($x->input_focus, $windows[$focus_idx]->id, 'Correct window focused') if $focus_idx >= 0;
}
sanity_check('H[ V[ a* V[ b c ] d ] e ]', 0);
sanity_check('H[ a b c d* ]', 3);
sanity_check('V[a b] V[c d*]', 3);
sanity_check('T[a b] S[c*]', 2);
cmp_tree(
msg => 'Simple focus test',
layout_before => 'H[a b] V[c* d]',
layout_after => 'H[a* b] V[c d]',
cb => sub {
cmd '[class=a] focus';
});
cmp_tree(
msg => 'Simple move test',
layout_before => 'H[a b] V[c* d]',
layout_after => 'H[a b] V[d c*]',
cb => sub {
cmd 'move down';
});
cmp_tree(
msg => 'Move from horizontal to vertical',
layout_before => 'H[a b] V[c d*]',
layout_after => 'H[b] V[c d a*]',
cb => sub {
cmd '[class=a] focus';
cmd 'move right, move right';
});
cmp_tree(
msg => 'Move unfocused non-leaf container',
layout_before => 'S[a b] V[c d* T[e f g]]',
layout_after => 'S[a T[e f g] b] V[c d*]',
cb => sub {
cmd '[con_mark=T1] move up, move up, move left, move up';
});
cmp_tree(
msg => 'Simple swap test',
layout_before => 'H[a b] V[c d*]',
layout_after => 'H[a d*] V[c b]',
cb => sub {
cmd '[class=b] swap with id ' . $_[0][3]->{id};
});
cmp_tree(
msg => 'Swap non-leaf containers',
layout_before => 'S[a b] V[c d*]',
layout_after => 'V[c d*] S[a b]',
cb => sub {
cmd '[con_mark=S1] swap with mark V1';
});
cmp_tree(
msg => 'Swap nested non-leaf containers',
layout_before => 'S[a b] V[c d* T[e f g]]',
layout_after => 'T[e f g] V[c d* S[a b]]',
cb => sub {
cmd '[con_mark=S1] swap with mark T1';
});
done_testing;