From 74870600fa54a48d4c1ce4b19861a9b0ce027fee Mon Sep 17 00:00:00 2001 From: Aqua-sama Date: Sun, 22 Mar 2020 14:59:04 +0200 Subject: lib/configuration improvements Configuration changes: - Configuration::value return type is now [[nodiscard]] - Configuration::value is now a generic template that only works with the exact types of the underlying std::variant - Add Configuration::value for standard library types compatible with the types of std::variant - Add Configuration::shortcut<> placeholder, and QAction and QKeySequence specializations as a convenient way to set up shortcuts - Deprecate setShortcut - Add Configuration::read_file convenience member that takes file path as parameter Format changes: - Configuration files can now have sections, specified as [section name]. Section names are prepended to keys. Section names cannot be nested. - Configuration files can now have @@include directives, causing another file to be read as well. The included file is not treated as nested into a section, and will overwrite values previously set. Others: - add some tests for libconfiguration. QAction/QKeySequence require a QApplication be set up, so the test application may require running xorg/wayland. old coverage: lines: 15.6% (960 out of 6172) branches: 9.9% (1187 out of 12012) new coverage: lines: 17.1% (1067 out of 6254) branches: 11.0% (1388 out of 12644) --- lib/configuration/configuration.cpp | 68 +++++++++-------- lib/configuration/configuration.h | 77 ++++++++++++------- lib/configuration/meson.build | 11 ++- lib/configuration/qt_specialization.cpp | 55 ++++++++++++++ lib/configuration/qt_specialization.h | 16 ++++ lib/configuration/test/defaultrc.ini | 20 +++++ lib/configuration/test/extrarc.ini | 8 ++ lib/configuration/test/parser.cpp | 126 ++++++++++++++++++++++++++++++++ meson.build | 4 +- test/conf/main.cpp | 54 -------------- test/conf/meson.build | 6 -- test/conf/smolbote.cfg | 5 -- 12 files changed, 322 insertions(+), 128 deletions(-) create mode 100644 lib/configuration/qt_specialization.cpp create mode 100644 lib/configuration/qt_specialization.h create mode 100644 lib/configuration/test/defaultrc.ini create mode 100644 lib/configuration/test/extrarc.ini create mode 100644 lib/configuration/test/parser.cpp delete mode 100644 test/conf/main.cpp delete mode 100644 test/conf/meson.build delete mode 100644 test/conf/smolbote.cfg diff --git a/lib/configuration/configuration.cpp b/lib/configuration/configuration.cpp index 34e84af..0fdd0c0 100644 --- a/lib/configuration/configuration.cpp +++ b/lib/configuration/configuration.cpp @@ -9,6 +9,7 @@ #include "configuration.h" #include #include +#include #include #include #include @@ -36,20 +37,42 @@ Configuration::Configuration(std::initializer_list &input) { - std::string line, key, value; + std::string line, section, key, value; std::istringstream is_line; while(std::getline(input, line)) { + if(line.rfind("@@") == 0) { + if(line.rfind("@@include ") == 0) { + read_file(line.substr(10)); + } + continue; + } if(line[0] == '#' || line.length() == 0) continue; + if(line.front() == '[' && line.back() == ']') { + section = line.substr(1, line.length() - 2) + '/'; + continue; + } + is_line.clear(); is_line.str(line); - if(std::getline(is_line, key, '=')) { - is_line >> value; + const auto pos = line.find_first_of('='); + if(pos != std::string::npos) { + key = section + line.substr(0, pos); + value = line.substr(pos + 1); strip(key); strip(value); @@ -82,45 +105,26 @@ Configuration *Configuration::instance() return s_conf.get(); } -void setShortcut(QAction *action, const char *name) +std::ostream &operator<<(std::ostream &out, const Configuration &obj) { - if(!s_conf) - throw new std::runtime_error("Trying to set a shortcut, but no configuration has been set!"); + if(obj.use_global) { + if(!s_conf) { + throw new std::runtime_error("Trying to use default Configuration, but none has been set!"); + } - if(const auto shortcutText = s_conf->value(name)) { - const QString tooltip = action->toolTip(); - action->setShortcut(QKeySequence::fromString(shortcutText.value())); - action->setToolTip(QString("%1 (%2)").arg(tooltip, shortcutText.value())); + out << *s_conf; + return out; } -#ifdef QT_DEBUG - else { - std::cout << "fixme: setShortcut called for " << name << ", but no such value exists!" << std::endl; - } -#endif -} -std::ostream &operator<<(std::ostream &out, const Configuration &obj) -{ // unordered_map is, well, unordered, so grab the keys and sort them before printing them std::vector keys; - - if(obj.use_global) { - if(!s_conf) - throw new std::runtime_error("Trying to use default Configuration, but none has been set!"); - - for(const auto &pair : *s_conf) - keys.emplace_back(pair.first); - } else { - for(const auto &pair : obj) - out << pair.first << "\n"; + for(const auto &pair : obj) { + keys.emplace_back(pair.first); } std::sort(keys.begin(), keys.end()); for(const auto &key : keys) { - if(obj.use_global) - out << key << "=" << s_conf->value(key.c_str()).value() << "\n"; - else - out << key << "=" << obj.value(key.c_str()).value() << "\n"; + out << key << "=" << obj.value(key.c_str()).value() << "\n"; } return out; diff --git a/lib/configuration/configuration.h b/lib/configuration/configuration.h index 4459dd0..3d0a49d 100644 --- a/lib/configuration/configuration.h +++ b/lib/configuration/configuration.h @@ -10,11 +10,11 @@ #define SMOLBOTE_CONFIGURATION_H #include -#include #include #include #include #include +#include #include #include #include @@ -33,23 +33,37 @@ typedef std::variant conf_value_t; +template +concept concept_value_t = std::is_arithmetic::value || std::is_same::value || std::is_constructible::value; + class consumable(unconsumed) Configuration : private std::unordered_map { friend std::ostream &operator<<(std::ostream &out, const Configuration &obj); public: return_typestate(unconsumed) explicit Configuration(); - return_typestate(unconsumed) explicit Configuration(std::initializer_list> l) noexcept; - - explicit Configuration(Configuration && other param_typestate(unconsumed)) = default; + return_typestate(unconsumed) explicit Configuration(Configuration && other param_typestate(unconsumed)) = default; ~Configuration() = default; + callable_when(unconsumed) void read_file(const std::string &location); callable_when(unconsumed) void read(std::basic_istream & input); template - callable_when(unconsumed) std::optional value(const char *path) const + callable_when(unconsumed) [[nodiscard]] std::optional value(const char *path) const + { + if(use_global) + return instance()->value(path); + + if(count(path) == 0) + return std::nullopt; + + return std::get(at(path)); + } + + template + callable_when(unconsumed) [[nodiscard]] std::optional value(const char *path) const { if(use_global) return instance()->value(path); @@ -61,33 +75,39 @@ public: // path is guaranteed to exist const auto value = at(path); - if constexpr(std::is_same_v || std::is_same_v) { - auto r = [&value]() { - if(std::holds_alternative(value)) - return std::get(value); - else if(std::holds_alternative(value)) - return std::to_string(std::get(value)); + if(std::holds_alternative(value)) { + if constexpr(std::is_arithmetic::value) + return static_cast(std::get(value)); + else if constexpr(std::is_constructible::value) + return T{ std::to_string(std::get(value)) }; + + } else if(std::holds_alternative(value)) { + if constexpr(std::is_constructible::value) + return std::get(value); + else if constexpr(std::is_constructible::value) { + if(std::get(value)) + return T{ "true" }; else - return std::get(value) ? std::string("true") : std::string("false"); - }(); + return T{ "false" }; + } - if(r.front() == '~') - r.replace(0, 1, m_homePath); + } else if(std::holds_alternative(value)) { + auto str = std::get(value); + if(str.front() == '~') + str.replace(0, 1, m_homePath); - if constexpr(std::is_same_v) - return std::make_optional(QString::fromStdString(r)); - else - return std::make_optional(r); - - } else if constexpr(std::is_same_v) { - return std::make_optional(QString::fromStdString(std::get(value)).split(';')); + if constexpr(std::is_constructible::value) + return T{ str }; + } - } else if(std::holds_alternative(value)) { - return std::optional(std::get(value)); - } else - return std::nullopt; + return std::nullopt; + } - } // std::optional value(path) const + template + callable_when(unconsumed) T &shortcut(T &, const char *) const + { + return T{}; + } static void move_global(std::unique_ptr && conf); @@ -98,7 +118,8 @@ private: const bool use_global = false; }; -void setShortcut(QAction *action, const char *name); +#include "qt_specialization.h" + std::ostream &operator<<(std::ostream &out, const Configuration &obj); #endif // SMOLBOTE_CONFIGURATION_H diff --git a/lib/configuration/meson.build b/lib/configuration/meson.build index 939a493..a0d915c 100644 --- a/lib/configuration/meson.build +++ b/lib/configuration/meson.build @@ -1,5 +1,14 @@ dep_configuration = declare_dependency( include_directories: include_directories('.'), - link_with: static_library('configuration', ['configuration.cpp'], dependencies: dep_qt5) + link_with: static_library('configuration', ['configuration.cpp', 'qt_specialization.cpp'], dependencies: dep_qt5) +) + +test('configuration: parser', + executable('configuration-parser', + sources: [ 'test/parser.cpp' ], + dependencies: [ dep_qt5, dep_gtest, dep_configuration ] + ), + env: environment({ 'CONFIGFILE' : meson.current_source_dir()/'test/defaultrc.ini' }), + workdir: meson.current_source_dir()/'test' ) diff --git a/lib/configuration/qt_specialization.cpp b/lib/configuration/qt_specialization.cpp new file mode 100644 index 0000000..4e79492 --- /dev/null +++ b/lib/configuration/qt_specialization.cpp @@ -0,0 +1,55 @@ +#include "configuration.h" +#include + +template <> +callable_when(unconsumed) [[nodiscard]] std::optional Configuration::value(const char *path) const +{ + const auto v = value(path); + if(!v) + return std::nullopt; + else + return QString::fromStdString(v.value()); +} + +template <> +callable_when(unconsumed) [[nodiscard]] std::optional Configuration::value(const char *path) const +{ + const auto v = value(path); + if(!v) + return std::nullopt; + else + return QString::fromStdString(v.value()).split(';'); +} + +void setShortcut(QAction *action, const char *name) +{ + Configuration conf; + + if(const auto shortcutText = conf.value(name)) { + const QString tooltip = action->toolTip(); + action->setShortcut(QKeySequence::fromString(shortcutText.value())); + action->setToolTip(QString("%1 (%2)").arg(tooltip, shortcutText.value())); + } else { + throw new std::runtime_error(std::string("fixme: setShortcut found no such value for ") + name); + } +} + +template <> +callable_when(unconsumed) QAction &Configuration::shortcut(QAction &action, const char *name) const +{ + if(const auto shortcut = value(name)) { + const QString old_tooltip = action.toolTip(); + action.setShortcut(QKeySequence::fromString(shortcut.value())); + action.setToolTip(QString("%1 (%2)").arg(old_tooltip, shortcut.value())); + } + return action; +} + +template <> +callable_when(unconsumed) QKeySequence &Configuration::shortcut(QKeySequence &sequence, const char *name) const +{ + if(const auto shortcut = value(name)) { + sequence = QKeySequence::fromString(shortcut.value()); + } + return sequence; +} diff --git a/lib/configuration/qt_specialization.h b/lib/configuration/qt_specialization.h new file mode 100644 index 0000000..8aabf6b --- /dev/null +++ b/lib/configuration/qt_specialization.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +template <> +callable_when(unconsumed) [[nodiscard]] std::optional Configuration::value(const char *path) const; +template <> +callable_when(unconsumed) [[nodiscard]] std::optional Configuration::value(const char *path) const; + +template <> +callable_when(unconsumed) QAction &Configuration::shortcut(QAction &, const char *) const; +template <> +callable_when(unconsumed) QKeySequence &Configuration::shortcut(QKeySequence &, const char *) const; + +[[deprecated("Use Configuration::shortcut(name) instead")]] void setShortcut(QAction *action, const char *name); diff --git a/lib/configuration/test/defaultrc.ini b/lib/configuration/test/defaultrc.ini new file mode 100644 index 0000000..f83bf73 --- /dev/null +++ b/lib/configuration/test/defaultrc.ini @@ -0,0 +1,20 @@ +# this is a comment and should be ignored + +name= Top level +over=default +#comment=val +number=12 +toggle=true +list=one;two;three;for four + +[main] +name=Section Testing +number=10 +toggle=false + +[qt] +shortcut=Ctrl+Q + +# include another file, absolute path or relative path to the rundir +@@include extrarc.ini + diff --git a/lib/configuration/test/extrarc.ini b/lib/configuration/test/extrarc.ini new file mode 100644 index 0000000..e22bf09 --- /dev/null +++ b/lib/configuration/test/extrarc.ini @@ -0,0 +1,8 @@ + +over=extra + +[extra] +name=extra section +number=12 +toggle=true + diff --git a/lib/configuration/test/parser.cpp b/lib/configuration/test/parser.cpp new file mode 100644 index 0000000..3ad64a5 --- /dev/null +++ b/lib/configuration/test/parser.cpp @@ -0,0 +1,126 @@ +#include "configuration.h" +#include +#include +#include +#include +#include + +class ConfigurationTest : public ::testing::Test +{ +protected: + void SetUp() override + { + conf.read_file(std::getenv("CONFIGFILE")); + } + + Configuration conf{ + { "name", std::string() }, + { "over", std::string() }, + { "other", std::string("not in cfg") }, // this entry is not in the conf file + { "comment", std::string() }, // commented out entry in the conf file + { "number", int(0) }, + { "toggle", bool(false) }, + + { "main/name", std::string() }, + { "main/number", int(0) }, + { "main/toggle", bool(true) }, + + { "extra/name", std::string() }, + { "extra/number", int(0) }, + { "extra/toggle", bool(false) }, + }; + + std::unique_ptr global_conf = std::make_unique>>({ + { "name", std::string("global") }, + { "number", int(123) }, + { "toggle", bool(true) }, + + }); +}; + +TEST_F(ConfigurationTest, NoSection) +{ + EXPECT_EQ(conf.value("name").value(), "Top level"); + EXPECT_EQ(conf.value("other").value(), "not in cfg"); + EXPECT_EQ(conf.value("comment").value(), ""); + EXPECT_EQ(conf.value("number").value(), 12); + EXPECT_TRUE(conf.value("toggle").value()); + EXPECT_FALSE(conf.value("nullopt")); + EXPECT_FALSE(conf.value("nullopt")); + EXPECT_FALSE(conf.value("nullopt")); +} + +TEST_F(ConfigurationTest, TypeCasts) +{ + EXPECT_EQ(conf.value("number").value(), "12"); + EXPECT_EQ(conf.value("toggle").value(), "true"); + EXPECT_EQ(conf.value("main/toggle").value(), "false"); +} + +TEST_F(ConfigurationTest, QtSpecialization) +{ + EXPECT_EQ(conf.value("name").value(), "Top level"); + EXPECT_EQ(conf.value("number").value(), "12"); + EXPECT_EQ(conf.value("toggle").value(), "true"); + EXPECT_EQ(conf.value("main/toggle").value(), "false"); + EXPECT_FALSE(conf.value("nullopt")); + + EXPECT_EQ(conf.value("list").value(), QStringList({ "one", "two", "three", "for four" })); + EXPECT_FALSE(conf.value("nullopt")); +} + +TEST_F(ConfigurationTest, QtShortcut) +{ + QAction action; + EXPECT_EQ(conf.shortcut(action, "qt/shortcut").shortcut().toString(), "Ctrl+Q"); + EXPECT_EQ(conf.shortcut(action, "qt/nil").shortcut().toString(), "Ctrl+Q"); + + QKeySequence sequence; + EXPECT_EQ(conf.shortcut(sequence, "qt/shortcut").toString(), "Ctrl+Q"); + EXPECT_EQ(conf.shortcut(sequence, "qt/nil").toString(), "Ctrl+Q"); +} + +TEST_F(ConfigurationTest, MainSection) +{ + EXPECT_EQ(conf.value("main/name").value(), "Section Testing"); + EXPECT_EQ(conf.value("main/number").value(), 10); + EXPECT_FALSE(conf.value("main/toggle").value()); +} + +TEST_F(ConfigurationTest, ExtraSection) +{ + EXPECT_EQ(conf.value("over").value(), "extra"); + EXPECT_EQ(conf.value("extra/name").value(), "extra section"); + EXPECT_EQ(conf.value("extra/number").value(), 12); + EXPECT_TRUE(conf.value("extra/toggle").value()); +} + +TEST_F(ConfigurationTest, GlobalInstance) +{ + std::stringstream output; + + output << *global_conf; + EXPECT_EQ(output.str(), "name=global\nnumber=123\ntoggle=true\n") << "operator<< on global_conf before move"; + + Configuration::move_global(std::move(global_conf)); + Configuration g; + EXPECT_TRUE(g.value("name")); + EXPECT_EQ(g.value("name").value(), "global"); + EXPECT_TRUE(g.value("number")); + EXPECT_EQ(g.value("number").value(), 123); + EXPECT_TRUE(g.value("toggle")); + EXPECT_EQ(g.value("toggle").value(), true); + EXPECT_FALSE(g.value("nullopt")); + + output.str(std::string()); + output << g; + EXPECT_EQ(output.str(), "name=global\nnumber=123\ntoggle=true\n") << "operator<< on global_conf after move"; +} + +int main(int argc, char **argv) +{ + QApplication a(argc, argv); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/meson.build b/meson.build index 3424554..08899fc 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('smolbote', ['cpp'], version: '0.1.0', - default_options: ['cpp_std=c++17', 'warning_level=3'], + default_options: ['cpp_std=c++2a', 'warning_level=3'], license: 'GPL3', meson_version: '>=0.52.0' ) @@ -31,6 +31,7 @@ add_project_arguments(cxx.get_supported_arguments([ '-ffunction-sections', # Place each function into its own section, better ASLR but larger executables '-fstack-protector-all', # Emit code to check for buffer overflows on all functions '-fstack-clash-protection', # Emit code to check for stack clash attacks + '-fconcepts', '-mspeculative-load-hardening', # Spectre v1 mitigation @@ -90,7 +91,6 @@ subdir('tools') #subdir('plugins/ConfigurationEditor') #subdir('plugins/ProfileEditor') -subdir('test/conf') subdir('test/firefox-bookmarks-json-parser') ssconfig = poi_sourceset.apply(cdata) diff --git a/test/conf/main.cpp b/test/conf/main.cpp deleted file mode 100644 index e499a4d..0000000 --- a/test/conf/main.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include -#include -#include "configuration.h" -#include - -int main(int, char**) -{ - std::fstream fs; - fs.open("smolbote.cfg", std::fstream::in); - assert(fs.is_open()); - - { - auto value_map = std::make_unique>>({ - { "test", std::string("unknown") }, - { "integer-1", 20 }, - { "integer-2", 30 } - }); - value_map->read(fs); - - Configuration::move_global(std::move(value_map)); - fs.close(); - } - - { - Configuration g; - assert(g.value("test") == "value"); - assert(g.value("plugins.path") == "/usr/local/lib/smolbote/plugins"); - assert(g.value("integer-1") == 20); - assert(g.value("integer-2") == 22); - } - -/* - { - const Configuration conf(std::move(value_map)); - - assert(conf.value("test") == "value"); - assert(conf.value("plugins.path") == "/usr/local/lib/smolbote/plugins"); - assert(conf.value("integer-1") == 20); - assert(conf.value("integer-2") == 22); - } -*/ - // Configuration is now destroyed - - - // This should be a compiler warning - //const Configuration c(std::move(value_map)); - //assert(c.value("test") == "value"); - - // This should be a compiler warning and runtime error - //assert(value_map.value("test").value() == "value"); - - return 0; -} diff --git a/test/conf/meson.build b/test/conf/meson.build deleted file mode 100644 index de67db7..0000000 --- a/test/conf/meson.build +++ /dev/null @@ -1,6 +0,0 @@ -e = executable('conf_test', - dependencies: [dep_qt5, dep_configuration], - sources: 'main.cpp' -) - -test('conf_test', e, workdir : meson.source_root()/'test/conf') diff --git a/test/conf/smolbote.cfg b/test/conf/smolbote.cfg deleted file mode 100644 index 25467ef..0000000 --- a/test/conf/smolbote.cfg +++ /dev/null @@ -1,5 +0,0 @@ -# this is a comment -test = value -plugins.path = /usr/local/lib/smolbote/plugins - -integer-2 = 22 -- cgit v1.2.1