diff options
Diffstat (limited to 'tests/unit/libutil-support')
4 files changed, 193 insertions, 0 deletions
diff --git a/tests/unit/libutil-support/tests/cli-literate-parser.cc b/tests/unit/libutil-support/tests/cli-literate-parser.cc index 3b2345e8e..c943a813e 100644 --- a/tests/unit/libutil-support/tests/cli-literate-parser.cc +++ b/tests/unit/libutil-support/tests/cli-literate-parser.cc @@ -171,4 +171,76 @@ auto CLILiterateParser::syntax() const -> std::vector<Node> const & return syntax_; } +auto CLILiterateParser::unparse(const std::string & prompt, const std::vector<Node> & syntax, size_t indent) + -> std::string +{ + std::string indent_str(indent, ' '); + std::ostringstream out{}; + + for (auto & node : syntax) { + switch (node.kind) { + case NodeKind::COMMENTARY: + out << node.text << "\n"; + break; + case NodeKind::COMMAND: + out << indent_str << prompt << node.text << "\n"; + break; + case NodeKind::OUTPUT: + out << indent_str << node.text << "\n"; + break; + } + } + + return out.str(); +} + +auto CLILiterateParser::tidyOutputForComparison(std::vector<Node> && syntax) -> std::vector<Node> +{ + std::vector<Node> newSyntax{}; + + // Eat trailing newlines, so assume that the very end was actually a command + bool lastWasCommand = true; + bool newLastWasCommand = true; + + auto v = std::ranges::reverse_view(syntax); + + for (auto it = v.begin(); it != v.end(); ++it) { + Node item = std::move(*it); + + lastWasCommand = newLastWasCommand; + // chomp commentary + if (item.kind == NodeKind::COMMENTARY) { + continue; + } + + if (item.kind == NodeKind::COMMAND) { + newLastWasCommand = true; + + if (item.text == "") { + // chomp empty commands + continue; + } + } + + if (item.kind == NodeKind::OUTPUT) { + // TODO: horrible + bool nextIsCommand = (it + 1 == v.end()) ? false : (it + 1)->kind == NodeKind::COMMAND; + std::string trimmedText = boost::algorithm::trim_right_copy(item.text); + if ((lastWasCommand || nextIsCommand) && trimmedText == "") { + // chomp empty text above or directly below commands + continue; + } + + // real output, stop chomping + newLastWasCommand = false; + + item = Node::mkOutput(std::move(trimmedText)); + } + newSyntax.push_back(std::move(item)); + } + + std::reverse(newSyntax.begin(), newSyntax.end()); + return newSyntax; +} + }; diff --git a/tests/unit/libutil-support/tests/cli-literate-parser.hh b/tests/unit/libutil-support/tests/cli-literate-parser.hh index 86a5bdd32..4cffd2ba9 100644 --- a/tests/unit/libutil-support/tests/cli-literate-parser.hh +++ b/tests/unit/libutil-support/tests/cli-literate-parser.hh @@ -81,9 +81,16 @@ public: /** Parses an input in a non-streaming fashion */ static auto parse(std::string prompt, std::string_view const & input, size_t indent = 2) -> std::vector<Node>; + /** Returns, losslessly, the string that would have generated a syntax tree */ + static auto unparse(std::string const & prompt, std::vector<Node> const & syntax, size_t indent = 2) -> std::string; + /** Consumes a CLILiterateParser and gives you the syntax out of it */ auto intoSyntax() && -> std::vector<Node>; + /** Tidies syntax to remove trailing whitespace from outputs and remove any + * empty prompts */ + static auto tidyOutputForComparison(std::vector<Node> && syntax) -> std::vector<Node>; + private: struct AccumulatingState diff --git a/tests/unit/libutil-support/tests/terminal-code-eater.cc b/tests/unit/libutil-support/tests/terminal-code-eater.cc new file mode 100644 index 000000000..51e1d565e --- /dev/null +++ b/tests/unit/libutil-support/tests/terminal-code-eater.cc @@ -0,0 +1,85 @@ +#include "terminal-code-eater.hh" +#include "debug-char.hh" +#include <assert.h> +#include <cstdint> +#include <iostream> + +namespace nix { + +static constexpr const bool DEBUG_EATER = false; + +void TerminalCodeEater::feed(char c, std::function<void(char)> on_char) +{ + auto isParamChar = [](char v) -> bool { return v >= 0x30 && v <= 0x3f; }; + auto isIntermediateChar = [](char v) -> bool { return v >= 0x20 && v <= 0x2f; }; + auto isFinalChar = [](char v) -> bool { return v >= 0x40 && v <= 0x7e; }; + if constexpr (DEBUG_EATER) { + std::cerr << "eater" << DebugChar{c} << "\n"; + } + + switch (state) { + case State::ExpectESC: + switch (c) { + case '\e': + transition(State::ExpectESCSeq); + return; + // Just eat \r, since it is part of clearing a line + case '\r': + return; + } + if constexpr (DEBUG_EATER) { + std::cerr << "eater uneat" << DebugChar{c} << "\n"; + } + on_char(c); + break; + case State::ExpectESCSeq: + switch (c) { + // CSI + case '[': + transition(State::InCSIParams); + return; + default: + transition(State::ExpectESC); + return; + } + break; + // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences + // A CSI sequence is: CSI [\x30-\x3f]* [\x20-\x2f]* [\x40-\x7e] + // ^ params ^ intermediates ^ final byte + case State::InCSIParams: + if (isFinalChar(c)) { + transition(State::ExpectESC); + return; + } else if (isIntermediateChar(c)) { + transition(State::InCSIIntermediates); + return; + } else if (isParamChar(c)) { + return; + } else { + // Corrupt escape sequence? Throw an assert, for now. + // transition(State::ExpectESC); + assert(false && "Corrupt terminal escape sequence"); + return; + } + break; + case State::InCSIIntermediates: + if (isFinalChar(c)) { + transition(State::ExpectESC); + return; + } else if (isIntermediateChar(c)) { + return; + } else { + // Corrupt escape sequence? Throw an assert, for now. + // transition(State::ExpectESC); + assert(false && "Corrupt terminal escape sequence in intermediates"); + return; + } + break; + } +} + +void TerminalCodeEater::transition(State new_state) +{ + state = new_state; +} +}; diff --git a/tests/unit/libutil-support/tests/terminal-code-eater.hh b/tests/unit/libutil-support/tests/terminal-code-eater.hh new file mode 100644 index 000000000..d904bcc20 --- /dev/null +++ b/tests/unit/libutil-support/tests/terminal-code-eater.hh @@ -0,0 +1,29 @@ +#pragma once +/// @file + +#include <functional> + +namespace nix { + +/** DFA that eats terminal escapes + * + * See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html + */ +class TerminalCodeEater +{ +public: + void feed(char c, std::function<void(char)> on_char); + +private: + enum class State { + ExpectESC, + ExpectESCSeq, + InCSIParams, + InCSIIntermediates, + }; + + State state = State::ExpectESC; + + void transition(State new_state); +}; +}; |