mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-29 09:06:15 +02:00
Merge pull request #3631 from andir/libutil-config-tests
Add unit tests for config.cc
This commit is contained in:
commit
f60ce4fa20
3 changed files with 387 additions and 50 deletions
|
@ -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});
|
res.emplace(opt.first, SettingInfo{opt.second.setting->to_string(), opt.second.setting->description});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (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);
|
||||||
|
|
||||||
|
string name = tokens[0];
|
||||||
|
|
||||||
|
vector<string>::iterator i = tokens.begin();
|
||||||
|
advance(i, 2);
|
||||||
|
|
||||||
|
set(name, concatStringsSep(" ", Strings(i, tokens.end()))); // FIXME: slow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void AbstractConfig::applyConfigFile(const Path & path)
|
void AbstractConfig::applyConfigFile(const Path & path)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
string contents = readFile(path);
|
string contents = readFile(path);
|
||||||
|
applyConfig(contents, 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 (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);
|
|
||||||
|
|
||||||
string name = tokens[0];
|
|
||||||
|
|
||||||
vector<string>::iterator i = tokens.begin();
|
|
||||||
advance(i, 2);
|
|
||||||
|
|
||||||
set(name, concatStringsSep(" ", Strings(i, tokens.end()))); // FIXME: slow
|
|
||||||
};
|
|
||||||
} catch (SysError &) { }
|
} catch (SysError &) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,38 @@
|
||||||
|
|
||||||
namespace nix {
|
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 Args;
|
||||||
class AbstractSetting;
|
class AbstractSetting;
|
||||||
class JSONPlaceholder;
|
class JSONPlaceholder;
|
||||||
|
@ -23,6 +55,10 @@ protected:
|
||||||
|
|
||||||
public:
|
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;
|
virtual bool set(const std::string & name, const std::string & value) = 0;
|
||||||
|
|
||||||
struct SettingInfo
|
struct SettingInfo
|
||||||
|
@ -31,18 +67,52 @@ public:
|
||||||
std::string description;
|
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;
|
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);
|
void applyConfigFile(const Path & path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the `overridden` flag of all Settings
|
||||||
|
*/
|
||||||
virtual void resetOverriden() = 0;
|
virtual void resetOverriden() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs all settings to JSON
|
||||||
|
* - out: JSONObject to write the configuration to
|
||||||
|
*/
|
||||||
virtual void toJSON(JSONObject & out) = 0;
|
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;
|
virtual void convertToArgs(Args & args, const std::string & category) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a warning for each unregistered setting
|
||||||
|
*/
|
||||||
void warnUnknownSettings();
|
void warnUnknownSettings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-applies all previously attempted changes to unknown settings
|
||||||
|
*/
|
||||||
void reapplyUnknownSettings();
|
void reapplyUnknownSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
264
src/libutil/tests/config.cc
Normal file
264
src/libutil/tests/config.cc
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue