Files
tai/taicpp/tests/mplex_core_test.cpp
T

510 lines
20 KiB
C++
Raw Normal View History

// 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 <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#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<std::string, long> 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<char>(std::stoi(s.substr(i + 1, 2), nullptr, 16)));
i += 2;
} else {
out.push_back('\\');
out.push_back(n);
}
}
return out;
}
std::vector<std::string> fields_of(const std::string& line) {
std::vector<std::string> 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<std::string>& items) {
std::string out = std::to_string(items.size());
for (const auto& item : items) out += "\x1f" + item;
return out;
}
std::vector<std::string> dec_list(const std::string& text) {
std::vector<std::string> 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<std::string> split_on(const std::string& text, char sep) {
std::vector<std::string> 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 <typename F>
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<std::string> parts, gets, ints;
for (const auto& [key, values] : args.flags) {
std::vector<std::string> 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<std::string> opts;
for (const auto& name : config.options.value_order) opts.push_back(name + "=" + show_val(config.options.values.at(name)));
std::vector<std::string> tabs;
for (const auto& [tname, table] : config.tables.items()) {
std::vector<std::string> 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<MplexPane>;
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<PanePtr> panes;
MplexLayoutNode::Ptr root;
int W = 80, H = 24;
int counter = 1;
};
PanePtr new_pane(int ident) { return std::make_shared<MplexPane>(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<std::string, std::string> vars;
auto resolver = [&](const std::string& name) -> std::optional<std::string> {
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<MplexOptions*> levels = {&g, &s, &w};
LayoutState layout;
std::unique_ptr<MplexConfig> fresh;
std::string line;
long lineno = 0;
while (std::getline(in, line)) {
++lineno;
std::vector<std::string> 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<Style> reset;
if (f[3] != "-") reset = decode_style(f[3]);
check(kind, label + " base=" + f[2], outcome([&] {
Style r = MplexStyle::parse(f[1], base, reset);
return frag_or_dash(r.fg) + "|" + frag_or_dash(r.bg) + "|" + std::to_string(r.attr);
}),
f[4]);
} else if (kind == "G") {
check(kind, label, MplexStyle::sgr(frag_color(f[1]), frag_color(f[2]), std::stoi(f[3])), f[4]);
} else if (kind == "O") {
check(kind, label + " " + f[2], outcome([&] { return show_val(MplexOptions::check(f[1], f[2])); }), f[3]);
} else if (kind == "D") {
std::vector<std::string> names, defaults;
for (const auto& d : MplexOptions::defs()) {
names.push_back(d.name);
defaults.push_back(show_val(d.def) + "~" + d.kind);
}
check(kind, "names", enc_list(names), f[1]);
check(kind, "defaults", enc_list(defaults), f[2]);
check(kind, "word separators", MplexOptions::WORD_SEPARATORS, f[3]);
} else if (kind == "OBEGIN") {
g = MplexOptions();
s = MplexOptions(&g);
w = MplexOptions(&s);
} else if (kind == "OOP") {
MplexOptions& obj = *levels[static_cast<size_t>(std::stoi(f[1]))];
const std::string& op = f[2];
const std::string& name = f[3];
std::string res;
if (op == "set" || op == "append") res = outcome([&] { return show_val(obj.set(name, f[4], op == "append")); });
else if (op == "get") res = outcome([&] { return show_val(obj.get(name)); });
else if (op == "toggle") res = outcome([&] { obj.toggle(name); return std::string(); });
else if (op == "unset") res = outcome([&] { obj.unset(name); return std::string(); });
else if (op == "is_set") res = std::string("OK:") + (obj.is_set(name) ? "True" : "False");
else res = outcome([&] { return MplexOptions::show(obj.get(name)); });
std::string dump;
for (size_t i = 0; i < levels.size(); ++i) {
std::vector<std::string> items;
for (const auto& n : levels[i]->value_order) items.push_back(n + "=" + show_val(levels[i]->values.at(n)));
if (i) dump += "\x1e";
dump += enc_list(items);
}
check(kind, label + " " + op, res, f[5]);
check("OOPDUMP", label + " " + op, dump, f[6]);
} else if (kind == "K") {
if (f[1] == "-") {
MplexConfig config;
check(kind, "fresh", cfg_dump(config), f[2]);
fresh = std::make_unique<MplexConfig>();
} else {
auto config = MplexConfig::load(f[1]);
check(kind, label, cfg_dump(*config), f[2]);
}
} else if (kind == "DEFAULTPATH") {
check(kind, label, MplexConfig::default_path("/home/x/.tai") + "|" + MplexConfig::default_path("rel"),
f[1] + "|" + f[2]);
} else if (kind == "LOOKUP") {
std::string got;
for (auto pair : std::vector<std::pair<std::string, std::string>>{{"prefix", "C-b"}, {"prefix", "nope"}, {"root", "C-b"},
{"nonexistent", "x"}, {"copy-mode", "Escape"}})
got += pair.first + "/" + pair.second + "=" + (fresh->lookup(pair.first, pair.second) ? "True" : "False") + ";";
check(kind, label, got, f[1]);
} else if (kind == "UNBIND") {
check(kind, label, fresh->unbind(f[1], f[2]) ? "1" : "0", f[3]);
} else if (kind == "V") {
vars[f[1]] = f[2];
} else if (kind == "F") {
check(kind, label, outcome([&] { return MplexFormat::expand(f[1], resolver); }), f[2]);
} else if (kind == "W") {
check(kind, label, outcome([&] { return MplexFormat::with_time(f[1], std::stod(f[2])); }), f[3]);
} else if (kind == "X") {
check(kind, label, MplexFormat::strip_styles(f[1]), f[2]);
} else if (kind == "TRUTHY") {
std::optional<std::string> v;
if (f[1] != "-") v = f[1].substr(1);
check(kind, label, MplexFormat::truthy(v) ? "1" : "0", f[2]);
} else if (kind == "MC") {
check(kind, label, std::to_string(MplexFormat::match_close(f[3], std::stol(f[4]), f[1][0], f[2][0])), f[5]);
} else if (kind == "SC") {
check(kind, label, enc_list(MplexFormat::split_commas(f[1])), f[2]);
} else if (kind == "LBEGIN") {
layout = LayoutState();
layout.panes = {new_pane(0)};
layout.root = std::make_shared<MplexLayoutNode>("pane", layout.panes[0], 80, 24);
MplexLayout::assign(layout.root, 0, 0, 80, 24);
sync_panes(layout.root);
} else if (kind == "LOP") {
const std::string& op = f[1];
auto& st = layout;
auto leaves_now = [&] { return st.root->leaves(); };
if (op == "init") {
check("LOP:init", label + " init", tree_dump(st.root), f[3]);
} else if (op == "split") {
auto parts = split_on(f[2], ' ');
size_t idx = std::stoul(parts[0]);
int size = parts[3] == "N" ? -1 : std::stoi(parts[3]);
auto leaves = leaves_now();
PanePtr np = new_pane(st.counter++);
std::string res = outcome([&] {
auto [nr, fresh_leaf] = MplexLayout::split(st.root, leaves[idx], parts[1], parts[2] == "1", np, size);
(void)fresh_leaf;
st.root = nr;
st.panes.push_back(np);
return tree_dump(st.root);
});
check("LOP:split", label + " split " + f[2], res, f[3]);
} else if (op == "remove") {
size_t idx = std::stoul(f[2]);
auto leaves = leaves_now();
PanePtr victim = leaves[idx]->pane;
st.root = MplexLayout::remove(st.root, leaves[idx]);
std::vector<PanePtr> kept;
for (auto& p : st.panes)
if (p != victim) kept.push_back(p);
st.panes = kept;
check("LOP:remove", label + " remove", tree_dump(st.root), f[3]);
} else if (op == "resize_by") {
auto parts = split_on(f[2], ' ');
auto leaves = leaves_now();
std::string res = outcome([&] {
MplexLayout::resize_by(leaves[std::stoul(parts[0])], parts[1], std::stoi(parts[2]), parts[3] == "1");
return tree_dump(st.root);
});
check("LOP:resize_by", label + " resize_by " + f[2], res, f[3]);
} else if (op == "resize_to") {
auto parts = split_on(f[2], ' ');
auto leaves = leaves_now();
std::string res = outcome([&] {
MplexLayout::resize_to(leaves[std::stoul(parts[0])], parts[1], std::stoi(parts[2]));
return tree_dump(st.root);
});
check("LOP:resize_to", label + " resize_to " + f[2], res, f[3]);
} else if (op == "assign") {
auto parts = split_on(f[2], ' ');
st.W = std::stoi(parts[0]);
st.H = std::stoi(parts[1]);
MplexLayout::assign(st.root, 0, 0, st.W, st.H);
sync_panes(st.root);
check("LOP:assign", label + " assign " + f[2], tree_dump(st.root), f[3]);
} else if (op == "active") {
auto values = dec_list(f[2]);
for (size_t i = 0; i < values.size() && i < st.panes.size(); ++i) st.panes[i]->last_active = std::stod(values[i]);
} else if (op == "neighbour") {
auto parts = split_on(f[2], ' ');
int id = std::stoi(parts[0]);
PanePtr current;
for (auto& p : st.panes)
if (p->id == id) current = p;
std::vector<PanePtr> panes;
for (auto& leaf : st.root->leaves()) panes.push_back(leaf->pane);
PanePtr got = MplexLayout::neighbour(panes, current, parts[1][0], std::stoi(parts[2]), std::stoi(parts[3]));
check("LOP:neighbour", label + " neighbour " + f[2], got ? std::to_string(got->id) : "-", f[3]);
} else if (op == "preset") {
auto parts = split_on(f[2], ' ');
int count = std::stoi(parts[1]);
std::vector<PanePtr> pset;
for (int i = 0; i < count; ++i) pset.push_back(new_pane(st.counter + i));
st.counter += count;
st.panes = pset;
st.root = MplexLayout::preset(parts[0], pset, std::stoi(parts[2]), std::stoi(parts[3]), std::stoi(parts[4]),
std::stoi(parts[5]));
check("LOP:preset", label + " preset " + f[2], tree_dump(st.root), f[3]);
}
} else if (kind == "LPRESET") {
auto parts = split_on(f[1], ' ');
int count = std::stoi(parts[1]);
std::vector<PanePtr> pset;
for (int i = 0; i < count; ++i) pset.push_back(new_pane(i));
auto root = MplexLayout::preset(parts[0], pset, std::stoi(parts[2]), std::stoi(parts[3]), std::stoi(parts[4]),
std::stoi(parts[5]));
check(kind, label + " pre " + f[1], tree_dump(root), f[2]);
MplexLayout::assign(root, 0, 0, std::stoi(parts[2]), std::stoi(parts[3]));
sync_panes(root);
check(kind, label + " post " + f[1], tree_dump(root), f[3]);
} else if (kind == "LSHARES") {
check(kind, label, enc_list([&] {
std::vector<std::string> out;
for (int v : MplexLayout::shares(std::stoi(f[1]), std::stoi(f[2]), f[3] == "1")) out.push_back(std::to_string(v));
return out;
}()),
f[4]);
} else {
std::cerr << "unknown case kind " << kind << " at line " << lineno << "\n";
++failures;
}
}
for (const auto& [kind, count] : per_kind_checks)
std::cout << kind << ": " << count << " checks, " << per_kind_failures[kind] << " failures\n";
std::cout << "known deviations applied (int64 saturation, negative #-hex colour): " << known_deviations << "\n";
std::cout << "TOTAL " << checks << " checks, " << failures << " failures\n";
return failures ? 1 : 0;
}