Implement a parser for a literate testing system for the repl
This parser can be reused for other purposes. It's inspired by
https://bitheap.org/cram/
Although eelco's impostor exists https://github.com/mobusoperandi/eelco,
it is not very nice to depend on out of tree testing frameworks with no
way to customize them.
Change-Id: Ifca50177e09730182baf0ebf829c3505bbb0274a
This commit is contained in:
parent
84727bebb4
commit
38571c50e6
1
Makefile
1
Makefile
|
@ -41,6 +41,7 @@ makefiles += \
|
|||
tests/functional/ca/local.mk \
|
||||
tests/functional/dyn-drv/local.mk \
|
||||
tests/functional/test-libstoreconsumer/local.mk \
|
||||
tests/functional/repl_characterization/local.mk \
|
||||
tests/functional/plugins/local.mk
|
||||
else
|
||||
makefiles += \
|
||||
|
|
|
@ -131,7 +131,7 @@ ifeq ($(HAVE_LIBCPUID), 1)
|
|||
endif
|
||||
|
||||
ifeq ($(ENABLE_BUILD), yes)
|
||||
nix_tests += test-libstoreconsumer.sh
|
||||
nix_tests += test-libstoreconsumer.sh test-repl-characterization.sh
|
||||
|
||||
ifeq ($(BUILD_SHARED_LIBS), 1)
|
||||
nix_tests += plugins.sh
|
||||
|
@ -142,6 +142,8 @@ $(d)/test-libstoreconsumer.sh.test $(d)/test-libstoreconsumer.sh.test-debug: \
|
|||
$(buildprefix)$(d)/test-libstoreconsumer/test-libstoreconsumer
|
||||
$(d)/plugins.sh.test $(d)/plugins.sh.test-debug: \
|
||||
$(buildprefix)$(d)/plugins/libplugintest.$(SO_EXT)
|
||||
$(d)/test-repl-characterization.sh.test $(d)/test-repl-characterization.sh.test-debug: \
|
||||
$(buildprefix)$(d)/repl_characterization/test-repl-characterization
|
||||
|
||||
install-tests += $(foreach x, $(nix_tests), $(d)/$(x))
|
||||
|
||||
|
|
1
tests/functional/repl_characterization/.gitignore
vendored
Normal file
1
tests/functional/repl_characterization/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
test-repl-characterization
|
11
tests/functional/repl_characterization/data/basic.ast
Normal file
11
tests/functional/repl_characterization/data/basic.ast
Normal file
|
@ -0,0 +1,11 @@
|
|||
Commentary "meow meow meow"
|
||||
Command "command"
|
||||
Output "output output one"
|
||||
Output ""
|
||||
Output ""
|
||||
Output "output output two"
|
||||
Commentary "meow meow"
|
||||
Command "command two"
|
||||
Output "output output output"
|
||||
Commentary "commentary"
|
||||
Output "output output output"
|
11
tests/functional/repl_characterization/data/basic.test
Normal file
11
tests/functional/repl_characterization/data/basic.test
Normal file
|
@ -0,0 +1,11 @@
|
|||
meow meow meow
|
||||
nix-repl> command
|
||||
output output one
|
||||
|
||||
|
||||
output output two
|
||||
meow meow
|
||||
nix-repl> command two
|
||||
output output output
|
||||
commentary
|
||||
output output output
|
15
tests/functional/repl_characterization/local.mk
Normal file
15
tests/functional/repl_characterization/local.mk
Normal file
|
@ -0,0 +1,15 @@
|
|||
programs += test-repl-characterization
|
||||
|
||||
test-repl-characterization_DIR := $(d)
|
||||
|
||||
# do not install
|
||||
test-repl-characterization_INSTALL_DIR :=
|
||||
|
||||
test-repl-characterization_SOURCES := \
|
||||
$(wildcard $(d)/*.cc) \
|
||||
|
||||
test-repl-characterization_CXXFLAGS += -I src/libutil -I tests/unit/libutil-support -DNIX_BIN_DIR="\"$(bindir)\""
|
||||
|
||||
test-repl-characterization_LIBS = libutil libutil-test-support
|
||||
|
||||
test-repl-characterization_LDFLAGS = $(THREAD_LDFLAGS) $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) $(LOWDOWN_LIBS) $(GTEST_LIBS)
|
|
@ -0,0 +1,42 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <optional>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "tests/characterization.hh"
|
||||
#include "tests/cli-literate-parser.hh"
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
namespace nix {
|
||||
|
||||
static constexpr const char * REPL_PROMPT = "nix-repl> ";
|
||||
|
||||
class ReplSessionTest : public CharacterizationTest
|
||||
{
|
||||
Path unitTestData = getUnitTestData();
|
||||
|
||||
public:
|
||||
Path goldenMaster(std::string_view testStem) const override
|
||||
{
|
||||
return unitTestData + "/" + testStem;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ReplSessionTest, parses)
|
||||
{
|
||||
writeTest("basic.ast", [this]() {
|
||||
const std::string content = readFile(goldenMaster("basic.test"));
|
||||
auto parser = CLILiterateParser{REPL_PROMPT};
|
||||
parser.feed(content);
|
||||
|
||||
std::ostringstream out{};
|
||||
for (auto & bit : parser.syntax()) {
|
||||
out << bit.print() << "\n";
|
||||
}
|
||||
return out.str();
|
||||
});
|
||||
}
|
||||
};
|
7
tests/functional/test-repl-characterization.sh
Normal file
7
tests/functional/test-repl-characterization.sh
Normal file
|
@ -0,0 +1,7 @@
|
|||
source common.sh
|
||||
|
||||
# You might think that this can trivially be moved into a makefile, however,
|
||||
# there is various environmental initialization of this shell framework that
|
||||
# seems load bearing and seemingly prevents the tests working inside the
|
||||
# builder.
|
||||
_NIX_TEST_UNIT_DATA=$(pwd)/repl_characterization/data ./repl_characterization/test-repl-characterization
|
|
@ -10,6 +10,7 @@ libutil-test-support_SOURCES := $(wildcard $(d)/tests/*.cc)
|
|||
|
||||
libutil-test-support_CXXFLAGS += $(libutil-tests_EXTRA_INCLUDES)
|
||||
|
||||
libutil-test-support_LIBS = libutil
|
||||
# libexpr so we can steal their string printer from print.cc
|
||||
libutil-test-support_LIBS = libutil libexpr
|
||||
|
||||
libutil-test-support_LDFLAGS := -pthread -lrapidcheck
|
||||
|
|
174
tests/unit/libutil-support/tests/cli-literate-parser.cc
Normal file
174
tests/unit/libutil-support/tests/cli-literate-parser.cc
Normal file
|
@ -0,0 +1,174 @@
|
|||
#include "cli-literate-parser.hh"
|
||||
#include "libexpr/print.hh"
|
||||
#include "debug-char.hh"
|
||||
#include "types.hh"
|
||||
#include "util.hh"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
namespace nix {
|
||||
|
||||
static constexpr const bool DEBUG_PARSER = false;
|
||||
|
||||
constexpr auto CLILiterateParser::stateDebug(State const & s) -> const char *
|
||||
{
|
||||
return std::visit(
|
||||
overloaded{// clang-format off
|
||||
[](Indent const&) -> const char * { return "indent"; },
|
||||
[](Commentary const&) -> const char * { return "indent"; },
|
||||
[](Prompt const&) -> const char * { return "prompt"; },
|
||||
[](Command const&) -> const char * { return "command"; },
|
||||
[](OutputLine const&) -> const char * { return "output_line"; }},
|
||||
// clang-format on
|
||||
s);
|
||||
}
|
||||
|
||||
auto CLILiterateParser::Node::print() const -> std::string
|
||||
{
|
||||
std::ostringstream s{};
|
||||
switch (kind) {
|
||||
case NodeKind::COMMENTARY:
|
||||
s << "Commentary ";
|
||||
break;
|
||||
case NodeKind::COMMAND:
|
||||
s << "Command ";
|
||||
break;
|
||||
case NodeKind::OUTPUT:
|
||||
s << "Output ";
|
||||
break;
|
||||
}
|
||||
printLiteralString(s, this->text);
|
||||
return s.str();
|
||||
}
|
||||
|
||||
void PrintTo(std::vector<CLILiterateParser::Node> const & nodes, std::ostream * os)
|
||||
{
|
||||
for (auto & node : nodes) {
|
||||
*os << node.print() << "\\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto CLILiterateParser::parse(std::string prompt, std::string_view const & input, size_t indent) -> std::vector<Node>
|
||||
{
|
||||
CLILiterateParser p{std::move(prompt), indent};
|
||||
p.feed(input);
|
||||
return std::move(p).intoSyntax();
|
||||
}
|
||||
|
||||
auto CLILiterateParser::intoSyntax() && -> std::vector<Node>
|
||||
{
|
||||
return std::move(this->syntax_);
|
||||
}
|
||||
|
||||
CLILiterateParser::CLILiterateParser(std::string prompt, size_t indent)
|
||||
: state_(indent == 0 ? State(Prompt{}) : State(Indent{}))
|
||||
, prompt_(prompt)
|
||||
, indent_(indent)
|
||||
, lastWasOutput_(false)
|
||||
, syntax_{}
|
||||
{
|
||||
assert(!prompt.empty());
|
||||
}
|
||||
|
||||
void CLILiterateParser::feed(char c)
|
||||
{
|
||||
if constexpr (DEBUG_PARSER) {
|
||||
std::cout << stateDebug(state_) << " " << DebugChar{c} << "\n";
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
onNewline();
|
||||
return;
|
||||
}
|
||||
|
||||
std::visit(
|
||||
overloaded{
|
||||
[&](Indent & s) {
|
||||
if (c == ' ') {
|
||||
if (++s.pos >= indent_) {
|
||||
transition(Prompt{});
|
||||
}
|
||||
} else {
|
||||
transition(Commentary{AccumulatingState{.lineAccumulator = std::string{c}}});
|
||||
}
|
||||
},
|
||||
[&](Prompt & s) {
|
||||
if (s.pos >= prompt_.length()) {
|
||||
transition(Command{AccumulatingState{.lineAccumulator = std::string{c}}});
|
||||
return;
|
||||
} else if (c == prompt_[s.pos]) {
|
||||
// good prompt character
|
||||
++s.pos;
|
||||
} else {
|
||||
// didn't match the prompt, so it must have actually been output.
|
||||
s.lineAccumulator.push_back(c);
|
||||
transition(OutputLine{AccumulatingState{.lineAccumulator = std::move(s.lineAccumulator)}});
|
||||
return;
|
||||
}
|
||||
s.lineAccumulator.push_back(c);
|
||||
},
|
||||
[&](AccumulatingState & s) { s.lineAccumulator.push_back(c); }},
|
||||
state_);
|
||||
}
|
||||
|
||||
void CLILiterateParser::onNewline()
|
||||
{
|
||||
State lastState = std::move(state_);
|
||||
bool newLastWasOutput = false;
|
||||
|
||||
syntax_.push_back(std::visit(
|
||||
overloaded{
|
||||
[&](Indent & s) {
|
||||
// XXX: technically this eats trailing spaces
|
||||
|
||||
// a newline following output is considered part of that output
|
||||
if (lastWasOutput_) {
|
||||
newLastWasOutput = true;
|
||||
return Node::mkOutput("");
|
||||
}
|
||||
return Node::mkCommentary("");
|
||||
},
|
||||
[&](Commentary & s) { return Node::mkCommentary(std::move(s.lineAccumulator)); },
|
||||
[&](Command & s) { return Node::mkCommand(std::move(s.lineAccumulator)); },
|
||||
[&](OutputLine & s) {
|
||||
newLastWasOutput = true;
|
||||
return Node::mkOutput(std::move(s.lineAccumulator));
|
||||
},
|
||||
[&](Prompt & s) {
|
||||
// INDENT followed by newline is also considered a blank output line
|
||||
return Node::mkOutput(std::move(s.lineAccumulator));
|
||||
}},
|
||||
lastState));
|
||||
|
||||
transition(Indent{});
|
||||
lastWasOutput_ = newLastWasOutput;
|
||||
}
|
||||
|
||||
void CLILiterateParser::feed(std::string_view s)
|
||||
{
|
||||
for (char ch : s) {
|
||||
feed(ch);
|
||||
}
|
||||
}
|
||||
|
||||
void CLILiterateParser::transition(State new_state)
|
||||
{
|
||||
// When we expect INDENT and we are parsing without indents, commentary
|
||||
// cannot exist, so we want to transition directly into PROMPT before
|
||||
// resuming normal processing.
|
||||
if (Indent * i = std::get_if<Indent>(&new_state); i != nullptr && indent_ == 0) {
|
||||
new_state = Prompt{AccumulatingState{}, i->pos};
|
||||
}
|
||||
|
||||
state_ = new_state;
|
||||
}
|
||||
|
||||
auto CLILiterateParser::syntax() const -> std::vector<Node> const &
|
||||
{
|
||||
return syntax_;
|
||||
}
|
||||
|
||||
};
|
127
tests/unit/libutil-support/tests/cli-literate-parser.hh
Normal file
127
tests/unit/libutil-support/tests/cli-literate-parser.hh
Normal file
|
@ -0,0 +1,127 @@
|
|||
#pragma once
|
||||
///@file
|
||||
|
||||
#include <compare>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace nix {
|
||||
/*
|
||||
* A DFA parser for literate test cases for CLIs.
|
||||
*
|
||||
* FIXME: implement merging of these, so you can auto update cases that have
|
||||
* comments.
|
||||
*
|
||||
* Format:
|
||||
* COMMENTARY
|
||||
* INDENT PROMPT COMMAND
|
||||
* INDENT OUTPUT
|
||||
*
|
||||
* e.g.
|
||||
* commentary commentary commentary
|
||||
* nix-repl> :t 1
|
||||
* an integer
|
||||
*
|
||||
* Yields:
|
||||
* Commentary "commentary commentary commentary"
|
||||
* 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.
|
||||
*/
|
||||
class CLILiterateParser
|
||||
{
|
||||
public:
|
||||
|
||||
enum class NodeKind {
|
||||
COMMENTARY,
|
||||
COMMAND,
|
||||
OUTPUT,
|
||||
};
|
||||
|
||||
struct Node
|
||||
{
|
||||
NodeKind kind;
|
||||
std::string text;
|
||||
std::strong_ordering operator<=>(Node const &) const = default;
|
||||
|
||||
static Node mkCommentary(std::string text)
|
||||
{
|
||||
return Node{.kind = NodeKind::COMMENTARY, .text = text};
|
||||
}
|
||||
|
||||
static Node mkCommand(std::string text)
|
||||
{
|
||||
return Node{.kind = NodeKind::COMMAND, .text = text};
|
||||
}
|
||||
|
||||
static Node mkOutput(std::string text)
|
||||
{
|
||||
return Node{.kind = NodeKind::OUTPUT, .text = text};
|
||||
}
|
||||
|
||||
auto print() const -> std::string;
|
||||
};
|
||||
|
||||
CLILiterateParser(std::string prompt, size_t indent = 2);
|
||||
|
||||
auto syntax() const -> std::vector<Node> const &;
|
||||
|
||||
/** Feeds a character into the parser */
|
||||
void feed(char c);
|
||||
|
||||
/** Feeds a string into the parser */
|
||||
void feed(std::string_view s);
|
||||
|
||||
/** 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>;
|
||||
|
||||
/** Consumes a CLILiterateParser and gives you the syntax out of it */
|
||||
auto intoSyntax() && -> std::vector<Node>;
|
||||
|
||||
private:
|
||||
|
||||
struct AccumulatingState
|
||||
{
|
||||
std::string lineAccumulator;
|
||||
};
|
||||
struct Indent
|
||||
{
|
||||
size_t pos = 0;
|
||||
};
|
||||
struct Commentary : public AccumulatingState
|
||||
{};
|
||||
struct Prompt : AccumulatingState
|
||||
{
|
||||
size_t pos = 0;
|
||||
};
|
||||
struct Command : public AccumulatingState
|
||||
{};
|
||||
struct OutputLine : public AccumulatingState
|
||||
{};
|
||||
|
||||
using State = std::variant<Indent, Commentary, Prompt, Command, OutputLine>;
|
||||
State state_;
|
||||
|
||||
constexpr static auto stateDebug(State const&) -> const char *;
|
||||
|
||||
const std::string prompt_;
|
||||
const size_t indent_;
|
||||
|
||||
/** Last line was output, so we consider a blank to be part of the output */
|
||||
bool lastWasOutput_;
|
||||
|
||||
std::vector<Node> syntax_;
|
||||
|
||||
void transition(State newState);
|
||||
void onNewline();
|
||||
};
|
||||
|
||||
// Override gtest printing for lists of nodes
|
||||
void PrintTo(std::vector<CLILiterateParser::Node> const & nodes, std::ostream * os);
|
||||
};
|
24
tests/unit/libutil-support/tests/debug-char.hh
Normal file
24
tests/unit/libutil-support/tests/debug-char.hh
Normal file
|
@ -0,0 +1,24 @@
|
|||
///@file
|
||||
#include <ostream>
|
||||
#include <boost/io/ios_state.hpp>
|
||||
|
||||
namespace nix {
|
||||
|
||||
struct DebugChar
|
||||
{
|
||||
char c;
|
||||
};
|
||||
|
||||
inline std::ostream & operator<<(std::ostream & s, DebugChar c)
|
||||
{
|
||||
boost::io::ios_flags_saver _ifs(s);
|
||||
|
||||
if (isprint(c.c)) {
|
||||
s << static_cast<char>(c.c);
|
||||
} else {
|
||||
s << std::hex << "0x" << (static_cast<unsigned int>(c.c) & 0xff);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue