Files
tai/taicpp/tests/mplex_copy_test.cpp
T

329 lines
12 KiB
C++

// Differential test of MplexCopyMode against tai.py's MplexCopyMode. Scenario file made by
// mplex_copy_gen.py (tmux 3.6 fixtures from test_tai.py, every default copy-mode binding, seeded fuzz);
// 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.
// Build and run: tests/mplex_copy_run.sh (writes only under taicpp/build/).
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
#include "mplex_args.h"
#include "mplex_canvas.h"
#include "mplex_commands.h"
#include "mplex_config.h"
#include "mplex_copymode.h"
#include "mplex_renderer.h"
#include "mplex_server.h"
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 {
std::string esc(const std::string& text) {
std::string out;
for (char ch : text) {
if (ch == '%') out += "%25";
else if (ch == '\t') out += "%09";
else if (ch == '\n') out += "%0A";
else if (ch == '\r') out += "%0D";
else out += ch;
}
return out;
}
std::string unesc(const std::string& text) {
std::string out;
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '%' && i + 2 < text.size()) {
out += static_cast<char>(std::stoi(text.substr(i + 1, 2), nullptr, 16));
i += 2;
} else {
out += text[i];
}
}
return out;
}
std::vector<std::string> split_tabs(const std::string& line) {
std::vector<std::string> out;
size_t start = 0;
while (true) {
size_t at = line.find('\t', start);
if (at == std::string::npos) {
out.push_back(line.substr(start));
break;
}
out.push_back(line.substr(start, at - start));
start = at + 1;
}
return out;
}
uint32_t crc32(const std::string& data) {
static uint32_t table[256];
static bool ready = false;
if (!ready) {
for (uint32_t n = 0; n < 256; ++n) {
uint32_t c = n;
for (int k = 0; k < 8; ++k) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
table[n] = c;
}
ready = true;
}
uint32_t crc = 0xFFFFFFFFu;
for (unsigned char byte : data) crc = table[(crc ^ byte) & 0xFF] ^ (crc >> 8);
return crc ^ 0xFFFFFFFFu;
}
std::string hex8(uint32_t value) {
char buf[16];
std::snprintf(buf, sizeof buf, "%08x", 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 parts;
for (const Row& row : block.grid) {
for (const Cell& cell : row) {
parts += to_utf8(cell.ch);
parts += "|" + color_sgr(cell.fg) + "|" + color_sgr(cell.bg) + "|" + std::to_string(cell.attr) + ";";
}
parts += "\n";
}
if (block.cursor) parts += "(" + std::to_string(block.cursor->first) + ", " + std::to_string(block.cursor->second) + ")";
else parts += "None";
return hex8(crc32(parts));
}
std::string pair_text(const std::optional<MplexCopyMode::Point>& value) {
if (!value) return "-";
return std::to_string(value->first) + "," + std::to_string(value->second);
}
std::string search_text(const std::optional<MplexPane::SearchState>& value) {
if (!value) return "-";
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::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::string out;
for (size_t i = 0; i < f.size(); ++i) out += (i ? "\t" : "") + f[i];
return out;
}
int failures = 0;
int shown = 0;
long checked_steps = 0;
long checked_scenarios = 0;
long tmux_checks = 0;
long live_total = 0;
long live_diff = 0;
void fail(const std::string& scenario, const std::string& what, const std::string& want, const std::string& got) {
++failures;
if (shown < 25) {
++shown;
std::cerr << "MISMATCH " << scenario << " " << what << "\n python: " << want << "\n c++ : " << got << "\n";
}
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "usage: mplex_copy_test SCENARIOS\n";
return 2;
}
std::ifstream in(argv[1]);
std::vector<std::string> lines;
for (std::string line; std::getline(in, line);) lines.push_back(line);
size_t at = 0;
// command table
{
auto fields = split_tabs(lines.at(at++));
std::vector<std::string> want(fields.begin() + 1, fields.end());
if (want != MplexCopyMode::command_names()) {
std::cerr << "command_names() differs from the Python c_* methods\n";
++failures;
}
}
while (at < lines.size()) {
auto s = split_tabs(lines[at++]);
if (s.empty() || s[0] != "S") {
std::cerr << "scenario file out of step at line " << at << "\n";
return 2;
}
std::string name = unesc(s[1]);
int rows = std::stoi(s[2]), cols = std::stoi(s[3]), history = std::stoi(s[4]);
std::string keys = s[5];
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");
server.options->set("mode-keys", keys);
auto pane = std::make_shared<MplexPane>(1, cols, rows, history, "/tmp");
std::vector<std::string> view_lines;
auto replace_nl = [](const std::string& text) {
std::string out;
for (char ch : text) {
if (ch == '\n') out += "\r\n";
else out += ch;
}
return out;
};
std::shared_ptr<MplexCopyMode> mode;
if (view) {
size_t start = 0;
while (true) {
size_t nl = content.find('\n', start);
if (nl == std::string::npos) {
view_lines.push_back(content.substr(start));
break;
}
view_lines.push_back(content.substr(start, nl - start));
start = nl + 1;
}
mode = std::make_shared<MplexCopyMode>(&server, pane, &view_lines);
} else {
pane->feed(replace_nl(content));
mode = std::make_shared<MplexCopyMode>(&server, pane, nullptr, false, exit_at_bottom);
}
pane->push_mode(mode);
++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::string want = e0[1] + " " + e0[2] + " " + e0[3] + " " + e0[4] + " " + e0[5] + " " + e0[6];
if (got != want) fail(name, "initial", want, got);
}
std::string last_state_cursor;
std::vector<std::string> tmux_line;
while (lines[at] != "X") {
auto step = split_tabs(lines[at++]);
std::string err;
if (step[0] == "T") {
tmux_line = step;
continue;
}
if (step[0] == "C") {
int count = std::stoi(step[1]);
std::vector<std::string> words;
for (size_t i = 2; i < step.size(); ++i) words.push_back(unesc(step[i]));
pane->prefix_count = count;
try {
mode->command(words[0], std::vector<std::string>(words.begin() + 1, words.end()), nullptr);
} 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] == "K") {
auto conn = std::make_shared<MplexClientConn>(&server, -1, 1);
try {
mode->key(unesc(step[1]), Bytes(), *conn);
} 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] == "F") {
pane->feed(replace_nl(unesc(step[1])));
} else if (step[0] == "R") {
pane->place(0, 0, std::stoi(step[2]), std::stoi(step[1]));
} else {
std::cerr << "bad step line " << lines[at - 1] << "\n";
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 want = lines[at++];
++checked_steps;
if (got != want) fail(name, "after step [" + lines[at - 2] + "]", want, got);
last_state_cursor = std::to_string(mode->cx) + " " + std::to_string(mode->cy) + " " + std::to_string(mode->oy);
}
++at; // "X"
if (!tmux_line.empty() && tmux_line[1] == "livecursor") {
++live_total;
if (last_state_cursor != tmux_line[2]) {
++live_diff;
if (live_diff <= 12)
std::cout << "tmux live differs: " << name << " tmux=" << tmux_line[2] << " mplex=" << last_state_cursor << "\n";
}
} else if (!tmux_line.empty()) {
auto& t = tmux_line;
++tmux_checks;
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());
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";
return failures ? 1 : 0;
}