about summary refs log tree commit diff
path: root/third_party/nix/src/tests/language-tests.cc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/nix/src/tests/language-tests.cc')
-rw-r--r--third_party/nix/src/tests/language-tests.cc290
1 files changed, 290 insertions, 0 deletions
diff --git a/third_party/nix/src/tests/language-tests.cc b/third_party/nix/src/tests/language-tests.cc
new file mode 100644
index 000000000000..46e8c6ea80f7
--- /dev/null
+++ b/third_party/nix/src/tests/language-tests.cc
@@ -0,0 +1,290 @@
+// This file defines the language test suite. Language tests are run
+// by evaluating a small snippet of Nix code (against a fake store),
+// serialising it to a string and comparing that to a known output.
+//
+// This test suite is a port of the previous language integration test
+// suite, and it's previous structure is retained.
+//
+// Test cases are written in nix files under lang/, following one of
+// four possible filename patterns which trigger different behaviours:
+//
+// 1. parse-fail-*.nix: These files contain expressions which should
+//    cause a parser failure.
+//
+// 2. parse-okay-*.nix: These files contain expressions which should
+//    parse fine.
+//
+// 3. eval-fail-*.nix: These files contain expressions which should
+//    parse, but fail to evaluate.
+//
+// 4. eval-okay-*.nix: These files contain expressions which should
+//    parse and evaluate fine. They have accompanying .exp files which
+//    contain the expected string representation of the evaluation.
+
+#include <algorithm>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+
+#include <absl/strings/ascii.h>
+#include <absl/strings/match.h>
+#include <absl/strings/str_cat.h>
+#include <absl/strings/str_split.h>
+#include <absl/strings/string_view.h>
+#include <glog/logging.h>
+#include <gtest/gtest-param-test.h>
+#include <gtest/gtest.h>
+#include <gtest/internal/gtest-param-util.h>
+
+#include "libexpr/eval-inline.hh"
+#include "libexpr/eval.hh"
+#include "libexpr/nixexpr.hh"
+#include "nix_config.h"
+#include "tests/dummy-store.hh"
+#include "tests/store-util.hh"
+
+namespace nix::tests {
+namespace {
+
+// List all the language test .nix files matching the given prefix.
+std::vector<std::filesystem::path> TestFilesFor(absl::string_view prefix) {
+  std::vector<std::filesystem::path> matching_files;
+
+  auto dir_iter =
+      std::filesystem::directory_iterator(NIX_SRC_DIR "/src/tests/lang");
+
+  for (auto& entry : dir_iter) {
+    if (!entry.is_regular_file()) {
+      continue;
+    }
+
+    auto filename = entry.path().filename().string();
+    if (absl::StartsWith(filename, prefix) &&
+        absl::EndsWith(filename, ".nix")) {
+      matching_files.push_back(entry.path());
+    }
+  }
+
+  std::sort(matching_files.begin(), matching_files.end());
+  return matching_files;
+}
+
+// Construct a test name from a path parameter, re-casing its name to
+// PascalCase. Googletest only accepts alphanumeric test-names, but
+// the file names are in kebab-case.
+std::string TestNameFor(
+    const testing::TestParamInfo<std::filesystem::path>& info) {
+  std::string name;
+
+  for (auto part :
+       absl::StrSplit(info.param.stem().string(), '-', absl::SkipEmpty())) {
+    std::string part_owned(part);
+    part_owned[0] = absl::ascii_toupper(part_owned[0]);
+    absl::StrAppend(&name, part_owned);
+  }
+
+  return name;
+}
+
+// Load the expected output of a given test as a string.
+std::string ExpectedOutputFor(absl::string_view stem) {
+  std::filesystem::path path(
+      absl::StrCat(NIX_SRC_DIR, "/src/tests/lang/", stem, ".exp"));
+
+  EXPECT_TRUE(std::filesystem::exists(path))
+      << stem << ": expected output file should exist";
+
+  std::ifstream input(path);
+  std::stringstream buffer;
+  buffer << input.rdbuf();
+  return std::string(absl::StripTrailingAsciiWhitespace(buffer.str()));
+}
+
+}  // namespace
+
+using nix::tests::DummyStore;
+
+class NixEnvironment : public testing::Environment {
+ public:
+  void SetUp() override {
+    google::InitGoogleLogging("--logtostderr=false");
+    nix::expr::InitGC();
+  }
+};
+
+::testing::Environment* const nix_env =
+    ::testing::AddGlobalTestEnvironment(new NixEnvironment);
+
+class ParserFailureTest : public testing::TestWithParam<std::filesystem::path> {
+};
+
+// Test pattern for files that should fail to parse.
+TEST_P(ParserFailureTest, Fails) {
+  std::shared_ptr<Store> store = std::make_shared<DummyStore>();
+  EvalState state({}, ref<Store>(store));
+  auto path = GetParam();
+
+  // There are multiple types of exceptions that the parser can throw,
+  // and the tests don't define which one they expect, so we need to
+  // allow all of these - but fail on other errors.
+  try {
+    state.parseExprFromFile(GetParam().string());
+    FAIL() << path.stem().string() << ": parsing should not succeed";
+  } catch (ParseError e) {
+    SUCCEED();
+  } catch (UndefinedVarError e) {
+    SUCCEED();
+  } catch (const std::exception& e) {
+    FAIL() << path.stem().string()
+           << ": unexpected parser exception: " << e.what();
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(Parser, ParserFailureTest,
+                         testing::ValuesIn(TestFilesFor("parse-fail-")),
+                         TestNameFor);
+
+class ParserSuccessTest : public testing::TestWithParam<std::filesystem::path> {
+};
+
+// Test pattern for files that should parse successfully.
+TEST_P(ParserSuccessTest, Parses) {
+  std::shared_ptr<Store> store = std::make_shared<DummyStore>();
+  EvalState state({}, ref<Store>(store));
+  auto path = GetParam();
+
+  EXPECT_NO_THROW(state.parseExprFromFile(GetParam().string()))
+      << path.stem().string() << ": parsing should succeed";
+
+  SUCCEED();
+}
+
+INSTANTIATE_TEST_SUITE_P(Parser, ParserSuccessTest,
+                         testing::ValuesIn(TestFilesFor("parse-okay-")),
+                         TestNameFor);
+
+class EvalFailureTest : public testing::TestWithParam<std::filesystem::path> {};
+
+// Test pattern for files that should fail to evaluate.
+TEST_P(EvalFailureTest, Fails) {
+  std::shared_ptr<Store> store = std::make_shared<DummyStore>();
+  EvalState state({}, ref<Store>(store));
+  auto path = GetParam();
+
+  Expr* expr = nullptr;
+  EXPECT_NO_THROW(expr = state.parseExprFromFile(GetParam().string()))
+      << path.stem().string() << ": should parse successfully";
+
+  // Again, there are multiple expected exception types and the tests
+  // don't specify which ones they are looking for.
+  try {
+    Value result;
+    state.eval(expr, result);
+    state.forceValue(result);
+    std::cout << result;
+    FAIL() << path.stem().string() << ": evaluating should not succeed";
+  } catch (AssertionError e) {
+    SUCCEED();
+  } catch (EvalError e) {
+    SUCCEED();
+  } catch (SysError e) {
+    SUCCEED();
+  } catch (ParseError /* sic! */ e) {
+    SUCCEED();
+  } catch (const std::exception& e) {
+    FAIL() << path.stem().string()
+           << ": unexpected evaluator exception: " << e.what();
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(Eval, EvalFailureTest,
+                         testing::ValuesIn(TestFilesFor("eval-fail-")),
+                         TestNameFor);
+
+class EvalSuccessTest : public testing::TestWithParam<std::filesystem::path> {};
+
+// Test pattern for files that should evaluate successfully.
+TEST_P(EvalSuccessTest, Succeeds) {
+  std::shared_ptr<Store> store = std::make_shared<DummyStore>();
+  EvalState state({}, ref<Store>(store));
+  auto path = GetParam();
+
+  Expr* expr = nullptr;
+  ASSERT_NO_THROW(expr = state.parseExprFromFile(GetParam().string()))
+      << path.stem().string() << ": should parse successfully";
+
+  Value result;
+
+  ASSERT_NO_THROW({
+    state.eval(expr, result);
+    state.forceValueDeep(result);
+  }) << path.stem().string()
+     << ": should evaluate successfully";
+
+  auto expected = ExpectedOutputFor(path.stem().string());
+  std::ostringstream value_str;
+  value_str << result;
+
+  EXPECT_EQ(expected, value_str.str()) << "evaluator output should match";
+}
+
+INSTANTIATE_TEST_SUITE_P(Eval, EvalSuccessTest,
+                         testing::ValuesIn(TestFilesFor("eval-okay-")),
+                         TestNameFor);
+
+class BlankStoreTest : public nix::StoreTest {
+  virtual void TestBody() override{};
+};
+
+class EvalStoreSuccessTest
+    : public testing::TestWithParam<std::filesystem::path> {
+ public:
+  virtual void TearDown() { store_test_.TearDown(); }
+
+  absl::StatusOr<std::unique_ptr<nix::LocalStore>> OpenTemporaryStore() {
+    return store_test_.OpenTemporaryStore();
+  }
+
+ private:
+  BlankStoreTest store_test_;
+};
+
+// Test pattern for files that should evaluate successfully but require a real
+// store.
+TEST_P(EvalStoreSuccessTest, Succeeds) {
+  absl::StatusOr<std::unique_ptr<nix::LocalStore>> store_ =
+      OpenTemporaryStore();
+  CHECK(store_.ok()) << "failed to open temporary store";
+  ref<Store> store = ref<Store>(store_->release());
+  EvalState state({}, store);
+  auto path = GetParam();
+
+  Expr* expr = nullptr;
+  ASSERT_NO_THROW(expr = state.parseExprFromFile(GetParam().string()))
+      << path.stem().string() << ": should parse successfully";
+
+  Value result;
+
+  ASSERT_NO_THROW({
+    state.eval(expr, result);
+    state.forceValueDeep(result);
+  }) << path.stem().string()
+     << ": should evaluate successfully";
+
+  auto expected = ExpectedOutputFor(path.stem().string());
+  std::ostringstream value_str;
+  value_str << result;
+
+  EXPECT_EQ(expected, value_str.str()) << "evaluator output should match";
+}
+
+INSTANTIATE_TEST_SUITE_P(Eval, EvalStoreSuccessTest,
+                         testing::ValuesIn(TestFilesFor("evalstore-okay-")),
+                         TestNameFor);
+
+}  // namespace nix::tests