Files
tai/taicpp/tests/mplex_spec_test.cpp
T
2026-10-25 06:48:34 +01:00

222 lines
9.8 KiB
C++

// 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 <algorithm>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <regex>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#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<heading>\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<size_t>(m.position(0)));
return rest;
}
std::vector<std::string> first_column(const std::string& text) {
std::vector<std::string> 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<DocBinding> documented_bindings(const std::string& text) {
std::vector<DocBinding> 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<std::string> documented = first_column(section(doc, "## Options"));
std::vector<std::string> 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<std::string> documented = first_column(section(doc, "## Commands"));
std::vector<std::string> 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<std::pair<std::string, std::string>> 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<DocBinding> documented = documented_bindings(section(doc, heading));
std::vector<DocBinding> 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<std::string> 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<std::string> 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;
}