// Differential test for the F3 classes: replays the scenarios written by mplex_model_gen.py // (which drives the real Python classes in tai.py) through the C++ classes and compares. // Usage: mplex_model_test CASES.jsonl SCRATCH_DIR // Also runs a real-pty test of MplexPane (spawn, feed, write, drain, close, tail_log, log rollover). #include #include #include #include #include #include #include #include #include #include #include #include "mplex_canvas.h" #include "mplex_chooser.h" #include "mplex_clientconn.h" #include "mplex_clockmode.h" #include "mplex_ctx.h" #include "mplex_flowscreen.h" #include "mplex_json.h" #include "mplex_panenumbers.h" #include "mplex_pane.h" #include "mplex_prompt.h" #include "mplex_py.h" #include "mplex_renderer.h" #include "mplex_server.h" #include "mplex_style.h" using namespace detz::mplex; namespace { int g_checks = 0; int g_failures = 0; std::map> g_by_kind; // kind -> (checks, failures) std::string g_kind; void fail(const std::string& what, const std::string& got, const std::string& want) { ++g_failures; ++g_by_kind[g_kind].second; if (g_failures <= 25) { std::cerr << "FAIL [" << g_kind << "] " << what << "\n got : " << got.substr(0, 600) << "\n want: " << want.substr(0, 600) << "\n"; } } void check(bool ok, const std::string& what) { ++g_checks; ++g_by_kind[g_kind].first; if (!ok) fail(what, "false", "true"); } Json canon(const Json& value) { if (value.is_object()) { auto items = value.as_object(); std::sort(items.begin(), items.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); for (auto& item : items) item.second = canon(item.second); return Json::object(items); } if (value.is_array()) { Json::Array items; for (const auto& item : value.as_array()) items.push_back(canon(item)); return Json::array(items); } return value; } std::string first_diff(const Json& a, const Json& b, const std::string& path) { if (a.kind() != b.kind()) return path + ": " + a.dumps() + " vs " + b.dumps(); if (a.is_array()) { const auto& x = a.as_array(); const auto& y = b.as_array(); for (size_t i = 0; i < std::min(x.size(), y.size()); ++i) { std::string d = first_diff(x[i], y[i], path + "[" + std::to_string(i) + "]"); if (!d.empty()) return d; } if (x.size() != y.size()) return path + ": length " + std::to_string(x.size()) + " vs " + std::to_string(y.size()); return ""; } if (a.is_object()) { const auto& x = a.as_object(); const auto& y = b.as_object(); for (size_t i = 0; i < std::min(x.size(), y.size()); ++i) { if (x[i].first != y[i].first) return path + ": key " + x[i].first + " vs " + y[i].first; std::string d = first_diff(x[i].second, y[i].second, path + "." + x[i].first); if (!d.empty()) return d; } if (x.size() != y.size()) return path + ": size"; return ""; } return a.dumps() == b.dumps() ? "" : path + ": " + a.dumps() + " vs " + b.dumps(); } void expect_json(const std::string& what, const Json& got, const Json& want) { ++g_checks; ++g_by_kind[g_kind].first; Json ca = canon(got), cb = canon(want); std::string a = ca.dumps(), b = cb.dumps(); if (a != b) fail(what, first_diff(ca, cb, "$"), "(got vs want at first difference)"); } void expect_str(const std::string& what, const std::string& got, const std::string& want) { ++g_checks; ++g_by_kind[g_kind].first; if (got != want) fail(what, got, want); } std::string hex(const std::string& data) { 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) { 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; } Json S(const std::string& s) { return Json::string(s); } Json I(long long v) { return Json::integer(v); } Json B(bool v) { return Json::boolean(v); } Json A(Json::Array items = {}) { return Json::array(std::move(items)); } Json frag(Color c) { return c == 0 ? Json() : S(color_sgr(c)); } Color color_of(const Json& j) { return j.is_null() ? 0 : color_from_sgr(j.as_string()); } Json cell_json(const Cell& c) { return A({I(static_cast(c.ch)), frag(c.fg), frag(c.bg), I(c.attr)}); } Cell cell_from(const Json& j) { const auto& a = j.as_array(); return Cell{static_cast(a[0].as_int()), color_of(a[1]), color_of(a[2]), static_cast(a[3].as_int())}; } Json grid_json(const Grid& g) { Json::Array rows; for (const Row& r : g) { Json::Array cells; for (const Cell& c : r) cells.push_back(cell_json(c)); rows.push_back(A(cells)); } return A(rows); } Grid grid_from(const Json& j) { Grid g; for (const Json& r : j.as_array()) { Row row; for (const Json& c : r.as_array()) row.push_back(cell_from(c)); g.push_back(row); } return g; } Json canvas_json(const MplexCanvas& c) { Json cursor; if (c.cursor) cursor = A({I(c.cursor->first), I(c.cursor->second)}); return Json::object({{"rows", I(c.rows)}, {"cols", I(c.cols)}, {"cells", grid_json(c.cells)}, {"cursor", cursor}}); } Style style_from(const Json& j) { const auto& a = j.as_array(); return Style{color_of(a[0]), color_of(a[1]), static_cast(a[2].as_int())}; } Json opt_int(const std::optional& v) { return v ? I(*v) : Json(); } std::string g_scratch; std::unique_ptr g_stub_server; MplexServer& stub_server() { if (!g_stub_server) { std::string home = g_scratch + "/cpp_stub_home"; ::mkdir(g_scratch.c_str(), 0700); ::mkdir(home.c_str(), 0700); g_stub_server = std::make_unique(home); } return *g_stub_server; } // ---- A..D ------------------------------------------------------------------------------------ void t_border(const Json& c) { const auto& f = c.get("flags")->as_array(); wchar_t got = MplexCanvas::border_char(f[0].as_bool(), f[1].as_bool(), f[2].as_bool(), f[3].as_bool(), c.get("lines")->as_string()); expect_json("border_char " + c.get("lines")->as_string(), I(static_cast(got)), *c.get("want")); } void t_encode(const Json& c) { const Json& cv = *c.get("canvas"); MplexCanvas canvas(static_cast(cv.get("rows")->as_int()), static_cast(cv.get("cols")->as_int())); canvas.cells = grid_from(*cv.get("cells")); const Json& cur = *cv.get("cursor"); if (!cur.is_null()) canvas.cursor = std::make_pair(static_cast(cur.as_array()[0].as_int()), static_cast(cur.as_array()[1].as_int())); const Json& prev = *c.get("prev"); Grid prev_grid; const Grid* prev_ptr = nullptr; if (!prev.is_null()) { prev_grid = grid_from(prev); prev_ptr = &prev_grid; } auto result = canvas.encode(prev_ptr); expect_str("encode bytes", hex(result.first), c.get("want")->as_string()); expect_json("encode snapshot", grid_json(result.second), *cv.get("cells")); } void t_statustext(const Json& c) { Style base = style_from(*c.get("base")); std::optional