diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f02c9b57..8af7a04a 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -15,9 +15,10 @@ jobs: export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf pkg install -y git # subprojects/date - pkg install -y evdev-proto gtk-layer-shell gtkmm30 jsoncpp libdbusmenu \ - libevdev libfmt libmpdclient libudev-devd meson pkgconf pulseaudio \ - scdoc sndio spdlog + pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ + libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ + pkgconf pulseaudio scdoc sndio spdlog run: | meson build -Dman-pages=enabled ninja -C build + meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index e550e20b..d4efbf8c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -23,3 +23,5 @@ jobs: run: meson -Dman-pages=enabled build - name: build run: ninja -C build + - name: test + run: meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/include/client.hpp b/include/client.hpp index e7fa1db0..bd80d0bd 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -3,11 +3,10 @@ #include #include #include -#include #include -#include #include "bar.hpp" +#include "config.hpp" struct zwlr_layer_shell_v1; struct zwp_idle_inhibitor_v1; @@ -29,18 +28,13 @@ class Client { struct zxdg_output_manager_v1 * xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; std::vector> bars; + Config config; private: Client() = default; - std::tuple getConfigs(const std::string &config, - const std::string &style) const; - void bindInterfaces(); - const std::string getValidPath(const std::vector &paths) const; + const std::string getStyle(const std::string &style); + void bindInterfaces(); void handleOutput(struct waybar_output &output); - bool isValidOutput(const Json::Value &config, struct waybar_output &output); - auto setupConfig(const std::string &config_file, int depth) -> void; - auto resolveConfigIncludes(Json::Value &config, int depth) -> void; - auto mergeConfig(Json::Value &a_config_, Json::Value &b_config_) -> void; auto setupCss(const std::string &css_file) -> void; struct waybar_output & getOutput(void *); std::vector getOutputConfigs(struct waybar_output &output); @@ -55,7 +49,6 @@ class Client { void handleMonitorRemoved(Glib::RefPtr monitor); void handleDeferredMonitorRemoval(Glib::RefPtr monitor); - Json::Value config_; Glib::RefPtr style_context_; Glib::RefPtr css_provider_; std::list outputs_; diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 00000000..82d55995 --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include + +#ifndef SYSCONFDIR +#define SYSCONFDIR "/etc" +#endif + +namespace waybar { + +class Config { + public: + static const std::vector CONFIG_DIRS; + + /* Try to find any of provided names in the supported set of config directories */ + static std::optional findConfigPath( + const std::vector &names, const std::vector &dirs = CONFIG_DIRS); + + Config() = default; + + void load(const std::string &config); + + Json::Value &getConfig() { return config_; } + + std::vector getOutputConfigs(const std::string &name, const std::string &identifier); + + private: + void setupConfig(Json::Value &dst, const std::string &config_file, int depth); + void resolveConfigIncludes(Json::Value &config, int depth); + void mergeConfig(Json::Value &a_config_, Json::Value &b_config_); + + std::string config_file_; + + Json::Value config_; +}; +} // namespace waybar diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 9dc6925f..997a48d8 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -87,8 +87,9 @@ Also a minimal example configuration can be found on the at the bottom of this m *include* ++ typeof: string|array ++ - Paths to additional configuration files. In case of duplicate options, the including file's value takes precedence. Make sure to avoid circular imports. - For a multi-bar config, specify at least an empty object for each bar also in every file being included. + Paths to additional configuration files. + Each file can contain a single object with any of the bar configuration options. In case of duplicate options, the first defined value takes precedence, i.e. including file -> first included file -> etc. Nested includes are permitted, but make sure to avoid circular imports. + For a multi-bar config, the include directive affects only current bar configuration object. # MODULE FORMAT diff --git a/meson.build b/meson.build index 835b70e3..c09fbb61 100644 --- a/meson.build +++ b/meson.build @@ -149,6 +149,7 @@ src_files = files( 'src/main.cpp', 'src/bar.cpp', 'src/client.cpp', + 'src/config.cpp', 'src/util/ustring_clen.cpp' ) @@ -359,6 +360,15 @@ if scdoc.found() endforeach endif +catch2 = dependency( + 'catch2', + fallback: ['catch2', 'catch2_dep'], + required: get_option('tests'), +) +if catch2.found() + subdir('test') +endif + clangtidy = find_program('clang-tidy', required: false) if clangtidy.found() diff --git a/meson_options.txt b/meson_options.txt index fefb3dc3..81e44689 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -10,3 +10,4 @@ option('mpd', type: 'feature', value: 'auto', description: 'Enable support for t option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') +option('tests', type: 'feature', value: 'auto', description: 'Enable tests') diff --git a/src/client.cpp b/src/client.cpp index ff6e7bf2..95f5a295 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -3,12 +3,10 @@ #include #include -#include #include #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" -#include "util/json.hpp" #include "wlr-layer-shell-unstable-v1-client-protocol.h" waybar::Client *waybar::Client::inst() { @@ -16,23 +14,6 @@ waybar::Client *waybar::Client::inst() { return c; } -const std::string waybar::Client::getValidPath(const std::vector &paths) const { - wordexp_t p; - - for (const std::string &path : paths) { - if (wordexp(path.c_str(), &p, 0) == 0) { - if (access(*p.we_wordv, F_OK) == 0) { - std::string result = *p.we_wordv; - wordfree(&p); - return result; - } - wordfree(&p); - } - } - - return std::string(); -} - void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { auto client = static_cast(data); @@ -70,29 +51,6 @@ void waybar::Client::handleOutput(struct waybar_output &output) { zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output); } -bool waybar::Client::isValidOutput(const Json::Value &config, struct waybar_output &output) { - if (config["output"].isArray()) { - for (auto const &output_conf : config["output"]) { - if (output_conf.isString() && - (output_conf.asString() == output.name || output_conf.asString() == output.identifier)) { - return true; - } - } - return false; - } else if (config["output"].isString()) { - auto config_output = config["output"].asString(); - if (!config_output.empty()) { - if (config_output.substr(0, 1) == "!") { - return config_output.substr(1) != output.name && - config_output.substr(1) != output.identifier; - } - return config_output == output.name || config_output == output.identifier; - } - } - - return true; -} - struct waybar::waybar_output &waybar::Client::getOutput(void *addr) { auto it = std::find_if( outputs_.begin(), outputs_.end(), [&addr](const auto &output) { return &output == addr; }); @@ -103,17 +61,7 @@ struct waybar::waybar_output &waybar::Client::getOutput(void *addr) { } std::vector waybar::Client::getOutputConfigs(struct waybar_output &output) { - std::vector configs; - if (config_.isArray()) { - for (auto const &config : config_) { - if (config.isObject() && isValidOutput(config, output)) { - configs.push_back(config); - } - } - } else if (isValidOutput(config_, output)) { - configs.push_back(config_); - } - return configs; + return config.getOutputConfigs(output.name, output.identifier); } void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) { @@ -203,94 +151,14 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr mon outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; }); } -std::tuple waybar::Client::getConfigs( - const std::string &config, const std::string &style) const { - auto config_file = config.empty() ? getValidPath({ - "$XDG_CONFIG_HOME/waybar/config", - "$XDG_CONFIG_HOME/waybar/config.jsonc", - "$HOME/.config/waybar/config", - "$HOME/.config/waybar/config.jsonc", - "$HOME/waybar/config", - "$HOME/waybar/config.jsonc", - "/etc/xdg/waybar/config", - "/etc/xdg/waybar/config.jsonc", - SYSCONFDIR "/xdg/waybar/config", - "./resources/config", - }) - : config; - auto css_file = style.empty() ? getValidPath({ - "$XDG_CONFIG_HOME/waybar/style.css", - "$HOME/.config/waybar/style.css", - "$HOME/waybar/style.css", - "/etc/xdg/waybar/style.css", - SYSCONFDIR "/xdg/waybar/style.css", - "./resources/style.css", - }) - : style; - if (css_file.empty() || config_file.empty()) { - throw std::runtime_error("Missing required resources files"); +const std::string waybar::Client::getStyle(const std::string &style) { + auto css_file = style.empty() ? Config::findConfigPath({"style.css"}) : style; + if (!css_file) { + throw std::runtime_error("Missing required resource files"); } - spdlog::info("Resources files: {}, {}", config_file, css_file); - return {config_file, css_file}; -} - -auto waybar::Client::setupConfig(const std::string &config_file, int depth) -> void { - if (depth > 100) { - throw std::runtime_error("Aborting due to likely recursive include in config files"); - } - std::ifstream file(config_file); - if (!file.is_open()) { - throw std::runtime_error("Can't open config file"); - } - std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - util::JsonParser parser; - Json::Value tmp_config_ = parser.parse(str); - if (tmp_config_.isArray()) { - for (auto &config_part : tmp_config_) { - resolveConfigIncludes(config_part, depth); - } - } else { - resolveConfigIncludes(tmp_config_, depth); - } - mergeConfig(config_, tmp_config_); -} - -auto waybar::Client::resolveConfigIncludes(Json::Value &config, int depth) -> void { - Json::Value includes = config["include"]; - if (includes.isArray()) { - for (const auto &include : includes) { - spdlog::info("Including resource file: {}", include.asString()); - setupConfig(getValidPath({include.asString()}), ++depth); - } - } else if (includes.isString()) { - spdlog::info("Including resource file: {}", includes.asString()); - setupConfig(getValidPath({includes.asString()}), ++depth); - } -} - -auto waybar::Client::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) -> void { - if (!a_config_) { - // For the first config - a_config_ = b_config_; - } else if (a_config_.isObject() && b_config_.isObject()) { - for (const auto &key : b_config_.getMemberNames()) { - if (a_config_[key].isObject() && b_config_[key].isObject()) { - mergeConfig(a_config_[key], b_config_[key]); - } else { - a_config_[key] = b_config_[key]; - } - } - } else if (a_config_.isArray() && b_config_.isArray()) { - // This can happen only on the top-level array of a multi-bar config - for (Json::Value::ArrayIndex i = 0; i < b_config_.size(); i++) { - if (a_config_[i].isObject() && b_config_[i].isObject()) { - mergeConfig(a_config_[i], b_config_[i]); - } - } - } else { - spdlog::error("Cannot merge config, conflicting or invalid JSON types"); - } -} + spdlog::info("Using CSS file {}", css_file.value()); + return css_file.value(); +}; auto waybar::Client::setupCss(const std::string &css_file) -> void { css_provider_ = Gtk::CssProvider::create(); @@ -329,14 +197,14 @@ void waybar::Client::bindInterfaces() { int waybar::Client::main(int argc, char *argv[]) { bool show_help = false; bool show_version = false; - std::string config; - std::string style; + std::string config_opt; + std::string style_opt; std::string bar_id; std::string log_level; auto cli = clara::detail::Help(show_help) | clara::detail::Opt(show_version)["-v"]["--version"]("Show version") | - clara::detail::Opt(config, "config")["-c"]["--config"]("Config path") | - clara::detail::Opt(style, "style")["-s"]["--style"]("Style path") | + clara::detail::Opt(config_opt, "config")["-c"]["--config"]("Config path") | + clara::detail::Opt(style_opt, "style")["-s"]["--style"]("Style path") | clara::detail::Opt( log_level, "trace|debug|info|warning|error|critical|off")["-l"]["--log-level"]("Log level") | @@ -367,8 +235,8 @@ int waybar::Client::main(int argc, char *argv[]) { throw std::runtime_error("Bar need to run under Wayland"); } wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj()); - auto [config_file, css_file] = getConfigs(config, style); - setupConfig(config_file, 0); + config.load(config_opt); + auto css_file = getStyle(style_opt); setupCss(css_file); bindInterfaces(); gtk_app->hold(); diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 00000000..63149cbd --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,153 @@ +#include "config.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "util/json.hpp" + +namespace waybar { + +const std::vector Config::CONFIG_DIRS = { + "$XDG_CONFIG_HOME/waybar/", + "$HOME/.config/waybar/", + "$HOME/waybar/", + "/etc/xdg/waybar/", + SYSCONFDIR "/xdg/waybar/", + "./resources/", +}; + +std::optional tryExpandPath(const std::string &path) { + wordexp_t p; + if (wordexp(path.c_str(), &p, 0) == 0) { + if (access(*p.we_wordv, F_OK) == 0) { + std::string result = *p.we_wordv; + wordfree(&p); + return result; + } + wordfree(&p); + } + return std::nullopt; +} + +std::optional Config::findConfigPath(const std::vector &names, + const std::vector &dirs) { + std::vector paths; + for (const auto &dir : dirs) { + for (const auto &name : names) { + if (auto res = tryExpandPath(dir + name); res) { + return res; + } + } + } + return std::nullopt; +} + +void Config::setupConfig(Json::Value &dst, const std::string &config_file, int depth) { + if (depth > 100) { + throw std::runtime_error("Aborting due to likely recursive include in config files"); + } + std::ifstream file(config_file); + if (!file.is_open()) { + throw std::runtime_error("Can't open config file"); + } + std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + util::JsonParser parser; + Json::Value tmp_config = parser.parse(str); + if (tmp_config.isArray()) { + for (auto &config_part : tmp_config) { + resolveConfigIncludes(config_part, depth); + } + } else { + resolveConfigIncludes(tmp_config, depth); + } + mergeConfig(dst, tmp_config); +} + +void Config::resolveConfigIncludes(Json::Value &config, int depth) { + Json::Value includes = config["include"]; + if (includes.isArray()) { + for (const auto &include : includes) { + spdlog::info("Including resource file: {}", include.asString()); + setupConfig(config, tryExpandPath(include.asString()).value_or(""), ++depth); + } + } else if (includes.isString()) { + spdlog::info("Including resource file: {}", includes.asString()); + setupConfig(config, tryExpandPath(includes.asString()).value_or(""), ++depth); + } +} + +void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) { + if (!a_config_) { + // For the first config + a_config_ = b_config_; + } else if (a_config_.isObject() && b_config_.isObject()) { + for (const auto &key : b_config_.getMemberNames()) { + // [] creates key with default value. Use `get` to avoid that. + if (a_config_.get(key, Json::Value::nullSingleton()).isObject() && + b_config_[key].isObject()) { + mergeConfig(a_config_[key], b_config_[key]); + } else if (!a_config_.isMember(key)) { + // do not allow overriding value set by top or previously included config + a_config_[key] = b_config_[key]; + } else { + spdlog::trace("Option {} is already set; ignoring value {}", key, b_config_[key]); + } + } + } else { + spdlog::error("Cannot merge config, conflicting or invalid JSON types"); + } +} +bool isValidOutput(const Json::Value &config, const std::string &name, + const std::string &identifier) { + if (config["output"].isArray()) { + for (auto const &output_conf : config["output"]) { + if (output_conf.isString() && + (output_conf.asString() == name || output_conf.asString() == identifier)) { + return true; + } + } + return false; + } else if (config["output"].isString()) { + auto config_output = config["output"].asString(); + if (!config_output.empty()) { + if (config_output.substr(0, 1) == "!") { + return config_output.substr(1) != name && config_output.substr(1) != identifier; + } + return config_output == name || config_output == identifier; + } + } + + return true; +} + +void Config::load(const std::string &config) { + auto file = config.empty() ? findConfigPath({"config", "config.jsonc"}) : config; + if (!file) { + throw std::runtime_error("Missing required resource files"); + } + config_file_ = file.value(); + spdlog::info("Using configuration file {}", config_file_); + setupConfig(config_, config_file_, 0); +} + +std::vector Config::getOutputConfigs(const std::string &name, + const std::string &identifier) { + std::vector configs; + if (config_.isArray()) { + for (auto const &config : config_) { + if (config.isObject() && isValidOutput(config, name, identifier)) { + configs.push_back(config); + } + } + } else if (isValidOutput(config_, name, identifier)) { + configs.push_back(config_); + } + return configs; +} + +} // namespace waybar diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 3ebc6e7b..fcd668c5 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -173,7 +173,7 @@ auto waybar::modules::Backlight::update() -> void { return; } - const auto percent = best->get_max() == 0 ? 100 : best->get_actual() * 100 / best->get_max(); + const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); label_.set_markup(fmt::format( format_, fmt::arg("percent", std::to_string(percent)), fmt::arg("icon", getIcon(percent)))); getState(percent); diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 991ccc4d..d9748eda 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -287,7 +287,11 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { if (array != nullptr) { g_free(array); } +#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68 + array = static_cast(g_memdup2(data, size)); +#else array = static_cast(g_memdup(data, size)); +#endif lwidth = width; lheight = height; } diff --git a/subprojects/catch2.wrap b/subprojects/catch2.wrap new file mode 100644 index 00000000..356c4063 --- /dev/null +++ b/subprojects/catch2.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = Catch2-2.13.3 +source_url = https://github.com/catchorg/Catch2/archive/v2.13.3.zip +source_filename = Catch2-2.13.3.zip +source_hash = 1804feb72bc15c0856b4a43aa586c661af9c3685a75973b6a8fc0b950c7cfd13 +patch_url = https://github.com/mesonbuild/catch2/releases/download/2.13.3-2/catch2.zip +patch_filename = catch2-2.13.3-2-wrap.zip +patch_hash = 21b590ab8c65b593ad5ee8f8e5b822bf9877b2c2672f97fbb52459751053eadf + +[provide] +catch2 = catch2_dep + diff --git a/test/config.cpp b/test/config.cpp new file mode 100644 index 00000000..f09f5da6 --- /dev/null +++ b/test/config.cpp @@ -0,0 +1,115 @@ +#define CATCH_CONFIG_MAIN +#include "config.hpp" + +#include + +TEST_CASE("Load simple config", "[config]") { + waybar::Config conf; + conf.load("test/config/simple.json"); + + SECTION("validate the config data") { + auto& data = conf.getConfig(); + REQUIRE(data["layer"].asString() == "top"); + REQUIRE(data["height"].asInt() == 30); + } + SECTION("select configs for configured output") { + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(configs.size() == 1); + } + SECTION("select configs for missing output") { + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(configs.empty()); + } +} + +TEST_CASE("Load config with multiple bars", "[config]") { + waybar::Config conf; + conf.load("test/config/multi.json"); + + SECTION("select multiple configs #1") { + auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0"); + REQUIRE(data.size() == 3); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[1]["layer"].asString() == "top"); + REQUIRE(data[1]["position"].asString() == "bottom"); + REQUIRE(data[1]["height"].asInt() == 21); + REQUIRE(data[2]["layer"].asString() == "overlay"); + REQUIRE(data[2]["position"].asString() == "right"); + REQUIRE(data[2]["height"].asInt() == 23); + } + SECTION("select multiple configs #2") { + auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(data.size() == 2); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[1]["layer"].asString() == "overlay"); + REQUIRE(data[1]["position"].asString() == "right"); + REQUIRE(data[1]["height"].asInt() == 23); + } + SECTION("select single config by output description") { + auto data = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["layer"].asString() == "overlay"); + REQUIRE(data[0]["position"].asString() == "left"); + REQUIRE(data[0]["height"].asInt() == 22); + } +} + +TEST_CASE("Load simple config with include", "[config]") { + waybar::Config conf; + conf.load("test/config/include.json"); + + SECTION("validate the config data") { + auto& data = conf.getConfig(); + // config override behavior: preserve first included value + REQUIRE(data["layer"].asString() == "top"); + REQUIRE(data["height"].asInt() == 30); + // config override behavior: preserve value from the top config + REQUIRE(data["position"].asString() == "top"); + // config override behavior: explicit null is still a value and should be preserved + REQUIRE((data.isMember("nullOption") && data["nullOption"].isNull())); + } + SECTION("select configs for configured output") { + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(configs.size() == 1); + } + SECTION("select configs for missing output") { + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(configs.empty()); + } +} + +TEST_CASE("Load multiple bar config with include", "[config]") { + waybar::Config conf; + conf.load("test/config/include-multi.json"); + + SECTION("bar config with sole include") { + auto data = conf.getOutputConfigs("OUT-0", "Fake ouptut #0"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 20); + } + + SECTION("bar config with output and include") { + auto data = conf.getOutputConfigs("OUT-1", "Fake output #1"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 21); + } + + SECTION("bar config with output override") { + auto data = conf.getOutputConfigs("OUT-2", "Fake output #2"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 22); + } + + SECTION("multiple levels of include") { + auto data = conf.getOutputConfigs("OUT-3", "Fake output #3"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 23); + } + + auto& data = conf.getConfig(); + REQUIRE(data.isArray()); + REQUIRE(data.size() == 4); + REQUIRE(data[0]["output"].asString() == "OUT-0"); +} diff --git a/test/config/include-1.json b/test/config/include-1.json new file mode 100644 index 00000000..7c47a882 --- /dev/null +++ b/test/config/include-1.json @@ -0,0 +1,7 @@ +{ + "layer": "top", + "position": "bottom", + "height": 30, + "output": ["HDMI-0", "DP-0"], + "nullOption": "not null" +} diff --git a/test/config/include-2.json b/test/config/include-2.json new file mode 100644 index 00000000..741194f1 --- /dev/null +++ b/test/config/include-2.json @@ -0,0 +1,3 @@ +{ + "layer": "bottom" +} diff --git a/test/config/include-multi-0.json b/test/config/include-multi-0.json new file mode 100644 index 00000000..a4c3fc17 --- /dev/null +++ b/test/config/include-multi-0.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-0", + "height": 20 +} diff --git a/test/config/include-multi-1.json b/test/config/include-multi-1.json new file mode 100644 index 00000000..2b28d6c5 --- /dev/null +++ b/test/config/include-multi-1.json @@ -0,0 +1,3 @@ +{ + "height": 21 +} diff --git a/test/config/include-multi-2.json b/test/config/include-multi-2.json new file mode 100644 index 00000000..f74c2b4e --- /dev/null +++ b/test/config/include-multi-2.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-1", + "height": 22 +} diff --git a/test/config/include-multi-3-0.json b/test/config/include-multi-3-0.json new file mode 100644 index 00000000..11cdd3f9 --- /dev/null +++ b/test/config/include-multi-3-0.json @@ -0,0 +1,3 @@ +{ + "height": 23 +} diff --git a/test/config/include-multi-3.json b/test/config/include-multi-3.json new file mode 100644 index 00000000..309fe15e --- /dev/null +++ b/test/config/include-multi-3.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-3", + "include": "test/config/include-multi-3-0.json" +} diff --git a/test/config/include-multi.json b/test/config/include-multi.json new file mode 100644 index 00000000..e128aba5 --- /dev/null +++ b/test/config/include-multi.json @@ -0,0 +1,16 @@ +[ + { + "include": "test/config/include-multi-0.json" + }, + { + "output": "OUT-1", + "include": "test/config/include-multi-1.json" + }, + { + "output": "OUT-2", + "include": "test/config/include-multi-2.json" + }, + { + "include": "test/config/include-multi-3.json" + } +] diff --git a/test/config/include.json b/test/config/include.json new file mode 100644 index 00000000..c46aaf24 --- /dev/null +++ b/test/config/include.json @@ -0,0 +1,5 @@ +{ + "include": ["test/config/include-1.json", "test/config/include-2.json"], + "position": "top", + "nullOption": null +} diff --git a/test/config/multi.json b/test/config/multi.json new file mode 100644 index 00000000..ed43a39e --- /dev/null +++ b/test/config/multi.json @@ -0,0 +1,25 @@ +[ + { + "layer": "bottom", + "height": 20, + "output": ["HDMI-0", "DP-0"] + }, + { + "position": "bottom", + "layer": "top", + "height": 21, + "output": ["DP-0"] + }, + { + "position": "left", + "layer": "overlay", + "height": 22, + "output": "Fake HDMI output #1" + }, + { + "position": "right", + "layer": "overlay", + "height": 23, + "output": "!HDMI-1" + } +] diff --git a/test/config/simple.json b/test/config/simple.json new file mode 100644 index 00000000..1cb1e3a1 --- /dev/null +++ b/test/config/simple.json @@ -0,0 +1,5 @@ +{ + "layer": "top", + "height": 30, + "output": ["HDMI-0", "DP-0"] +} diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 00000000..85b9771f --- /dev/null +++ b/test/meson.build @@ -0,0 +1,21 @@ +test_inc = include_directories('../include') +test_dep = [ + catch2, + fmt, + jsoncpp, + spdlog, +] + +config_test = executable( + 'config_test', + 'config.cpp', + '../src/config.cpp', + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'Configuration test', + config_test, + workdir: meson.source_root(), +)