// Differential test of the Mplex core classes (args, command parser, style, options, config, // format, layout) against tai.py. The expected values are produced by tests/mplex_core_gen.py // from the real Python classes (build/core_cases.txt). Run from taicpp/: // python3 -I tests/mplex_core_gen.py && build/mplex_core_test #include #include #include #include #include #include #include "mplex_args.h" #include "mplex_commands.h" #include "mplex_config.h" #include "mplex_format.h" #include "mplex_keys.h" #include "mplex_layout.h" #include "mplex_options.h" #include "mplex_pane.h" #include "mplex_py.h" #include "mplex_style.h" using namespace detz::mplex; namespace { long failures = 0, checks = 0, known_deviations = 0; // Documented deviation: MplexArgs::int_flag returns a 64 bit value; a Python int beyond that // saturates (every consumer clamps it). Rewrites such expected values in the "ints" section. std::string saturate_big_ints(const std::string& want) { size_t cut = want.rfind('\x1e'); if (cut == std::string::npos) return want; std::string head = want.substr(0, cut + 1), tail = want.substr(cut + 1), out; for (size_t i = 0; i < tail.size();) { size_t j = i; if (tail[j] == '-') ++j; size_t digits = j; while (digits < tail.size() && tail[digits] >= '0' && tail[digits] <= '9') ++digits; if (digits - j >= 19 && (j == 0 || tail[j - 1] != 'x') && i > 0 && tail[i - 1] == '=') { ++known_deviations; out += tail[i] == '-' ? "-9223372036854775808" : "9223372036854775807"; i = digits; } else { out.push_back(tail[i]); ++i; } } return head + out; } std::map per_kind_checks, per_kind_failures; void check(const std::string& kind, const std::string& label, const std::string& got, const std::string& want) { ++checks; ++per_kind_checks[kind]; if (got == want) return; ++failures; ++per_kind_failures[kind]; if (per_kind_failures[kind] <= 6) { std::cerr << "MISMATCH [" << kind << "] " << label << "\n got : " << got << "\n want: " << want << "\n"; } } std::string unesc(const std::string& s) { std::string out; for (size_t i = 0; i < s.size(); ++i) { if (s[i] != '\\' || i + 1 >= s.size()) { out.push_back(s[i]); continue; } char n = s[++i]; if (n == 'n') out.push_back('\n'); else if (n == 't') out.push_back('\t'); else if (n == 'r') out.push_back('\r'); else if (n == '\\') out.push_back('\\'); else if (n == 'x' && i + 2 < s.size()) { out.push_back(static_cast(std::stoi(s.substr(i + 1, 2), nullptr, 16))); i += 2; } else { out.push_back('\\'); out.push_back(n); } } return out; } std::vector fields_of(const std::string& line) { std::vector out; size_t start = 0; while (true) { size_t tab = line.find('\t', start); if (tab == std::string::npos) { out.push_back(unesc(line.substr(start))); break; } out.push_back(unesc(line.substr(start, tab - start))); start = tab + 1; } return out; } std::string enc_list(const std::vector& items) { std::string out = std::to_string(items.size()); for (const auto& item : items) out += "\x1f" + item; return out; } std::vector dec_list(const std::string& text) { std::vector out; size_t start = 0; bool first = true; while (true) { size_t sep = text.find('\x1f', start); std::string piece = text.substr(start, sep == std::string::npos ? std::string::npos : sep - start); if (first) first = false; else out.push_back(piece); if (sep == std::string::npos) break; start = sep + 1; } return out; } std::vector split_on(const std::string& text, char sep) { std::vector out; size_t start = 0; while (true) { size_t at = text.find(sep, start); if (at == std::string::npos) { out.push_back(text.substr(start)); break; } out.push_back(text.substr(start, at - start)); start = at + 1; } return out; } template std::string outcome(F func) { try { return "OK:" + func(); } catch (const MplexError& exc) { return std::string("ERR:") + exc.what(); } catch (const ValueError&) { return "EXC:ValueError"; } catch (const std::out_of_range&) { return "EXC:KeyError"; } catch (const std::exception& exc) { return std::string("EXC:?") + exc.what(); } } std::string show_val(const OptValue& v) { if (v.is_bool()) return v.b ? "bool:True" : "bool:False"; if (v.is_int()) return "int:" + std::to_string(v.i); return "str:" + v.s; } std::string frag_or_dash(Color c) { return c ? color_sgr(c) : "-"; } Color frag_color(const std::string& s) { return s == "-" ? 0 : color_from_sgr(s); } // ---- args ------------------------------------------------------------------------------ std::string args_dump(const MplexArgs& args) { std::vector parts, gets, ints; for (const auto& [key, values] : args.flags) { std::vector vals; for (const auto& v : values) vals.push_back(v.is_true ? "T" : "S" + v.text); parts.push_back(std::string(1, key) + ":" + enc_list(vals)); } for (const auto& [key, values] : args.flags) { (void)values; auto got = args.get(key); const auto& last = args.flags.at(key).back(); gets.push_back(std::string(1, key) + "=" + (last.is_true ? "True" : py::repr(*got))); std::string all = "["; bool first = true; for (const auto& v : args.flags.at(key)) { if (!first) all += ", "; first = false; all += v.is_true ? "True" : py::repr(v.text); } all += "]"; gets.push_back(std::string(1, key) + "*" + all); try { auto n = args.int_flag(key, 7); ints.push_back(std::string(1, key) + "=" + std::to_string(*n)); } catch (const MplexError& exc) { ints.push_back(std::string(1, key) + "=ERR " + exc.what()); } } return enc_list(parts) + "\x1e" + enc_list(args.positional) + "\x1e" + enc_list(gets) + "\x1e" + enc_list(ints); } // ---- config ------------------------------------------------------------------------------ std::string cfg_dump(const MplexConfig& config) { std::vector opts; for (const auto& name : config.options.value_order) opts.push_back(name + "=" + show_val(config.options.values.at(name))); std::vector tabs; for (const auto& [tname, table] : config.tables.items()) { std::vector rows; for (const auto& [key, b] : table.items()) rows.push_back(key + "\x1d" + b.command + "\x1d" + (b.repeat ? "1" : "0") + "\x1d" + b.note); tabs.push_back(tname + "\x1c" + enc_list(rows)); } return enc_list(opts) + "\x1e" + enc_list(tabs) + "\x1e" + enc_list(config.errors) + "\x1e" + std::to_string(config.skipped) + "\x1e" + config.path; } // ---- layout ------------------------------------------------------------------------------ using PanePtr = std::shared_ptr; std::string tree_dump(const MplexLayoutNode::Ptr& node) { if (!node) return "None"; if (node->kind == "pane") { const auto& p = node->pane; return "P" + std::to_string(p->id) + "(" + std::to_string(node->x) + "," + std::to_string(node->y) + "," + std::to_string(node->w) + "," + std::to_string(node->h) + ")[" + std::to_string(p->x) + "," + std::to_string(p->y) + "," + std::to_string(p->w) + "," + std::to_string(p->h) + "]"; } std::string out = node->kind + (node->reverse ? "r" : "") + "(" + std::to_string(node->x) + "," + std::to_string(node->y) + "," + std::to_string(node->w) + "," + std::to_string(node->h) + "){"; for (size_t i = 0; i < node->children.size(); ++i) { if (i) out += ","; out += tree_dump(node->children[i]); } return out + "}"; } void sync_panes(const MplexLayoutNode::Ptr& root) { if (!root) return; for (auto& leaf : root->leaves()) { leaf->pane->x = leaf->x; leaf->pane->y = leaf->y; leaf->pane->w = leaf->w; leaf->pane->h = leaf->h; } } struct LayoutState { std::vector panes; MplexLayoutNode::Ptr root; int W = 80, H = 24; int counter = 1; }; PanePtr new_pane(int ident) { return std::make_shared(ident, 1, 1, 0, "", "0"); } } // namespace int main(int argc, char** argv) { const std::string path = argc > 1 ? argv[1] : "build/core_cases.txt"; setenv("TZ", "UTC", 1); tzset(); setenv("VISUAL", "nvi", 1); unsetenv("EDITOR"); std::ifstream in(path); if (!in) { std::cerr << "cannot open " << path << " (run tests/mplex_core_gen.py first)\n"; return 2; } std::map vars; auto resolver = [&](const std::string& name) -> std::optional { if (!name.empty() && name[0] == '(') return "SH<" + name.substr(1) + ">"; auto found = vars.find(name); if (found == vars.end()) return std::nullopt; return found->second; }; MplexOptions g, s(&g), w(&s); std::vector levels = {&g, &s, &w}; LayoutState layout; std::unique_ptr fresh; std::string line; long lineno = 0; while (std::getline(in, line)) { ++lineno; std::vector f = fields_of(line); const std::string& kind = f[0]; std::string label = "line " + std::to_string(lineno) + " " + (f.size() > 1 ? f[1] : ""); if (kind == "P") { check(kind, label, outcome([&] { auto commands = MplexCommandParser::parse(f[1]); std::string out; for (size_t i = 0; i < commands.size(); ++i) { if (i) out += "\x1e"; for (size_t j = 0; j < commands[i].size(); ++j) { if (j) out += "\x1f"; out += commands[i][j]; } } return out + "|" + std::to_string(commands.size()); }), f[2]); } else if (kind == "Q") { check(kind, label, MplexCommandParser::quote(f[1]), f[2]); } else if (kind == "A") { check(kind, label + " " + f[5], outcome([&] { return args_dump(MplexArgs::parse(f[1], f[2], dec_list(f[5]), std::stoi(f[3]), std::stoi(f[4]))); }), saturate_big_ints(f[6])); } else if (kind == "C") { std::string want = f[3]; if (want.find(";-") != std::string::npos) { // documented deviation: negative "#-f0000" components ++known_deviations; want = "ERR:bad colour: " + py::lower(py::strip(f[1])); } check(kind, label, outcome([&] { Color c = MplexStyle::color(f[1], f[2] == "1"); return c ? "'" + color_sgr(c) + "'" : std::string("None"); }), want); } else if (kind == "S") { auto decode_style = [](const std::string& text) { auto parts = split_on(text, '\x1e'); auto pair = dec_list(parts[0]); return Style{frag_color(pair[0]), frag_color(pair[1]), std::stoi(parts[1])}; }; Style base = decode_style(f[2]); std::optional