-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
965 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
test-repl-characterization |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
60
tests/functional/repl_characterization/data/basic_repl.test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
101
tests/functional/repl_characterization/repl_characterization.cc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); }); | ||
} | ||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
}; |
Oops, something went wrong.