lix/tests/functional/repl_characterization/repl_characterization.cc
alois31 81a0624d76
libexpr/print: pretty-print idempotently
When pretty-printing is enabled, previously an unforced thunk would trigger
indentation, even when it subsequently does not evaluate to a nested structure.
The resulting output looked inconsistent, and furthermore pretty-printing was
not idempotent (since pretty-printing the same value again, which is now fully
evaluated, will not trigger indentation).
When strict evaluation is enabled, force the item before inspecting its type,
so that it is properly known whether it contains a nested structure.
Furthermore, there is no need to cause indentation for unforced thunks, since
the very next operation will be printing them as `«thunk»`.

This is mostly a port of https://github.com/NixOS/nix/pull/11100 , but we only
force the item when it's going to be forced anyway due to strict
pretty-printing, and a new test was written since the REPL testing framework in
Lix is different.

Co-Authored-By: Robert Hensing <robert@roberthensing.nl>
Change-Id: Ib7560fe531d09e05ca6b2037a523fe21a26d9d58
2024-07-18 18:41:28 +02:00

191 lines
5.9 KiB
C++

#include <gtest/gtest.h>
#include <boost/algorithm/string/replace.hpp>
#include <optional>
#include <string>
#include <string_view>
#include <unistd.h>
#include "test-session.hh"
#include "tests/characterization.hh"
#include "tests/cli-literate-parser.hh"
#include "strings.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, {});
}
};
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(repl_overlays);
REPL_TEST(repl_overlays_compose);
REPL_TEST(repl_overlays_destructure_without_dotdotdot_errors);
REPL_TEST(repl_overlays_destructure_without_formals_ok);
REPL_TEST(repl_overlays_error);
REPL_TEST(repl_printing);
REPL_TEST(stack_vars);
REPL_TEST(errors);
REPL_TEST(idempotent);
}; // namespace nix