// Differential test of MplexCommands (F6): the same scripted corpus of `--mplex ` calls // runs against the C++ server (a built cli binary) and against tai.py's Python server, each in // a throwaway TAI_HOME, and every command's output and exit status must be identical after // normalising times, homes and temp paths. A few layout scenarios are also checked against real // tmux 3.6 (the oracle named in mplex.md). // // g++ -std=c++23 -Wall -Wextra -Isrc/mplex tests/mplex_commands_test.cpp -o build/mplex_commands_test // build/mplex_commands_test [--filter NAME] [--keep] // // It links nothing from src/mplex: both servers run as separate processes. #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct Scenario { std::string name; std::vector lines; // "!..." raw shell, "!sleep N" pause, otherwise args after --mplex }; struct Impl { std::string name; std::string cli; // command prefix, e.g. "'/x/mplex_cli' --mplex" std::string home; std::string tmp; }; std::string g_root; bool g_keep = false; std::string run_shell(const std::string& command, int* rc) { int out[2]; if (::pipe(out) != 0) return "pipe failed"; pid_t pid = ::fork(); if (pid == 0) { ::dup2(out[1], 1); ::dup2(out[1], 2); ::close(out[0]); ::close(out[1]); int devnull = ::open("/dev/null", O_RDONLY); if (devnull >= 0) ::dup2(devnull, 0); ::execl("/bin/sh", "sh", "-c", command.c_str(), static_cast(nullptr)); _exit(127); } ::close(out[1]); std::string data; char buf[4096]; for (;;) { ssize_t got = ::read(out[0], buf, sizeof(buf)); if (got <= 0) break; data.append(buf, static_cast(got)); } ::close(out[0]); int status = 0; ::waitpid(pid, &status, 0); *rc = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); return data; } std::string shquote(const std::string& text) { std::string out = "'"; for (char ch : text) { if (ch == '\'') out += "'\\''"; else out.push_back(ch); } return out + "'"; } std::string normalise(std::string text, const Impl& impl) { auto replace_all = [&](const std::string& from, const std::string& to) { if (from.empty()) return; size_t pos = 0; while ((pos = text.find(from, pos)) != std::string::npos) { text.replace(pos, from.size(), to); pos += to.size(); } }; replace_all(impl.tmp, ""); replace_all(impl.home, ""); // The regex engines differ (std::wregex versus Python re): only the wording of the error differs. text = std::regex_replace(text, std::regex("bad regular expression: [^\\n]*"), "bad regular expression: "); text = std::regex_replace(text, std::regex("[0-9][0-9]:[0-9][0-9]:[0-9][0-9]"), "HH:MM:SS"); text = std::regex_replace(text, std::regex("created [A-Za-z]{3} [A-Za-z]{3} [ 0-9]{1,2} HH:MM:SS [0-9]{4}"), "created "); text = std::regex_replace(text, std::regex("[0-9][0-9]:[0-9][0-9] [0-9][0-9]-[A-Za-z]{3}-[0-9][0-9]"), ""); return text; } std::string env_prefix(const Impl& impl) { return "env -u TMUX -u TAI_PLEXER_ACTIVE -u TAI_PLEXER_SOCKET -u TAI_PLEXER_PANE TAI_HOME=" + shquote(impl.home) + " HOME=" + shquote(impl.tmp) + " T=" + shquote(impl.tmp) + " SHELL=/bin/sh TERM=xterm LANG=C.UTF-8 "; } std::vector corpus(); } // namespace namespace { std::vector corpus() { std::vector all; const std::string S = "sleep 600"; auto sess = [&](const std::string& extra) { return "new-session -d " + extra + " '" + S + "'"; }; all.push_back({"sessions", { "list-sessions", "has-session", "has-session -t nope", "kill-session -t nope", sess("-s main -x 100 -y 30"), "list-sessions", "list-sessions -F '#{session_id} #{session_name} #{session_windows} #{window_width}x#{window_height} #{session_attached}'", sess("-s two -n w2"), sess("-s two"), "new-session -d -s 'bad:name'", "new-session -d -s 'bad.name'", "new-session -A -d -s two", "new-session -A -D -d -s two", "has-session -t main", "has-session -t two", "has-session -t nope", "has-session -t =ma", "has-session -t m", "has-session -t '$0'", "has-session -t '$99'", "rename-session -t two three", "rename-session -t three main", "rename-session -t three 'a.b'", "rename-session -t three ''", "rename-session -t nope zz", "list-sessions -F '#{session_id} #{session_name}'", "list-sessions -f '#{==:#{session_name},three}' -F '#{session_name}'", "list-sessions -F '#{session_windows}x'", "ls -F '#{session_name}'", "new-session -d -s sized -x 50 -y 12 'sleep 600'", "list-windows -t sized -F '#{window_width}x#{window_height}'", "new-session -d -s named -n nm -P -F '#{session_name}:#{window_name}'", "new-session -d -P", "new-session -d -P -F '#{session_id}'", "new-session -d -s envs -e FOO=bar -e BAZ= 'sleep 600'", "kill-session -t sized", "kill-session -t sized", "kill-session -a -t main", "list-sessions -F '#{session_name}'", "kill-session", "list-sessions", "kill-server", "!sleep 0.2", "list-sessions", }}); all.push_back({"windows", { sess("-s main -x 100 -y 30"), "!sleep 0.2", "new-window -t main -n alpha 'sleep 600'", "new-window -t main: -n beta -P -F '#{window_index}:#{window_name}'", "new-window -t main:7 -n seven", "new-window -a -t main:0 -n after0", "new-window -b -t main:0 -n before0", "new-window -d -n detached", "new-window -t main:7 -n clash", "new-window -k -t main:7 -n replaced", "list-windows -t main -F '#{window_index}:#{window_name}#{window_flags}:#{window_active}:#{window_last_flag}:#{window_id}'", "select-window -t main:alpha", "select-window -t :=2", "select-window -t main:99", "select-window -t nowin", "next-window", "previous-window", "last-window", "select-window -l", "select-window -n", "select-window -p", "select-window -T -t main:alpha", "select-window -T -t main:beta", "next-window -a", "previous-window -a", "list-windows -F '#{window_index}:#{window_name}#{window_flags}'", "rename-window -t main:0 foo", "rename-window ''", "rename-window -t main:99 x", "rename-window verylongnamebbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "list-windows -F '#{window_index}:#{window_name}'", "move-window -s main:1 -t main:20", "move-window -s main:20 -t main:3", "move-window -s main:3 -t main:5 -d", "move-window -s main:5", "move-window -s main:alpha -t main:0", "move-window -k -s main:alpha -t main:0", "move-window -a -s main:0 -t main:1", "move-window -b -s main:1 -t main:0", "list-windows -F '#{window_index}:#{window_name}#{window_flags}'", "swap-window -s main:0 -t main:1", "swap-window -d -s main:0 -t main:1", "swap-window -s main:0 -t main:99", "move-window -r", "move-window -r -t main", "list-windows -F '#{window_index}:#{window_name}#{window_flags}'", "find-window foo", "find-window -N alp", "find-window -r '^al'", "find-window -r '(' ", "find-window -i ALPHA", "find-window zzzzz", "find-window -C sleep", "find-window -T nothing", "list-windows -a -F '#{session_name}:#{window_index}'", "list-windows -a -f '#{window_active}' -F '#{window_index}'", "kill-window -t main:99", "kill-window -t main:7", "kill-window -a -t main:2", "list-windows -F '#{window_index}:#{window_name}#{window_flags}'", sess("-s other"), "move-window -s main:2 -t other:9", "!sleep 1.5", "list-windows -t other -F '#{window_index}:#{window_name}'", "list-windows -t main -F '#{window_index}:#{window_name}'", "swap-window -s main:2 -t other:9", "move-window -s other:9 -t main:2", "move-window -s other:0 -t main:44", "list-sessions -F '#{session_name} #{session_windows}'", "kill-window -a", "kill-window", "kill-window", }}); all.push_back({"panes", { sess("-s main -x 100 -y 30"), "!sleep 0.2", "split-window -h", "split-window -v", "split-window -h -b -P -F '#{pane_index}:#{pane_id}:#{pane_width}x#{pane_height}'", "split-window -d -l 5", "split-window -l 30%", "split-window -l 50% -h", "split-window -l abc", "split-window -l 1 -h", "list-panes -F '#{pane_index} #{pane_id} #{pane_width}x#{pane_height} #{pane_left},#{pane_top} #{pane_active} #{pane_last}'", "select-pane -U", "select-pane -D", "select-pane -L", "select-pane -R", "select-pane -l", "select-pane -t 0", "select-pane -t %1", "select-pane -t 99", "select-pane -m", "list-panes -F '#{pane_index} #{pane_marked} #{pane_marked_set}'", "select-pane -M", "select-pane -T 'my title'", "list-panes -F '#{pane_index} #{pane_title}'", "select-pane -e", "select-pane -d", "resize-pane -U", "resize-pane -D 3", "resize-pane -L 2", "resize-pane -R 4", "resize-pane -x 20", "resize-pane -y 5", "resize-pane -x 30%", "resize-pane -x abc", "resize-pane -D abc", "resize-pane -M", "resize-pane -T", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "resize-pane -Z", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{window_zoomed_flag}'", "resize-pane -Z", "swap-pane -U", "swap-pane -D", "swap-pane -s 0 -t 1", "swap-pane", "select-pane -m -t 0", "swap-pane -t 1", "list-panes -F '#{pane_index} #{pane_id} #{pane_width}x#{pane_height}'", "rotate-window", "rotate-window -D", "rotate-window -U", "list-panes -F '#{pane_index} #{pane_id} #{pane_width}x#{pane_height} #{pane_active}'", "last-pane", "last-pane", "kill-pane -t 99", "kill-pane -t 1", "kill-pane -a -t 0", "list-panes -F '#{pane_index} #{pane_id} #{pane_width}x#{pane_height}'", "last-pane", "split-window -Z", "list-panes -F '#{pane_index} #{window_zoomed_flag}'", "split-window -h -P", "break-pane", "break-pane -s 0 -n broken -d -P -F '#{window_index}:#{window_name}'", "break-pane -t 9", "list-windows -F '#{window_index}:#{window_name}#{window_flags}'", "split-window", "break-pane -d -t 9", "break-pane -d -t 3", "list-windows -F '#{window_index}:#{window_name}#{window_flags}'", "clear-history", "clear-history -t 99", }}); all.push_back({"layouts", { sess("-s main -x 100 -y 30"), "!sleep 0.2", "split-window", "split-window", "split-window", "split-window", "select-layout even-horizontal", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout even-vertical", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout main-horizontal", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout main-vertical", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout tiled", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout main-horizontal-mirrored", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout main-vertical-mirrored", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout even", "select-layout main-h", "select-layout main-horizontal-m", "select-layout nonsense", "select-layout", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height}'", "select-layout -n", "select-layout -p", "select-layout -o", "next-layout", "next-layout", "previous-layout", "previous-layout", "previous-layout", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-layout -E", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "set-option -w main-pane-width 40", "set-option -w main-pane-height 10", "select-layout main-vertical", "select-layout main-horizontal", "list-panes -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'", "select-pane -t 2", "resize-pane -U 2", "select-pane -D", "select-pane -U", "select-pane -L", "select-pane -R", "list-panes -F '#{pane_index} #{pane_active} #{pane_width}x#{pane_height}'", }}); all.push_back({"keys", { sess("-s main"), "list-keys -T root", "list-keys -T prefix | head -5", "list-keys -1", "list-keys -T copy-mode-vi -1", "list-keys -T nosuch", "list-keys -N | head -8", "list-keys -N -1", "list-keys -Na | head -4", "list-keys -N -T root", "list-keys -N -P 'pfx ' -1", "list-keys -N c", "list-keys -N zzzz", "list-keys c", "list-keys -T prefix c", "bind-key -n F5 new-window", "bind-key -T prefix -r M-x 'split-window -h'", "bind-key -N 'a note' X display-message hi", "bind-key", "bind-key Z", "bind-key Z nonsense-cmd", "bind-key nokeyxx new-window", "bind-key -T custom a list-keys", "list-keys -T root", "list-keys -T custom", "list-keys X", "list-keys -N X", "list-keys -T prefix M-x", "unbind-key -n F5", "unbind-key F5", "unbind-key -q F5", "unbind-key -T custom a", "unbind-key", "unbind-key -a -T custom", "list-keys -T custom", "unbind-key -a", "list-keys -T prefix", "unbind-key -a -T root", "list-keys | head -3", "bind-key a if-shell 'true' 'display-message -p yes' 'display-message -p no'", "bind-key b send-keys -X cancel", "list-keys -T prefix a", "list-keys -T prefix b", "bind-key q command-prompt -p 'x,y' 'display-message \"%1 %2\"'", "list-keys -T prefix q", "list-commands", "list-commands -F x", }}); all.push_back({"options", { sess("-s main -x 100 -y 30"), "show-options -g | head -70", "show-options", "show-options -w", "show-options -g status", "show-options -gv status", "show-options -gv prefix", "show-options -g prefix2", "show-options -gv mode-keys", "show-options -g status-left", "show-options -v status-right", "show-options -gq nosuch", "show-options -g nosuch", "show-options -g stat", "show-options -g history", "show-options -A", "show-window-options", "show-window-options -g | head -5", "set-option -g status off", "show-options -g status", "set-option -g status", "show-options -gv status", "set-option -g status on", "set-option -g prefix C-a", "show-options -g prefix", "set-option -g prefix None", "show-options -g prefix", "set-option -g prefix nonsense", "set-option -g prefix2 F12", "show-options -gv prefix2", "set-option -g history-limit 50", "set-option -g history-limit 999999", "set-option -g history-limit abc", "show-options -gv history-limit", "set-option -g base-index 1", "show-options -gv base-index", "new-window", "list-windows -F '#{window_index}'", "set-option -g status-left '[#S] '", "set-option -ga status-left 'more'", "show-options -g status-left", "set-option -g status-left-style 'fg=red,bg=blue,bold'", "set-option -g status-left-style 'fg=nosuch'", "set-option -g status-style 'bad=1'", "show-options -g status-left-style", "set-option -gu status-left", "show-options -g status-left", "set-option -go status-position top", "set-option -go status-position bottom", "set-option -g status-position top", "set-option -go status-position bottom", "show-options -gv status-position", "set-option -gq nosuch 1", "set-option -g nosuch 1", "set-option -q nosuch 1", "set-option", "set-option -g mode-keys vi", "set-option -g mode-keys bad", "set-option -w -t main:0 automatic-rename off", "show-window-options -t main:0 automatic-rename", "show-options -w -t main:0 automatic-rename", "set-option -t main status-interval 3", "show-options -t main status-interval", "show-options -v -t main status-interval", "set-window-option -t main:0 main-pane-width 33", "show-window-options -t main:0 main-pane-width", "set-window-option -g mode-keys emacs", "set-option -F -g status-left '#{session_name}-x'", "show-options -gv status-left", "set-option -g escape-time 500", "set-option -g escape-time 100000", "set-option -g word-separators ' -_'", "show-options -g word-separators", "set-option -g pane-border-lines double", "set-option -g pane-border-lines other", "set-option -g clock-mode-style 12", "set-option -g clock-mode-colour red", "set-option -g clock-mode-colour #zzzzzz", "set-option -g clock-mode-colour '#ff0000'", "show-options -g clock-mode-colour", "show-options -g copy-command", "set-option -g copy-command 'cat >/dev/null'", "show-options -g copy-command", "set-option -g set-clipboard external", "set-option -g buffer-limit 3", "set-option -g buffer-limit 0", }}); all.push_back({"buffers", { sess("-s main"), "!sleep 0.1", "list-buffers", "show-buffer", "delete-buffer", "paste-buffer", "save-buffer $T/b.txt", "set-buffer", "show-buffer -b nope", "delete-buffer -b nope", "set-buffer hello", "set-buffer -b named 'two\nlines'", "set-buffer -b third third", "list-buffers", "list-buffers -F '#{buffer_name}:#{buffer_size}:#{buffer_sample}'", "list-buffers -f '#{==:#{buffer_name},named}'", "show-buffer", "show-buffer -b named", "set-buffer -a -b named MORE", "show-buffer -b named", "set-buffer -a -b nope x", "set-buffer -n renamed -b named", "set-buffer -n x -b nope", "list-buffers -F '#{buffer_name}'", "save-buffer -b renamed $T/b1.txt", "!cat $T/b1.txt", "save-buffer -a -b renamed $T/b1.txt", "!cat $T/b1.txt", "save-buffer $T/nodir/x.txt", "save-buffer -b nope $T/b2.txt", "save-buffer", "load-buffer $T/b1.txt", "load-buffer -b loaded $T/b1.txt", "load-buffer $T/nofile", "load-buffer $T", "list-buffers -F '#{buffer_name}:#{buffer_size}'", "show-buffer -b loaded", "capture-pane -b cap -p", "list-buffers -F '#{buffer_name}'", "delete-buffer -b loaded", "delete-buffer", "list-buffers -F '#{buffer_name}'", "set-buffer -b e1 ä€x", "list-buffers -F '#{buffer_name}:#{buffer_size}:#{buffer_sample}'", "choose-buffer", "choose-client", "choose-tree", "set-option -g buffer-limit 2", "set-buffer 1", "set-buffer 2", "set-buffer 3", "list-buffers -F '#{buffer_name}'", }}); all.push_back({"panes_io", { "new-session -d -s main -x 80 -y 24 'printf \"line1\\nline2\\n\"; exec cat'", "!sleep 0.5", "capture-pane -p", "capture-pane -p -S 0 -E 3", "capture-pane -p -S - -E -", "capture-pane -p -N -E 1", "capture-pane -a", "capture-pane -a -q", "capture-pane -p -S abc", "capture-pane -p -S -5", "capture-pane -p -E 100", "capture-pane -b mycap", "show-buffer -b mycap", "send-keys 'hello world' Enter", "!sleep 0.3", "capture-pane -p -E 4", "send-keys -l 'literal Enter'", "!sleep 0.2", "capture-pane -p -E 6", "send-keys Enter", "send-keys C-c", "send-keys -H 68 69 0d", "!sleep 0.3", "send-keys -H zz", "send-keys -H 100", "capture-pane -p -E 9", "send-keys", "send-keys -N 3 a", "!sleep 0.3", "capture-pane -p -E 12", "send-keys -R", "send-keys -M x", "send-keys -X cancel", "send-keys -F '#{pane_id}' Enter", "!sleep 0.3", "capture-pane -p -E 13", "send-keys -t 99 x", "set-buffer 'pasted text\nsecond'", "paste-buffer", "!sleep 0.3", "capture-pane -p -E 17", "paste-buffer -r -s X", "paste-buffer -d", "list-buffers", "paste-buffer -p -b nope", "send-prefix", "send-prefix -2", "set-option -g prefix2 F12", "send-prefix -2", "set-option -g prefix None", "send-prefix", "display-message -p '#{pane_current_command} #{pane_width} #{pane_height} #{history_size} #{cursor_x} #{cursor_y}'", "display-message -p", "display-message -p 'hello #S #I #W #P #T #F'", "display-message -p '#{pane_dead} #{pane_mode} #{alternate_on}'", "display-message -p -t 99 x", "display-message hi", "display-message -d 5 -p x", "display-message -p '#[fg=red]styled#[default]'", }}); all.push_back({"modes", { "new-session -d -s main -x 80 -y 24 'printf \"alpha beta\\ngamma delta\\n\"; exec cat'", "!sleep 0.5", "copy-mode", "display-message -p '#{pane_in_mode} #{pane_mode} #{copy_cursor_x} #{copy_cursor_y} #{scroll_position}'", "send-keys -X cursor-down", "send-keys -X next-word", "send-keys -X end-of-line", "send-keys -X begin-selection", "send-keys -X start-of-line", "display-message -p '#{selection_present} #{copy_cursor_x} #{copy_cursor_y} #{copy_cursor_line} #{copy_cursor_word}'", "send-keys -X copy-selection", "list-buffers -F '#{buffer_size}:#{buffer_sample}'", "send-keys -X nosuchcommand", "send-keys -X", "send-keys -X cursor-up", "send-keys -X search-forward gam", "send-keys -X search-again", "send-keys -X search-reverse", "send-keys -N 2 -X cursor-right", "send-keys -X cancel", "display-message -p '#{pane_in_mode}'", "send-keys -X cancel", "copy-mode -u", "display-message -p '#{pane_in_mode} #{scroll_position}'", "copy-mode -q", "display-message -p '#{pane_in_mode}'", "copy-mode -e", "copy-mode -H", "copy-mode -M", "copy-mode -q", "clock-mode", "display-message -p '#{pane_mode}'", "clock-mode", "send-keys q", "display-message -p '#{pane_mode}'", "copy-mode", "send-keys Escape", "display-message -p '#{pane_in_mode}'", "copy-mode", "send-keys -X search-forward-incremental =alpha", "send-keys -X search-forward-incremental +", "send-keys -X search-backward-incremental x", "send-keys -X jump-forward a", "send-keys -X jump-to-forward l", "send-keys -X goto-line 1", "send-keys -F -X search-forward '#{copy_cursor_word}'", "copy-mode -q", "choose-buffer", "set-buffer abc", "choose-buffer", "display-message -p '#{pane_mode}'", "send-keys Down", "send-keys v", "send-keys q", "choose-tree", "display-message -p '#{pane_mode}'", "send-keys Down Up j k g G", "send-keys Right Left l h + -", "send-keys t v", "send-keys Enter", "display-message -p '#{pane_mode}'", "choose-tree -s", "send-keys q", "choose-tree -w -Z", "send-keys q", "choose-client", "send-keys q", "find-window -N main", "split-window", "find-window -C alpha", "display-message -p '#{pane_mode}'", "send-keys q", "display-panes", "display-panes -d 10", "command-prompt", "command-prompt -p 'q' 'display-message hi'", "confirm-before kill-pane", "confirm-before -p 'sure?' kill-pane", "switch-client -n", "switch-client -p", "switch-client -l", "switch-client -t main", "switch-client -T root", "switch-client", "detach-client", "detach-client -a", "detach-client -s main", "detach-client -t 99", "suspend-client", "refresh-client", "list-clients", "list-clients -F '#{client_tty}'", "attach-session", "attach-session -t nope", }}); all.push_back({"shell", { sess("-s main"), "!sleep 0.1", "if-shell true 'display-message -p yes' 'display-message -p no'", "if-shell false 'display-message -p yes' 'display-message -p no'", "if-shell false 'display-message -p yes'", "if-shell -F '1' 'display-message -p fyes'", "if-shell -F '' 'display-message -p fyes' 'display-message -p fno'", "if-shell -F '#{==:#{session_name},main}' 'display-message -p named'", "if-shell 'exit 3' x", "if-shell true ''", "if-shell true", "run-shell 'echo out; echo err >&2'", "run-shell 'exit 7'", "run-shell", "run-shell -C 'display-message -p viaC'", "run-shell -d 1 x", "run-shell -b 'touch $T/bg.flag'", "!sleep 0.3", "!ls $T/bg.flag", "run-shell 'printf \"a\\n\\n\"'", "run-shell '#{session_name}'", "run-shell -t 99 x", "run-shell 'pwd'", "run-shell 'echo $FOO'", "run-shell 'sleep 7'", "run-shell 'printf \\\\303\\\\251'", "run-shell 'printf \\\\377x'", "!printf 'set-option -g status-interval 9\\n# comment\\n\\n display-message -p fromfile\\nnosuchcmd\\n' > $T/c1.conf", "source-file $T/c1.conf", "show-options -gv status-interval", "source-file $T/nofile", "source-file -q $T/nofile", "source-file", "!printf '[options]\\nstatus-interval = 7\\nnosuch = 1\\n[keys.prefix]\\n\"Z\" = \"new-window\"\\n\"Y\" = \"bogus\"\\n' > $T/c2.toml", "source-file $T/c2.toml", "show-options -gv status-interval", "list-keys -T prefix Z", "show-messages | tail -4", "source-file $T/c1.conf $T/c2.toml", "!printf 'not toml [[' > $T/c3.toml", "source-file $T/c3.toml", "show-messages | tail -2", "source-file $T", "!printf 'display-message -p a ; display-message -p b\\n' > $T/c4.conf", "source-file $T/c4.conf", "display-message -p a ';' display-message -p b", "display-message -p a \\; display-message -p b", "!printf 'source-file $T/c5.conf\\n' > $T/c5.conf", "source-file $T/c5.conf", "frobnicate", "kill-ser", "list-s", "new", "display", "ls", "has", "kill-", "choose", "show-m", "unbind", "select-", "set", "a", "", "display-menu", "display-popup", "pipe-pane", "join-pane", "set-hook", "wait-for", "respawn-pane", "kill-server", "!sleep 0.2", "ls", }}); return all; } struct Outcome { std::string text; int rc = 0; }; Outcome run_line(const Impl& impl, const std::string& line) { Outcome out; if (!line.empty() && line[0] == '!') { std::string raw = line.substr(1); if (raw.rfind("sleep ", 0) == 0) { std::string command = raw; out.text = run_shell(command, &out.rc); out.text.clear(); out.rc = 0; return out; } out.text = run_shell(env_prefix(impl) + "sh -c " + shquote(raw), &out.rc); return out; } std::string command = env_prefix(impl) + "sh -c " + shquote(impl.cli + " " + line); out.text = run_shell(command, &out.rc); return out; } void reset_home(const Impl& impl) { int rc; run_shell(env_prefix(impl) + "sh -c " + shquote(impl.cli + " kill-server >/dev/null 2>&1; sleep 0.2; rm -rf " + shquote(impl.home) + " " + shquote(impl.tmp) + "; mkdir -p " + shquote(impl.home) + " " + shquote(impl.tmp)), &rc); } bool contains_fatal(const std::string& text) { return text.find("could not start the mplex server") != std::string::npos || text.find("Traceback") != std::string::npos || text.find("path too long") != std::string::npos || text.find("SyntaxError") != std::string::npos || text.find("terminate called") != std::string::npos; } int g_checks = 0; int g_failures = 0; bool run_scenario(const Scenario& scenario, Impl& cpp, Impl& py) { reset_home(cpp); reset_home(py); bool ok = true; int index = 0; for (const std::string& line : scenario.lines) { ++index; Outcome a = run_line(cpp, line); Outcome b = run_line(py, line); std::string ta = normalise(a.text, cpp), tb = normalise(b.text, py); if (!line.empty() && line[0] == '!' && line.rfind("!sleep", 0) == 0) continue; if (contains_fatal(ta) || contains_fatal(tb)) { std::cout << "HARNESS ERROR [" << scenario.name << " #" << index << "] " << line << "\n--- cpp\n" << ta << "\n--- python\n" << tb << "\n"; ++g_failures; return false; } ++g_checks; if (ta != tb || a.rc != b.rc) { ++g_failures; ok = false; std::cout << "MISMATCH [" << scenario.name << " #" << index << "] " << line << "\n--- cpp (rc " << a.rc << ")\n" << ta << "\n--- python (rc " << b.rc << ")\n" << tb << "\n"; } } run_line(cpp, "kill-server"); run_line(py, "kill-server"); if (!g_keep) { int rc; run_shell("rm -rf " + shquote(cpp.home) + " " + shquote(cpp.tmp) + " " + shquote(py.home) + " " + shquote(py.tmp), &rc); } return ok; } // Real tmux 3.6 oracle for pane geometry: the same split sequence and layout in tmux and mplex. bool tmux_scenarios(Impl& cpp) { int rc; std::string have = run_shell("command -v tmux", &rc); if (rc != 0) { std::cout << "tmux not installed, geometry oracle skipped\n"; return true; } bool ok = true; const char* layouts[] = {"even-horizontal", "even-vertical", "main-horizontal", "main-vertical", "tiled", "main-horizontal-mirrored", "main-vertical-mirrored"}; for (int panes = 2; panes <= 5; ++panes) { for (const char* layout : layouts) { reset_home(cpp); std::string sock = cpp.tmp + "/tmux.sock"; std::string t = "tmux -S " + shquote(sock) + " -f /dev/null "; std::string script = t + "new-session -d -x 100 -y 30 -s o 'sleep 600'"; for (int i = 1; i < panes; ++i) script += " && " + t + "split-window -d -t o 'sleep 600'"; script += std::string(" && ") + t + "select-layout -t o " + layout + " && " + t + "list-panes -t o -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'; " + t + "kill-server"; std::string want = run_shell(script, &rc); Outcome first = run_line(cpp, "new-session -d -s o -x 100 -y 30 'sleep 600'"); (void)first; std::string commands; for (int i = 1; i < panes; ++i) run_line(cpp, "split-window -d -t o 'sleep 600'"); run_line(cpp, std::string("select-layout -t o ") + layout); Outcome got = run_line(cpp, "list-panes -t o -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_left},#{pane_top}'"); ++g_checks; if (got.text != want) { // tmux has no status line in -x/-y sized detached sessions beyond one row: both use 31 vs 30 rows ++g_failures; ok = false; std::cout << "TMUX MISMATCH panes=" << panes << " layout=" << layout << "\n--- mplex\n" << got.text << "--- tmux\n" << want; } run_line(cpp, "kill-server"); } } return ok; } } // namespace int main(int argc, char** argv) { if (argc < 3) { std::cerr << "usage: " << argv[0] << " [--filter NAME] [--keep] [--no-tmux]\n"; return 2; } std::string cli = argv[1]; g_root = argv[2]; std::string filter; bool tmux = true; for (int i = 3; i < argc; ++i) { if (!std::strcmp(argv[i], "--filter") && i + 1 < argc) filter = argv[++i]; else if (!std::strcmp(argv[i], "--keep")) g_keep = true; else if (!std::strcmp(argv[i], "--no-tmux")) tmux = false; } char cwd[4096]; if (!::getcwd(cwd, sizeof(cwd))) cwd[0] = 0; // AF_UNIX socket paths are limited to 107 bytes, so the work dir has to be short (MPLEX_WORK overrides). const char* work = std::getenv("MPLEX_WORK"); std::string base = work ? work : "/tmp/mplex_commands_work"; ::mkdir(base.c_str(), 0755); Impl cpp{"cpp", shquote(cli) + " --mplex", base + "/cpp-home", base + "/cpp-tmp"}; // tai.py is imported (so its bytecode is cached under build/) instead of run as a script: 40k lines // compile in about a second on every call otherwise. std::string boot = "import sys; sys.path.insert(0, '" + g_root + "'); import tai; sys.exit(tai.main())"; Impl py{"python", "python3 -I -X pycache_prefix=" + shquote(base + "/pyc") + " -c " + shquote(boot) + " --mplex", base + "/py-home", base + "/py-tmp"}; int scenarios = 0, failed = 0; for (const Scenario& scenario : corpus()) { if (!filter.empty() && scenario.name != filter) continue; ++scenarios; if (!run_scenario(scenario, cpp, py)) ++failed; } if (tmux && (filter.empty() || filter == "tmux")) { ++scenarios; if (!tmux_scenarios(cpp)) ++failed; } std::cout << "mplex_commands_test: " << scenarios << " scenarios, " << g_checks << " comparisons, " << g_failures << " mismatches, " << failed << " scenarios failed\n"; return g_failures == 0 ? 0 : 1; }