Port of the grr client of tai.py: client, SSE stream, session, store, router, shell, views, chat, panes, file viewer and the unix control socket; GrrTui::run is the --tui entry. Verified against tai.py --tui on a real grr server over a pty: 236 steps, screen text and per-cell styles identical except clocks, spinner frames and server-made ids. docs/tui.md holds the symbol table and gaps, docs/wiring/tui.md the integrator hooks. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
104 lines
5.6 KiB
C++
104 lines
5.6 KiB
C++
// tui_spec: drift checks of grrtui.md against the C++ client (mirrors test_tai.py
|
|
// GrrSpecTests). grrtui.md is the specification; it must list exactly what the code
|
|
// has. Run from taicpp/; the doc is ../grrtui.md (override with GRRTUI_MD).
|
|
#include "GrrConst.h"
|
|
#include "GrrControl.h"
|
|
#include "GrrShell.h"
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <regex>
|
|
#include <sstream>
|
|
using namespace detz::tui;
|
|
|
|
static int failures = 0;
|
|
static void check(bool ok, const std::string& what) {
|
|
if (!ok) {
|
|
failures++;
|
|
std::cout << "FAIL " << what << "\n";
|
|
}
|
|
}
|
|
|
|
static std::string read_doc() {
|
|
const char* env = std::getenv("GRRTUI_MD");
|
|
std::ifstream in(env ? env : "../grrtui.md");
|
|
std::stringstream ss;
|
|
ss << in.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
// text of "## Heading" up to the next heading of level 1 to 3
|
|
static std::string section(const std::string& doc, const std::string& heading) {
|
|
size_t start = doc.find("\n" + heading + "\n");
|
|
if (start == std::string::npos) return std::string();
|
|
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;
|
|
}
|
|
|
|
int main() {
|
|
std::string doc = read_doc();
|
|
check(!doc.empty(), "grrtui.md is readable");
|
|
if (doc.empty()) return 1;
|
|
|
|
// route table: | `pattern` | `ViewName` |
|
|
{
|
|
std::string text = section(doc, "## Screens");
|
|
std::vector<std::pair<std::string, std::string>> rows;
|
|
static const std::regex row("(^|\n)\\| `([^`]+)` \\| `([^`]+)` \\|");
|
|
for (std::sregex_iterator it(text.begin(), text.end(), row), end; it != end; ++it) rows.emplace_back((*it)[2], (*it)[3]);
|
|
check(rows == GrrConst::routes(), "the route table matches the code");
|
|
check(!rows.empty(), "the route table is not empty");
|
|
// every route's view can be built
|
|
}
|
|
// key table: | `key` | action |
|
|
{
|
|
std::string text = section(doc, "## Keys");
|
|
std::vector<std::pair<std::string, std::string>> rows;
|
|
static const std::regex row("(^|\n)\\| `([^`]+)` \\| (.+?) \\|(?=\n|$)");
|
|
for (std::sregex_iterator it(text.begin(), text.end(), row), end; it != end; ++it) rows.emplace_back((*it)[2], (*it)[3]);
|
|
check(rows == GrrConst::keys(), "the key table matches the code");
|
|
}
|
|
// caps: every GRR_* number is documented with its value, and nothing else is
|
|
{
|
|
std::vector<std::pair<std::string, double>> caps = {
|
|
{"GRR_HTTP_TIMEOUT", GRR_HTTP_TIMEOUT}, {"GRR_SSE_TIMEOUT", GRR_SSE_TIMEOUT}, {"GRR_SSE_RETRY", GRR_SSE_RETRY},
|
|
{"GRR_SSE_LINE_MAX", static_cast<double>(GRR_SSE_LINE_MAX)}, {"GRR_BODY_MAX", static_cast<double>(GRR_BODY_MAX)},
|
|
{"GRR_SIDE_W", GRR_SIDE_W}, {"GRR_SIDE_MIN_COLS", GRR_SIDE_MIN_COLS}, {"GRR_PANE_MIN_COLS", GRR_PANE_MIN_COLS},
|
|
{"GRR_PANE_MIN_W", GRR_PANE_MIN_W}, {"GRR_PANE_MAX_W", GRR_PANE_MAX_W}, {"GRR_MESSAGE_KEEP", GRR_MESSAGE_KEEP},
|
|
{"GRR_TOAST_SECS", GRR_TOAST_SECS}, {"GRR_BANNER_SECS", GRR_BANNER_SECS}, {"GRR_TOAST_MAX", GRR_TOAST_MAX},
|
|
{"GRR_COMPOSER_MAX_ROWS", GRR_COMPOSER_MAX_ROWS}, {"GRR_VIEWER_MAX_BYTES", static_cast<double>(GRR_VIEWER_MAX_BYTES)},
|
|
{"GRR_DRAFT_KEEP", GRR_DRAFT_KEEP}, {"GRR_PROMPT_QUIET", GRR_PROMPT_QUIET}, {"GRR_PROMPT_ASKS", GRR_PROMPT_ASKS},
|
|
{"GRR_PROMPT_MAX", GRR_PROMPT_MAX}, {"GRR_LIST_MORE", GRR_LIST_MORE}, {"GRR_MENU_MAX_H", GRR_MENU_MAX_H},
|
|
{"GRR_CONTROL_JOBS", GRR_CONTROL_JOBS}, {"GRR_CONTROL_LINE", GRR_CONTROL_LINE}, {"GRR_CONTROL_WAIT", GRR_CONTROL_WAIT},
|
|
{"GRR_CONTROL_WAIT_MAX", GRR_CONTROL_WAIT_MAX}, {"GRR_CONTROL_KEYS_MAX", GRR_CONTROL_KEYS_MAX},
|
|
};
|
|
std::string text = section(doc, "## Caps");
|
|
std::map<std::string, double> documented;
|
|
static const std::regex row("(^|\n)\\| `(GRR_[A-Z_]+)` \\| ([0-9.]+) \\|");
|
|
for (std::sregex_iterator it(text.begin(), text.end(), row), end; it != end; ++it) documented[(*it)[2]] = std::stod((*it)[3]);
|
|
check(documented.size() == caps.size(), "the caps table lists every cap (" + std::to_string(documented.size()) + " documented, " + std::to_string(caps.size()) + " in code)");
|
|
for (auto& c : caps) {
|
|
auto d = documented.find(c.first);
|
|
check(d != documented.end(), c.first + " is documented");
|
|
if (d != documented.end()) check(std::fabs(d->second - c.second) < 1e-9, c.first + " has the documented value");
|
|
}
|
|
// the control class carries the same numbers
|
|
check(GrrControl::JOBS == GRR_CONTROL_JOBS && GrrControl::LINE == static_cast<size_t>(GRR_CONTROL_LINE) && GrrControl::WAIT == GRR_CONTROL_WAIT &&
|
|
GrrControl::WAIT_MAX == GRR_CONTROL_WAIT_MAX && GrrControl::KEYS_MAX == static_cast<size_t>(GRR_CONTROL_KEYS_MAX),
|
|
"GrrControl uses the documented caps");
|
|
}
|
|
// shapes and pass-through events are named in the doc
|
|
for (auto& shape : GrrConst::shapes()) check(doc.find("`" + shape + "`") != std::string::npos, "shape " + shape + " is named");
|
|
for (auto& name : GrrShell::pass_events()) check(doc.find("`" + name + "`") != std::string::npos, "event " + name + " is named");
|
|
// the flags are documented
|
|
check(doc.find("--tui-local") != std::string::npos, "--tui-local is documented");
|
|
check(doc.find("GRR_URL") != std::string::npos, "GRR_URL is documented");
|
|
|
|
std::cout << (failures ? "FAILED " + std::to_string(failures) : std::string("ok")) << "\n";
|
|
return failures ? 1 : 0;
|
|
}
|