mplex: MplexCommands port (68 commands) with differential tests against tai.py, tmux and a pty client

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 01:44:53 +02:00
co-authored by Claude Sonnet 5.5
parent edddfdd601
commit 2cf83537e8
3 changed files with 3051 additions and 0 deletions
+490
View File
@@ -0,0 +1,490 @@
// Differential test of MplexCommands (F6): the same scripted corpus of `--mplex <command>` 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 <cli binary> <repo root holding tai.py> [--filter NAME] [--keep]
//
// It links nothing from src/mplex: both servers run as separate processes.
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <string>
#include <vector>
namespace {
struct Scenario {
std::string name;
std::vector<std::string> 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<char*>(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<size_t>(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, "<TMP>");
replace_all(impl.home, "<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: <re error>");
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 <DATE>");
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]"), "<CLOCK>");
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<Scenario> corpus();
} // namespace
namespace {
std::vector<Scenario> corpus() {
std::vector<Scenario> 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] << " <cli binary> <repo root> [--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;
}