// Mirror of MplexSpecTests and UiflowDocTests (test_tai.py): ../mplex.md and ../uiflow.md are the // specification, and must list exactly what the C++ tables hold. Tables compared: options, // commands, default key bindings, copy-mode commands, caps, wire kinds, flow caps. // Usage: mplex_spec_test [path-to-repo-root] (default: the directory above taicpp/) #include #include #include #include #include #include #include #include #include #include #include "mplex_commands.h" #include "mplex_config.h" #include "mplex_copymode.h" #include "mplex_options.h" #include "mplex_server.h" #include "mplex_plexer_flow.h" using namespace detz::mplex; namespace { int failures = 0; int checks = 0; void check(bool ok, const std::string& what) { ++checks; if (!ok) { ++failures; std::cerr << "FAIL: " << what << "\n"; } } std::string slurp(const std::string& path) { std::ifstream in(path, std::ios::binary); std::stringstream ss; ss << in.rdbuf(); return ss.str(); } // section(heading): text after "\n\n" up to the next "\n#..### " heading. std::string section(const std::string& doc, const std::string& heading) { size_t start = doc.find("\n" + heading + "\n"); if (start == std::string::npos) { check(false, "heading missing: " + heading); return ""; } std::string rest = doc.substr(start + heading.size() + 2); static const std::regex next("\n#{1,3} "); std::smatch m; if (std::regex_search(rest, m, next)) return rest.substr(0, static_cast(m.position(0))); return rest; } std::vector first_column(const std::string& text) { std::vector out; size_t pos = 0; while (pos < text.size()) { size_t eol = text.find('\n', pos); if (eol == std::string::npos) eol = text.size(); std::string line = text.substr(pos, eol - pos); pos = eol + 1; if (line.rfind("| `", 0) != 0) continue; size_t close = line.find("` |", 3); if (close == std::string::npos) continue; out.push_back(line.substr(3, close - 3)); } return out; } struct DocBinding { std::string key, command; bool repeat; }; std::string unescape_pipes(std::string text) { size_t pos; while ((pos = text.find("\\|")) != std::string::npos) text.replace(pos, 2, "|"); return text; } // ^\| `(.+?)` \| `(.+?)` \| (yes)? (non-greedy, per line) std::vector documented_bindings(const std::string& text) { std::vector out; static const std::regex row("^\\| `(.+?)` \\| `(.+?)` \\| (yes)?"); size_t pos = 0; while (pos < text.size()) { size_t eol = text.find('\n', pos); if (eol == std::string::npos) eol = text.size(); std::string line = text.substr(pos, eol - pos); pos = eol + 1; std::smatch m; if (std::regex_search(line, m, row)) out.push_back({unescape_pipes(m[1].str()), unescape_pipes(m[2].str()), m[3].matched && !m[3].str().empty()}); } return out; } bool contains(const std::string& doc, const std::string& needle) { return doc.find(needle) != std::string::npos; } void em_dash_free(const std::string& doc, const std::string& what) { check(!contains(doc, "\xe2\x80\x94"), what + " contains an em dash"); } } // namespace int main(int argc, char** argv) { std::string root = argc > 1 ? argv[1] : "../.."; std::string doc = slurp(root + "/mplex.md"); std::string flow = slurp(root + "/uiflow.md"); check(!doc.empty(), "mplex.md readable under " + root); check(!flow.empty(), "uiflow.md readable under " + root); // test_options_table_matches_the_code { std::vector documented = first_column(section(doc, "## Options")); std::vector code; for (const auto& def : MplexOptions::defs()) code.push_back(def.name); std::sort(documented.begin(), documented.end()); std::sort(code.begin(), code.end()); check(documented == code, "options table matches the code"); } // test_commands_table_matches_the_code { std::vector documented = first_column(section(doc, "## Commands")); std::vector code; for (const auto& spec : mplex_command_specs()) code.push_back(spec.name); std::sort(documented.begin(), documented.end()); std::sort(code.begin(), code.end()); check(documented == code, "commands table matches the code"); } // test_unsupported_commands_are_all_named for (const auto& name : mplex_unsupported()) if (name != "choose-tree-mouse") check(contains(doc, "`" + name + "`"), "unsupported command named: " + name); // test_every_default_binding_is_documented { const std::vector> tables = { {"prefix", "### prefix table (after `C-b`)"}, {"copy-mode", "### copy-mode table (`mode-keys emacs`)"}, {"copy-mode-vi", "### copy-mode-vi table (`mode-keys vi`)"}}; for (const auto& [table, heading] : tables) { std::vector documented = documented_bindings(section(doc, heading)); std::vector expected; for (const auto& key : mplex_default_keys()) if (table == key.table) expected.push_back({key.key, key.command, key.repeat}); bool same = documented.size() == expected.size(); for (size_t i = 0; same && i < documented.size(); ++i) same = documented[i].key == expected[i].key && documented[i].command == expected[i].command && documented[i].repeat == expected[i].repeat; check(same, "default bindings documented in order: " + table); } } // test_every_copy_mode_command_is_documented for (const auto& name : MplexCopyMode::command_names()) check(contains(doc, "`" + name + "`"), "copy-mode command documented: " + name); // test_every_cap_is_documented (+ the values the spec table states) { const std::vector caps = { "MPLEX_MAX_SESSIONS", "MPLEX_MAX_WINDOWS", "MPLEX_MAX_PANES", "MPLEX_MAX_CLIENTS", "MPLEX_FRAME_MAX", "MPLEX_CLIENT_OUT_MAX", "MPLEX_PANE_IN_MAX", "MPLEX_HISTORY_CAP", "MPLEX_LOG_MAX", "MPLEX_BUFFERS_CAP", "MPLEX_MESSAGES_MAX", "MPLEX_PROMPT_HISTORY_CAP", "MPLEX_COMMAND_DEPTH_MAX", "MPLEX_LOOP_ERRORS_MAX", "MPLEX_SHELL_TIMEOUT", "MPLEX_CONFIG_MAX", "MPLEX_PASTE_MAX", "MPLEX_RENDER_INTERVAL", "MPLEX_REAP_KILL_AFTER", "MPLEX_FLOW_MAX", "MPLEX_FLOW_KEEP"}; for (const auto& name : caps) check(contains(doc, "`" + name + "`"), "cap documented: " + name); check(MPLEX_MAX_SESSIONS == 64 && MPLEX_MAX_WINDOWS == 256 && MPLEX_MAX_PANES == 256 && MPLEX_MAX_CLIENTS == 32, "object caps 64 / 256 / 256 / 32"); check(MPLEX_FRAME_MAX == (4L << 20) && MPLEX_CLIENT_OUT_MAX == (4L << 20) && MPLEX_PANE_IN_MAX == (4L << 20), "frame, client out and pane in caps are 4 MiB"); check(MPLEX_HISTORY_CAP == 200000, "history cap 200000"); check(MPLEX_LOG_MAX == (16L << 20), "log max 16 MiB"); check(MPLEX_BUFFERS_CAP == 100, "buffers cap 100"); check(MPLEX_MESSAGES_MAX == 200 && MPLEX_PROMPT_HISTORY_CAP == 100, "messages 200, prompt history 100"); check(MPLEX_COMMAND_DEPTH_MAX == 16, "command depth 16"); check(MPLEX_LOOP_ERRORS_MAX == 50, "loop errors 50"); check(MPLEX_SHELL_TIMEOUT == 5.0, "shell timeout 5 s"); check(MPLEX_CONFIG_MAX == (1L << 20), "config max 1 MiB"); check(MPLEX_PASTE_MAX == (4L << 20), "paste max 4 MiB"); check(MPLEX_RENDER_INTERVAL == 0.012, "render interval 12 ms"); check(MPLEX_REAP_KILL_AFTER == 2.0, "reap kill after 2 s"); check(MPLEX_FLOW_MAX == 16 && MPLEX_FLOW_KEEP == 300, "flow max 16, keep 300 s"); } // test_wire_kinds_are_documented for (const char* kind : {"H", "I", "R", "A", "O", "M", "E", "D", "X", "Z"}) check(contains(doc, std::string("`") + kind + "`"), std::string("wire kind documented: ") + kind); // test_no_em_dashes em_dash_free(doc, "mplex.md"); em_dash_free(flow, "uiflow.md"); em_dash_free(slurp(root + "/taicpp/docs/mplex.md"), "taicpp/docs/mplex.md"); em_dash_free(slurp(root + "/taicpp/docs/wiring/mplex.md"), "taicpp/docs/wiring/mplex.md"); // UiflowDocTests.test_every_cap_is_documented (with the values) { const std::vector caps = { "PLEXER_FLOW_VERSION", "PLEXER_FLOW_SCREENS_MAX", "PLEXER_FLOW_FIELDS_MAX", "PLEXER_FLOW_OPTIONS_MAX", "PLEXER_FLOW_ACTIONS_MAX", "PLEXER_FLOW_VISITS_MAX", "PLEXER_FLOW_ID_MAX", "PLEXER_FLOW_TITLE_MAX", "PLEXER_FLOW_TEXT_MAX", "PLEXER_FLOW_LABEL_MAX", "PLEXER_FLOW_ANSWER_MAX", "PLEXER_FLOW_TIMEOUT_MIN", "PLEXER_FLOW_TIMEOUT_DEFAULT", "PLEXER_FLOW_TIMEOUT_MAX", "PLEXER_FLOW_POLL", "MPLEX_FLOW_MAX", "MPLEX_FLOW_KEEP"}; for (const auto& name : caps) check(contains(flow, "`" + name + "`"), "flow cap documented: " + name); namespace pf = detz::mplex_flow; check(pf::PLEXER_FLOW_VERSION == 1 && pf::PLEXER_FLOW_SCREENS_MAX == 32 && pf::PLEXER_FLOW_FIELDS_MAX == 12 && pf::PLEXER_FLOW_OPTIONS_MAX == 12 && pf::PLEXER_FLOW_ACTIONS_MAX == 12 && pf::PLEXER_FLOW_VISITS_MAX == 64 && pf::PLEXER_FLOW_ID_MAX == 64 && pf::PLEXER_FLOW_TITLE_MAX == 200 && pf::PLEXER_FLOW_TEXT_MAX == 2000 && pf::PLEXER_FLOW_LABEL_MAX == 200 && pf::PLEXER_FLOW_ANSWER_MAX == 2000, "flow caps match uiflow.md table"); check(pf::PLEXER_FLOW_TIMEOUT_MIN == 5 && pf::PLEXER_FLOW_TIMEOUT_DEFAULT == 300 && pf::PLEXER_FLOW_TIMEOUT_MAX == 600 && pf::PLEXER_FLOW_POLL == 0.25, "flow timeouts 5 / 300 / 600 and poll 0.25"); } // UiflowDocTests.test_contract_words_are_documented for (const char* word : {"choice", "text", "info", "message", "show_if", "$done", "$cancel", "next", "actions", "cancelled", "timeout"}) check(contains(flow, word), std::string("contract word documented: ") + word); std::printf("mplex_spec_test: %d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; }