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:
2026-10-25 01:59:31 +02:00
co-authored by Claude Sonnet 5.5
parent 80b914b33b
commit 43aab23534
4 changed files with 170 additions and 93 deletions
+64 -57
View File
@@ -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;
}