diff --git a/regress/layout-custom.sh b/regress/layout-custom.sh new file mode 100644 index 000000000..e9537b9e5 --- /dev/null +++ b/regress/layout-custom.sh @@ -0,0 +1,986 @@ +#!/bin/sh + +# Tests of the custom layout dumper and evaluator in layout-custom.c, and of +# the JSON tokenizer and parser in json.c that the current layout format is +# built on. +# +# layout_dump is reached through the #{window_layout} and +# #{window_visible_layout} formats and layout_parse through +# "select-layout ". json.c has no command of its own either: +# layout_construct sniffs the first non-blank character and hands anything +# starting with '{' to json_parse, so select-layout is the only way into it +# from the shell as well. +# +# Both layout formats are covered: +# - the current (v2) JSON format, which is what every client except an old +# control client sees; +# - the legacy (v1) format, which is still produced for a control client that +# has not asked for the "new-layouts" flag, and which is still accepted by +# the parser (the version is sniffed from the first character). +# +# This exercises: +# - dumping a single pane, a split, the "a" (active) and "l" (last pane) keys +# and the "z" key of a floating pane; +# - #{window_visible_layout} agreeing with #{window_layout}; +# - the JSON syntax itself: insignificant whitespace, backslash escapes inside +# strings, the number and boolean forms, and one failure for each way json.c +# can reject an input that a layout string can carry; +# - a dump being parsed back to exactly the same layout (round trip), after +# another layout has been applied in between, and the same for a layout with +# two floating panes in it; +# - parsing a hand-written v2 layout; +# - "i" deciding which pane goes in which cell, checked with a layout whose +# cells are written in a different order from their indexes; +# - the same layouts with their fields in reversed and scrambled orders, +# including "c" before "t" and "V" after "L", neither of which changes the +# order the fields are read in; +# - a layout with more cells than the window has panes having the bottom right +# cells dropped, in both formats; +# - a layout naming no active or last pane leaving the active pane where it was +# and emptying the last pane stack, whether it leaves "a" out or gives it as +# false; +# - parsing a v1 layout and dumping it back as v1 through a control client, +# with the checksum computed here independently of layout_checksum(), and a +# v1 layout leaving the active pane and last pane stack untouched; +# - the legacy format meeting the floating panes it cannot represent: a v1 dump +# dropping the floating cells, both where that leaves the node they were in +# with one child so that it collapses, where it does not, and where adjacent +# nested floating-only subtrees are dropped, and a v1 layout being applied to +# a window that has floating panes without disturbing them, whether the tiled +# layout it names is a single cell or a split; +# - a window whose only tiled pane has been killed, which leaves it with a +# floating cell as its layout root or with a root node holding nothing but +# floating cells, producing no v1 dump at all, and being parsed as v1; +# - the %layout-change notification, in both formats at once: two control +# clients watching one layout change, only one of which has asked for new +# layouts, and the number of notifications a change produces in each format; +# - failures: a bad v1 header, checksum or body, a wrong version, a missing or +# duplicated root cell, missing sizes, sizes out of range, bad cell types, a +# pane cell missing "i", leaf cells with children and node cells with fewer +# than two, more than one active pane, too few cells for the panes and +# inconsistent sizes. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null + exit 1 +} + +# must_equal $what $got $expected +must_equal() +{ + if [ "$2" != "$3" ]; then + echo "$1 wrong." >&2 + echo "Expected: '$3'" >&2 + echo "But got: '$2'" >&2 + $TMUX kill-server 2>/dev/null + exit 1 + fi +} + +# must_differ $what $got $unwanted +must_differ() +{ + [ "$2" != "$3" ] || fail "$1 unchanged: '$2'" +} + +# must_contain $what $got $wanted +must_contain() +{ + case "$2" in + *"$3"*) ;; + *) fail "$1: '$2' does not contain '$3'";; + esac +} + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + out=$($TMUX "$@" 2>&1) || fail "Command failed (expected success): $* ($out)" +} + +# check_fail $cmd... +# +# Run a command and require that it fails. The error text itself is never +# checked anywhere in this test: the wording of a message is not part of what +# the layout formats promise, so matching on it only makes the test fail when a +# message is reworded. +check_fail() +{ + $TMUX "$@" >/dev/null 2>&1 && + fail "Command succeeded (expected failure): $*" +} + +# layout $target +# +# The layout of a window with pane ids replaced by %N, so that the expected +# strings do not depend on which ids the server handed out. +layout() +{ + $TMUX display-message -p -t "$1" '#{window_layout}' | + sed 's/%[0-9][0-9]*/%N/g' +} + +# visible_layout $target +# +# As layout(), but the visible (zoomed) layout. +visible_layout() +{ + $TMUX display-message -p -t "$1" '#{window_visible_layout}' | + sed 's/%[0-9][0-9]*/%N/g' +} + +# raw_layout $target +# +# The layout of a window with the real pane ids left in place. +raw_layout() +{ + $TMUX display-message -p -t "$1" '#{window_layout}' +} + +# v1_layout $target +# +# The legacy (v1) dump of a window, which is what a control client that has not +# asked for the "new-layouts" flag is sent. A control client wraps its output in +# %begin/%end guard lines, which are dropped here. +v1_layout() +{ + $TMUX -C display-message -p -t "$1" '#{window_layout}' | grep -v '^%' +} + +# v1 $body +# +# Prefix a legacy (v1) layout body with its checksum. This is a separate +# implementation of layout_checksum(): a 16 bit rotate right then add, so a +# mistake in either one shows up as a mismatch. +v1() +{ + awk -v s="$1" 'BEGIN { + for (i = 32; i < 127; i++) + ord[sprintf("%c", i)] = i + csum = 0 + for (i = 1; i <= length(s); i++) { + csum = int(csum / 2) + (csum % 2) * 32768 + csum = (csum + ord[substr(s, i, 1)]) % 65536 + } + printf "%04x,%s\n", csum, s + }' +} + +# A pane cell is dumped as its geometry, then "a" if it is the active pane or +# "l" with its position on the last pane stack if it is on it, then "i" with +# its pane index, then "z" if it is floating, then "I" with its pane id. +ONE='{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%N"}}' + +check_ok new-session -d -s L -x 80 -y 24 -n one + +p0=$($TMUX display-message -p -t L:one.0 '#{pane_id}') + +# A single leaf cell filling the window. A pane cell must carry "i", its pane +# index; "I", its pane id, is written by the dumper and is here so that the cell +# is the same shape as a dumped one. The JSON checks below care about the syntax +# around the cell rather than the cell itself. +LEAF='{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$p0"'"}' + +# --------------------------------------------------------------------------- +# Dumping a single pane. + +# The root cell of a new window is the pane itself, and it is the active pane +# so it has "a" rather than "l". +must_equal 'Single pane layout' "$(layout L:one)" "$ONE" + +# Nothing is zoomed, so the visible layout is the same. +must_equal 'Single pane visible layout' "$(visible_layout L:one)" "$ONE" + +# --------------------------------------------------------------------------- +# More cells than panes. + +# The bottom right cells are closed until as many are left as there are panes, +# so a two cell layout applied to a one pane window collapses back to the +# single pane filling the window: the cell that is left takes the space of the +# one that was closed. The window has one pane to name, so the cell that is +# closed carries an id belonging to no pane of it. +check_ok select-layout -t L:one \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$p0"'"},{"t":"p","w":80,"h":12,"x":0,"y":12,"i":1,"I":"%999"}]}}' +must_equal 'Trimmed layout' "$(layout L:one)" "$ONE" + +# --------------------------------------------------------------------------- +# The JSON syntax. +# +# These run on the one pane window and are written so that what they prove +# depends on json.c rather than on the layout evaluation in layout-custom.c: +# an accepted layout is only required to leave the window as its single pane, +# and values that are not part of the layout format are carried on keys +# layout-custom.c never looks at ("n", "b" and so on), which it skips, so +# numbers, booleans and escapes can be exercised on their own. +# +# Objects nested in an array nested in an object are not checked here: every +# split layout below is one. +# +# Two of json.c's rejections cannot be reached from the shell and so are not +# covered: json_parse_tokens() refusing a top level that is not an object, +# because layout_construct() only calls json_parse() once the string already +# starts with '{'; and the maximum object depth, which needs a layout built by a +# program rather than one written out here. + +# check_json_ok $what $layout +# +# select-layout must parse $layout and leave the window as its single pane. +check_json_ok() +{ + check_ok select-layout -t L:one "$2" + must_equal "Layout after '$1'" "$(layout L:one)" "$ONE" +} + +# check_json_fail $what $layout +# +# select-layout must reject $layout. +check_json_fail() +{ + $TMUX select-layout -t L:one "$2" >/dev/null 2>&1 && + fail "$1: select-layout succeeded (expected failure)" +} + +# Whitespace between tokens is skipped. A number is scanned up to the ',', ']', +# '}' or whitespace that ends it, so a space after a number is fine but one +# inside it is not. +check_json_ok 'Spaces between tokens' \ + '{ "V" : 2 , "L" : { "t" : "p" , "w" : 80 , "h" : 24 , "x" : 0 , "y" : 0 , "i" : 0 , "I" : "'"$p0"'" } }' + +check_json_ok 'Newlines and tabs between tokens' "$(printf '{ +\t"V": 2, +\t"L": { +\t\t"t": "p", +\t\t"w": 80, +\t\t"h": 24, +\t\t"x": 0, +\t\t"y": 0, +\t\t"i": 0, +\t\t"I": "%s" +\t} +}' "$p0")" + +check_json_ok 'Carriage returns between tokens' \ + "$(printf '{\r"V":2,\r"L":%s\r}' "$LEAF")" + +# A backslash makes the tokenizer consume the next character whatever it is, so +# an escaped quote does not end the string. The key is not one that +# layout-custom.c looks at, so all that is being checked is that the string +# ended in the right place and the object still parsed. +check_json_ok 'Escaped quote in a string' \ + '{"V":2,"a\"b":0,"L":'"$LEAF"'}' + +# An escaped backslash immediately before the closing quote: the escape has to +# be cleared again so that the quote after it does end the string. +check_json_ok 'Escaped backslash before the closing quote' \ + '{"V":2,"a\\":0,"L":'"$LEAF"'}' + +# Numbers and booleans, again on keys layout-custom.c ignores, so only json.c +# decides whether they are accepted. +check_json_ok 'Zero' '{"V":2,"n":0,"L":'"$LEAF"'}' +check_json_ok 'Several digits' '{"V":2,"n":1234567,"L":'"$LEAF"'}' +check_json_ok 'Negative number' '{"V":2,"n":-42,"L":'"$LEAF"'}' +check_json_ok 'Booleans' '{"V":2,"b":true,"d":false,"L":'"$LEAF"'}' + +# Tokenizer failures. A value that runs to the end of the input has no +# terminator, so it is the tokenizer rather than the parser that gives up. Both +# the number scan and the string scan have to notice this, and with the closing +# quote escaped there is no terminator left either. +check_json_fail 'Unterminated number' '{"V":2' +check_json_fail 'Unterminated string' '{"V":"x' +check_json_fail 'Escaped closing quote' '{"V":2,"L":{"t":"p\"}}' + +# Something that is not a quoted string where a key belongs. +check_json_fail 'Missing key' '{"V":2,,"L":'"$LEAF"'}' + +# A key not followed by ':'. +check_json_fail 'Missing colon' '{"V","L":2}' + +# A bare word that is neither "true", "false" nor a number. This is where +# "null" ends up. +check_json_fail 'Unknown literal' '{"V":null,"L":'"$LEAF"'}' + +# A ':' with no value after it, so the token where the value belongs is one the +# object parser has no case for. +check_json_fail 'Missing value' '{"V":}' + +# A ',' with nothing after it, and a value with no ',' before the next key. +check_json_fail 'Trailing comma in an object' '{"V":2,"L":'"$LEAF"',}' +check_json_fail 'Missing comma in an object' '{"V":2 "L":'"$LEAF"'}' + +# Arrays hold objects and nothing else. +check_json_fail 'Non-object in an array' \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":["x"]}}' +check_json_fail 'Trailing comma in an array' \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":['"$LEAF"',]}}' + +# An empty string is two adjacent quotes with no value token between them, +# which the string parser does not accept. +check_json_fail 'Empty string' '{"V":2,"L":""}' + +# A number token that strtoll does not consume all of. +check_json_fail 'Number with trailing characters' '{"V":8a,"L":'"$LEAF"'}' + +# Anything after the top level object. +check_json_fail 'Data after the top level object' '{"V":2,"L":'"$LEAF"'}{}' + +# None of the rejections touched the layout. +must_equal 'Layout after rejected parses' "$(layout L:one)" "$ONE" + +# --------------------------------------------------------------------------- +# Dumping a split. + +check_ok new-window -d -t L:2 -n two +q0=$($TMUX display-message -p -t L:two.0 '#{pane_id}') + +# -l 12 gives the new (bottom) pane 12 lines, leaving 11 for the top pane and +# one for the border between them. With -d the top pane stays active. +check_ok split-window -d -v -l 12 -t L:two.0 +q1=$($TMUX display-message -p -t L:two.1 '#{pane_id}') + +# Nothing has changed the active pane, so the last pane stack is still empty +# and the bottom pane has neither "a" nor "l". +must_equal 'Split layout' "$(layout L:two)" \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"i":1,"I":"%N"}]}}' + +# --------------------------------------------------------------------------- +# The active and last pane keys. + +# Selecting the bottom pane makes it active and pushes the top pane onto the +# last pane stack, where it is at index 0. +check_ok select-pane -t "$q1" +must_equal 'Layout after select-pane' "$(layout L:two)" \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"l":0,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"a":true,"i":1,"I":"%N"}]}}' + +# Selecting the top pane again swaps the two keys over. "i" and "I" do not +# move: they are the pane's position in the window and its id. +check_ok select-pane -t "$q0" +SPLIT='{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"l":0,"i":1,"I":"%N"}]}}' +must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT" + +# --------------------------------------------------------------------------- +# The visible layout. + +# With nothing zoomed the two layout formats agree. +# +# The zoomed case is deliberately not covered here. While a pane is zoomed +# #{window_layout} dumps the saved (unzoomed) layout and +# #{window_visible_layout} the zoomed one, but that depends on how zooming +# stashes the layout root rather than on anything in layout-custom.c. +must_equal 'Visible layout' "$(visible_layout L:two)" "$SPLIT" + +# --------------------------------------------------------------------------- +# Round trip. + +# Make the two panes obviously uneven so that the layout applied in between +# cannot be mistaken for the saved one. A resize shows up in the dump as the +# new cell sizes and offsets. +check_ok resize-pane -t "$q0" -y 5 +saved=$(raw_layout L:two) +must_equal 'Resized layout' "$(layout L:two)" \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":5,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":18,"x":0,"y":6,"l":0,"i":1,"I":"%N"}]}}' + +check_ok select-layout -t L:two even-vertical +must_differ 'Layout after even-vertical' "$(raw_layout L:two)" "$saved" + +# Parsing a dump gives back exactly the same dump, pane ids included. The panes +# go back into the cells that named them: the cells are ordered by "i" and then +# given the window's panes in order, so a cell dumped with "i":k must come back +# the k'th. +check_ok select-layout -t L:two "$saved" +must_equal 'Round tripped layout' "$(raw_layout L:two)" "$saved" + +# --------------------------------------------------------------------------- +# Parsing a hand-written layout. + +# Laid out over several lines to keep it readable; that the whitespace is +# skipped at all is json.c's business, what matters here is that the cells come +# out of it in the right shape. +# +# "a" and "l" are given on the cells so that the active pane and the last pane +# stack are pinned by the layout rather than left to whatever a layout that +# names neither happens to produce. +check_ok select-layout -t L:two "$(printf '{ + "V": 2, + "L": { + "t": "h", + "w": 80, + "h": 24, + "x": 0, + "y": 0, + "c": [ + {"t": "p", "w": 30, "h": 24, "x": 0, "y": 0, "a": true, "i": 0, "I": "%s"}, + {"t": "p", "w": 49, "h": 24, "x": 31, "y": 0, "l": 0, "i": 1, "I": "%s"} + ] + } +}' "$q0" "$q1")" +must_equal 'Hand-written layout' "$(layout L:two)" \ + '{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":30,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":49,"h":24,"x":31,"y":0,"l":0,"i":1,"I":"%N"}]}}' + +# The panes are assigned to the cells in order. +must_equal 'First pane width' \ + "$($TMUX display-message -p -t "$q0" '#{pane_width}')" '30' +must_equal 'Second pane width' \ + "$($TMUX display-message -p -t "$q1" '#{pane_width}')" '49' + +# --------------------------------------------------------------------------- +# Field order. + +# Fields are looked up by key once the object has been parsed, so the order +# they are written in must give the same layout. Here every object has its keys +# reversed: "c" comes before "t" and "V" comes after "L", neither of which +# changes the order they are read in - the cell type is always read before the +# children and the version before the layout. +check_ok select-layout -t L:two \ + '{"L":{"c":[{"I":"'"$q0"'","i":0,"a":true,"y":0,"x":0,"h":8,"w":80,"t":"p"},{"I":"'"$q1"'","i":1,"l":0,"y":9,"x":0,"h":15,"w":80,"t":"p"}],"y":0,"x":0,"h":24,"w":80,"t":"v"},"V":2}' +must_equal 'Reversed field order' "$(layout L:two)" \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":15,"x":0,"y":9,"l":0,"i":1,"I":"%N"}]}}' + +# Keys interleaved rather than simply reversed, with "c" in the middle. This +# time "a" is on the second cell, so the second pane becomes the active one: +# which pane is active comes from the layout, while "i" and "I" still come from +# the window. The first cell names neither "a" nor "l", so its pane is neither +# active nor on the last pane stack and the dump gives it neither key. +check_ok select-layout -t L:two \ + '{"V":2,"L":{"h":24,"c":[{"w":40,"t":"p","y":0,"i":0,"h":24,"I":"'"$q0"'","x":0},{"a":true,"h":24,"I":"'"$q1"'","w":39,"y":0,"t":"p","i":1,"x":41}],"w":80,"y":0,"t":"h","x":0}}' +must_equal 'Scrambled field order' "$(layout L:two)" \ + '{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":40,"h":24,"x":0,"y":0,"i":0,"I":"%N"},{"t":"p","w":39,"h":24,"x":41,"y":0,"a":true,"i":1,"I":"%N"}]}}' + +# --------------------------------------------------------------------------- +# The legacy (v1) format. + +# The layout just applied, in v1: a left/right cell is written with braces and +# a top/bottom cell with brackets, and each leaf carries its pane id without +# the leading %. +v1body="80x24,0,0{40x24,0,0,${q0#%},39x24,41,0,${q1#%}}" + +# A control client that has not asked for new layouts is dumped v1. +must_equal 'v1 dump' "$(v1_layout L:two)" "$(v1 "$v1body")" + +# With the new-layouts flag the same client is dumped v2 instead. The flag is +# set with "attach -f" rather than refresh-client because refresh-client needs +# a current client, which a control client that has not attached has not got. +got=$(printf "display-message -p -t L:two '#{window_layout}'\n" | + $TMUX -C attach -f new-layouts -t L 2>&1 | grep -v '^%') +must_contain 'v2 dump for control client' "$got" '{"V":2,"L":' + +# A v1 layout with a correct checksum is parsed, and dumping v1 again gives +# back the same string. That is the whole of what v1 carries: the cells take +# the sizes and offsets from the body, and the panes are assigned to them in +# order, which is what puts the same two ids back in the same two places. It is +# checked in v1 rather than against a v2 dump so that nothing v1 has no opinion +# on - the active pane, the last pane stack, the pane index - comes into it. +v1vsplit="80x24,0,0[80x11,0,0,${q0#%},80x12,0,12,${q1#%}]" +check_ok select-layout -t L:two "$(v1 "$v1vsplit")" +must_equal 'v1 round trip' "$(v1_layout L:two)" "$(v1 "$v1vsplit")" + +# v1 names no active pane, last pane or z-index and must disturb none of them. +# Applying the v1 form of the layout the window already has therefore leaves +# even the v2 dump the same byte for byte, last pane stack included. +check_ok select-pane -t "$q1" +check_ok select-pane -t "$q0" +before=$(raw_layout L:two) +check_ok select-layout -t L:two "$(v1 "$v1vsplit")" +must_equal 'v1 leaves the active and last panes alone' \ + "$(raw_layout L:two)" "$before" + +# A v1 layout with more cells than the window has panes is trimmed like any +# other: the bottom right cell is closed and the cell above it takes its eight +# rows and the border between them, leaving 16. Pane ids in a v1 body are not +# used to place panes, so the third cell can carry any id. +v1three="80x24,0,0[80x7,0,0,${q0#%},80x7,0,8,${q1#%},80x8,0,16,999]" +check_ok select-layout -t L:two "$(v1 "$v1three")" +must_equal 'v1 layout trimmed' "$(v1_layout L:two)" \ + "$(v1 "80x24,0,0[80x7,0,0,${q0#%},80x16,0,8,${q1#%}]")" + +# --------------------------------------------------------------------------- +# Pane assignment order. + +# "i" is what decides which pane goes into which cell: the cells are ordered by +# it and then handed the window's panes in order, so the cell with "i":0 takes +# the first pane of the window wherever that cell sits in the layout. Here the +# cells are written the other way round from their indexes - the first cell in +# the string is "i":1 and the second "i":0 - so the first pane of the window +# has to come out in the second cell. +# +# Every other layout above lists its cells in the same order as their indexes, +# which is the order the tree is walked in, so this is the only check that can +# tell the two apart. +check_ok select-layout -t L:two \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"i":1,"I":"'"$q1"'"},{"t":"p","w":80,"h":15,"x":0,"y":9,"i":0,"I":"'"$q0"'"}]}}' +must_equal 'First pane height' \ + "$($TMUX display-message -p -t "$q0" '#{pane_height}')" '15' +must_equal 'Second pane height' \ + "$($TMUX display-message -p -t "$q1" '#{pane_height}')" '8' + +# So the dump carries the two ids the other way round from every dump above, +# and with them their indexes, which are the panes' positions in the window and +# have not moved. Neither cell named an active or last pane, so the pane that +# was active still is - it is now the one in the second cell. +swapped='{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"i":1,"I":"'"$q1"'"},{"t":"p","w":80,"h":15,"x":0,"y":9,"a":true,"i":0,"I":"'"$q0"'"}]}}' +must_equal 'Layout with the panes swapped' "$(raw_layout L:two)" "$swapped" + +# And that dump round trips, indexes out of order and all. +check_ok select-layout -t L:two "$swapped" +must_equal 'Round tripped swapped layout' "$(raw_layout L:two)" "$swapped" + +# --------------------------------------------------------------------------- +# Cells that name no active or last pane. + +# "a" and "l" are the only things that decide which pane is active and what is +# on the last pane stack, so a layout naming neither leaves the active pane +# where it was and empties the stack. Here the first pane of the window is +# active and the second is at index 0 of the stack beforehand; afterwards the +# first pane is still active and the stack is empty, so the second pane has no +# "l". +check_ok select-pane -t "$q1" +check_ok select-pane -t "$q0" +check_ok select-layout -t L:two \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"'"$q1"'"}]}}' +must_equal 'Layout naming no active pane' "$(layout L:two)" \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"%N"}]}}' + +# "a" may be given as false, which says the same as leaving it out: this pane +# is not the active one. A layout where every cell says so names no active pane +# at all and so leaves the active pane alone, exactly as the layout above did. +check_ok select-layout -t L:two \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":10,"x":0,"y":0,"a":false,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":13,"x":0,"y":11,"a":false,"i":1,"I":"'"$q1"'"}]}}' +must_equal 'Layout with only false active panes' "$(layout L:two)" \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":10,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":13,"x":0,"y":11,"i":1,"I":"%N"}]}}' + +# --------------------------------------------------------------------------- +# Failures. +# +# Each of these is a different reason for a layout to be rejected, but only the +# rejection itself is checked; the message that comes back with it is not. + +# check_layout_fail $layout +# +# select-layout must reject $layout. +check_layout_fail() +{ + check_fail select-layout -t L:two "$1" +} + +# A rejected layout must leave the window alone, whatever it was. +unchanged=$(raw_layout L:two) + +# Not JSON and not a checksum. +check_layout_fail 'garbage' + +# A v1 body with its checksum left off, and a string of nothing but hex digits. +# A v1 header is four hex digits and a comma; neither of these has one, so there +# is no header and nothing to check a body against. +check_layout_fail '80x24,0,0' +check_layout_fail 'ab' + +# A v1 header with the checksum of a different body. +good=$(v1 '80x24,0,0') +check_layout_fail "${good%%,*},80x24,0,1" + +# A correct checksum over a body that is not a layout: a cell with no offsets, +# and a top to bottom cell closed with '}' instead of ']'. layout_construct_v1 +# returns NULL for both. +check_layout_fail "$(v1 '80x24')" +check_layout_fail "$(v1 '80x24,0,0[80x11,0,0,80x12,0,12}')" + +# Fewer cells than the window has panes; unlike the other way around this +# cannot be fixed up. +check_layout_fail '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}' + +# The children of a top to bottom cell must all be the width of their parent. +check_layout_fail \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":40,"h":12,"x":0,"y":12,"i":1,"I":"'"$q1"'"}]}}' + +# The rest are valid JSON, so it is layout_parse_json() and +# layout_parse_json_layout() doing the rejecting rather than json.c. Each of +# them is a layout that would be applied but for the one thing being checked. + +# Two root cells. +check_layout_fail \ + '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"},"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}' + +# A missing "y". A cell needs all four of "w", "h", "x" and "y". +check_layout_fail '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"i":0,"I":"'"$q0"'"}}' + +# Cell sizes are bounded below by one column or row and above by 10000 of +# either. Both cases are otherwise complete two cell layouts, so the size is +# the only thing wrong with them. +check_layout_fail \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":0,"h":12,"x":0,"y":12,"i":1,"I":"'"$q1"'"}]}}' +check_layout_fail \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":10001,"x":0,"y":12,"i":1,"I":"'"$q1"'"}]}}' + +# An unknown cell type: only "h", "v" and "p" exist. +check_layout_fail '{"V":2,"L":{"t":"q","w":80,"h":24,"x":0,"y":0}}' + +# A pane cell needs "i", its pane index. It is "i" that says which pane goes in +# the cell; "I" is the pane id the cell was dumped with and is not read back. +check_layout_fail '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"I":"'"$q0"'"}}' + +# A node cell must have more than one child and a leaf cell must have none. A +# node is written with no "c" at all, with an empty one and with a single child. +check_layout_fail '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0}}' +check_layout_fail '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[]}}' +check_layout_fail \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}]}}' +check_layout_fail \ + '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'","c":[{"t":"p","w":80,"h":24,"x":0,"y":0,"i":1,"I":"'"$q1"'"}]}}' + +# Only one cell may be the active pane. +check_layout_fail \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":12,"x":0,"y":12,"a":true,"i":1,"I":"'"$q1"'"}]}}' + +# The same rejections apply whatever order the fields are written in: a leaf +# with children when "c" comes first, and a node with no children and a bad cell +# type when "t" comes last. +check_layout_fail \ + '{"V":2,"L":{"c":[{"t":"p","w":80,"h":24,"x":0,"y":0,"i":1,"I":"'"$q1"'"}],"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}' +check_layout_fail '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"v"}}' +check_layout_fail '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"q"}}' + +# A child that fails after a sibling has already been parsed and added to the +# parent. This is the case the cleanup at the end of layout_parse_json_layout +# exists for: the children built so far have to be freed along with the parent +# that is never returned. The second child has no "y". +check_layout_fail \ + '{"V":2,"L":{"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":12,"x":0,"i":1,"I":"'"$q1"'"}],"t":"v","w":80,"h":24,"x":0,"y":0}}' + +# No root cell at all. Every other rejection above comes from a cell that +# failed to parse; this one is the check for "L" itself. +check_layout_fail '{"V":2}' + +# The wrong version, with "V" before and after "L". Fields are looked up by +# key, so the version is read before the layout either way and the position of +# "V" in the string makes no difference. +check_layout_fail '{"V":1,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}' +check_layout_fail '{"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"},"V":1}' + +# None of that touched the layout. +must_equal 'Layout after failures' "$(raw_layout L:two)" "$unchanged" + +# --------------------------------------------------------------------------- +# Floating panes. + +check_ok new-window -d -t L:3 -n float +check_ok select-window -t L:float +check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 'sleep 100' +check_ok new-pane -d -x 30 -y 8 -X 30 -Y 10 'sleep 100' + +# The tiled pane and the two floating ones. A floating pane goes on the end of +# the window's pane list, so the pane indexes are in the order the panes were +# made whatever order their cells end up in. +f0=$($TMUX display-message -p -t L:float.0 '#{pane_id}') +fa=$($TMUX display-message -p -t L:float.1 '#{pane_id}') +fb=$($TMUX display-message -p -t L:float.2 '#{pane_id}') + +# A floating cell is dumped with its z-index, which is what marks it as +# floating when the layout is parsed back. Two of them, so that there is an +# order between them to get wrong: the newer floating pane is in front, and a +# cell's "z" is its place in that order counting from the front. +floating=$(raw_layout L:float) +must_contain 'Floating layout front z-index' "$floating" '"z":0' +must_contain 'Floating layout back z-index' "$floating" '"z":1' + +# Each floating cell goes in after the cell of the pane that was current when +# it was made, which is the tiled pane both times, so the newer floating cell +# is written before the older one while its pane comes after in the window. +# The dump therefore has its cells in one order and their indexes in another, +# and only comes back the same if the panes go by index. +check_ok select-layout -t L:float "$floating" +must_equal 'Floating layout after round trip' "$(raw_layout L:float)" "$floating" + +# --------------------------------------------------------------------------- +# Floating panes and the legacy (v1) format. +# +# v1 has no way to write a floating pane down, so the two formats cannot say the +# same thing about a window that has one. Dumping v1 takes a copy of the layout, +# deletes the floating cells from the copy and dumps what is left; parsing v1 +# rearranges the tiled panes and leaves the floating ones where they are. None +# of this is reached above: every v1 check so far runs on a window that has no +# floating panes, and every floating pane check so far is in v2. + +# float_state $target +# +# Everything about a floating pane that a v1 layout has no way to carry, so that +# applying one can be checked against all of it at once. +float_state() +{ + $TMUX display-message -p -t "$1" \ + '#{pane_floating_flag} #{pane_width}x#{pane_height} #{pane_left},#{pane_top} #{pane_z}' +} + +# Deleting both floating cells from the copy leaves the root node with a single +# child, and a node with a single child collapses into it, so the root of the +# copy is the tiled cell and the dump is that cell on its own filling the +# window. +must_equal 'v1 dump with floating panes' "$(v1_layout L:float)" \ + "$(v1 "80x24,0,0,${f0#%}")" + +# The cells are deleted from the copy, so the window itself comes through a v1 +# dump untouched - floating panes, z-indexes and all. +must_equal 'Layout after a v1 dump' "$(raw_layout L:float)" "$floating" + +# The same with a split, where deleting the floating cell still leaves two +# children behind and the node it was in does not collapse. +check_ok new-window -d -t L:4 -n mixed +m0=$($TMUX display-message -p -t L:mixed.0 '#{pane_id}') +check_ok split-window -d -v -l 12 -t L:mixed.0 +m1=$($TMUX display-message -p -t L:mixed.1 '#{pane_id}') +check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 -t L:mixed.0 'sleep 100' +mf=$($TMUX display-message -p -t L:mixed.2 '#{pane_id}') + +# A floating pane takes no space from the tiled layout, so the two tiled cells +# are the same 11 and 12 rows the split gave them. +must_equal 'v1 dump with a split and a floating pane' "$(v1_layout L:mixed)" \ + "$(v1 "80x24,0,0[80x11,0,0,${m0#%},80x12,0,12,${m1#%}]")" + +# A v1 layout applied to a window that has a floating pane rearranges the tiled +# panes and must leave the floating one exactly as it was: v1 names no floating +# pane, so there is nothing in it for one to be changed by. The top pane goes +# from 11 rows to 7 and the bottom one from 12 to 16. +v1mixed="80x24,0,0[80x7,0,0,${m0#%},80x16,0,8,${m1#%}]" +mfbefore=$(float_state "$mf") +check_ok select-layout -t L:mixed "$(v1 "$v1mixed")" +must_equal 'v1 layout with a floating pane' "$(v1_layout L:mixed)" \ + "$(v1 "$v1mixed")" +must_equal 'Floating pane after a v1 layout' "$(float_state "$mf")" "$mfbefore" +must_equal 'Panes after a v1 layout' \ + "$($TMUX display-message -p -t L:mixed '#{window_panes}')" '3' + +# When the tiled layout a v1 string names is a single cell there is no node in +# the new layout for the floating cells to go back into, so one is made: the +# root cell is replaced by a top to bottom node holding it and the floating +# cells go on the end. Nothing else here reaches that. +fabefore=$(float_state "$fa") +fbbefore=$(float_state "$fb") +check_ok select-layout -t L:float "$(v1 "80x24,0,0,${f0#%}")" +must_equal 'v1 single cell layout with floating panes' "$(v1_layout L:float)" \ + "$(v1 "80x24,0,0,${f0#%}")" +must_equal 'Front floating pane after a v1 layout' "$(float_state "$fb")" \ + "$fbbefore" +must_equal 'Back floating pane after a v1 layout' "$(float_state "$fa")" \ + "$fabefore" +must_equal 'Panes after a v1 single cell layout' \ + "$($TMUX display-message -p -t L:float '#{window_panes}')" '3' + +# Adjacent subtrees containing only floating panes used to be a distinct case: +# dumping v1 made a copy of the v2 tree and deleted floating cells from the +# copy, but deleting the last floating cell in the first subtree collapsed the +# parent and could leave the outer traversal holding a stale pointer to the +# second subtree. +check_ok new-window -d -t L:5 -n nested +n0=$($TMUX display-message -p -t L:nested.0 '#{pane_id}') +check_ok split-window -d -v -l 12 -t L:nested.0 +n1=$($TMUX display-message -p -t L:nested.1 '#{pane_id}') +check_ok split-window -d -v -l 6 -t L:nested.1 +n2=$($TMUX display-message -p -t L:nested.2 '#{pane_id}') +check_ok split-window -d -v -l 3 -t L:nested.2 +n3=$($TMUX display-message -p -t L:nested.3 '#{pane_id}') +check_ok split-window -d -v -l 2 -t L:nested.3 +n4=$($TMUX display-message -p -t L:nested.4 '#{pane_id}') +check_ok select-layout -t L:nested \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"h","w":30,"h":10,"x":0,"y":0,"c":[{"t":"p","w":10,"h":5,"x":2,"y":2,"i":0,"z":0},{"t":"p","w":12,"h":6,"x":5,"y":5,"i":1,"z":1}]},{"t":"h","w":30,"h":10,"x":0,"y":0,"c":[{"t":"p","w":14,"h":7,"x":8,"y":8,"i":2,"z":2},{"t":"p","w":16,"h":8,"x":11,"y":11,"i":3,"z":3}]},{"t":"p","w":80,"h":24,"x":0,"y":0,"i":4}]}}' +must_equal 'v1 dump with nested floating-only subtrees' \ + "$(v1_layout L:nested)" "$(v1 "80x24,0,0,${n4#%}")" +check_ok kill-window -t L:nested + +# --------------------------------------------------------------------------- +# A window with no tiled panes. +# +# Killing the last tiled pane of a window that has floating panes does not kill +# the window: that only happens when the pane being killed is the last one +# counting the floating ones. What is left is a window whose layout root is +# either a floating cell on its own, or a node holding nothing but floating +# cells, depending on how many are left. v1 has no way to write either down, so +# it must not try: a layout with no tiled panes in it produces no v1 dump at +# all, and #{window_layout} comes back empty for a client being sent v1. What v2 +# makes of such a window is a separate question and is not checked here. +# +# A dead server dumps nothing either, so the checks below have to establish that +# the server is still there before an empty dump is allowed to mean anything. + +# no_hang $cmd... +# +# Run a command whose result is not being checked, but which has to come back: +# only the server surviving it is checked afterwards, and a server wedged rather +# than killed would otherwise show up as the test never finishing. +no_hang() +{ + if command -v timeout >/dev/null 2>&1; then + timeout 10 "$@" >/dev/null 2>&1 + else + "$@" >/dev/null 2>&1 + fi + return 0 +} + +# One floating pane left. The node it and the tiled cell were in is down to a +# single child, so it collapses and the floating cell becomes the root. +check_ok new-window -d -t L:5 -n gone1 +g0=$($TMUX display-message -p -t L:gone1.0 '#{pane_id}') +check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 -t L:gone1.0 'sleep 100' +check_ok kill-pane -t "$g0" +must_equal 'Panes left with one floating pane' \ + "$($TMUX display-message -p -t L:gone1 '#{window_panes}')" '1' + +# The floating cell is the root and there is nothing tiled under it, so there is +# no v1 dump to make. In particular the floating cell must not be written out on +# its own, which would be a layout claiming the window is the size and position +# of the floating pane with no pane in it at all. +got=$(v1_layout L:gone1) +check_ok display-message -p alive +must_equal 'v1 dump with one floating pane and no tiled panes' "$got" '' + +# Two floating panes left, so the node keeps two children, does not collapse, +# and stays the root with nothing but floating cells in it. +check_ok new-window -d -t L:6 -n gone2 +h0=$($TMUX display-message -p -t L:gone2.0 '#{pane_id}') +check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 -t L:gone2.0 'sleep 100' +check_ok new-pane -d -x 30 -y 8 -X 30 -Y 10 -t L:gone2.0 'sleep 100' +check_ok kill-pane -t "$h0" +must_equal 'Panes left with two floating panes' \ + "$($TMUX display-message -p -t L:gone2 '#{window_panes}')" '2' + +# The node is the root this time rather than the floating cell, but it has no +# tiled cell anywhere under it either, so there is still no v1 dump to make - +# and making one must not take the server with it. +got=$(v1_layout L:gone2) +check_ok display-message -p alive +must_equal 'v1 dump with two floating panes and no tiled panes' "$got" '' + +# Nor must parsing a v1 layout against it. There is no tiled pane for the +# layout to name, so whether it is applied or rejected is the format's business; +# it just has to be one of the two. +no_hang $TMUX select-layout -t L:gone2 "$(v1 '80x24,0,0,999')" +check_ok display-message -p alive + +check_ok kill-window -t L:gone1 +check_ok kill-window -t L:gone2 + +# --------------------------------------------------------------------------- +# Control mode notifications. +# +# %layout-change is what a control client actually reads a layout from, and it +# carries both #{window_layout} and #{window_visible_layout}. Its template is +# expanded once per client (control-notify.c), so two clients watching the same +# window must be told about the same change in different formats: v1 for the +# one that has not asked for new layouts, v2 for the one that has. +# +# The dumps above go through "-C display-message", which only ever reaches the +# format callbacks for the client asking. This needs clients that stay +# attached while something else changes the layout, so they go on the end of +# fifos and the change is made from outside. + +DIR=$(mktemp -d) || fail 'Could not make a temporary directory' +OLDIN="$DIR/old-in" +OLDOUT="$DIR/old-out" +NEWIN="$DIR/new-in" +NEWOUT="$DIR/new-out" +OLDPID= +NEWPID= + +cleanup() +{ + [ -n "$OLDPID" ] && kill "$OLDPID" 2>/dev/null + [ -n "$NEWPID" ] && kill "$NEWPID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup EXIT + +# wait_for $file $text +# +# Wait for $text to appear in a control client's output. +wait_for() +{ + i=0 + while [ "$i" -lt 6 ]; do + grep -F -- "$2" "$1" >/dev/null 2>&1 && return 0 + sleep 1 + i=$((i + 1)) + done + echo "missing from $1: $2" >&2 + cat "$1" >&2 + return 1 +} + +mkfifo "$OLDIN" "$NEWIN" || fail 'Could not make the control client fifos' +: >"$OLDOUT" +: >"$NEWOUT" + +$TMUX -C attach -t L <"$OLDIN" >"$OLDOUT" 2>&1 & +OLDPID=$! +exec 4>"$OLDIN" +$TMUX -C attach -f new-layouts -t L <"$NEWIN" >"$NEWOUT" 2>&1 & +NEWPID=$! +exec 5>"$NEWIN" + +# Both clients have to be attached before the layout changes, or they miss the +# notification entirely. +printf 'display-message -p ready\n' >&4 +printf 'display-message -p ready\n' >&5 +wait_for "$OLDOUT" ready || fail 'Control client without new-layouts did not attach' +wait_for "$NEWOUT" ready || fail 'Control client with new-layouts did not attach' + +wid=$($TMUX display-message -p -t L:two '#{window_id}') + +# One layout change, made by a third client so that neither of the two is the +# one running the command. 8 lines for the top pane leaves 15 for the bottom +# and one for the border. +check_ok resize-pane -t "$q0" -y 8 + +# Nothing is zoomed, so both fields of the notification carry the same layout. +# The v2 one is compared against the dump rather than a literal so that it is +# the two formats being checked and not the geometry again. +v2now=$(raw_layout L:two) +v1now=$(v1 "80x24,0,0[80x8,0,0,${q0#%},80x15,0,9,${q1#%}]") +wait_for "$NEWOUT" "%layout-change $wid $v2now $v2now " || + fail 'No v2 %layout-change for the client with new-layouts' +wait_for "$OLDOUT" "%layout-change $wid $v1now $v1now " || + fail 'No v1 %layout-change for the client without new-layouts' + +# How many notifications one layout change produces, which differs by format +# on purpose. cmd_select_layout_exec() fires window-layout-changed for any +# layout it applies, and layout_parse() fires it again for a v1 one, so v1 +# arrives twice - which is what master does for every layout, and what control +# clients written against it expect. v2 is new and has no such clients, so it +# gets the single notification. Counting the delta rather than the total, with +# a settle in between, keeps this independent of what has already been sent. +n1=$(grep -c "%layout-change $wid " "$OLDOUT") +check_ok select-layout -t L:two \ + '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"'"$q1"'"}]}}' +sleep 2 +n2=$(grep -c "%layout-change $wid " "$OLDOUT") +must_equal 'Notifications for a v2 layout' "$((n2 - n1))" '1' + +check_ok select-layout -t L:two "$(v1 "$v1vsplit")" +sleep 2 +n3=$(grep -c "%layout-change $wid " "$OLDOUT") +must_equal 'Notifications for a v1 layout' "$((n3 - n2))" '2' + +# And the client that did not ask for new layouts must never have been sent +# one, in that notification or any other. +grep -F '{"V":2,' "$OLDOUT" >/dev/null 2>&1 && + fail 'Control client without new-layouts was sent a v2 layout' + +if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died." >&2 + exit 1 +fi + +$TMUX kill-server 2>/dev/null +exit 0