forked from lix-project/lix
- Use a recursive descent parser so that it's easy to extend.
- Add `@args` to enable customizing command-line arguments
- Add `@should-start` to enable `nix repl` tests that error before
entering the REPL
- Make sure to read all stdout output before comparing. This catches
some extra output we were tossing out before!
Change-Id: I5522555df4c313024ab15cd10f9f04e7293bda3a
190 lines
5.8 KiB
C++
190 lines
5.8 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <boost/algorithm/string/replace.hpp>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unistd.h>
|
|
|
|
#include "escape-string.hh"
|
|
#include "test-session.hh"
|
|
#include "tests/characterization.hh"
|
|
#include "tests/cli-literate-parser.hh"
|
|
#include "tests/terminal-code-eater.hh"
|
|
#include "util.hh"
|
|
|
|
using namespace std::string_literals;
|
|
|
|
namespace nix {
|
|
|
|
static constexpr const std::string_view REPL_PROMPT = "nix-repl> ";
|
|
|
|
// ASCII ENQ character
|
|
static constexpr const std::string_view AUTOMATION_PROMPT = "\x05";
|
|
|
|
static std::string_view trimOutLog(std::string_view outLog)
|
|
{
|
|
const std::string trailer = "\n"s + AUTOMATION_PROMPT;
|
|
if (outLog.ends_with(trailer)) {
|
|
outLog.remove_suffix(trailer.length());
|
|
}
|
|
return outLog;
|
|
}
|
|
|
|
class ReplSessionTest : public CharacterizationTest
|
|
{
|
|
Path unitTestData = getUnitTestData();
|
|
|
|
public:
|
|
Path goldenMaster(std::string_view testStem) const override
|
|
{
|
|
return unitTestData + "/" + testStem;
|
|
}
|
|
|
|
void runReplTest(const std::string content, std::vector<std::string> extraArgs = {}) const
|
|
{
|
|
auto parsed = cli_literate_parser::parse(
|
|
content, cli_literate_parser::Config{.prompt = std::string(REPL_PROMPT), .indent = 2}
|
|
);
|
|
parsed.interpolatePwd(unitTestData);
|
|
|
|
// FIXME: why does this need two --quiets
|
|
// show-trace is on by default due to test configuration, but is not a
|
|
// standard
|
|
Strings args{
|
|
"--quiet",
|
|
"repl",
|
|
"--quiet",
|
|
"--option",
|
|
"show-trace",
|
|
"false",
|
|
"--offline",
|
|
"--extra-experimental-features",
|
|
"repl-automation",
|
|
};
|
|
args.insert(args.end(), extraArgs.begin(), extraArgs.end());
|
|
args.insert(args.end(), parsed.args.begin(), parsed.args.end());
|
|
|
|
auto nixBin = canonPath(getEnvNonEmpty("NIX_BIN_DIR").value_or(NIX_BIN_DIR));
|
|
|
|
auto process = RunningProcess::start(nixBin + "/nix", args);
|
|
auto session = TestSession(std::string(AUTOMATION_PROMPT), std::move(process));
|
|
|
|
for (auto & event : parsed.syntax) {
|
|
std::visit(
|
|
overloaded{
|
|
[&](const cli_literate_parser::Command & e) {
|
|
ASSERT_TRUE(session.waitForPrompt());
|
|
if (e.text == ":quit") {
|
|
// If we quit the repl explicitly, we won't have a
|
|
// prompt when we're done.
|
|
parsed.shouldStart = false;
|
|
}
|
|
session.runCommand(e.text);
|
|
},
|
|
[&](const auto & e) {},
|
|
},
|
|
event
|
|
);
|
|
}
|
|
if (parsed.shouldStart) {
|
|
ASSERT_TRUE(session.waitForPrompt());
|
|
}
|
|
session.close();
|
|
|
|
auto replacedOutLog =
|
|
boost::algorithm::replace_all_copy(session.outLog, unitTestData, "$TEST_DATA");
|
|
auto cleanedOutLog = trimOutLog(replacedOutLog);
|
|
|
|
auto parsedOutLog = cli_literate_parser::parse(
|
|
std::string(cleanedOutLog),
|
|
cli_literate_parser::Config{.prompt = std::string(AUTOMATION_PROMPT), .indent = 0}
|
|
);
|
|
|
|
auto expected = parsed.tidyOutputForComparison();
|
|
auto actual = parsedOutLog.tidyOutputForComparison();
|
|
|
|
ASSERT_EQ(expected, actual);
|
|
}
|
|
|
|
void runReplTestPath(const std::string_view & nameBase, std::vector<std::string> extraArgs)
|
|
{
|
|
auto nixPath = goldenMaster(nameBase + ".nix");
|
|
if (pathExists(nixPath)) {
|
|
extraArgs.push_back("-f");
|
|
extraArgs.push_back(nixPath);
|
|
}
|
|
readTest(nameBase + ".test", [this, extraArgs](std::string input) {
|
|
runReplTest(input, extraArgs);
|
|
});
|
|
}
|
|
|
|
void runReplTestPath(const std::string_view & nameBase)
|
|
{
|
|
runReplTestPath(nameBase, {});
|
|
}
|
|
|
|
void runDebuggerTest(const std::string_view & nameBase)
|
|
{
|
|
runReplTestPath(nameBase, {"--debugger"});
|
|
}
|
|
};
|
|
|
|
TEST_F(ReplSessionTest, round_trip)
|
|
{
|
|
writeTest("basic.test", [this]() {
|
|
const std::string content = readFile(goldenMaster("basic.test"));
|
|
auto parsed = cli_literate_parser::parse(
|
|
content, cli_literate_parser::Config{.prompt = std::string(REPL_PROMPT)}
|
|
);
|
|
|
|
std::ostringstream out{};
|
|
for (auto & node : parsed.syntax) {
|
|
cli_literate_parser::unparseNode(out, node, true);
|
|
}
|
|
return out.str();
|
|
});
|
|
}
|
|
|
|
TEST_F(ReplSessionTest, tidy)
|
|
{
|
|
writeTest("basic.ast", [this]() {
|
|
const std::string content = readFile(goldenMaster("basic.test"));
|
|
auto parsed = cli_literate_parser::parse(
|
|
content, cli_literate_parser::Config{.prompt = std::string(REPL_PROMPT)}
|
|
);
|
|
std::ostringstream out{};
|
|
for (auto & node : parsed.syntax) {
|
|
out << debugNode(node) << "\n";
|
|
}
|
|
return out.str();
|
|
});
|
|
writeTest("basic_tidied.ast", [this]() {
|
|
const std::string content = readFile(goldenMaster("basic.test"));
|
|
auto parsed = cli_literate_parser::parse(
|
|
content, cli_literate_parser::Config{.prompt = std::string(REPL_PROMPT)}
|
|
);
|
|
auto tidied = parsed.tidyOutputForComparison();
|
|
std::ostringstream out{};
|
|
for (auto & node : tidied) {
|
|
out << debugNode(node) << "\n";
|
|
}
|
|
return out.str();
|
|
});
|
|
}
|
|
|
|
#define REPL_TEST(name) \
|
|
TEST_F(ReplSessionTest, name) \
|
|
{ \
|
|
runReplTestPath(#name); \
|
|
}
|
|
|
|
REPL_TEST(basic_repl);
|
|
REPL_TEST(no_nested_debuggers);
|
|
REPL_TEST(regression_9917);
|
|
REPL_TEST(regression_9918);
|
|
REPL_TEST(regression_l145);
|
|
REPL_TEST(stack_vars);
|
|
|
|
}; // namespace nix
|