mplex: copy mode test runs against the real server and all real sources; fix -Wmaybe-uninitialized
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -708,18 +708,24 @@ bool MplexCopyMode::c_next_matching_bracket(const Words&, MplexClientConn*) {
|
||||
int ay = abs_y();
|
||||
if (!llen(ay)) return false;
|
||||
wchar_t here = char_at(cx, ay);
|
||||
std::optional<Point> found = std::nullopt;
|
||||
Point target{0, 0};
|
||||
bool have = false; // plain flag instead of an optional: GCC -Wmaybe-uninitialized false positive
|
||||
auto attempt = [&](int x, bool forward) {
|
||||
std::optional<Point> hit = match_bracket(x, ay, forward);
|
||||
if (hit) {
|
||||
target = *hit;
|
||||
have = true;
|
||||
}
|
||||
};
|
||||
if (vi_keys()) {
|
||||
if (contains(L"([{", here)) found = match_bracket(cx, ay, true);
|
||||
else if (contains(L")]}", here)) found = match_bracket(cx, ay, false);
|
||||
if (contains(L"([{", here)) attempt(cx, true);
|
||||
else if (contains(L")]}", here)) attempt(cx, false);
|
||||
} else {
|
||||
for (int x : {cx, cx + 1}) {
|
||||
if (contains(L"([{", char_at(x, ay)) && !found.has_value() && (x == cx || here == L' '))
|
||||
found = match_bracket(x, ay, true);
|
||||
if (contains(L"([{", char_at(x, ay)) && !have && (x == cx || here == L' ')) attempt(x, true);
|
||||
}
|
||||
}
|
||||
if (found.has_value()) {
|
||||
Point target = found.value();
|
||||
if (have) {
|
||||
goto_pos(target.first, target.second);
|
||||
} else {
|
||||
next_word_end(true);
|
||||
@@ -731,16 +737,24 @@ bool MplexCopyMode::c_previous_matching_bracket(const Words&, MplexClientConn*)
|
||||
int ay = abs_y();
|
||||
if (!llen(ay)) return false;
|
||||
wchar_t here = char_at(cx, ay);
|
||||
std::optional<Point> found;
|
||||
Point target{0, 0};
|
||||
bool have = false;
|
||||
auto attempt = [&](int x) {
|
||||
std::optional<Point> hit = match_bracket(x, ay, false);
|
||||
if (hit) {
|
||||
target = *hit;
|
||||
have = true;
|
||||
}
|
||||
};
|
||||
if (contains(L")]}", here)) {
|
||||
found = match_bracket(cx, ay, false);
|
||||
attempt(cx);
|
||||
} else if (vi_keys()) {
|
||||
if (contains(L"([{", here) || contains(L"([{", char_at(cx + 1, ay))) return false;
|
||||
} else if (cx > 0 && contains(L")]}", char_at(cx - 1, ay))) {
|
||||
found = match_bracket(cx - 1, ay, false);
|
||||
attempt(cx - 1);
|
||||
}
|
||||
if (found) {
|
||||
goto_pos(found->first, found->second);
|
||||
if (have) {
|
||||
goto_pos(target.first, target.second);
|
||||
} else {
|
||||
previous_word(true);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ usage: python3 -I mplex_copy_gen.py REPO_ROOT OUT_FILE [fuzz_count] [seed]
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import socket
|
||||
import tempfile
|
||||
import zlib
|
||||
|
||||
@@ -56,21 +57,46 @@ class Recorder:
|
||||
self.lines.append("\t".join(str(f) for f in fields))
|
||||
|
||||
|
||||
def state(mode, pane, server, err, closed):
|
||||
last_copy = "-"
|
||||
if server.copied:
|
||||
text, command, append = server.copied[-1]
|
||||
last_copy = "%s/%s/%d" % (esc(text), esc(command), 1 if append else 0)
|
||||
def state(mode, pane, server, client, err, closed):
|
||||
top = server.buffers[0]["data"] if server.buffers else ""
|
||||
shown = "-"
|
||||
if client.message is not None:
|
||||
shown = esc(client.message[0])
|
||||
prompt = "-" if client.prompt is None else esc(client.prompt.label + client.prompt.text)
|
||||
return "\t".join([
|
||||
"E", str(mode.cx), str(mode.cy), str(mode.oy), str(mode.hist()), pair(mode.anchor), pair(mode.mark),
|
||||
"1" if mode.rect else "0", "1" if mode.line_select else "0", search_text(mode.search),
|
||||
search_text(pane.last_search), "1" if mode.show_position else "0", "1" if closed else "0",
|
||||
str(len(server.copied)), last_copy, str(len(server.messages)),
|
||||
esc(server.messages[-1]) if server.messages else "-", str(mode.count), str(mode.lastcx), str(mode.lastsx),
|
||||
esc(err), "%08x" % digest(mode), str(pane.history_size()), str(pane.prefix_count),
|
||||
str(len(server.buffers)), esc(top), str(len(server.messages)),
|
||||
esc(server.messages[-1][1]) if server.messages else "-", str(mode.count), str(mode.lastcx), str(mode.lastsx),
|
||||
esc(err), "%08x" % digest(mode), str(pane.history_size()), str(pane.prefix_count), shown, prompt,
|
||||
])
|
||||
|
||||
|
||||
HOME = tempfile.mkdtemp(prefix="home.", dir=os.path.dirname(os.path.abspath(out_path)))
|
||||
|
||||
|
||||
def make_fixture(keys, pane):
|
||||
"""A real MplexServer (not listening) with one session and window holding pane, and one attached client."""
|
||||
server = tai.MplexServer(HOME)
|
||||
server.options.set("mode-keys", keys)
|
||||
session = tai.MplexSession(server.next_id("session"), "0", server.options, "/", {})
|
||||
session.size = (pane.w, pane.h + 1)
|
||||
server.sessions[session.id] = session
|
||||
window = tai.MplexWindow(server.next_id("window"), "w", pane.w, pane.h, session.options)
|
||||
window.root = tai.MplexLayoutNode("pane", pane=pane, w=pane.w, h=pane.h)
|
||||
window.active = pane
|
||||
pane.window = window
|
||||
server.panes[pane.id] = pane
|
||||
server.register_window(session, window, 0, True)
|
||||
left, right = socket.socketpair()
|
||||
client = tai.MplexClientConn(server, left, server.next_id("client"))
|
||||
client.session = session
|
||||
client.attached = True
|
||||
server.clients.append(client)
|
||||
return server, client, (left, right)
|
||||
|
||||
|
||||
rec = Recorder()
|
||||
scenarios = 0
|
||||
steps_total = 0
|
||||
@@ -82,8 +108,7 @@ def run_scenario(name, rows, cols, history, keys, content, steps, lines=None, ex
|
||||
pane = tai.MplexPane(1, cols, rows, history, tempfile.gettempdir())
|
||||
if lines is None:
|
||||
pane.feed(content.replace("\n", "\r\n").encode("utf-8"))
|
||||
server = test_tai.MplexStubServer()
|
||||
server.options.set("mode-keys", keys)
|
||||
server, client, pair_sockets = make_fixture(keys, pane)
|
||||
if lines is not None:
|
||||
mode = tai.MplexCopyMode(server, pane, lines=lines)
|
||||
else:
|
||||
@@ -112,7 +137,24 @@ def run_scenario(name, rows, cols, history, keys, content, steps, lines=None, ex
|
||||
elif step[0] == "K":
|
||||
rec.add("K", esc(step[1]))
|
||||
try:
|
||||
mode.key(step[1], b"", None)
|
||||
mode.key(step[1], b"", client)
|
||||
except tai.MplexError as exc:
|
||||
err = "M:" + str(exc)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
err = "X:" + str(exc)
|
||||
elif step[0] in ("Y", "Z"):
|
||||
rec.add(step[0], esc(step[1]) if step[0] == "Y" else "")
|
||||
try:
|
||||
if step[0] == "Z":
|
||||
if client.prompt is not None:
|
||||
client.prompt.key(server, client, "Escape", b"\x1b")
|
||||
elif client.prompt is not None:
|
||||
for ch in step[1]:
|
||||
if client.prompt is None:
|
||||
break
|
||||
client.prompt.key(server, client, "Space" if ch == " " else ch, ch.encode("utf-8"))
|
||||
if client.prompt is not None:
|
||||
client.prompt.key(server, client, "Enter", b"\r")
|
||||
except tai.MplexError as exc:
|
||||
err = "M:" + str(exc)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -124,12 +166,14 @@ def run_scenario(name, rows, cols, history, keys, content, steps, lines=None, ex
|
||||
rec.add("R", step[1], step[2])
|
||||
pane.place(0, 0, step[2], step[1])
|
||||
closed = mode not in pane.modes
|
||||
rec.lines.append(state(mode, pane, server, err, closed))
|
||||
rec.lines.append(state(mode, pane, server, client, err, closed))
|
||||
if closed:
|
||||
break
|
||||
if expect is not None:
|
||||
rec.add("T", expect[0], esc(expect[1]))
|
||||
rec.add("X")
|
||||
for sock in pair_sockets:
|
||||
sock.close()
|
||||
|
||||
|
||||
# ---- command table -----------------------------------------------------------------------------
|
||||
@@ -174,6 +218,19 @@ for table in ("copy-mode", "copy-mode-vi"):
|
||||
content = "\n".join("line %d (a b) {c} a,b,c" % n for n in range(40)) + "\n"
|
||||
run_scenario("key-%s-%s" % (table, key), 6, 30, 100, "vi" if table == "copy-mode-vi" else "emacs", content,
|
||||
[("C", 1, ["page-up"]), ("C", 1, ["begin-selection"]), ("K", key)])
|
||||
# command-prompt bindings through the real server: open the prompt with the key, answer it (or cancel it)
|
||||
PROMPT_ANSWERS = ["5", "0", "12", "x", "line 3", "(a b)", "9", "(", "line", "LINE", "{c}", "a,", " ", "-1"]
|
||||
for table in ("copy-mode", "copy-mode-vi"):
|
||||
for key, binding in list(config.tables[table].items()):
|
||||
if not binding.command.startswith("command-prompt"):
|
||||
continue
|
||||
content = "\n".join("line %d (a b) {c} a,b,c" % n for n in range(40)) + "\n"
|
||||
for answer in PROMPT_ANSWERS:
|
||||
run_scenario("prompt-%s-%s-%s" % (table, key, answer), 6, 30, 100, "vi" if table == "copy-mode-vi" else "emacs",
|
||||
content, [("C", 1, ["history-top"]), ("K", key), ("Y", answer), ("C", 1, ["search-again"]),
|
||||
("C", 1, ["jump-again"])])
|
||||
run_scenario("promptesc-%s-%s" % (table, key), 6, 30, 100, "vi" if table == "copy-mode-vi" else "emacs",
|
||||
content, [("K", key), ("Z", ""), ("C", 1, ["cursor-down"])])
|
||||
|
||||
# ---- seeded fuzz ----------------------------------------------------------------------------------------
|
||||
rng = random.Random(seed)
|
||||
@@ -212,6 +269,10 @@ def pick_step():
|
||||
if rng.random() < 0.08:
|
||||
table = config.tables["copy-mode-vi" if rng.random() < 0.5 else "copy-mode"]
|
||||
return ("K", rng.choice(list(table)))
|
||||
if rng.random() < 0.06:
|
||||
return ("Y", rng.choice(PROMPT_ANSWERS))
|
||||
if rng.random() < 0.02:
|
||||
return ("Z", "")
|
||||
pick = rng.random()
|
||||
if pick < 0.08:
|
||||
return ("C", count, [rng.choice(JUMPS), rng.choice(JARGS)])
|
||||
@@ -315,4 +376,7 @@ tmux_live(int(os.environ.get("F4_TMUX_LIVE", "240")))
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(rec.lines) + "\n")
|
||||
import shutil # noqa: E402
|
||||
|
||||
shutil.rmtree(HOME, ignore_errors=True)
|
||||
sys.stderr.write("scenarios=%d steps=%d\n" % (scenarios, steps_total))
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
#!/bin/sh
|
||||
# Build and run the MplexCopyMode differential test. Writes only under taicpp/build/f4.
|
||||
# usage: tests/mplex_copy_run.sh [fuzz_count] [seed] (SAN=1 builds with ASan and UBSan)
|
||||
# Build and run the MplexCopyMode differential test against ALL real sources. Writes only under taicpp/build/f4.
|
||||
# usage: tests/mplex_copy_run.sh [fuzz_count] [seed] (SAN=1 builds with ASan and UBSan; F4_TMUX_LIVE=N live tmux cases)
|
||||
set -e
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
tc=$(cd "$here/.." && pwd)
|
||||
repo=$(cd "$tc/.." && pwd)
|
||||
out="$tc/build/f4"
|
||||
mkdir -p "$out"
|
||||
srcs=""
|
||||
for f in "$tc"/src/mplex/*.cpp; do
|
||||
case "$f" in
|
||||
*/mplex_server.cpp|*/mplex_commands.cpp|*/mplex_client.cpp) continue ;;
|
||||
esac
|
||||
srcs="$srcs $f"
|
||||
done
|
||||
flags="-O2"
|
||||
bin="$out/mplex_copy_test"
|
||||
if [ -n "$SAN" ]; then
|
||||
flags="-g -O1 -fsanitize=address,undefined"
|
||||
bin="$out/mplex_copy_test_san"
|
||||
fi
|
||||
# The test supplies its own MplexServer stub and the MplexCommands destructor it needs.
|
||||
g++ -std=c++23 -Wall -Wextra $flags -DF4_STUB_COMMANDS -I"$tc/src/mplex" "$tc/tests/mplex_copy_test.cpp" $srcs \
|
||||
-Wl,--unresolved-symbols=ignore-in-object-files -o "$bin"
|
||||
g++ -std=c++23 -Wall -Wextra $flags -I"$tc/src/mplex" -I"$tc/src/plexer" "$tc/tests/mplex_copy_test.cpp" \
|
||||
"$tc"/src/mplex/*.cpp "$tc"/src/plexer/plexer_flow.cpp -o "$bin"
|
||||
python3 -I "$tc/tests/mplex_copy_gen.py" "$repo" "$out/scenarios.txt" "${1:-3600}" "${2:-20251025}"
|
||||
"$bin" "$out/scenarios.txt"
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
// every step's full state line (cursor, scroll, selection, mark, search, copies, messages, render checksum)
|
||||
// must match the Python oracle byte for byte. Fixture scenarios also carry the value tmux 3.6 produced.
|
||||
//
|
||||
// The MplexServer here is the small stub the Python tests use (MplexStubServer): options, bindings, a copy sink.
|
||||
// Every scenario runs against a real MplexServer (not listening, throwaway TAI_HOME) with one real session, window,
|
||||
// pane and attached MplexClientConn; key presses go through the real run_binding and the real command set, and
|
||||
// prompt answers through the real MplexPrompt. Linked against all real sources, no stubs.
|
||||
// Build and run: tests/mplex_copy_run.sh (writes only under taicpp/build/).
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
@@ -15,9 +18,12 @@
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "mplex_args.h"
|
||||
#include "mplex_canvas.h"
|
||||
#include "mplex_commands.h"
|
||||
#include "mplex_clientconn.h"
|
||||
#include "mplex_config.h"
|
||||
#include "mplex_copymode.h"
|
||||
#include "mplex_renderer.h"
|
||||
@@ -25,41 +31,6 @@
|
||||
|
||||
using namespace detz::mplex;
|
||||
|
||||
namespace {
|
||||
std::vector<std::tuple<std::string, std::string, bool>> g_copied;
|
||||
std::vector<std::string> g_messages;
|
||||
} // namespace
|
||||
|
||||
#ifdef F4_STUB_COMMANDS
|
||||
struct detz::mplex::MplexCommands::Impl {};
|
||||
detz::mplex::MplexCommands::~MplexCommands() = default;
|
||||
#endif
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
MplexServer::MplexServer(const std::string& home_in) : home(home_in) {
|
||||
config = std::make_shared<MplexConfig>();
|
||||
options = &config->options;
|
||||
started = 0.0;
|
||||
}
|
||||
MplexServer::~MplexServer() = default;
|
||||
|
||||
MplexOptions* MplexServer::pane_options(const std::shared_ptr<MplexPane>&) { return options; }
|
||||
void MplexServer::message(const std::shared_ptr<MplexClientConn>&, const std::string& text, std::optional<long>) {
|
||||
g_messages.push_back(text);
|
||||
}
|
||||
void MplexServer::mark_dirty(const std::shared_ptr<MplexWindow>&) {}
|
||||
void MplexServer::store_copy(const std::string& text, const std::string& command, bool append) {
|
||||
g_copied.emplace_back(text, command, append);
|
||||
}
|
||||
void MplexServer::run_binding(const std::shared_ptr<MplexClientConn>&, const MplexBinding& binding,
|
||||
const std::shared_ptr<MplexPane>& pane) {
|
||||
auto words = MplexCommandParser::parse(binding.command)[0];
|
||||
auto mode = std::dynamic_pointer_cast<MplexCopyMode>(pane->top_mode());
|
||||
mode->command(words[2], std::vector<std::string>(words.begin() + 3, words.end()), nullptr);
|
||||
}
|
||||
|
||||
} // namespace detz::mplex
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -125,9 +96,8 @@ std::string hex8(uint32_t value) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string digest(MplexCopyMode& mode) {
|
||||
alignas(16) static char scratch[sizeof(MplexClientConn)]; // render() ignores its client
|
||||
ModeBlock block = mode.render(*reinterpret_cast<MplexClientConn*>(scratch));
|
||||
std::string digest(MplexCopyMode& mode, MplexClientConn& client) {
|
||||
ModeBlock block = mode.render(client);
|
||||
std::string parts;
|
||||
for (const Row& row : block.grid) {
|
||||
for (const Cell& cell : row) {
|
||||
@@ -151,20 +121,19 @@ std::string search_text(const std::optional<MplexPane::SearchState>& value) {
|
||||
return esc(value->text) + "," + (value->forward ? "1" : "0") + "," + (value->literal ? "1" : "0");
|
||||
}
|
||||
|
||||
std::string state_line(MplexCopyMode& mode, const std::shared_ptr<MplexPane>& pane, const std::string& err, bool closed) {
|
||||
std::string last_copy = "-";
|
||||
if (!g_copied.empty()) {
|
||||
const auto& [text, command, append] = g_copied.back();
|
||||
last_copy = esc(text) + "/" + esc(command) + "/" + (append ? "1" : "0");
|
||||
}
|
||||
std::string state_line(MplexCopyMode& mode, const std::shared_ptr<MplexPane>& pane, MplexServer& server,
|
||||
MplexClientConn& client, const std::string& err, bool closed) {
|
||||
std::string top = server.buffers.empty() ? "" : server.buffers[0].data;
|
||||
std::string shown = client.message ? esc(client.message->first) : "-";
|
||||
std::string prompt = client.prompt ? esc(to_utf8(client.prompt->label + client.prompt->text)) : "-";
|
||||
std::vector<std::string> f = {
|
||||
"E", std::to_string(mode.cx), std::to_string(mode.cy), std::to_string(mode.oy), std::to_string(mode.hist()),
|
||||
pair_text(mode.anchor), pair_text(mode.mark), mode.rect ? "1" : "0", mode.line_select ? "1" : "0",
|
||||
search_text(mode.search), search_text(pane->last_search), mode.show_position ? "1" : "0", closed ? "1" : "0",
|
||||
std::to_string(g_copied.size()), last_copy, std::to_string(g_messages.size()),
|
||||
g_messages.empty() ? "-" : esc(g_messages.back()), std::to_string(mode.count), std::to_string(mode.lastcx),
|
||||
std::to_string(mode.lastsx), esc(err), digest(mode), std::to_string(pane->history_size()),
|
||||
std::to_string(pane->prefix_count)};
|
||||
std::to_string(server.buffers.size()), esc(top), std::to_string(server.messages.size()),
|
||||
server.messages.empty() ? "-" : esc(server.messages.back().second), std::to_string(mode.count),
|
||||
std::to_string(mode.lastcx), std::to_string(mode.lastsx), esc(err), digest(mode, client),
|
||||
std::to_string(pane->history_size()), std::to_string(pane->prefix_count), shown, prompt};
|
||||
std::string out;
|
||||
for (size_t i = 0; i < f.size(); ++i) out += (i ? "\t" : "") + f[i];
|
||||
return out;
|
||||
@@ -193,6 +162,15 @@ int main(int argc, char** argv) {
|
||||
std::cerr << "usage: mplex_copy_test SCENARIOS\n";
|
||||
return 2;
|
||||
}
|
||||
// throwaway TAI_HOME next to the scenario file (under taicpp/build/), removed at the end
|
||||
std::string scenario_path = argv[1];
|
||||
size_t slash = scenario_path.rfind('/');
|
||||
std::string home_template = (slash == std::string::npos ? std::string(".") : scenario_path.substr(0, slash)) + "/home.XXXXXX";
|
||||
if (mkdtemp(home_template.data()) == nullptr) {
|
||||
std::cerr << "cannot create a throwaway TAI_HOME\n";
|
||||
return 2;
|
||||
}
|
||||
const std::string home = home_template;
|
||||
std::ifstream in(argv[1]);
|
||||
std::vector<std::string> lines;
|
||||
for (std::string line; std::getline(in, line);) lines.push_back(line);
|
||||
@@ -218,9 +196,7 @@ int main(int argc, char** argv) {
|
||||
std::string content = unesc(s[6]);
|
||||
bool view = s[7] == "1";
|
||||
bool exit_at_bottom = s[8] == "1";
|
||||
g_copied.clear();
|
||||
g_messages.clear();
|
||||
MplexServer server("/tmp");
|
||||
MplexServer server(home);
|
||||
server.options->set("mode-keys", keys);
|
||||
auto pane = std::make_shared<MplexPane>(1, cols, rows, history, "/tmp");
|
||||
std::vector<std::string> view_lines;
|
||||
@@ -250,11 +226,25 @@ int main(int argc, char** argv) {
|
||||
mode = std::make_shared<MplexCopyMode>(&server, pane, nullptr, false, exit_at_bottom);
|
||||
}
|
||||
pane->push_mode(mode);
|
||||
// one real session and window holding the pane, one attached client (see make_fixture in the generator)
|
||||
auto session = std::make_shared<MplexSession>(server.next_id("session"), "0", server.options, "/", MplexPane::EnvList{});
|
||||
session->size = std::make_pair(pane->w, pane->h + 1);
|
||||
server.sessions.set(session->id, session);
|
||||
auto window = std::make_shared<MplexWindow>(server.next_id("window"), "w", pane->w, pane->h, &session->options);
|
||||
window->root = std::make_shared<MplexLayoutNode>("pane", pane, pane->w, pane->h);
|
||||
window->active = pane;
|
||||
pane->set_window(window);
|
||||
server.panes.set(pane->id, pane);
|
||||
server.register_window(session, window, 0, true);
|
||||
auto client = std::make_shared<MplexClientConn>(&server, -1, server.next_id("client"));
|
||||
client->session = session;
|
||||
client->attached = true;
|
||||
server.clients.push_back(client);
|
||||
++checked_scenarios;
|
||||
{
|
||||
auto e0 = split_tabs(lines[at++]);
|
||||
std::string got = std::to_string(mode->cx) + " " + std::to_string(mode->cy) + " " + std::to_string(mode->oy) + " " +
|
||||
std::to_string(mode->hist()) + " " + mode->name + " " + digest(*mode);
|
||||
std::to_string(mode->hist()) + " " + mode->name + " " + digest(*mode, *client);
|
||||
std::string want = e0[1] + " " + e0[2] + " " + e0[3] + " " + e0[4] + " " + e0[5] + " " + e0[6];
|
||||
if (got != want) fail(name, "initial", want, got);
|
||||
}
|
||||
@@ -280,9 +270,25 @@ int main(int argc, char** argv) {
|
||||
err = std::string("X:") + exc.what();
|
||||
}
|
||||
} else if (step[0] == "K") {
|
||||
auto conn = std::make_shared<MplexClientConn>(&server, -1, 1);
|
||||
try {
|
||||
mode->key(unesc(step[1]), Bytes(), *conn);
|
||||
mode->key(unesc(step[1]), Bytes(), *client);
|
||||
} catch (const MplexError& exc) {
|
||||
err = std::string("M:") + exc.what();
|
||||
} catch (const std::exception& exc) {
|
||||
err = std::string("X:") + exc.what();
|
||||
}
|
||||
} else if (step[0] == "Y" || step[0] == "Z") {
|
||||
try {
|
||||
if (step[0] == "Z") {
|
||||
if (client->prompt) client->prompt->key(server, *client, "Escape", "\x1b");
|
||||
} else if (client->prompt) {
|
||||
UStr typed = to_ustr(unesc(step[1]));
|
||||
for (wchar_t ch : typed) {
|
||||
if (!client->prompt) break;
|
||||
client->prompt->key(server, *client, ch == L' ' ? std::string("Space") : to_utf8(ch), to_utf8(ch));
|
||||
}
|
||||
if (client->prompt) client->prompt->key(server, *client, "Enter", "\r");
|
||||
}
|
||||
} catch (const MplexError& exc) {
|
||||
err = std::string("M:") + exc.what();
|
||||
} catch (const std::exception& exc) {
|
||||
@@ -297,7 +303,7 @@ int main(int argc, char** argv) {
|
||||
return 2;
|
||||
}
|
||||
bool closed = std::find(pane->modes.begin(), pane->modes.end(), mode) == pane->modes.end();
|
||||
std::string got = state_line(*mode, pane, err, closed);
|
||||
std::string got = state_line(*mode, pane, server, *client, err, closed);
|
||||
std::string want = lines[at++];
|
||||
++checked_steps;
|
||||
if (got != want) fail(name, "after step [" + lines[at - 2] + "]", want, got);
|
||||
@@ -317,12 +323,13 @@ int main(int argc, char** argv) {
|
||||
if (t[1] == "cursor") {
|
||||
if (last_state_cursor != t[2]) fail(name, "tmux cursor", t[2], last_state_cursor);
|
||||
} else {
|
||||
std::string got = g_copied.empty() ? "" : std::get<0>(g_copied.back());
|
||||
std::string got = server.top_buffer_text();
|
||||
if (got != unesc(t[2])) fail(name, "tmux copied text", unesc(t[2]), got);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << "scenarios=" << checked_scenarios << " steps=" << checked_steps << " tmux_fixture_checks=" << tmux_checks
|
||||
<< " failures=" << failures << " tmux_live=" << live_total << " tmux_live_differ=" << live_diff << "\n";
|
||||
std::filesystem::remove_all(home);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user