// 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 [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 #include #include #include #include #include #include #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(std::stoi(text.substr(i, 2), nullptr, 16))); return out; } std::vector split_tab(const std::string& line) { std::vector 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(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 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(msg.size()) * 8; data.push_back(static_cast(0x80)); while (data.size() % 64 != 56) data.push_back('\0'); for (int i = 7; i >= 0; --i) data.push_back(static_cast((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(static_cast(data[off + i * 4])) << 24) | (static_cast(static_cast(data[off + i * 4 + 1])) << 16) | (static_cast(static_cast(data[off + i * 4 + 2])) << 8) | static_cast(static_cast(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& 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(hist)); std::vector 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& 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 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; }