#!/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 an empty v1 body with a checksum, 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. # # Zoomed layouts are checked below with floating panes. 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}')" # Version 1 cell sizes and offsets have the same limits as version 2. In # particular, a width which overflows an unsigned integer must be rejected # before it can be used to resize a pane. check_layout_fail \ "$(v1 '80x24,0,0{20x24,0,0,39999999999999999999999999999x24,21,0}')" # 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" # A dump taken while zoomed must retain the unzoomed floating z-indexes. # Check hidden floats, floats above zoom, and a mixture of the two. for flags in '' A mixed; do check_ok new-window -d -t L: -n zoom zt=$($TMUX display-message -p -t L:zoom '#{pane_id}') check_ok split-window -d -h -t "$zt" case "$flags" in A|mixed) first=-Ad ;; *) first=-d ;; esac case "$flags" in A) second=-Ad ;; *) second=-d ;; esac check_ok new-pane "$first" -t "$zt" -x 20 -y 6 '' check_ok new-pane "$second" -t "$zt" -x 30 -y 8 '' for pane in 0 2 3; do check_ok select-pane -t "L:zoom.$pane" before=$(raw_layout L:zoom) legacy=$(v1_layout L:zoom) check_ok resize-pane -Z -t "L:zoom.$pane" during=$(raw_layout L:zoom) must_equal 'Layout while zoomed' "$during" "$before" must_equal 'Legacy layout while zoomed' \ "$(v1_layout L:zoom)" "$legacy" must_equal 'Zoom after dumping layout' \ "$($TMUX display-message -p -t L:zoom '#{window_zoomed_flag}')" 1 must_differ 'Visible layout while zoomed' \ "$(visible_layout L:zoom)" "$(layout L:zoom)" check_ok select-layout -t L:zoom "$during" must_equal 'Round trip from zoomed layout' \ "$(raw_layout L:zoom)" "$before" done check_ok kill-window -t L:zoom done # --------------------------------------------------------------------------- # 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 # only an empty v1 body to dump. 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" '0000,' # 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 the v1 body is still empty - # 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" '0000,' # 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