Files
tai/taicpp/tests/mplex_vt_test.cpp
T

255 lines
9.1 KiB
C++

// Differential test: MplexVtScreen, tui_parse_one and MplexKeys against tai.py.
// Build and run (from taicpp/): see the top of tests/mplex_vt_oracle.py and the `mplex-vt-test` rule
// in src/mplex/mplex.mk. usage: mplex_vt_test <tai.py> [cases file]
// The oracle script generates seeded cases (fixed streams plus fuzz) together with the results
// Python produced; this program replays them through the C++ port and compares every one.
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "mplex_input.h"
#include "mplex_keys.h"
#include "mplex_py.h"
#include "mplex_vtscreen.h"
using namespace detz::mplex;
namespace {
std::string hexof(const std::string& data) {
if (data.empty()) return "-";
static const char* digits = "0123456789abcdef";
std::string out;
for (unsigned char c : data) {
out.push_back(digits[c >> 4]);
out.push_back(digits[c & 15]);
}
return out;
}
std::string unhex(const std::string& text) {
if (text == "-") return "";
std::string out;
for (size_t i = 0; i + 1 < text.size(); i += 2) out.push_back(static_cast<char>(std::stoi(text.substr(i, 2), nullptr, 16)));
return out;
}
std::vector<std::string> split_tab(const std::string& line) {
std::vector<std::string> parts;
std::string cur;
for (char c : line) {
if (c == '\t') {
parts.push_back(cur);
cur.clear();
} else {
cur.push_back(c);
}
}
parts.push_back(cur);
return parts;
}
std::string cell_text(const Cell& cell) {
if (!cell.present()) return "-";
char buf[64];
std::snprintf(buf, sizeof buf, "%x:", static_cast<unsigned>(cell.ch));
std::string fg = color_sgr(cell.fg), bg = color_sgr(cell.bg);
return std::string(buf) + (fg.empty() ? "_" : fg) + ":" + (bg.empty() ? "_" : bg) + ":" + std::to_string(cell.attr);
}
std::string row_text(const Row& row) {
if (row.empty()) return ".";
std::string out;
for (size_t i = 0; i < row.size(); ++i) {
if (i) out += " ";
out += cell_text(row[i]);
}
return out;
}
std::string dump(const MplexVtScreen& s) {
std::string out;
out += "size " + std::to_string(s.rows) + " " + std::to_string(s.cols) + "\n";
out += "cur " + std::to_string(s.cx) + " " + std::to_string(s.cy) + " " + (s.cursor_visible ? "1" : "0") + "\n";
std::string fg = color_sgr(s.fg), bg = color_sgr(s.bg);
out += "sgr " + (fg.empty() ? "_" : fg) + " " + (bg.empty() ? "_" : bg) + " " + std::to_string(s.attr) + "\n";
out += "region " + std::to_string(s.region_top) + " " + std::to_string(s.region_bottom) + " " + (s.lr_mode ? "1" : "0") + " " +
std::to_string(s.margin_left) + " " + std::to_string(s.margin_right) + "\n";
out += "modes " + std::to_string(s.app_cursor ? 1 : 0) + " " + std::to_string(s.bracketed_paste ? 1 : 0) + " " +
std::to_string(s.alt_active() ? 1 : 0) + "\n";
out += "title " + hexof(s.title) + "\n";
out += "replies " + hexof(s.replies) + "\n";
if (!s.history) {
out += "nohist\n";
} else {
out += "hist " + std::to_string(s.history_pushed) + " " + std::to_string(s.history->size()) + " " +
std::to_string(s.history->maxlen) + "\n";
for (const Row& row : s.history->rows) out += "h " + row_text(row) + "\n";
}
for (const Row& row : s.grid) out += "g " + row_text(row) + "\n";
out += "lines ";
std::vector<std::string> lines = s.text_lines();
for (size_t i = 0; i < lines.size(); ++i) {
if (i) out += "|";
out += hexof(lines[i]);
}
out += "\n";
out += "text " + hexof(s.text()) + "\n";
out += "text3 " + hexof(s.text(3)) + "\n";
// Python joins with "\n" and has no trailing newline
out.pop_back();
return out;
}
// ---- sha1 (to compare against hashlib.sha1) ------------------------------------------------
std::string sha1(const std::string& msg) {
uint32_t h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476, h4 = 0xC3D2E1F0;
std::string data = msg;
uint64_t bits = static_cast<uint64_t>(msg.size()) * 8;
data.push_back(static_cast<char>(0x80));
while (data.size() % 64 != 56) data.push_back('\0');
for (int i = 7; i >= 0; --i) data.push_back(static_cast<char>((bits >> (i * 8)) & 0xFF));
auto rol = [](uint32_t v, int n) { return (v << n) | (v >> (32 - n)); };
for (size_t off = 0; off < data.size(); off += 64) {
uint32_t w[80];
for (int i = 0; i < 16; ++i)
w[i] = (static_cast<uint32_t>(static_cast<unsigned char>(data[off + i * 4])) << 24) |
(static_cast<uint32_t>(static_cast<unsigned char>(data[off + i * 4 + 1])) << 16) |
(static_cast<uint32_t>(static_cast<unsigned char>(data[off + i * 4 + 2])) << 8) |
static_cast<uint32_t>(static_cast<unsigned char>(data[off + i * 4 + 3]));
for (int i = 16; i < 80; ++i) w[i] = rol(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
uint32_t a = h0, b = h1, c = h2, d = h3, e = h4;
for (int i = 0; i < 80; ++i) {
uint32_t f, k;
if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; }
else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; }
else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; }
else { f = b ^ c ^ d; k = 0xCA62C1D6; }
uint32_t t = rol(a, 5) + f + e + k + w[i];
e = d; d = c; c = rol(b, 30); b = a; a = t;
}
h0 += a; h1 += b; h2 += c; h3 += d; h4 += e;
}
char buf[48];
std::snprintf(buf, sizeof buf, "%08x%08x%08x%08x%08x", h0, h1, h2, h3, h4);
return buf;
}
int failures = 0;
int checked_vt = 0, checked_ops = 0, checked_key = 0;
void run_vt_case(size_t lineno, const std::vector<std::string>& parts) {
int rows = std::stoi(parts[1]), cols = std::stoi(parts[2]), hist = std::stoi(parts[3]);
size_t eq = 4;
while (parts[eq] != "=") ++eq;
MplexVtScreen screen(rows, cols);
if (hist >= 0) screen.history = RowHistory(static_cast<size_t>(hist));
std::vector<std::string> expected;
{
std::istringstream in(parts[eq + 1]);
std::string d;
while (in >> d) expected.push_back(d);
}
size_t index = 0;
for (size_t i = 4; i < eq; ++i) {
const std::string& op = parts[i];
std::string exc;
try {
if (op[0] == 'f') {
screen.feed(unhex(op.substr(2)));
} else if (op[0] == 'r') {
int r = 0, c = 0;
std::sscanf(op.c_str() + 2, "%dx%d", &r, &c);
screen.resize(r, c);
} else {
screen.take_replies();
}
} catch (const ValueError&) {
exc = "\nexc ValueError";
}
std::string text = dump(screen) + exc;
++checked_ops;
if (index >= expected.size() || sha1(text) != expected[index]) {
++failures;
std::cerr << "VT MISMATCH case line " << lineno << " (0-based case " << lineno << "), op " << index << "\n";
std::ofstream("build/vt_fail_" + std::to_string(lineno) + ".txt") << text << "\n";
return;
}
++index;
}
++checked_vt;
}
void run_key_case(size_t lineno, const std::vector<std::string>& parts) {
const std::string& kind = parts[1];
size_t eq = 2;
while (parts[eq] != "=") ++eq;
std::string expected = parts[eq + 1];
for (size_t i = eq + 2; i < parts.size(); ++i) expected += "\t" + parts[i];
std::string got;
try {
if (kind == "canon") {
got = "ok " + hexof(MplexKeys::canonical(unhex(parts[2])));
} else if (kind == "tobytes") {
got = "ok " + hexof(MplexKeys::to_bytes(unhex(parts[2]), parts[3] == "1"));
} else if (kind == "namechar") {
got = "ok " + hexof(MplexKeys::name_of_char(unhex(parts[2])));
} else if (kind == "appcur") {
got = "ok " + hexof(MplexKeys::app_cursor_form(unhex(parts[2])));
} else {
auto split = MplexKeys::split(unhex(parts[2]), parts[3] == "1");
got = "ok ";
for (size_t i = 0; i < split.tokens.size(); ++i) {
if (i) got += ";";
got += hexof(split.tokens[i].first) + "/" + hexof(split.tokens[i].second);
}
got += " " + hexof(split.rest);
}
} catch (const MplexError& exc) {
got = "err " + hexof(exc.what());
}
++checked_key;
// error texts embed the raw spec; the oracle stores str(exc) hex, so compare directly
if (got != expected) {
++failures;
std::cerr << "KEY MISMATCH line " << lineno << " kind " << kind << "\n got " << got << "\n expected " << expected << "\n";
}
}
} // namespace
int main(int argc, char** argv) {
std::string tai_py = argc > 1 ? argv[1] : "../tai.py";
std::string cases = argc > 2 ? argv[2] : "build/mplex_vt_cases.txt";
if (argc <= 2) {
std::string cmd = "python3 -I tests/mplex_vt_oracle.py '" + tai_py + "' '" + cases + "'";
if (std::system(cmd.c_str()) != 0) {
std::cerr << "oracle failed\n";
return 2;
}
}
std::ifstream in(cases);
if (!in) {
std::cerr << "no cases file " << cases << "\n";
return 2;
}
std::string line;
size_t vt_index = 0, lineno = 0;
while (std::getline(in, line)) {
++lineno;
std::vector<std::string> parts = split_tab(line);
if (parts[0] == "VT") {
run_vt_case(vt_index++, parts);
} else if (parts[0] == "KEY") {
run_key_case(lineno, parts);
}
}
std::cout << "vt cases ok: " << checked_vt << " of " << vt_index << ", ops compared: " << checked_ops
<< ", key cases compared: " << checked_key << ", failures: " << failures << "\n";
return failures ? 1 : 0;
}