Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
lf- committed Mar 6, 2024
1 parent 47cce1e commit 11b391a
Show file tree
Hide file tree
Showing 14 changed files with 965 additions and 1 deletion.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ makefiles += \
tests/functional/git-hashing/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
endif

Expand Down
1 change: 1 addition & 0 deletions tests/functional/repl_characterization/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-repl-characterization
11 changes: 11 additions & 0 deletions tests/functional/repl_characterization/data/basic.ast
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions tests/functional/repl_characterization/data/basic.test
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions tests/functional/repl_characterization/data/basic_repl.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
nix-repl> 1 + 1
2

nix-repl> :doc builtins.head
Synopsis: builtins.head list

Return the first element of a list; abort evaluation if
the argument isn’t a list or is an empty list. You can
test whether a list is empty by comparing it with [].

nix-repl> f = a: "" + a

Expect the trace to not contain any traceback:

nix-repl> f 2
error:
… while evaluating a path segment
at «string»:1:10:
1| a: "" + a
| ^

error: cannot coerce an integer to a string: 2

nix-repl> :te
showing error traces

Expect the trace to have traceback:

nix-repl> f 2
error:
… from call site
at «string»:1:1:
1| f 2
| ^

… while calling anonymous lambda
at «string»:1:2:
1| a: "" + a
| ^

… while evaluating a path segment
at «string»:1:10:
1| a: "" + a
| ^

error: cannot coerce an integer to a string: 2

Turning it off should also work:

nix-repl> :te
not showing error traces

nix-repl> f 2
error:
… while evaluating a path segment
at «string»:1:10:
1| a: "" + a
| ^

error: cannot coerce an integer to a string: 2
17 changes: 17 additions & 0 deletions tests/functional/repl_characterization/local.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
programs += test-repl-characterization

test-repl-characterization_DIR := $(d)

test-repl-characterization_ENV := _NIX_TEST_UNIT_DATA=$(d)/data

# 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

test-repl-characterization_LIBS = libutil libutil-test-support

test-repl-characterization_LDFLAGS = $(THREAD_LDFLAGS) $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) $(LOWDOWN_LIBS) $(GTEST_LIBS)
101 changes: 101 additions & 0 deletions tests/functional/repl_characterization/repl_characterization.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <gtest/gtest.h>

#include <string>
#include <string_view>
#include <optional>
#include <unistd.h>

#include "test-session.hh"
#include "file-descriptor.hh"
#include "processes.hh"
#include "source-accessor.hh"
#include "tests/characterization.hh"
#include "tests/cli-literate-parser.hh"
#include "tests/terminal-code-eater.hh"

using namespace std::string_literals;

namespace nix {

static constexpr const char * REPL_PROMPT = "nix-repl> ";

// ASCII ENQ character
static constexpr const char * 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(std::string_view const & content, std::vector<std::string> extraArgs = {}) const
{
auto syntax = CLILiterateParser::parse(REPL_PROMPT, content);

Strings args{"--quiet", "repl", "--quiet", "--extra-experimental-features", "repl-automation"};
args.insert(args.end(), extraArgs.begin(), extraArgs.end());

// TODO: why the fuck does this need two --quiets
auto process = RunningProcess::start("nix", args);
auto session = TestSession{AUTOMATION_PROMPT, std::move(process)};

const auto expectedOutput = CLILiterateParser::unparse(REPL_PROMPT, syntax, 0);

for (auto & bit : syntax) {
if (bit->kind() != CLILiterateParser::NodeKind::COMMAND) {
continue;
}

if (!session.waitForPrompt()) {
ASSERT_TRUE(false);
}
session.runCommand(bit->text());
}
if (!session.waitForPrompt()) {
ASSERT_TRUE(false);
}
session.close();

auto parsedOutLog = CLILiterateParser::parse(AUTOMATION_PROMPT, trimOutLog(session.outLog), 0);

CLILiterateParser::tidyOutputForComparison(parsedOutLog);
CLILiterateParser::tidyOutputForComparison(syntax);

ASSERT_EQ(parsedOutLog, syntax);
}
};

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();
});
}

TEST_F(ReplSessionTest, repl_basic)
{
readTest("basic_repl.test", [this](std::string input) { runReplTest(input); });
}

};
163 changes: 163 additions & 0 deletions tests/functional/repl_characterization/test-session.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include <iostream>

