aboutsummaryrefslogtreecommitdiff
path: root/src/libutil
diff options
context:
space:
mode:
Diffstat (limited to 'src/libutil')
-rw-r--r--src/libutil/config.cc95
-rw-r--r--src/libutil/config.hh70
-rw-r--r--src/libutil/error.cc72
-rw-r--r--src/libutil/error.hh24
-rw-r--r--src/libutil/logging.cc7
-rw-r--r--src/libutil/logging.hh33
-rw-r--r--src/libutil/lru-cache.hh1
-rw-r--r--src/libutil/tests/config.cc264
-rw-r--r--src/libutil/tests/hash.cc80
-rw-r--r--src/libutil/tests/json.cc193
-rw-r--r--src/libutil/tests/local.mk2
-rw-r--r--src/libutil/tests/logging.cc255
-rw-r--r--src/libutil/tests/lru-cache.cc130
-rw-r--r--src/libutil/tests/tests.cc6
-rw-r--r--src/libutil/tests/url.cc266
-rw-r--r--src/libutil/tests/xml-writer.cc105
-rw-r--r--src/libutil/url.cc1
-rw-r--r--src/libutil/url.hh6
18 files changed, 1507 insertions, 103 deletions
diff --git a/src/libutil/config.cc b/src/libutil/config.cc
index f03e444ec..8fc700a2b 100644
--- a/src/libutil/config.cc
+++ b/src/libutil/config.cc
@@ -65,60 +65,63 @@ void Config::getSettings(std::map<std::string, SettingInfo> & res, bool override
res.emplace(opt.first, SettingInfo{opt.second.setting->to_string(), opt.second.setting->description});
}
-void AbstractConfig::applyConfigFile(const Path & path)
-{
- try {
- string contents = readFile(path);
-
- unsigned int pos = 0;
-
- while (pos < contents.size()) {
- string line;
- while (pos < contents.size() && contents[pos] != '\n')
- line += contents[pos++];
- pos++;
-
- string::size_type hash = line.find('#');
- if (hash != string::npos)
- line = string(line, 0, hash);
-
- vector<string> tokens = tokenizeString<vector<string> >(line);
- if (tokens.empty()) continue;
+void AbstractConfig::applyConfig(const std::string & contents, const std::string & path) {
+ unsigned int pos = 0;
+
+ while (pos < contents.size()) {
+ string line;
+ while (pos < contents.size() && contents[pos] != '\n')
+ line += contents[pos++];
+ pos++;
+
+ string::size_type hash = line.find('#');
+ if (hash != string::npos)
+ line = string(line, 0, hash);
+
+ vector<string> tokens = tokenizeString<vector<string> >(line);
+ if (tokens.empty()) continue;
+
+ if (tokens.size() < 2)
+ throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
+
+ auto include = false;
+ auto ignoreMissing = false;
+ if (tokens[0] == "include")
+ include = true;
+ else if (tokens[0] == "!include") {
+ include = true;
+ ignoreMissing = true;
+ }
- if (tokens.size() < 2)
+ if (include) {
+ if (tokens.size() != 2)
throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
-
- auto include = false;
- auto ignoreMissing = false;
- if (tokens[0] == "include")
- include = true;
- else if (tokens[0] == "!include") {
- include = true;
- ignoreMissing = true;
+ auto p = absPath(tokens[1], dirOf(path));
+ if (pathExists(p)) {
+ applyConfigFile(p);
+ } else if (!ignoreMissing) {
+ throw Error("file '%1%' included from '%2%' not found", p, path);
}
+ continue;
+ }
- if (include) {
- if (tokens.size() != 2)
- throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
- auto p = absPath(tokens[1], dirOf(path));
- if (pathExists(p)) {
- applyConfigFile(p);
- } else if (!ignoreMissing) {
- throw Error("file '%1%' included from '%2%' not found", p, path);
- }
- continue;
- }
+ if (tokens[1] != "=")
+ throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
- if (tokens[1] != "=")
- throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
+ string name = tokens[0];
- string name = tokens[0];
+ vector<string>::iterator i = tokens.begin();
+ advance(i, 2);
- vector<string>::iterator i = tokens.begin();
- advance(i, 2);
+ set(name, concatStringsSep(" ", Strings(i, tokens.end()))); // FIXME: slow
+ };
+}
- set(name, concatStringsSep(" ", Strings(i, tokens.end()))); // FIXME: slow
- };
+void AbstractConfig::applyConfigFile(const Path & path)
+{
+ try {
+ string contents = readFile(path);
+ applyConfig(contents, path);
} catch (SysError &) { }
}
diff --git a/src/libutil/config.hh b/src/libutil/config.hh
index 7ea78fdaf..5c7a70a2e 100644
--- a/src/libutil/config.hh
+++ b/src/libutil/config.hh
@@ -7,6 +7,38 @@
namespace nix {
+/**
+ * The Config class provides Nix runtime configurations.
+ *
+ * What is a Configuration?
+ * A collection of uniquely named Settings.
+ *
+ * What is a Setting?
+ * Each property that you can set in a configuration corresponds to a
+ * `Setting`. A setting records value and description of a property
+ * with a default and optional aliases.
+ *
+ * A valid configuration consists of settings that are registered to a
+ * `Config` object instance:
+ *
+ * Config config;
+ * Setting<std::string> systemSetting{&config, "x86_64-linux", "system", "the current system"};
+ *
+ * The above creates a `Config` object and registers a setting called "system"
+ * via the variable `systemSetting` with it. The setting defaults to the string
+ * "x86_64-linux", it's description is "the current system". All of the
+ * registered settings can then be accessed as shown below:
+ *
+ * std::map<std::string, Config::SettingInfo> settings;
+ * config.getSettings(settings);
+ * config["system"].description == "the current system"
+ * config["system"].value == "x86_64-linux"
+ *
+ *
+ * The above retrieves all currently known settings from the `Config` object
+ * and adds them to the `settings` map.
+ */
+
class Args;
class AbstractSetting;
class JSONPlaceholder;
@@ -23,6 +55,10 @@ protected:
public:
+ /**
+ * Sets the value referenced by `name` to `value`. Returns true if the
+ * setting is known, false otherwise.
+ */
virtual bool set(const std::string & name, const std::string & value) = 0;
struct SettingInfo
@@ -31,18 +67,52 @@ public:
std::string description;
};
+ /**
+ * Adds the currently known settings to the given result map `res`.
+ * - res: map to store settings in
+ * - overridenOnly: when set to true only overridden settings will be added to `res`
+ */
virtual void getSettings(std::map<std::string, SettingInfo> & res, bool overridenOnly = false) = 0;
+ /**
+ * Parses the configuration in `contents` and applies it
+ * - contents: configuration contents to be parsed and applied
+ * - path: location of the configuration file
+ */
+ void applyConfig(const std::string & contents, const std::string & path = "<unknown>");
+
+ /**
+ * Applies a nix configuration file
+ * - path: the location of the config file to apply
+ */
void applyConfigFile(const Path & path);
+ /**
+ * Resets the `overridden` flag of all Settings
+ */
virtual void resetOverriden() = 0;
+ /**
+ * Outputs all settings to JSON
+ * - out: JSONObject to write the configuration to
+ */
virtual void toJSON(JSONObject & out) = 0;
+ /**
+ * Converts settings to `Args` to be used on the command line interface
+ * - args: args to write to
+ * - category: category of the settings
+ */
virtual void convertToArgs(Args & args, const std::string & category) = 0;
+ /**
+ * Logs a warning for each unregistered setting
+ */
void warnUnknownSettings();
+ /**
+ * Re-applies all previously attempted changes to unknown settings
+ */
void reapplyUnknownSettings();
};
diff --git a/src/libutil/error.cc b/src/libutil/error.cc
index 585059f22..a713fe9db 100644
--- a/src/libutil/error.cc
+++ b/src/libutil/error.cc
@@ -10,13 +10,16 @@ namespace nix {
const std::string nativeSystem = SYSTEM;
-
+// addPrefix is used for show-trace. Strings added with addPrefix
+// will print ahead of the error itself.
BaseError & BaseError::addPrefix(const FormatOrString & fs)
{
prefix_ = fs.s + prefix_;
return *this;
}
+// c++ std::exception descendants must have a 'const char* what()' function.
+// This stringifies the error and caches it for use by what(), or similarly by msg().
const string& BaseError::calcWhat() const
{
if (what_.has_value())
@@ -124,25 +127,25 @@ void getCodeLines(NixCode &nixCode)
}
}
-
+// if nixCode contains lines of code, print them to the ostream, indicating the error column.
void printCodeLines(std::ostream &out, const string &prefix, const NixCode &nixCode)
{
// previous line of code.
if (nixCode.prevLineOfCode.has_value()) {
- out << fmt("%1% %|2$5d|| %3%",
- prefix,
- (nixCode.errPos.line - 1),
- *nixCode.prevLineOfCode)
- << std::endl;
+ out << std::endl
+ << fmt("%1% %|2$5d|| %3%",
+ prefix,
+ (nixCode.errPos.line - 1),
+ *nixCode.prevLineOfCode);
}
if (nixCode.errLineOfCode.has_value()) {
// line of code containing the error.
- out << fmt("%1% %|2$5d|| %3%",
- prefix,
- (nixCode.errPos.line),
- *nixCode.errLineOfCode)
- << std::endl;
+ out << std::endl
+ << fmt("%1% %|2$5d|| %3%",
+ prefix,
+ (nixCode.errPos.line),
+ *nixCode.errLineOfCode);
// error arrows for the column range.
if (nixCode.errPos.column > 0) {
int start = nixCode.errPos.column;
@@ -153,20 +156,21 @@ void printCodeLines(std::ostream &out, const string &prefix, const NixCode &nixC
std::string arrows("^");
- out << fmt("%1% |%2%" ANSI_RED "%3%" ANSI_NORMAL,
- prefix,
- spaces,
- arrows) << std::endl;
+ out << std::endl
+ << fmt("%1% |%2%" ANSI_RED "%3%" ANSI_NORMAL,
+ prefix,
+ spaces,
+ arrows);
}
}
// next line of code.
if (nixCode.nextLineOfCode.has_value()) {
- out << fmt("%1% %|2$5d|| %3%",
- prefix,
- (nixCode.errPos.line + 1),
- *nixCode.nextLineOfCode)
- << std::endl;
+ out << std::endl
+ << fmt("%1% %|2$5d|| %3%",
+ prefix,
+ (nixCode.errPos.line + 1),
+ *nixCode.nextLineOfCode);
}
}
@@ -239,17 +243,15 @@ std::ostream& operator<<(std::ostream &out, const ErrorInfo &einfo)
levelString,
einfo.name,
dashes,
- einfo.programName.value_or(""))
- << std::endl;
+ einfo.programName.value_or(""));
else
out << fmt("%1%%2%" ANSI_BLUE " -----%3% %4%" ANSI_NORMAL,
prefix,
levelString,
dashes,
- einfo.programName.value_or(""))
- << std::endl;
+ einfo.programName.value_or(""));
- // filename, line, column.
+ bool nl = false; // intersperse newline between sections.
if (einfo.nixCode.has_value()) {
switch (einfo.nixCode->errPos.origin) {
case foFile: {
@@ -273,30 +275,36 @@ std::ostream& operator<<(std::ostream &out, const ErrorInfo &einfo)
default:
throw Error("invalid FileOrigin in errPos");
}
+ nl = true;
}
// description
if (einfo.description != "") {
- out << prefix << einfo.description << std::endl;
- out << prefix << std::endl;
+ if (nl)
+ out << std::endl << prefix;
+ out << std::endl << prefix << einfo.description;
+ nl = true;
}
-
if (einfo.nixCode.has_value()) {
NixCode nixcode = *einfo.nixCode;
getCodeLines(nixcode);
// lines of code.
if (nixcode.errLineOfCode.has_value()) {
+ if (nl)
+ out << std::endl << prefix;
printCodeLines(out, prefix, nixcode);
- out << prefix << std::endl;
+ nl = true;
}
}
// hint
if (einfo.hint.has_value()) {
- out << prefix << *einfo.hint << std::endl;
- out << prefix << std::endl;
+ if (nl)
+ out << std::endl << prefix;
+ out << std::endl << prefix << *einfo.hint;
+ nl = true;
}
return out;
diff --git a/src/libutil/error.hh b/src/libutil/error.hh
index d60c0fefe..bd17a47c1 100644
--- a/src/libutil/error.hh
+++ b/src/libutil/error.hh
@@ -22,6 +22,23 @@
namespace nix {
+/*
+
+This file defines two main structs/classes used in nix error handling.
+
+ErrorInfo provides a standard payload of error information, with conversion to string
+happening in the logger rather than at the call site.
+
+BaseError is the ancestor of nix specific exceptions (and Interrupted), and contains
+an ErrorInfo.
+
+ErrorInfo structs are sent to the logger as part of an exception, or directly with the
+logError or logWarning macros.
+
+See the error-demo.cc program for usage examples.
+
+*/
+
typedef enum {
lvlError = 0,
lvlWarn,
@@ -38,7 +55,7 @@ typedef enum {
foString
} FileOrigin;
-
+// ErrPos indicates the location of an error in a nix file.
struct ErrPos {
int line = 0;
int column = 0;
@@ -50,6 +67,7 @@ struct ErrPos {
return line != 0;
}
+ // convert from the Pos struct, found in libexpr.
template <class P>
ErrPos& operator=(const P &pos)
{
@@ -74,8 +92,6 @@ struct NixCode {
std::optional<string> nextLineOfCode;
};
-// -------------------------------------------------
-// ErrorInfo.
struct ErrorInfo {
Verbosity level;
string name;
@@ -164,7 +180,7 @@ public:
{
errNo = errno;
auto hf = hintfmt(args...);
- err.hint = hintfmt("%1% : %2%", normaltxt(hf.str()), strerror(errNo));
+ err.hint = hintfmt("%1%: %2%", normaltxt(hf.str()), strerror(errNo));
}
virtual const char* sname() const override { return "SysError"; }
diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc
index 071a7ec7e..41378b0db 100644
--- a/src/libutil/logging.cc
+++ b/src/libutil/logging.cc
@@ -251,10 +251,9 @@ bool handleJSONLogMessage(const std::string & msg,
}
} catch (std::exception & e) {
- logError(
- ErrorInfo {
- .name = "Json log message",
- .hint = hintfmt("bad log message from builder: %s", e.what())
+ logError({
+ .name = "Json log message",
+ .hint = hintfmt("bad log message from builder: %s", e.what())
});
}
diff --git a/src/libutil/logging.hh b/src/libutil/logging.hh
index 39692a291..eeb7233e9 100644
--- a/src/libutil/logging.hh
+++ b/src/libutil/logging.hh
@@ -150,9 +150,23 @@ bool handleJSONLogMessage(const std::string & msg,
extern Verbosity verbosity; /* suppress msgs > this */
-/* Print a message if the current log level is at least the specified
- level. Note that this has to be implemented as a macro to ensure
- that the arguments are evaluated lazily. */
+/* Print a message with the standard ErrorInfo format.
+ In general, use these 'log' macros for reporting problems that may require user
+ intervention or that need more explanation. Use the 'print' macros for more
+ lightweight status messages. */
+#define logErrorInfo(level, errorInfo...) \
+ do { \
+ if (level <= nix::verbosity) { \
+ logger->logEI(level, errorInfo); \
+ } \
+ } while (0)
+
+#define logError(errorInfo...) logErrorInfo(lvlError, errorInfo)
+#define logWarning(errorInfo...) logErrorInfo(lvlWarn, errorInfo)
+
+/* Print a string message if the current log level is at least the specified
+ level. Note that this has to be implemented as a macro to ensure that the
+ arguments are evaluated lazily. */
#define printMsg(level, args...) \
do { \
if (level <= nix::verbosity) { \
@@ -166,18 +180,7 @@ extern Verbosity verbosity; /* suppress msgs > this */
#define debug(args...) printMsg(lvlDebug, args)
#define vomit(args...) printMsg(lvlVomit, args)
-#define logErrorInfo(level, errorInfo...) \
- do { \
- if (level <= nix::verbosity) { \
- logger->logEI(level, errorInfo); \
- } \
- } while (0)
-
-#define logError(errorInfo...) logErrorInfo(lvlError, errorInfo)
-#define logWarning(errorInfo...) logErrorInfo(lvlWarn, errorInfo)
-
-
-
+/* if verbosity >= lvlWarn, print a message with a yellow 'warning:' prefix. */
template<typename... Args>
inline void warn(const std::string & fs, const Args & ... args)
{
diff --git a/src/libutil/lru-cache.hh b/src/libutil/lru-cache.hh
index 8b83f842c..6ef4a3e06 100644
--- a/src/libutil/lru-cache.hh
+++ b/src/libutil/lru-cache.hh
@@ -1,5 +1,6 @@
#pragma once
+#include <cassert>
#include <map>
#include <list>
#include <optional>
diff --git a/src/libutil/tests/config.cc b/src/libutil/tests/config.cc
new file mode 100644
index 000000000..74c59fd31
--- /dev/null
+++ b/src/libutil/tests/config.cc
@@ -0,0 +1,264 @@
+#include "json.hh"
+#include "config.hh"
+#include "args.hh"
+
+#include <sstream>
+#include <gtest/gtest.h>
+
+namespace nix {
+
+ /* ----------------------------------------------------------------------------
+ * Config
+ * --------------------------------------------------------------------------*/
+
+ TEST(Config, setUndefinedSetting) {
+ Config config;
+ ASSERT_EQ(config.set("undefined-key", "value"), false);
+ }
+
+ TEST(Config, setDefinedSetting) {
+ Config config;
+ std::string value;
+ Setting<std::string> foo{&config, value, "name-of-the-setting", "description"};
+ ASSERT_EQ(config.set("name-of-the-setting", "value"), true);
+ }
+
+ TEST(Config, getDefinedSetting) {
+ Config config;
+ std::string value;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> foo{&config, value, "name-of-the-setting", "description"};
+
+ config.getSettings(settings, /* overridenOnly = */ false);
+ const auto iter = settings.find("name-of-the-setting");
+ ASSERT_NE(iter, settings.end());
+ ASSERT_EQ(iter->second.value, "");
+ ASSERT_EQ(iter->second.description, "description");
+ }
+
+ TEST(Config, getDefinedOverridenSettingNotSet) {
+ Config config;
+ std::string value;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> foo{&config, value, "name-of-the-setting", "description"};
+
+ config.getSettings(settings, /* overridenOnly = */ true);
+ const auto e = settings.find("name-of-the-setting");
+ ASSERT_EQ(e, settings.end());
+ }
+
+ TEST(Config, getDefinedSettingSet1) {
+ Config config;
+ std::string value;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> setting{&config, value, "name-of-the-setting", "description"};
+
+ setting.assign("value");
+
+ config.getSettings(settings, /* overridenOnly = */ false);
+ const auto iter = settings.find("name-of-the-setting");
+ ASSERT_NE(iter, settings.end());
+ ASSERT_EQ(iter->second.value, "value");
+ ASSERT_EQ(iter->second.description, "description");
+ }
+
+ TEST(Config, getDefinedSettingSet2) {
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> setting{&config, "", "name-of-the-setting", "description"};
+
+ ASSERT_TRUE(config.set("name-of-the-setting", "value"));
+
+ config.getSettings(settings, /* overridenOnly = */ false);
+ const auto e = settings.find("name-of-the-setting");
+ ASSERT_NE(e, settings.end());
+ ASSERT_EQ(e->second.value, "value");
+ ASSERT_EQ(e->second.description, "description");
+ }
+
+ TEST(Config, addSetting) {
+ class TestSetting : public AbstractSetting {
+ public:
+ TestSetting() : AbstractSetting("test", "test", {}) {}
+ void set(const std::string & value) {}
+ std::string to_string() const { return {}; }
+ };
+
+ Config config;
+ TestSetting setting;
+
+ ASSERT_FALSE(config.set("test", "value"));
+ config.addSetting(&setting);
+ ASSERT_TRUE(config.set("test", "value"));
+ }
+
+ TEST(Config, withInitialValue) {
+ const StringMap initials = {
+ { "key", "value" },
+ };
+ Config config(initials);
+
+ {
+ std::map<std::string, Config::SettingInfo> settings;
+ config.getSettings(settings, /* overridenOnly = */ false);
+ ASSERT_EQ(settings.find("key"), settings.end());
+ }
+
+ Setting<std::string> setting{&config, "default-value", "key", "description"};
+
+ {
+ std::map<std::string, Config::SettingInfo> settings;
+ config.getSettings(settings, /* overridenOnly = */ false);
+ ASSERT_EQ(settings["key"].value, "value");
+ }
+ }
+
+ TEST(Config, resetOverriden) {
+ Config config;
+ config.resetOverriden();
+ }
+
+ TEST(Config, resetOverridenWithSetting) {
+ Config config;
+ Setting<std::string> setting{&config, "", "name-of-the-setting", "description"};
+
+ {
+ std::map<std::string, Config::SettingInfo> settings;
+
+ setting.set("foo");
+ ASSERT_EQ(setting.get(), "foo");
+ config.getSettings(settings, /* overridenOnly = */ true);
+ ASSERT_TRUE(settings.empty());
+ }
+
+ {
+ std::map<std::string, Config::SettingInfo> settings;
+
+ setting.override("bar");
+ ASSERT_TRUE(setting.overriden);
+ ASSERT_EQ(setting.get(), "bar");
+ config.getSettings(settings, /* overridenOnly = */ true);
+ ASSERT_FALSE(settings.empty());
+ }
+
+ {
+ std::map<std::string, Config::SettingInfo> settings;
+
+ config.resetOverriden();
+ ASSERT_FALSE(setting.overriden);
+ config.getSettings(settings, /* overridenOnly = */ true);
+ ASSERT_TRUE(settings.empty());
+ }
+ }
+
+ TEST(Config, toJSONOnEmptyConfig) {
+ std::stringstream out;
+ { // Scoped to force the destructor of JSONObject to write the final `}`
+ JSONObject obj(out);
+ Config config;
+ config.toJSON(obj);
+ }
+
+ ASSERT_EQ(out.str(), "{}");
+ }
+
+ TEST(Config, toJSONOnNonEmptyConfig) {
+ std::stringstream out;
+ { // Scoped to force the destructor of JSONObject to write the final `}`
+ JSONObject obj(out);
+
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> setting{&config, "", "name-of-the-setting", "description"};
+ setting.assign("value");
+
+ config.toJSON(obj);
+ }
+ ASSERT_EQ(out.str(), R"#({"name-of-the-setting":{"description":"description","value":"value"}})#");
+ }
+
+ TEST(Config, setSettingAlias) {
+ Config config;
+ Setting<std::string> setting{&config, "", "some-int", "best number", { "another-int" }};
+ ASSERT_TRUE(config.set("some-int", "1"));
+ ASSERT_EQ(setting.get(), "1");
+ ASSERT_TRUE(config.set("another-int", "2"));
+ ASSERT_EQ(setting.get(), "2");
+ ASSERT_TRUE(config.set("some-int", "3"));
+ ASSERT_EQ(setting.get(), "3");
+ }
+
+ /* FIXME: The reapplyUnknownSettings method doesn't seem to do anything
+ * useful (these days). Whenever we add a new setting to Config the
+ * unknown settings are always considered. In which case is this function
+ * actually useful? Is there some way to register a Setting without calling
+ * addSetting? */
+ TEST(Config, DISABLED_reapplyUnknownSettings) {
+ Config config;
+ ASSERT_FALSE(config.set("name-of-the-setting", "unknownvalue"));
+ Setting<std::string> setting{&config, "default", "name-of-the-setting", "description"};
+ ASSERT_EQ(setting.get(), "default");
+ config.reapplyUnknownSettings();
+ ASSERT_EQ(setting.get(), "unknownvalue");
+ }
+
+ TEST(Config, applyConfigEmpty) {
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ config.applyConfig("");
+ config.getSettings(settings);
+ ASSERT_TRUE(settings.empty());
+ }
+
+ TEST(Config, applyConfigEmptyWithComment) {
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ config.applyConfig("# just a comment");
+ config.getSettings(settings);
+ ASSERT_TRUE(settings.empty());
+ }
+
+ TEST(Config, applyConfigAssignment) {
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> setting{&config, "", "name-of-the-setting", "description"};
+ config.applyConfig(
+ "name-of-the-setting = value-from-file #useful comment\n"
+ "# name-of-the-setting = foo\n"
+ );
+ config.getSettings(settings);
+ ASSERT_FALSE(settings.empty());
+ ASSERT_EQ(settings["name-of-the-setting"].value, "value-from-file");
+ }
+
+ TEST(Config, applyConfigWithReassignedSetting) {
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> setting{&config, "", "name-of-the-setting", "description"};
+ config.applyConfig(
+ "name-of-the-setting = first-value\n"
+ "name-of-the-setting = second-value\n"
+ );
+ config.getSettings(settings);
+ ASSERT_FALSE(settings.empty());
+ ASSERT_EQ(settings["name-of-the-setting"].value, "second-value");
+ }
+
+ TEST(Config, applyConfigFailsOnMissingIncludes) {
+ Config config;
+ std::map<std::string, Config::SettingInfo> settings;
+ Setting<std::string> setting{&config, "", "name-of-the-setting", "description"};
+
+ ASSERT_THROW(config.applyConfig(
+ "name-of-the-setting = value-from-file\n"
+ "# name-of-the-setting = foo\n"
+ "include /nix/store/does/not/exist.nix"
+ ), Error);
+ }
+
+ TEST(Config, applyConfigInvalidThrows) {
+ Config config;
+ ASSERT_THROW(config.applyConfig("value == key"), UsageError);
+ ASSERT_THROW(config.applyConfig("value "), UsageError);
+ }
+}
diff --git a/src/libutil/tests/hash.cc b/src/libutil/tests/hash.cc
new file mode 100644
index 000000000..7cb439817
--- /dev/null
+++ b/src/libutil/tests/hash.cc
@@ -0,0 +1,80 @@
+#include "hash.hh"
+#include <gtest/gtest.h>
+
+namespace nix {
+
+ /* ----------------------------------------------------------------------------
+ * hashString
+ * --------------------------------------------------------------------------*/
+
+ TEST(hashString, testKnownMD5Hashes1) {
+ // values taken from: https://tools.ietf.org/html/rfc1321
+ auto s1 = "";
+ auto hash = hashString(HashType::htMD5, s1);
+ ASSERT_EQ(hash.to_string(Base::Base16), "md5:d41d8cd98f00b204e9800998ecf8427e");
+ }
+
+ TEST(hashString, testKnownMD5Hashes2) {
+ // values taken from: https://tools.ietf.org/html/rfc1321
+ auto s2 = "abc";
+ auto hash = hashString(HashType::htMD5, s2);
+ ASSERT_EQ(hash.to_string(Base::Base16), "md5:900150983cd24fb0d6963f7d28e17f72");
+ }
+
+ TEST(hashString, testKnownSHA1Hashes1) {
+ // values taken from: https://tools.ietf.org/html/rfc3174
+ auto s = "abc";
+ auto hash = hashString(HashType::htSHA1, s);
+ ASSERT_EQ(hash.to_string(Base::Base16),"sha1:a9993e364706816aba3e25717850c26c9cd0d89d");
+ }
+
+ TEST(hashString, testKnownSHA1Hashes2) {
+ // values taken from: https://tools.ietf.org/html/rfc3174
+ auto s = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
+ auto hash = hashString(HashType::htSHA1, s);
+ ASSERT_EQ(hash.to_string(Base::Base16),"sha1:84983e441c3bd26ebaae4aa1f95129e5e54670f1");
+ }
+
+ TEST(hashString, testKnownSHA256Hashes1) {
+ // values taken from: https://tools.ietf.org/html/rfc4634
+ auto s = "abc";
+
+ auto hash = hashString(HashType::htSHA256, s);
+ ASSERT_EQ(hash.to_string(Base::Base16),
+ "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
+ }
+
+ TEST(hashString, testKnownSHA256Hashes2) {
+ // values taken from: https://tools.ietf.org/html/rfc4634
+ auto s = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
+ auto hash = hashString(HashType::htSHA256, s);
+ ASSERT_EQ(hash.to_string(Base::Base16),
+ "sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
+ }
+
+ TEST(hashString, testKnownSHA512Hashes1) {
+ // values taken from: https://tools.ietf.org/html/rfc4634
+ auto s = "abc";
+ auto hash = hashString(HashType::htSHA512, s);
+ ASSERT_EQ(hash.to_string(Base::Base16),
+ "sha512:ddaf35a193617abacc417349ae20413112e6fa4e89a9"
+ "7ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd"
+ "454d4423643ce80e2a9ac94fa54ca49f");
+ }
+
+ TEST(hashString, testKnownSHA512Hashes2) {
+ // values taken from: https://tools.ietf.org/html/rfc4634
+ auto s = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu";
+
+ auto hash = hashString(HashType::htSHA512, s);
+ ASSERT_EQ(hash.to_string(Base::Base16),
+ "sha512:8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa1"
+ "7299aeadb6889018501d289e4900f7e4331b99dec4b5433a"
+ "c7d329eeb6dd26545e96e55b874be909");
+ }
+
+ TEST(hashString, hashingWithUnknownAlgoExits) {
+ auto s = "unknown";
+ ASSERT_DEATH(hashString(HashType::htUnknown, s), "");
+ }
+}
diff --git a/src/libutil/tests/json.cc b/src/libutil/tests/json.cc
new file mode 100644
index 000000000..dea73f53a
--- /dev/null
+++ b/src/libutil/tests/json.cc
@@ -0,0 +1,193 @@
+#include "json.hh"
+#include <gtest/gtest.h>
+#include <sstream>
+
+namespace nix {
+
+ /* ----------------------------------------------------------------------------
+ * toJSON
+ * --------------------------------------------------------------------------*/
+
+ TEST(toJSON, quotesCharPtr) {
+ const char* input = "test";
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "\"test\"");
+ }
+
+ TEST(toJSON, quotesStdString) {
+ std::string input = "test";
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "\"test\"");
+ }
+
+ TEST(toJSON, convertsNullptrtoNull) {
+ auto input = nullptr;
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "null");
+ }
+
+ TEST(toJSON, convertsNullToNull) {
+ const char* input = 0;
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "null");
+ }
+
+
+ TEST(toJSON, convertsFloat) {
+ auto input = 1.024f;
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "1.024");
+ }
+
+ TEST(toJSON, convertsDouble) {
+ const double input = 1.024;
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "1.024");
+ }
+
+ TEST(toJSON, convertsBool) {
+ auto input = false;
+ std::stringstream out;
+ toJSON(out, input);
+
+ ASSERT_EQ(out.str(), "false");
+ }
+
+ TEST(toJSON, quotesTab) {
+ std::stringstream out;
+ toJSON(out, "\t");
+
+ ASSERT_EQ(out.str(), "\"\\t\"");
+ }
+
+ TEST(toJSON, quotesNewline) {
+ std::stringstream out;
+ toJSON(out, "\n");
+
+ ASSERT_EQ(out.str(), "\"\\n\"");
+ }
+
+ TEST(toJSON, quotesCreturn) {
+ std::stringstream out;
+ toJSON(out, "\r");
+
+ ASSERT_EQ(out.str(), "\"\\r\"");
+ }
+
+ TEST(toJSON, quotesCreturnNewLine) {
+ std::stringstream out;
+ toJSON(out, "\r\n");
+
+ ASSERT_EQ(out.str(), "\"\\r\\n\"");
+ }
+
+ TEST(toJSON, quotesDoublequotes) {
+ std::stringstream out;
+ toJSON(out, "\"");
+
+ ASSERT_EQ(out.str(), "\"\\\"\"");
+ }
+
+ TEST(toJSON, substringEscape) {
+ std::stringstream out;
+ const char *s = "foo\t";
+ toJSON(out, s+3, s + strlen(s));
+
+ ASSERT_EQ(out.str(), "\"\\t\"");
+ }
+
+ /* ----------------------------------------------------------------------------
+ * JSONObject
+ * --------------------------------------------------------------------------*/
+
+ TEST(JSONObject, emptyObject) {
+ std::stringstream out;
+ {
+ JSONObject t(out);
+ }
+ ASSERT_EQ(out.str(), "{}");
+ }
+
+ TEST(JSONObject, objectWithList) {
+ std::stringstream out;
+ {
+ JSONObject t(out);
+ auto l = t.list("list");
+ l.elem("element");
+ }
+ ASSERT_EQ(out.str(), R"#({"list":["element"]})#");
+ }
+
+ TEST(JSONObject, objectWithListIndent) {
+ std::stringstream out;
+ {
+ JSONObject t(out, true);
+ auto l = t.list("list");
+ l.elem("element");
+ }
+ ASSERT_EQ(out.str(),
+R"#({
+ "list": [
+ "element"
+ ]
+})#");
+ }
+
+ TEST(JSONObject, objectWithPlaceholderAndList) {
+ std::stringstream out;
+ {
+ JSONObject t(out);
+ auto l = t.placeholder("list");
+ l.list().elem("element");
+ }
+
+ ASSERT_EQ(out.str(), R"#({"list":["element"]})#");
+ }
+
+ TEST(JSONObject, objectWithPlaceholderAndObject) {
+ std::stringstream out;
+ {
+ JSONObject t(out);
+ auto l = t.placeholder("object");
+ l.object().attr("key", "value");
+ }
+
+ ASSERT_EQ(out.str(), R"#({"object":{"key":"value"}})#");
+ }
+
+ /* ----------------------------------------------------------------------------
+ * JSONList
+ * --------------------------------------------------------------------------*/
+
+ TEST(JSONList, empty) {
+ std::stringstream out;
+ {
+ JSONList l(out);
+ }
+ ASSERT_EQ(out.str(), R"#([])#");
+ }
+
+ TEST(JSONList, withElements) {
+ std::stringstream out;
+ {
+ JSONList l(out);
+ l.elem("one");
+ l.object();
+ l.placeholder().write("three");
+ }
+ ASSERT_EQ(out.str(), R"#(["one",{},"three"])#");
+ }
+}
+
diff --git a/src/libutil/tests/local.mk b/src/libutil/tests/local.mk
index a297edb64..815e18560 100644
--- a/src/libutil/tests/local.mk
+++ b/src/libutil/tests/local.mk
@@ -8,7 +8,7 @@ libutil-tests_INSTALL_DIR :=
libutil-tests_SOURCES := $(wildcard $(d)/*.cc)
-libutil-tests_CXXFLAGS += -I src/libutil
+libutil-tests_CXXFLAGS += -I src/libutil -I src/libexpr
libutil-tests_LIBS = libutil
diff --git a/src/libutil/tests/logging.cc b/src/libutil/tests/logging.cc
new file mode 100644
index 000000000..fbdc91253
--- /dev/null
+++ b/src/libutil/tests/logging.cc
@@ -0,0 +1,255 @@
+#include "logging.hh"
+#include "nixexpr.hh"
+#include "util.hh"
+
+#include <gtest/gtest.h>
+
+namespace nix {
+
+ /* ----------------------------------------------------------------------------
+ * logEI
+ * --------------------------------------------------------------------------*/
+
+ TEST(logEI, catpuresBasicProperties) {
+
+ MakeError(TestError, Error);
+ ErrorInfo::programName = std::optional("error-unit-test");
+
+ try {
+ throw TestError("an error for testing purposes");
+ } catch (Error &e) {
+ testing::internal::CaptureStderr();
+ logger->logEI(e.info());
+ auto str = testing::internal::GetCapturedStderr();
+
+ ASSERT_STREQ(str.c_str(),"\x1B[31;1merror:\x1B[0m\x1B[34;1m --- TestError ------------------------------------ error-unit-test\x1B[0m\nan error for testing purposes\n");
+ }
+ }
+
+ TEST(logEI, appendingHintsToPreviousError) {
+
+ MakeError(TestError, Error);
+ ErrorInfo::programName = std::optional("error-unit-test");
+
+ try {
+ auto e = Error("initial error");
+ throw TestError(e.info());
+ } catch (Error &e) {
+ ErrorInfo ei = e.info();
+ ei.hint = hintfmt("%s; subsequent error message.", normaltxt(e.info().hint ? e.info().hint->str() : ""));
+
+ testing::internal::CaptureStderr();
+ logger->logEI(ei);
+ auto str = testing::internal::GetCapturedStderr();
+
+ ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- TestError ------------------------------------ error-unit-test\x1B[0m\n\x1B[33;1m\x1B[0minitial error\x1B[0m; subsequent error message.\n");
+ }
+
+ }
+
+ TEST(logEI, picksUpSysErrorExitCode) {
+
+ MakeError(TestError, Error);
+ ErrorInfo::programName = std::optional("error-unit-test");
+
+ try {
+ auto x = readFile(-1);
+ }
+ catch (SysError &e) {
+ testing::internal::CaptureStderr();
+ logError(e.info());
+ auto str = testing::internal::GetCapturedStderr();
+
+ ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- SysError ------------------------------------- error-unit-test\x1B[0m\n\x1B[33;1m\x1B[0mstatting file\x1B[0m: \x1B[33;1mBad file descriptor\x1B[0m\n");
+
+ }
+ }
+
+ TEST(logEI, loggingErrorOnInfoLevel) {
+ testing::internal::CaptureStderr();
+
+ logger->logEI({ .level = lvlInfo,
+ .name = "Info name",
+ .description = "Info description",
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[32;1minfo:\x1B[0m\x1B[34;1m --- Info name ------------------------------------- error-unit-test\x1B[0m\nInfo description\n");
+ }
+
+ TEST(logEI, loggingErrorOnTalkativeLevel) {
+ verbosity = lvlTalkative;
+
+ testing::internal::CaptureStderr();
+
+ logger->logEI({ .level = lvlTalkative,
+ .name = "Talkative name",
+ .description = "Talkative description",
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[32;1mtalk:\x1B[0m\x1B[34;1m --- Talkative name -------------------------------- error-unit-test\x1B[0m\nTalkative description\n");
+ }
+
+ TEST(logEI, loggingErrorOnChattyLevel) {
+ verbosity = lvlChatty;
+
+ testing::internal::CaptureStderr();
+
+ logger->logEI({ .level = lvlChatty,
+ .name = "Chatty name",
+ .description = "Talkative description",
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[32;1mchat:\x1B[0m\x1B[34;1m --- Chatty name ----------------------------------- error-unit-test\x1B[0m\nTalkative description\n");
+ }
+
+ TEST(logEI, loggingErrorOnDebugLevel) {
+ verbosity = lvlDebug;
+
+ testing::internal::CaptureStderr();
+
+ logger->logEI({ .level = lvlDebug,
+ .name = "Debug name",
+ .description = "Debug description",
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[33;1mdebug:\x1B[0m\x1B[34;1m --- Debug name ----------------------------------- error-unit-test\x1B[0m\nDebug description\n");
+ }
+
+ TEST(logEI, loggingErrorOnVomitLevel) {
+ verbosity = lvlVomit;
+
+ testing::internal::CaptureStderr();
+
+ logger->logEI({ .level = lvlVomit,
+ .name = "Vomit name",
+ .description = "Vomit description",
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[32;1mvomit:\x1B[0m\x1B[34;1m --- Vomit name ----------------------------------- error-unit-test\x1B[0m\nVomit description\n");
+ }
+
+ /* ----------------------------------------------------------------------------
+ * logError
+ * --------------------------------------------------------------------------*/
+
+
+ TEST(logError, logErrorWithoutHintOrCode) {
+ testing::internal::CaptureStderr();
+
+ logError({
+ .name = "name",
+ .description = "error description",
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- name ----------------------------------------- error-unit-test\x1B[0m\nerror description\n");
+ }
+
+ TEST(logError, logErrorWithPreviousAndNextLinesOfCode) {
+ SymbolTable testTable;
+ auto problem_file = testTable.create("myfile.nix");
+
+ testing::internal::CaptureStderr();
+
+ logError({
+ .name = "error name",
+ .description = "error with code lines",
+ .hint = hintfmt("this hint has %1% templated %2%!!",
+ "yellow",
+ "values"),
+ .nixCode = NixCode {
+ .errPos = Pos(problem_file, 40, 13),
+ .prevLineOfCode = "previous line of code",
+ .errLineOfCode = "this is the problem line of code",
+ .nextLineOfCode = "next line of code",
+ }});
+
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- error name ----------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nerror with code lines\n\n 39| previous line of code\n 40| this is the problem line of code\n | \x1B[31;1m^\x1B[0m\n 41| next line of code\n\nthis hint has \x1B[33;1myellow\x1B[0m templated \x1B[33;1mvalues\x1B[0m!!\n");
+ }
+
+ TEST(logError, logErrorWithoutLinesOfCode) {
+ SymbolTable testTable;
+ auto problem_file = testTable.create("myfile.nix");
+ testing::internal::CaptureStderr();
+
+ logError({
+ .name = "error name",
+ .description = "error without any code lines.",
+ .hint = hintfmt("this hint has %1% templated %2%!!",
+ "yellow",
+ "values"),
+ .nixCode = NixCode {
+ .errPos = Pos(problem_file, 40, 13)
+ }});
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- error name ----------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nerror without any code lines.\n\nthis hint has \x1B[33;1myellow\x1B[0m templated \x1B[33;1mvalues\x1B[0m!!\n");
+ }
+
+ TEST(logError, logErrorWithOnlyHintAndName) {
+ SymbolTable testTable;
+ auto problem_file = testTable.create("myfile.nix");
+ testing::internal::CaptureStderr();
+
+ logError({
+ .name = "error name",
+ .hint = hintfmt("hint %1%", "only"),
+ .nixCode = NixCode {
+ .errPos = Pos(problem_file, 40, 13)
+ }});
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- error name ----------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nhint \x1B[33;1monly\x1B[0m\n");
+
+ }
+
+ /* ----------------------------------------------------------------------------
+ * logWarning
+ * --------------------------------------------------------------------------*/
+
+ TEST(logWarning, logWarningWithNameDescriptionAndHint) {
+ testing::internal::CaptureStderr();
+
+ logWarning({
+ .name = "name",
+ .description = "error description",
+ .hint = hintfmt("there was a %1%", "warning"),
+ });
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[33;1mwarning:\x1B[0m\x1B[34;1m --- name --------------------------------------- error-unit-test\x1B[0m\nerror description\n\nthere was a \x1B[33;1mwarning\x1B[0m\n");
+ }
+
+ TEST(logWarning, logWarningWithFileLineNumAndCode) {
+
+ SymbolTable testTable;
+ auto problem_file = testTable.create("myfile.nix");
+
+ testing::internal::CaptureStderr();
+
+ logWarning({
+ .name = "warning name",
+ .description = "warning description",
+ .hint = hintfmt("this hint has %1% templated %2%!!",
+ "yellow",
+ "values"),
+ .nixCode = NixCode {
+ .errPos = Pos(problem_file, 40, 13),
+ .prevLineOfCode = std::nullopt,
+ .errLineOfCode = "this is the problem line of code",
+ .nextLineOfCode = std::nullopt
+ }});
+
+
+ auto str = testing::internal::GetCapturedStderr();
+ ASSERT_STREQ(str.c_str(), "\x1B[33;1mwarning:\x1B[0m\x1B[34;1m --- warning name ------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nwarning description\n\n 40| this is the problem line of code\n | \x1B[31;1m^\x1B[0m\n\nthis hint has \x1B[33;1myellow\x1B[0m templated \x1B[33;1mvalues\x1B[0m!!\n");
+ }
+
+}
diff --git a/src/libutil/tests/lru-cache.cc b/src/libutil/tests/lru-cache.cc
new file mode 100644
index 000000000..091d3d5ed
--- /dev/null
+++ b/src/libutil/tests/lru-cache.cc
@@ -0,0 +1,130 @@
+#include "lru-cache.hh"
+#include <gtest/gtest.h>
+
+namespace nix {
+
+ /* ----------------------------------------------------------------------------
+ * size
+ * --------------------------------------------------------------------------*/
+
+ TEST(LRUCache, sizeOfEmptyCacheIsZero) {
+ LRUCache<std::string, std::string> c(10);
+ ASSERT_EQ(c.size(), 0);
+ }
+
+ TEST(LRUCache, sizeOfSingleElementCacheIsOne) {
+ LRUCache<std::string, std::string> c(10);
+ c.upsert("foo", "bar");
+ ASSERT_EQ(c.size(), 1);
+ }
+
+ /* ----------------------------------------------------------------------------
+ * upsert / get
+ * --------------------------------------------------------------------------*/
+
+ TEST(LRUCache, getFromEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ auto val = c.get("x");
+ ASSERT_EQ(val.has_value(), false);
+ }
+
+ TEST(LRUCache, getExistingValue) {
+ LRUCache<std::string, std::string> c(10);
+ c.upsert("foo", "bar");
+ auto val = c.get("foo");
+ ASSERT_EQ(val, "bar");
+ }
+
+ TEST(LRUCache, getNonExistingValueFromNonEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ c.upsert("foo", "bar");
+ auto val = c.get("another");
+ ASSERT_EQ(val.has_value(), false);
+ }
+
+ TEST(LRUCache, upsertOnZeroCapacityCache) {
+ LRUCache<std::string, std::string> c(0);
+ c.upsert("foo", "bar");
+ auto val = c.get("foo");
+ ASSERT_EQ(val.has_value(), false);
+ }
+
+ TEST(LRUCache, updateExistingValue) {
+ LRUCache<std::string, std::string> c(1);
+ c.upsert("foo", "bar");
+
+ auto val = c.get("foo");
+ ASSERT_EQ(val.value_or("error"), "bar");
+ ASSERT_EQ(c.size(), 1);
+
+ c.upsert("foo", "changed");
+ val = c.get("foo");
+ ASSERT_EQ(val.value_or("error"), "changed");
+ ASSERT_EQ(c.size(), 1);
+ }
+
+ TEST(LRUCache, overwriteOldestWhenCapacityIsReached) {
+ LRUCache<std::string, std::string> c(3);
+ c.upsert("one", "eins");
+ c.upsert("two", "zwei");
+ c.upsert("three", "drei");
+
+ ASSERT_EQ(c.size(), 3);
+ ASSERT_EQ(c.get("one").value_or("error"), "eins");
+
+ // exceed capacity
+ c.upsert("another", "whatever");
+
+ ASSERT_EQ(c.size(), 3);
+ // Retrieving "one" makes it the most recent element thus
+ // two will be the oldest one and thus replaced.
+ ASSERT_EQ(c.get("two").has_value(), false);
+ ASSERT_EQ(c.get("another").value(), "whatever");
+ }
+
+ /* ----------------------------------------------------------------------------
+ * clear
+ * --------------------------------------------------------------------------*/
+
+ TEST(LRUCache, clearEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ c.clear();
+ ASSERT_EQ(c.size(), 0);
+ }
+
+ TEST(LRUCache, clearNonEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ c.upsert("one", "eins");
+ c.upsert("two", "zwei");
+ c.upsert("three", "drei");
+ ASSERT_EQ(c.size(), 3);
+ c.clear();
+ ASSERT_EQ(c.size(), 0);
+ }
+
+ /* ----------------------------------------------------------------------------
+ * erase
+ * --------------------------------------------------------------------------*/
+
+ TEST(LRUCache, eraseFromEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ ASSERT_EQ(c.erase("foo"), false);
+ ASSERT_EQ(c.size(), 0);
+ }
+
+ TEST(LRUCache, eraseMissingFromNonEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ c.upsert("one", "eins");
+ ASSERT_EQ(c.erase("foo"), false);
+ ASSERT_EQ(c.size(), 1);
+ ASSERT_EQ(c.get("one").value_or("error"), "eins");
+ }
+
+ TEST(LRUCache, eraseFromNonEmptyCache) {
+ LRUCache<std::string, std::string> c(10);
+ c.upsert("one", "eins");
+ ASSERT_EQ(c.erase("one"), true);
+ ASSERT_EQ(c.size(), 0);
+ ASSERT_EQ(c.get("one").value_or("empty"), "empty");
+ }
+}
diff --git a/src/libutil/tests/tests.cc b/src/libutil/tests/tests.cc
index 5abfe2c9c..8e77ccbe1 100644
--- a/src/libutil/tests/tests.cc
+++ b/src/libutil/tests/tests.cc
@@ -5,6 +5,8 @@
namespace nix {
+/* ----------- tests for util.hh ------------------------------------------------*/
+
/* ----------------------------------------------------------------------------
* absPath
* --------------------------------------------------------------------------*/
@@ -15,6 +17,9 @@ namespace nix {
ASSERT_EQ(p, "/");
}
+
+
+
TEST(absPath, turnsEmptyPathIntoCWD) {
char cwd[PATH_MAX+1];
auto p = absPath("");
@@ -581,5 +586,4 @@ namespace nix {
ASSERT_EQ(filterANSIEscapes(s, true), "foo bar baz" );
}
-
}
diff --git a/src/libutil/tests/url.cc b/src/libutil/tests/url.cc
new file mode 100644
index 000000000..80646ad3e
--- /dev/null
+++ b/src/libutil/tests/url.cc
@@ -0,0 +1,266 @@
+#include "url.hh"
+#include <gtest/gtest.h>
+
+namespace nix {
+
+/* ----------- tests for url.hh --------------------------------------------------*/
+
+ string print_map(std::map<string, string> m) {
+ std::map<string, string>::iterator it;
+ string s = "{ ";
+ for (it = m.begin(); it != m.end(); ++it) {
+ s += "{ ";
+ s += it->first;
+ s += " = ";
+ s += it->second;
+ s += " } ";
+ }
+ s += "}";
+ return s;
+ }
+
+
+ std::ostream& operator<<(std::ostream& os, const ParsedURL& p) {
+ return os << "\n"
+ << "url: " << p.url << "\n"
+ << "base: " << p.base << "\n"
+ << "scheme: " << p.scheme << "\n"
+ << "authority: " << p.authority.value() << "\n"
+ << "path: " << p.path << "\n"
+ << "query: " << print_map(p.query) << "\n"
+ << "fragment: " << p.fragment << "\n";
+ }
+
+ TEST(parseURL, parsesSimpleHttpUrl) {
+ auto s = "http://www.example.org/file.tar.gz";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "http://www.example.org/file.tar.gz",
+ .base = "http://www.example.org/file.tar.gz",
+ .scheme = "http",
+ .authority = "www.example.org",
+ .path = "/file.tar.gz",
+ .query = (StringMap) { },
+ .fragment = "",
+ };
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+ TEST(parseURL, parsesSimpleHttpsUrl) {
+ auto s = "https://www.example.org/file.tar.gz";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "https://www.example.org/file.tar.gz",
+ .base = "https://www.example.org/file.tar.gz",
+ .scheme = "https",
+ .authority = "www.example.org",
+ .path = "/file.tar.gz",
+ .query = (StringMap) { },
+ .fragment = "",
+ };
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+ TEST(parseURL, parsesSimpleHttpUrlWithQueryAndFragment) {
+ auto s = "https://www.example.org/file.tar.gz?download=fast&when=now#hello";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "https://www.example.org/file.tar.gz",
+ .base = "https://www.example.org/file.tar.gz",
+ .scheme = "https",
+ .authority = "www.example.org",
+ .path = "/file.tar.gz",
+ .query = (StringMap) { { "download", "fast" }, { "when", "now" } },
+ .fragment = "hello",
+ };
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+ TEST(parseURL, parsesSimpleHttpUrlWithComplexFragment) {
+ auto s = "http://www.example.org/file.tar.gz?field=value#?foo=bar%23";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "http://www.example.org/file.tar.gz",
+ .base = "http://www.example.org/file.tar.gz",
+ .scheme = "http",
+ .authority = "www.example.org",
+ .path = "/file.tar.gz",
+ .query = (StringMap) { { "field", "value" } },
+ .fragment = "?foo=bar#",
+ };
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+
+ TEST(parseURL, parseIPv4Address) {
+ auto s = "http://127.0.0.1:8080/file.tar.gz?download=fast&when=now#hello";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "http://127.0.0.1:8080/file.tar.gz",
+ .base = "https://127.0.0.1:8080/file.tar.gz",
+ .scheme = "http",
+ .authority = "127.0.0.1:8080",
+ .path = "/file.tar.gz",
+ .query = (StringMap) { { "download", "fast" }, { "when", "now" } },
+ .fragment = "hello",
+ };
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+ TEST(parseURL, parseIPv6Address) {
+ auto s = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080",
+ .base = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080",
+ .scheme = "http",
+ .authority = "[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080",
+ .path = "",
+ .query = (StringMap) { },
+ .fragment = "",
+ };
+
+ ASSERT_EQ(parsed, expected);
+
+ }
+
+ TEST(parseURL, parseEmptyQueryParams) {
+ auto s = "http://127.0.0.1:8080/file.tar.gz?&&&&&";
+ auto parsed = parseURL(s);
+ ASSERT_EQ(parsed.query, (StringMap) { });
+ }
+
+ TEST(parseURL, parseUserPassword) {
+ auto s = "http://user:pass@www.example.org:8080/file.tar.gz";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "http://user:pass@www.example.org/file.tar.gz",
+ .base = "http://user:pass@www.example.org/file.tar.gz",
+ .scheme = "http",
+ .authority = "user:pass@www.example.org:8080",
+ .path = "/file.tar.gz",
+ .query = (StringMap) { },
+ .fragment = "",
+ };
+
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+ TEST(parseURL, parseFileURLWithQueryAndFragment) {
+ auto s = "file:///none/of/your/business";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "",
+ .base = "",
+ .scheme = "file",
+ .authority = "",
+ .path = "/none/of/your/business",
+ .query = (StringMap) { },
+ .fragment = "",
+ };
+
+ ASSERT_EQ(parsed, expected);
+
+ }
+
+ TEST(parseURL, parsedUrlsIsEqualToItself) {
+ auto s = "http://www.example.org/file.tar.gz";
+ auto url = parseURL(s);
+
+ ASSERT_TRUE(url == url);
+ }
+
+ TEST(parseURL, parseFTPUrl) {
+ auto s = "ftp://ftp.nixos.org/downloads/nixos.iso";
+ auto parsed = parseURL(s);
+
+ ParsedURL expected {
+ .url = "ftp://ftp.nixos.org/downloads/nixos.iso",
+ .base = "ftp://ftp.nixos.org/downloads/nixos.iso",
+ .scheme = "ftp",
+ .authority = "ftp.nixos.org",
+ .path = "/downloads/nixos.iso",
+ .query = (StringMap) { },
+ .fragment = "",
+ };
+
+ ASSERT_EQ(parsed, expected);
+ }
+
+ TEST(parseURL, parsesAnythingInUriFormat) {
+ auto s = "whatever://github.com/NixOS/nixpkgs.git";
+ auto parsed = parseURL(s);
+ }
+
+ TEST(parseURL, parsesAnythingInUriFormatWithoutDoubleSlash) {
+ auto s = "whatever:github.com/NixOS/nixpkgs.git";
+ auto parsed = parseURL(s);
+ }
+
+ TEST(parseURL, emptyStringIsInvalidURL) {
+ ASSERT_THROW(parseURL(""), Error);
+ }
+
+ /* ----------------------------------------------------------------------------
+ * decodeQuery
+ * --------------------------------------------------------------------------*/
+
+ TEST(decodeQuery, emptyStringYieldsEmptyMap) {
+ auto d = decodeQuery("");
+ ASSERT_EQ(d, (StringMap) { });
+ }
+
+ TEST(decodeQuery, simpleDecode) {
+ auto d = decodeQuery("yi=one&er=two");
+ ASSERT_EQ(d, ((StringMap) { { "yi", "one" }, { "er", "two" } }));
+ }
+
+ TEST(decodeQuery, decodeUrlEncodedArgs) {
+ auto d = decodeQuery("arg=%3D%3D%40%3D%3D");
+ ASSERT_EQ(d, ((StringMap) { { "arg", "==@==" } }));
+ }
+
+ TEST(decodeQuery, decodeArgWithEmptyValue) {
+ auto d = decodeQuery("arg=");
+ ASSERT_EQ(d, ((StringMap) { { "arg", ""} }));
+ }
+
+ /* ----------------------------------------------------------------------------
+ * percentDecode
+ * --------------------------------------------------------------------------*/
+
+ TEST(percentDecode, decodesUrlEncodedString) {
+ string s = "==@==";
+ string d = percentDecode("%3D%3D%40%3D%3D");
+ ASSERT_EQ(d, s);
+ }
+
+ TEST(percentDecode, multipleDecodesAreIdempotent) {
+ string once = percentDecode("%3D%3D%40%3D%3D");
+ string twice = percentDecode(once);
+
+ ASSERT_EQ(once, twice);
+ }
+
+ TEST(percentDecode, trailingPercent) {
+ string s = "==@==%";
+ string d = percentDecode("%3D%3D%40%3D%3D%25");
+
+ ASSERT_EQ(d, s);
+ }
+
+}
diff --git a/src/libutil/tests/xml-writer.cc b/src/libutil/tests/xml-writer.cc
new file mode 100644
index 000000000..adcde25c9
--- /dev/null
+++ b/src/libutil/tests/xml-writer.cc
@@ -0,0 +1,105 @@
+#include "xml-writer.hh"
+#include <gtest/gtest.h>
+#include <sstream>
+
+namespace nix {
+
+ /* ----------------------------------------------------------------------------
+ * XMLWriter
+ * --------------------------------------------------------------------------*/
+
+ TEST(XMLWriter, emptyObject) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n");
+ }
+
+ TEST(XMLWriter, objectWithEmptyElement) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ t.openElement("foobar");
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar></foobar>");
+ }
+
+ TEST(XMLWriter, objectWithElementWithAttrs) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ XMLAttrs attrs = {
+ { "foo", "bar" }
+ };
+ t.openElement("foobar", attrs);
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar foo=\"bar\"></foobar>");
+ }
+
+ TEST(XMLWriter, objectWithElementWithEmptyAttrs) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ XMLAttrs attrs = {};
+ t.openElement("foobar", attrs);
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar></foobar>");
+ }
+
+ TEST(XMLWriter, objectWithElementWithAttrsEscaping) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ XMLAttrs attrs = {
+ { "<key>", "<value>" }
+ };
+ t.openElement("foobar", attrs);
+ }
+
+ // XXX: While "<value>" is escaped, "<key>" isn't which I think is a bug.
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar <key>=\"&lt;value&gt;\"></foobar>");
+ }
+
+ TEST(XMLWriter, objectWithElementWithAttrsIndented) {
+ std::stringstream out;
+ {
+ XMLWriter t(true, out);
+ XMLAttrs attrs = {
+ { "foo", "bar" }
+ };
+ t.openElement("foobar", attrs);
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar foo=\"bar\">\n</foobar>\n");
+ }
+
+ TEST(XMLWriter, writeEmptyElement) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ t.writeEmptyElement("foobar");
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar />");
+ }
+
+ TEST(XMLWriter, writeEmptyElementWithAttributes) {
+ std::stringstream out;
+ {
+ XMLWriter t(false, out);
+ XMLAttrs attrs = {
+ { "foo", "bar" }
+ };
+ t.writeEmptyElement("foobar", attrs);
+
+ }
+
+ ASSERT_EQ(out.str(), "<?xml version='1.0' encoding='utf-8'?>\n<foobar foo=\"bar\" />");
+ }
+
+}
diff --git a/src/libutil/url.cc b/src/libutil/url.cc
index 5d5328e5d..88c09eef9 100644
--- a/src/libutil/url.cc
+++ b/src/libutil/url.cc
@@ -4,6 +4,7 @@
namespace nix {
std::regex refRegex(refRegexS, std::regex::ECMAScript);
+std::regex badGitRefRegex(badGitRefRegexS, std::regex::ECMAScript);
std::regex revRegex(revRegexS, std::regex::ECMAScript);
std::regex flakeIdRegex(flakeIdRegexS, std::regex::ECMAScript);
diff --git a/src/libutil/url.hh b/src/libutil/url.hh
index a184eadce..2ef88ef2a 100644
--- a/src/libutil/url.hh
+++ b/src/libutil/url.hh
@@ -49,6 +49,12 @@ const static std::string pathRegex = "(?:" + segmentRegex + "(?:/" + segmentRege
const static std::string refRegexS = "[a-zA-Z0-9][a-zA-Z0-9_.-]*"; // FIXME: check
extern std::regex refRegex;
+// Instead of defining what a good Git Ref is, we define what a bad Git Ref is
+// This is because of the definition of a ref in refs.c in https://github.com/git/git
+// See tests/fetchGitRefs.sh for the full definition
+const static std::string badGitRefRegexS = "//|^[./]|/\\.|\\.\\.|[[:cntrl:][:space:]:?^~\[]|\\\\|\\*|\\.lock$|\\.lock/|@\\{|[/.]$|^@$|^$";
+extern std::regex badGitRefRegex;
+
// A Git revision (a SHA-1 commit hash).
const static std::string revRegexS = "[0-9a-fA-F]{40}";
extern std::regex revRegex;