Merge pull request #1243 from alebastr/config-unittest

Unit-tests for configuration includes
pull/1250/head^2
Alex 2021-09-17 20:56:09 +02:00 committed by GitHub
commit b028a47d57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 457 additions and 162 deletions

View File

@ -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

View File

@ -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

View File

@ -3,11 +3,10 @@
#include <fmt/format.h>
#include <gdk/gdk.h>
#include <gdk/gdkwayland.h>
#include <unistd.h>
#include <wayland-client.h>
#include <wordexp.h>
#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<std::unique_ptr<Bar>> bars;
Config config;
private:
Client() = default;
std::tuple<const std::string, const std::string> getConfigs(const std::string &config,
const std::string &style) const;
void bindInterfaces();
const std::string getValidPath(const std::vector<std::string> &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<Json::Value> getOutputConfigs(struct waybar_output &output);
@ -55,7 +49,6 @@ class Client {
void handleMonitorRemoved(Glib::RefPtr<Gdk::Monitor> monitor);
void handleDeferredMonitorRemoval(Glib::RefPtr<Gdk::Monitor> monitor);
Json::Value config_;
Glib::RefPtr<Gtk::StyleContext> style_context_;
Glib::RefPtr<Gtk::CssProvider> css_provider_;
std::list<struct waybar_output> outputs_;

39
include/config.hpp 100644
View File

@ -0,0 +1,39 @@
#pragma once
#include <json/json.h>
#include <optional>
#include <string>
#ifndef SYSCONFDIR
#define SYSCONFDIR "/etc"
#endif
namespace waybar {
class Config {
public:
static const std::vector<std::string> CONFIG_DIRS;
/* Try to find any of provided names in the supported set of config directories */
static std::optional<std::string> findConfigPath(
const std::vector<std::string> &names, const std::vector<std::string> &dirs = CONFIG_DIRS);
Config() = default;
void load(const std::string &config);
Json::Value &getConfig() { return config_; }
std::vector<Json::Value> 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

View File

@ -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

View File

@ -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()

View File

@ -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')

View File

@ -3,12 +3,10 @@
#include <fmt/ostream.h>
#include <spdlog/spdlog.h>
#include <fstream>
#include <iostream>
#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<std::string> &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<Client *>(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<Json::Value> waybar::Client::getOutputConfigs(struct waybar_output &output) {
std::vector<Json::Value> 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<Gdk::Monitor> mon
outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; });
}
std::tuple<const std::string, const std::string> 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<char>(file)), std::istreambuf_iterator<char>());
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();

153
src/config.cpp 100644
View File

@ -0,0 +1,153 @@
#include "config.hpp"
#include <fmt/ostream.h>
#include <spdlog/spdlog.h>
#include <unistd.h>
#include <wordexp.h>
#include <fstream>
#include <stdexcept>
#include "util/json.hpp"
namespace waybar {
const std::vector<std::string> Config::CONFIG_DIRS = {
"$XDG_CONFIG_HOME/waybar/",
"$HOME/.config/waybar/",
"$HOME/waybar/",
"/etc/xdg/waybar/",
SYSCONFDIR "/xdg/waybar/",
"./resources/",
};
std::optional<std::string> 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<std::string> Config::findConfigPath(const std::vector<std::string> &names,
const std::vector<std::string> &dirs) {
std::vector<std::string> 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<char>(file)), std::istreambuf_iterator<char>());
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<Json::Value> Config::getOutputConfigs(const std::string &name,
const std::string &identifier) {
std::vector<Json::Value> 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

View File

@ -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

115
test/config.cpp 100644
View File

@ -0,0 +1,115 @@
#define CATCH_CONFIG_MAIN
#include "config.hpp"
#include <catch2/catch.hpp>
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");
}

View File

@ -0,0 +1,7 @@
{
"layer": "top",
"position": "bottom",
"height": 30,
"output": ["HDMI-0", "DP-0"],
"nullOption": "not null"
}

View File

@ -0,0 +1,3 @@
{
"layer": "bottom"
}

View File

@ -0,0 +1,4 @@
{
"output": "OUT-0",
"height": 20
}

View File

@ -0,0 +1,3 @@
{
"height": 21
}

View File

@ -0,0 +1,4 @@
{
"output": "OUT-1",
"height": 22
}

View File

@ -0,0 +1,3 @@
{
"height": 23
}

View File

@ -0,0 +1,4 @@
{
"output": "OUT-3",
"include": "test/config/include-multi-3-0.json"
}

View File

@ -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"
}
]

View File

@ -0,0 +1,5 @@
{
"include": ["test/config/include-1.json", "test/config/include-2.json"],
"position": "top",
"nullOption": null
}

View File

@ -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"
}
]

View File

@ -0,0 +1,5 @@
{
"layer": "top",
"height": 30,
"output": ["HDMI-0", "DP-0"]
}

21
test/meson.build 100644
View File

@ -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(),
)