lix/tests/functional/repl_characterization/test-session.cc
Rebecca Turner 83729b2075 Rewrite REPL test parser
- 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
2024-03-29 18:50:15 -07:00

185 lines
4.7 KiB
C++

#include <iostream>
#include <span>
#include <unistd.h>
#include "test-session.hh"
#include "util.hh"
#include "escape-char.hh"
namespace nix {
static constexpr const bool DEBUG_REPL_PARSER = false;
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");
}
execv(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),
};
}
[[gnu::unused]]
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 constexpr (DEBUG_REPL_PARSER) {
std::cerr << "transition " << new_state << " for " << MaybeHexEscapedChar{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;
}
bool TestSession::readOutThen(ReadOutThenCallback cb)
{
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;
}
switch (cb(std::span(buf.data(), res))) {
case ReadOutThenCallbackResult::Stop:
return true;
case ReadOutThenCallbackResult::Continue:
continue;
}
}
}
bool TestSession::waitForPrompt()
{
bool notEof = readOutThen([&](std::span<char> s) -> ReadOutThenCallbackResult {
bool foundPrompt = false;
for (auto ch : s) {
// foundPrompt = foundPrompt || outputParser.feed(buf[i]);
bool wasEaten = true;
eater.feed(ch, [&](char c) {
wasEaten = false;
foundPrompt = outputParser.feed(ch) || foundPrompt;
outLog.push_back(c);
});
if constexpr (DEBUG_REPL_PARSER) {
std::cerr << "raw " << MaybeHexEscapedChar{ch} << (wasEaten ? " [eaten]" : "") << "\n";
}
}
return foundPrompt ? ReadOutThenCallbackResult::Stop : ReadOutThenCallbackResult::Continue;
});
return notEof;
}
void TestSession::wait()
{
readOutThen([&](std::span<char> s) {
for (auto ch : s) {
eater.feed(ch, [&](char c) {
outputParser.feed(c);
outLog.push_back(c);
});
}
// just keep reading till we hit eof
return ReadOutThenCallbackResult::Continue;
});
}
void TestSession::close()
{
proc.procStdin.close();
wait();
proc.procStdout.close();
}
void TestSession::runCommand(std::string command)
{
if constexpr (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);
}
};