aboutsummaryrefslogtreecommitdiff
path: root/tests/unit
diff options
context:
space:
mode:
authorJade Lovelace <lix@jade.fyi>2024-03-09 23:59:50 -0800
committerJade Lovelace <lix@jade.fyi>2024-03-15 12:31:16 -0700
commit18ed6c3bdf2c03c763c0d128083f507295bcf864 (patch)
tree7b3a03bad074f50ecdff4d331ca5ce18d463ec62 /tests/unit
parent38571c50e6dc0ee910e9e7619e482fdbbfd644e1 (diff)
Implement a repl characterization test system
This allows for automating using the repl without needing a PTY, with very easy to write test files. Change-Id: Ia8d7854edd91f93477638942cb6fc261354e6035
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/libutil-support/tests/cli-literate-parser.cc72
-rw-r--r--tests/unit/libutil-support/tests/cli-literate-parser.hh7
-rw-r--r--tests/unit/libutil-support/tests/terminal-code-eater.cc85
-rw-r--r--tests/unit/libutil-support/tests/terminal-code-eater.hh29
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);
+};
+};