#include "test-session.hh"
#include "util.hh"

namespace nix {

static constexpr const bool DEBUG_REPL_PARSER = false;

struct DebugChar
{
char c;
};

static std::ostream & operator<<(std::ostream & s, DebugChar c)
{
if (isprint(c.c)) {
s << static_cast<char>(c.c);
} else {
s << std::hex << "0x" << (static_cast<unsigned int>(c.c) & 0xff) << std::dec;
}
return s;
}

RunningProcess RunningProcess::start(std::string executable, Strings args)
{
args.push_front(executable);

Pipe procStdin{};
Pipe procStdout{};

procStdin.create();
procStdout.create();

// This is separate from runProgram2 because we have different IO requirements
pid_t pid = startProcess([&]() {
if (dup2(procStdout.writeSide.get(), STDOUT_FILENO) == -1)
throw SysError("dupping stdout");
if (dup2(procStdin.readSide.get(), STDIN_FILENO) == -1)
throw SysError("dupping stdin");
procStdin.writeSide.close();
procStdout.readSide.close();
if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
throw SysError("dupping stderr");
execvp(executable.c_str(), stringsToCharPtrs(args).data());
throw SysError("exec did not happen");
});

procStdout.writeSide.close();
procStdin.readSide.close();

return RunningProcess{
.pid = pid,
.procStdin = std::move(procStdin),
.procStdout = std::move(procStdout),
};
}

static std::ostream & operator<<(std::ostream & os, ReplOutputParser::State s)
{
switch (s) {
case ReplOutputParser::State::Prompt:
os << "prompt";
break;
case ReplOutputParser::State::Context:
os << "context";
break;
}
return os;
}

void ReplOutputParser::transition(State new_state, char responsible_char, bool wasPrompt)
{
if (DEBUG_REPL_PARSER) {
std::cerr << "transition " << new_state << " for " << DebugChar{responsible_char}
<< (wasPrompt ? " [prompt]" : "") << "\n";
}
state = new_state;
pos_in_prompt = 0;
}

bool ReplOutputParser::feed(char c)
{
if (c == '\n') {
transition(State::Prompt, c);
return false;
}
switch (state) {
case State::Context:
break;
case State::Prompt:
if (pos_in_prompt == prompt.length() - 1 && prompt[pos_in_prompt] == c) {
transition(State::Context, c, true);
return true;
}
if (pos_in_prompt >= prompt.length() - 1 || prompt[pos_in_prompt] != c) {
transition(State::Context, c);
break;
}
pos_in_prompt++;
break;
}
return false;
}

/** Waits for the prompt and then returns if a prompt was found */
bool TestSession::waitForPrompt()
{
std::vector<char> buf(1024);

for (;;) {
ssize_t res = read(proc.procStdout.readSide.get(), buf.data(), buf.size());

if (res < 0) {
throw SysError("read");
}
if (res == 0) {
return false;
}

bool foundPrompt = false;
for (ssize_t i = 0; i < res; ++i) {
// foundPrompt = foundPrompt || outputParser.feed(buf[i]);
bool wasEaten = true;
eater.feed(buf[i], [&](char c) {
wasEaten = false;
foundPrompt = outputParser.feed(buf[i]) || foundPrompt;

outLog.push_back(c);
});

if (DEBUG_REPL_PARSER) {
std::cerr << "raw " << DebugChar{buf[i]} << (wasEaten ? " [eaten]" : "") << "\n";
}
}

if (foundPrompt) {
return true;
}
}
}

void TestSession::close()
{
proc.procStdin.close();
proc.procStdout.close();
}

void TestSession::runCommand(std::string command)
{
if (DEBUG_REPL_PARSER)
std::cerr << "runCommand " << command << "\n";
command += "\n";
// We have to feed a newline into the output parser, since Nix might not
// give us a newline before a prompt in all cases (it might clear line
// first, e.g.)
outputParser.feed('\n');
// Echo is disabled, so we have to make our own
outLog.append(command);
writeFull(proc.procStdin.writeSide.get(), command, false);
}

};
Loading

0 comments on commit 11b391a

Please sign in to comment.