aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAqua-sama <aqua@iserlohn-fortress.net>2020-03-22 14:59:04 +0200
committerAqua-sama <aqua@iserlohn-fortress.net>2020-03-22 14:59:04 +0200
commit74870600fa54a48d4c1ce4b19861a9b0ce027fee (patch)
treed79eb50c18f1f62e937a1d1fe2e7180c7c14d06a
parentRemove ProfileInterface (diff)
downloadsmolbote-74870600fa54a48d4c1ce4b19861a9b0ce027fee.tar.xz
lib/configuration improvements
Configuration changes: - Configuration::value return type is now [[nodiscard]] - Configuration::value<T> is now a generic template that only works with the exact types of the underlying std::variant - Add Configuration::value<concept_value_t> 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)
-rw-r--r--lib/configuration/configuration.cpp68
-rw-r--r--lib/configuration/configuration.h77
-rw-r--r--lib/configuration/meson.build11
-rw-r--r--lib/configuration/qt_specialization.cpp55
-rw-r--r--lib/configuration/qt_specialization.h16
-rw-r--r--lib/configuration/test/defaultrc.ini20
-rw-r--r--lib/configuration/test/extrarc.ini8
-rw-r--r--lib/configuration/test/parser.cpp126
-rw-r--r--meson.build4
-rw-r--r--test/conf/main.cpp54
-rw-r--r--test/conf/meson.build6
-rw-r--r--test/conf/smolbote.cfg5
12 files changed, 322 insertions, 128 deletions
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 <QStandardPaths>
#include <algorithm>
+#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
@@ -36,20 +37,42 @@ Configuration::Configuration(std::initializer_list<std::pair<std::string, conf_v
}
}
+void Configuration::read_file(const std::string &location)
+{
+ std::fstream fs(location, std::fstream::in);
+ if(fs.is_open()) {
+ read(fs);
+ fs.close();
+ }
+}
+
void Configuration::read(std::basic_istream<char> &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<QString>(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<std::string> 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<std::string>(key.c_str()).value() << "\n";
- else
- out << key << "=" << obj.value<std::string>(key.c_str()).value() << "\n";
+ out << key << "=" << obj.value<std::string>(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 <QAction>
-#include <QString>
#include <initializer_list>
#include <memory>
#include <optional>
#include <string>
+#include <type_traits>
#include <unordered_map>
#include <variant>
#include <vector>
@@ -33,23 +33,37 @@
typedef std::variant<std::string, int, bool> conf_value_t;
+template <typename T>
+concept concept_value_t = std::is_arithmetic<T>::value || std::is_same<T, bool>::value || std::is_constructible<T, std::string>::value;
+
class consumable(unconsumed) Configuration : private std::unordered_map<std::string, conf_value_t>
{
friend std::ostream &operator<<(std::ostream &out, const Configuration &obj);
public:
return_typestate(unconsumed) explicit Configuration();
-
return_typestate(unconsumed) explicit Configuration(std::initializer_list<std::pair<std::string, conf_value_t>> 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<char> & input);
template <typename T>
- callable_when(unconsumed) std::optional<T> value(const char *path) const
+ callable_when(unconsumed) [[nodiscard]] std::optional<T> value(const char *path) const
+ {
+ if(use_global)
+ return instance()->value<T>(path);
+
+ if(count(path) == 0)
+ return std::nullopt;
+
+ return std::get<T>(at(path));
+ }
+
+ template <concept_value_t T>
+ callable_when(unconsumed) [[nodiscard]] std::optional<T> value(const char *path) const
{
if(use_global)
return instance()->value<T>(path);
@@ -61,33 +75,39 @@ public:
// path is guaranteed to exist
const auto value = at(path);
- if constexpr(std::is_same_v<T, QString> || std::is_same_v<T, std::string>) {
- auto r = [&value]() {
- if(std::holds_alternative<std::string>(value))
- return std::get<std::string>(value);
- else if(std::holds_alternative<int>(value))
- return std::to_string(std::get<int>(value));
+ if(std::holds_alternative<int>(value)) {
+ if constexpr(std::is_arithmetic<T>::value)
+ return static_cast<T>(std::get<int>(value));
+ else if constexpr(std::is_constructible<T, std::string>::value)
+ return T{ std::to_string(std::get<int>(value)) };
+
+ } else if(std::holds_alternative<bool>(value)) {
+ if constexpr(std::is_constructible<T, bool>::value)
+ return std::get<bool>(value);
+ else if constexpr(std::is_constructible<T, const char *>::value) {
+ if(std::get<bool>(value))
+ return T{ "true" };
else
- return std::get<bool>(value) ? std::string("true") : std::string("false");
- }();
+ return T{ "false" };
+ }
- if(r.front() == '~')
- r.replace(0, 1, m_homePath);
+ } else if(std::holds_alternative<std::string>(value)) {
+ auto str = std::get<std::string>(value);
+ if(str.front() == '~')
+ str.replace(0, 1, m_homePath);
- if constexpr(std::is_same_v<T, QString>)
- return std::make_optional(QString::fromStdString(r));
- else
- return std::make_optional(r);
-
- } else if constexpr(std::is_same_v<T, QStringList>) {
- return std::make_optional(QString::fromStdString(std::get<std::string>(value)).split(';'));
+ if constexpr(std::is_constructible<T, std::string>::value)
+ return T{ str };
+ }
- } else if(std::holds_alternative<T>(value)) {
- return std::optional<T>(std::get<T>(value));
- } else
- return std::nullopt;
+ return std::nullopt;
+ }
- } // std::optional<T> value(path) const
+ template <typename T>
+ callable_when(unconsumed) T &shortcut(T &, const char *) const
+ {
+ return T{};
+ }
static void move_global(std::unique_ptr<Configuration> && 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 <stdexcept>
+
+template <>
+callable_when(unconsumed) [[nodiscard]] std::optional<QString> Configuration::value(const char *path) const
+{
+ const auto v = value<std::string>(path);
+ if(!v)
+ return std::nullopt;
+ else
+ return QString::fromStdString(v.value());
+}
+
+template <>
+callable_when(unconsumed) [[nodiscard]] std::optional<QStringList> Configuration::value(const char *path) const
+{
+ const auto v = value<std::string>(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<QString>(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<QString>(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<QString>(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 <QString>
+#include <QStringList>
+
+template <>
+callable_when(unconsumed) [[nodiscard]] std::optional<QString> Configuration::value(const char *path) const;
+template <>
+callable_when(unconsumed) [[nodiscard]] std::optional<QStringList> 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<QAction>(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 <QApplication>
+#include <cstdlib>
+#include <fstream>
+#include <gtest/gtest.h>
+#include <sstream>
+
+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<Configuration> global_conf = std::make_unique<Configuration, std::initializer_list<std::pair<std::string, conf_value_t>>>({
+ { "name", std::string("global") },
+ { "number", int(123) },
+ { "toggle", bool(true) },
+
+ });
+};
+
+TEST_F(ConfigurationTest, NoSection)
+{
+ EXPECT_EQ(conf.value<std::string>("name").value(), "Top level");
+ EXPECT_EQ(conf.value<std::string>("other").value(), "not in cfg");
+ EXPECT_EQ(conf.value<std::string>("comment").value(), "");
+ EXPECT_EQ(conf.value<int>("number").value(), 12);
+ EXPECT_TRUE(conf.value<bool>("toggle").value());
+ EXPECT_FALSE(conf.value<int>("nullopt"));
+ EXPECT_FALSE(conf.value<bool>("nullopt"));
+ EXPECT_FALSE(conf.value<std::string>("nullopt"));
+}
+
+TEST_F(ConfigurationTest, TypeCasts)
+{
+ EXPECT_EQ(conf.value<std::string>("number").value(), "12");
+ EXPECT_EQ(conf.value<std::string>("toggle").value(), "true");
+ EXPECT_EQ(conf.value<std::string>("main/toggle").value(), "false");
+}
+
+TEST_F(ConfigurationTest, QtSpecialization)
+{
+ EXPECT_EQ(conf.value<QString>("name").value(), "Top level");
+ EXPECT_EQ(conf.value<QString>("number").value(), "12");
+ EXPECT_EQ(conf.value<QString>("toggle").value(), "true");
+ EXPECT_EQ(conf.value<QString>("main/toggle").value(), "false");
+ EXPECT_FALSE(conf.value<QString>("nullopt"));
+
+ EXPECT_EQ(conf.value<QStringList>("list").value(), QStringList({ "one", "two", "three", "for four" }));
+ EXPECT_FALSE(conf.value<QStringList>("nullopt"));
+}
+
+TEST_F(ConfigurationTest, QtShortcut)
+{
+ QAction action;
+ EXPECT_EQ(conf.shortcut<QAction>(action, "qt/shortcut").shortcut().toString(), "Ctrl+Q");
+ EXPECT_EQ(conf.shortcut<QAction>(action, "qt/nil").shortcut().toString(), "Ctrl+Q");
+
+ QKeySequence sequence;
+ EXPECT_EQ(conf.shortcut<QKeySequence>(sequence, "qt/shortcut").toString(), "Ctrl+Q");
+ EXPECT_EQ(conf.shortcut<QKeySequence>(sequence, "qt/nil").toString(), "Ctrl+Q");
+}
+
+TEST_F(ConfigurationTest, MainSection)
+{
+ EXPECT_EQ(conf.value<std::string>("main/name").value(), "Section Testing");
+ EXPECT_EQ(conf.value<int>("main/number").value(), 10);
+ EXPECT_FALSE(conf.value<bool>("main/toggle").value());
+}
+
+TEST_F(ConfigurationTest, ExtraSection)
+{
+ EXPECT_EQ(conf.value<std::string>("over").value(), "extra");
+ EXPECT_EQ(conf.value<std::string>("extra/name").value(), "extra section");
+ EXPECT_EQ(conf.value<int>("extra/number").value(), 12);
+ EXPECT_TRUE(conf.value<bool>("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<std::string>("name"));
+ EXPECT_EQ(g.value<std::string>("name").value(), "global");
+ EXPECT_TRUE(g.value<int>("number"));
+ EXPECT_EQ(g.value<int>("number").value(), 123);
+ EXPECT_TRUE(g.value<bool>("toggle"));
+ EXPECT_EQ(g.value<bool>("toggle").value(), true);
+ EXPECT_FALSE(g.value<std::string>("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 <fstream>
-#include <iostream>
-#include <string>
-#include "configuration.h"
-#include <cassert>
-
-int main(int, char**)
-{
- std::fstream fs;
- fs.open("smolbote.cfg", std::fstream::in);
- assert(fs.is_open());
-
- {
- auto value_map = std::make_unique<Configuration, std::initializer_list<std::pair<std::string, conf_value_t>>>({
- { "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<std::string>("test") == "value");
- assert(g.value<std::string>("plugins.path") == "/usr/local/lib/smolbote/plugins");
- assert(g.value<int>("integer-1") == 20);
- assert(g.value<int>("integer-2") == 22);
- }
-
-/*
- {
- const Configuration conf(std::move(value_map));
-
- assert(conf.value<std::string>("test") == "value");
- assert(conf.value<std::string>("plugins.path") == "/usr/local/lib/smolbote/plugins");
- assert(conf.value<int>("integer-1") == 20);
- assert(conf.value<int>("integer-2") == 22);
- }
-*/
- // Configuration is now destroyed
-
-
- // This should be a compiler warning
- //const Configuration c(std::move(value_map));
- //assert(c.value<std::string>("test") == "value");
-
- // This should be a compiler warning and runtime error
- //assert(value_map.value<std::string>("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