tests: remove forgotten cli-literate-parser
When removing the f1 repl characterization test suit, these files were kept on accident, despite them being dead code now Change-Id: I13b2c5a96004787c26fe7209e4cb3aeedd8ada15
This commit is contained in:
@@ -1,436 +0,0 @@
|
||||
#include "cli-literate-parser.hh"
|
||||
#include "lix/libutil/escape-string.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
#include <ranges>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <variant>
|
||||
|
||||
#include "cli-literate-parser.hh"
|
||||
#include "lix/libutil/escape-string.hh"
|
||||
#include "lix/libutil/fmt.hh"
|
||||
#include "lix/libutil/shlex.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
|
||||
static constexpr const bool DEBUG_PARSER = false;
|
||||
|
||||
using namespace std::string_literals;
|
||||
using namespace boost::algorithm;
|
||||
|
||||
namespace nix {
|
||||
|
||||
namespace cli_literate_parser {
|
||||
|
||||
struct Parser
|
||||
{
|
||||
Parser(const std::string input, Config config)
|
||||
: input(input)
|
||||
, rest(this->input)
|
||||
, prompt(config.prompt)
|
||||
, indentString(config.indent, ' ')
|
||||
, lastWasOutput(false)
|
||||
, syntax{}
|
||||
{
|
||||
assert(!prompt.empty());
|
||||
}
|
||||
|
||||
const std::string input;
|
||||
std::string_view rest;
|
||||
const std::string prompt;
|
||||
const std::string indentString;
|
||||
|
||||
/** Last line was output, so we consider a blank to be part of the output */
|
||||
bool lastWasOutput;
|
||||
|
||||
/**
|
||||
* Nodes of syntax being built.
|
||||
*/
|
||||
std::vector<Node> syntax;
|
||||
|
||||
auto dbg(std::string_view state) -> void
|
||||
{
|
||||
std::cout << state << ": ";
|
||||
escapeString(
|
||||
std::cout,
|
||||
rest,
|
||||
{
|
||||
.maxLength = 40,
|
||||
.outputAnsiColors = true,
|
||||
.escapeNonPrinting = true,
|
||||
}
|
||||
);
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto pushNode(T node) -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
std::cout << debugNode(node);
|
||||
}
|
||||
syntax.emplace_back(node);
|
||||
}
|
||||
|
||||
auto parseLiteral(const char c) -> bool
|
||||
{
|
||||
if (rest.starts_with(c)) {
|
||||
rest.remove_prefix(1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto parseLiteral(const std::string_view & literal) -> bool
|
||||
{
|
||||
if (rest.starts_with(literal)) {
|
||||
rest.remove_prefix(literal.length());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto parseBool() -> bool
|
||||
{
|
||||
auto result = false;
|
||||
if (parseLiteral("true")) {
|
||||
result = true;
|
||||
} else if (parseLiteral("false")) {
|
||||
result = false;
|
||||
} else {
|
||||
throw ParseError("true or false", std::string(rest));
|
||||
}
|
||||
auto untilNewline = parseUntilNewline();
|
||||
if (!untilNewline.empty()) {
|
||||
throw ParseError("nothing after true or false", untilNewline);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto parseUntilNewline() -> std::string
|
||||
{
|
||||
auto pos = rest.find('\n');
|
||||
if (pos == std::string_view::npos) {
|
||||
throw ParseError("text and then newline", std::string(rest));
|
||||
} else {
|
||||
// `parseOutput()` sets this to true anyways.
|
||||
lastWasOutput = false;
|
||||
auto result = std::string(rest, 0, pos);
|
||||
rest.remove_prefix(pos + 1);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
auto parseIndent() -> bool
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("indent");
|
||||
}
|
||||
if (indentString.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parseLiteral(indentString)) {
|
||||
pushNode(Indent(indentString));
|
||||
return true;
|
||||
} else {
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("indent failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto parseCommand() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("command");
|
||||
}
|
||||
auto untilNewline = parseUntilNewline();
|
||||
pushNode(Command(untilNewline));
|
||||
}
|
||||
|
||||
auto parsePrompt() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("prompt");
|
||||
}
|
||||
if (parseLiteral(prompt)) {
|
||||
pushNode(Prompt(prompt));
|
||||
if (rest.empty()) {
|
||||
return;
|
||||
}
|
||||
parseCommand();
|
||||
} else {
|
||||
parseOutput();
|
||||
}
|
||||
}
|
||||
|
||||
auto parseOutput() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("output");
|
||||
}
|
||||
auto untilNewline = parseUntilNewline();
|
||||
pushNode(Output(untilNewline));
|
||||
lastWasOutput = true;
|
||||
}
|
||||
|
||||
auto parseAtSign() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("@ symbol");
|
||||
}
|
||||
if (!parseLiteral('@')) {
|
||||
parseOutputOrCommentary();
|
||||
}
|
||||
|
||||
if (parseLiteral("args ")) {
|
||||
parseArgs();
|
||||
} else if (parseLiteral("should-start ")) {
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("@should-start");
|
||||
}
|
||||
auto shouldStart = parseBool();
|
||||
pushNode(ShouldStart{shouldStart});
|
||||
}
|
||||
}
|
||||
|
||||
auto parseArgs() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("@args");
|
||||
}
|
||||
auto untilNewline = parseUntilNewline();
|
||||
pushNode(Args(untilNewline));
|
||||
}
|
||||
|
||||
auto parseOutputOrCommentary() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("output/commentary");
|
||||
}
|
||||
auto oldLastWasOutput = lastWasOutput;
|
||||
auto untilNewline = parseUntilNewline();
|
||||
|
||||
auto trimmed = trim_right_copy(untilNewline);
|
||||
|
||||
if (oldLastWasOutput && trimmed.empty()) {
|
||||
pushNode(Output{trimmed});
|
||||
} else {
|
||||
pushNode(Commentary{untilNewline});
|
||||
}
|
||||
}
|
||||
|
||||
auto parseStartOfLine() -> void
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
dbg("start of line");
|
||||
}
|
||||
if (parseIndent()) {
|
||||
parsePrompt();
|
||||
} else {
|
||||
parseAtSign();
|
||||
}
|
||||
}
|
||||
|
||||
auto parse() && -> ParseResult
|
||||
{
|
||||
// Begin the recursive descent parser at the start of a new line.
|
||||
while (!rest.empty()) {
|
||||
parseStartOfLine();
|
||||
}
|
||||
return std::move(*this).intoParseResult();
|
||||
}
|
||||
|
||||
auto intoParseResult() && -> ParseResult
|
||||
{
|
||||
// Do another pass over the nodes to produce auxiliary results like parsed
|
||||
// command line arguments.
|
||||
std::vector<std::string> args;
|
||||
std::vector<Node> newSyntax;
|
||||
auto shouldStart = true;
|
||||
|
||||
for (auto & node : syntax) {
|
||||
std::visit(
|
||||
overloaded{
|
||||
[&](Args & e) {
|
||||
auto split = shell_split(e.text);
|
||||
args.insert(args.end(), split.begin(), split.end());
|
||||
},
|
||||
[&](ShouldStart & e) { shouldStart = e.shouldStart; },
|
||||
[&](auto & e) {},
|
||||
},
|
||||
node
|
||||
);
|
||||
|
||||
newSyntax.push_back(node);
|
||||
}
|
||||
|
||||
return ParseResult{
|
||||
.syntax = std::move(newSyntax),
|
||||
.args = std::move(args),
|
||||
.shouldStart = shouldStart,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
template<typename View>
|
||||
auto tidySyntax(View syntax) -> std::vector<Node>
|
||||
{
|
||||
// Note: Setting `lastWasCommand` lets us trim blank lines at the start and
|
||||
// end of the output stream.
|
||||
auto lastWasCommand = true;
|
||||
std::vector<Node> newSyntax;
|
||||
|
||||
for (auto & node : syntax) {
|
||||
// Only compare `Command` and `Output` nodes.
|
||||
if (std::visit([&](auto && e) { return !e.shouldCompare(); }, node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove blank lines before and after commands. This lets us keep nice
|
||||
// whitespace in the test files.
|
||||
auto shouldKeep = std::visit(
|
||||
overloaded{
|
||||
[&](Command & e) {
|
||||
lastWasCommand = true;
|
||||
auto trimmed = trim_right_copy(e.text);
|
||||
if (trimmed.empty()) {
|
||||
return false;
|
||||
} else {
|
||||
e.text = trimmed;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[&](Output & e) {
|
||||
std::string trimmed = trim_right_copy(e.text);
|
||||
if (lastWasCommand && trimmed.empty()) {
|
||||
// NB: Keep `lastWasCommand` true in this branch so we
|
||||
// can keep pruning empty output lines.
|
||||
return false;
|
||||
} else {
|
||||
e.text = trimmed;
|
||||
lastWasCommand = false;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[&](auto & e) {
|
||||
lastWasCommand = false;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
node
|
||||
);
|
||||
|
||||
if (shouldKeep) {
|
||||
newSyntax.push_back(node);
|
||||
}
|
||||
}
|
||||
|
||||
return newSyntax;
|
||||
}
|
||||
|
||||
auto ParseResult::tidyOutputForComparison() -> std::vector<Node>
|
||||
{
|
||||
auto reversed = tidySyntax(std::ranges::reverse_view(syntax));
|
||||
auto unreversed = tidySyntax(std::ranges::reverse_view(reversed));
|
||||
return unreversed;
|
||||
}
|
||||
|
||||
void ParseResult::interpolatePwd(std::string_view pwd)
|
||||
{
|
||||
std::vector<std::string> newArgs;
|
||||
for (auto & arg : args) {
|
||||
newArgs.push_back(replaceStrings(arg, "${PWD}", pwd));
|
||||
}
|
||||
args = std::move(newArgs);
|
||||
}
|
||||
|
||||
const char * ParseError::what() const noexcept
|
||||
{
|
||||
if (what_) {
|
||||
return what_->c_str();
|
||||
} else {
|
||||
auto escaped = escapeString(rest, {.maxLength = 256, .escapeNonPrinting = true});
|
||||
auto hint = HintFmt("Parse error: Expected %1%, got:\n%2%", expected, Uncolored(escaped));
|
||||
what_ = hint.str();
|
||||
return what_->c_str();
|
||||
}
|
||||
}
|
||||
|
||||
auto parse(const std::string input, Config config) -> ParseResult
|
||||
{
|
||||
return Parser(input, config).parse();
|
||||
}
|
||||
|
||||
std::ostream & operator<<(std::ostream & output, const Args & node)
|
||||
{
|
||||
return output << "@args " << node.text;
|
||||
}
|
||||
|
||||
std::ostream & operator<<(std::ostream & output, const ShouldStart & node)
|
||||
{
|
||||
return output << "@should-start " << (node.shouldStart ? "true" : "false");
|
||||
}
|
||||
|
||||
std::ostream & operator<<(std::ostream & output, const TextNode & rhs)
|
||||
{
|
||||
return output << rhs.text;
|
||||
}
|
||||
|
||||
void unparseNode(std::ostream & output, const Node & node, bool withNewline)
|
||||
{
|
||||
std::visit(
|
||||
[&](const auto & n) { output << n << (withNewline && n.emitNewlineAfter() ? "\n" : ""); },
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::string gtestFormat(T & value)
|
||||
{
|
||||
std::ostringstream formatted;
|
||||
unparseNode(formatted, value, true);
|
||||
auto str = formatted.str();
|
||||
// Needs to be the literal string `\n` and not a newline character to
|
||||
// trigger gtest diff printing. Yes seriously.
|
||||
boost::algorithm::replace_all(str, "\n", "\\n");
|
||||
return str;
|
||||
}
|
||||
|
||||
void PrintTo(const std::vector<Node> & nodes, std::ostream * output)
|
||||
{
|
||||
for (auto & node : nodes) {
|
||||
*output << gtestFormat(node);
|
||||
}
|
||||
}
|
||||
|
||||
std::string debugNode(const Node & node)
|
||||
{
|
||||
std::ostringstream output;
|
||||
output << std::visit([](const auto & n) { return n.kind(); }, node) << ": ";
|
||||
std::ostringstream contents;
|
||||
unparseNode(contents, node, false);
|
||||
escapeString(output, contents.str(), {.escapeNonPrinting = true});
|
||||
return output.str();
|
||||
}
|
||||
|
||||
auto ParseResult::debugPrint(std::ostream & output) -> void
|
||||
{
|
||||
::nix::cli_literate_parser::debugPrint(output, syntax);
|
||||
}
|
||||
|
||||
void debugPrint(std::ostream & output, std::vector<Node> & nodes)
|
||||
{
|
||||
for (auto & node : nodes) {
|
||||
output << debugNode(node) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cli_literate_parser
|
||||
} // namespace nix
|
||||
@@ -1,202 +0,0 @@
|
||||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "lix/libutil/error.hh"
|
||||
#include <compare>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace nix {
|
||||
namespace cli_literate_parser {
|
||||
|
||||
// ------------------------- NODES -------------------------
|
||||
//
|
||||
// To update golden test files while preserving commentary output and other `@`
|
||||
// directives, we need to keep commentary output around after parsing.
|
||||
|
||||
struct BaseNode {
|
||||
virtual ~BaseNode() = default;
|
||||
|
||||
virtual auto shouldCompare() const -> bool { return false; }
|
||||
|
||||
virtual auto kind() const -> std::string = 0;
|
||||
virtual auto emitNewlineAfter() const -> bool = 0;
|
||||
|
||||
auto operator<=>(const BaseNode & rhs) const = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* A node containing text. The text should be identical to how the node was
|
||||
* written in the input file.
|
||||
*/
|
||||
struct TextNode : BaseNode {
|
||||
std::string text;
|
||||
|
||||
explicit TextNode(std::string text) : text(text) {}
|
||||
auto operator<=>(const TextNode & rhs) const = default;
|
||||
};
|
||||
|
||||
std::ostream &operator<<(std::ostream &output, const TextNode &node);
|
||||
|
||||
// clang-format off
|
||||
#define DECLARE_TEXT_NODE(NAME, NEEDS_NEWLINE, SHOULD_COMPARE) \
|
||||
struct NAME : TextNode { \
|
||||
using TextNode::TextNode; \
|
||||
~NAME() override = default; \
|
||||
\
|
||||
auto kind() const -> std::string override { return #NAME; } \
|
||||
auto emitNewlineAfter() const -> bool override { return NEEDS_NEWLINE; } \
|
||||
auto shouldCompare() const -> bool override { return SHOULD_COMPARE; } \
|
||||
auto operator<=>(const NAME & rhs) const = default; \
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
/* name, needsNewline, shouldCompare */
|
||||
DECLARE_TEXT_NODE(Prompt, false, false)
|
||||
DECLARE_TEXT_NODE(Command, true, true)
|
||||
DECLARE_TEXT_NODE(Output, true, true)
|
||||
DECLARE_TEXT_NODE(Commentary, true, false)
|
||||
DECLARE_TEXT_NODE(Args, true, false)
|
||||
DECLARE_TEXT_NODE(Indent, false, false)
|
||||
|
||||
#undef DECLARE_TEXT_NODE
|
||||
|
||||
struct ShouldStart : BaseNode {
|
||||
bool shouldStart;
|
||||
|
||||
ShouldStart(bool shouldStart) : shouldStart(shouldStart) {}
|
||||
~ShouldStart() override = default;
|
||||
auto emitNewlineAfter() const -> bool override { return true; }
|
||||
auto kind() const -> std::string override { return "should-start"; }
|
||||
|
||||
auto operator<=>(const ShouldStart &rhs) const = default;
|
||||
};
|
||||
std::ostream &operator<<(std::ostream &output, const ShouldStart &node);
|
||||
|
||||
/**
|
||||
* Any syntax node, including those that are cosmetic.
|
||||
*/
|
||||
using Node = std::variant<Prompt, Command, Output, Commentary, Args,
|
||||
ShouldStart, Indent>;
|
||||
|
||||
/** Unparses a node into the exact text that would have created it, including a
|
||||
* newline at the end if present, if withNewline is set */
|
||||
void unparseNode(std::ostream &output, const Node &node,
|
||||
bool withNewline = true);
|
||||
|
||||
std::string debugNode(const Node &node);
|
||||
void debugPrint(std::ostream &output, std::vector<Node> &nodes);
|
||||
|
||||
/**
|
||||
* Override gtest printing for lists of nodes.
|
||||
*/
|
||||
void PrintTo(std::vector<Node> const &nodes, std::ostream *output);
|
||||
|
||||
/**
|
||||
* The result of parsing a test file.
|
||||
*/
|
||||
struct ParseResult {
|
||||
/**
|
||||
* A set of nodes that can be used to reproduce the input file. This is used
|
||||
* to implement updating the test files.
|
||||
*/
|
||||
std::vector<Node> syntax;
|
||||
|
||||
/**
|
||||
* Extra CLI arguments.
|
||||
*/
|
||||
std::vector<std::string> args;
|
||||
|
||||
/**
|
||||
* Should the program start successfully?
|
||||
*/
|
||||
bool shouldStart = false;
|
||||
|
||||
/**
|
||||
* Replace `$PWD` with the given value in `args`.
|
||||
*/
|
||||
void interpolatePwd(std::string_view pwd);
|
||||
|
||||
/**
|
||||
* Tidy `syntax` to remove unnecessary nodes.
|
||||
*/
|
||||
auto tidyOutputForComparison() -> std::vector<Node>;
|
||||
|
||||
auto debugPrint(std::ostream &output) -> void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A parse error.
|
||||
*/
|
||||
struct ParseError : BaseException {
|
||||
std::string expected;
|
||||
std::string rest;
|
||||
|
||||
ParseError(std::string expected, std::string rest)
|
||||
: expected(expected), rest(rest) {}
|
||||
|
||||
const char *what() const noexcept override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Cached formatted contents of `what()`.
|
||||
*/
|
||||
mutable std::optional<std::string> what_;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
/**
|
||||
* The prompt string to look for.
|
||||
*/
|
||||
std::string prompt;
|
||||
/**
|
||||
* The number of spaces of indent for commands and output.
|
||||
*/
|
||||
size_t indent = 2;
|
||||
};
|
||||
|
||||
/*
|
||||
* A recursive descent parser for literate test cases for CLIs.
|
||||
*
|
||||
* FIXME: implement merging of these, so you can auto update cases that have
|
||||
* comments.
|
||||
*
|
||||
* Syntax:
|
||||
* ```
|
||||
* ( COMMENTARY
|
||||
* | INDENT PROMPT COMMAND
|
||||
* | INDENT OUTPUT
|
||||
* | @args ARGS
|
||||
* | @should-start ( true | false )) *
|
||||
* ```
|
||||
*
|
||||
* e.g.
|
||||
* ```
|
||||
* commentary commentary commentary
|
||||
* @args --foo
|
||||
* @should-start false
|
||||
* nix-repl> :t 1
|
||||
* an integer
|
||||
* ```
|
||||
*
|
||||
* Yields something like:
|
||||
* ```
|
||||
* Commentary "commentary commentary commentary"
|
||||
* Args "--foo"
|
||||
* ShouldStart false
|
||||
* Command ":t 1"
|
||||
* Output "an integer"
|
||||
* ```
|
||||
*
|
||||
* Note: one Output line is generated for each line of the sources, because
|
||||
* this is effectively necessary to be able to align them in the future to
|
||||
* auto-update tests.
|
||||
*/
|
||||
auto parse(std::string input, Config config) -> ParseResult;
|
||||
|
||||
}; // namespace cli_literate_parser
|
||||
}; // namespace nix
|
||||
@@ -20,7 +20,6 @@ default_test_env = {
|
||||
}
|
||||
|
||||
libutil_test_support_sources = files(
|
||||
'libutil-support/tests/cli-literate-parser.cc',
|
||||
'libutil-support/tests/hash.cc',
|
||||
'libutil-support/tests/terminal-code-eater.cc',
|
||||
'libutil-support/tests/test-data.cc',
|
||||
|
||||
Reference in New Issue
Block a user