255 lines
9.1 KiB
C++
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;
|
|
}
|