diff --git a/debian/libmiral7.symbols b/debian/libmiral7.symbols index 192c2df8320..46560b2caba 100644 --- a/debian/libmiral7.symbols +++ b/debian/libmiral7.symbols @@ -434,6 +434,8 @@ libmiral.so.7 libmiral7 #MINVER# (c++)"vtable for miral::MinimalWindowManager@MIRAL_5.0" 5.0.0 (c++)"vtable for miral::WindowManagementPolicy@MIRAL_5.0" 5.0.0 MIRAL_5.1@MIRAL_5.1 5.1.0 + (c++)"miral::ConfigFile::ConfigFile(miral::MirRunner&, std::filesystem::__cxx11::path, miral::ConfigFile::Mode, std::function >&, std::filesystem::__cxx11::path const&)>)@MIRAL_5.1" 5.1.0 + (c++)"miral::ConfigFile::~ConfigFile()@MIRAL_5.1" 5.1.0 (c++)"miral::Decorations::Decorations(std::shared_ptr)@MIRAL_5.1" 5.1.0 (c++)"miral::Decorations::always_csd()@MIRAL_5.1" 5.1.0 (c++)"miral::Decorations::always_ssd()@MIRAL_5.1" 5.1.0 diff --git a/include/miral/miral/config_file.h b/include/miral/miral/config_file.h new file mode 100644 index 00000000000..5edf291247c --- /dev/null +++ b/include/miral/miral/config_file.h @@ -0,0 +1,66 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIRAL_CONFIG_FILE_H +#define MIRAL_CONFIG_FILE_H + +#include + +#include +#include +#include + +namespace miral { class MirRunner; class FdHandle; } + +namespace miral +{ +/** + * Utility to locate and monitor a configuration file via the XDG Base Directory + * Specification. Vis: ($XDG_CONFIG_HOME or $HOME/.config followed by + * $XDG_CONFIG_DIRS). If, instead of a filename, a path is given, then the base + * directories are not applied. + * + * If mode is `no_reloading`, then the file is loaded on startup and not reloaded + * + * If mode is `reload_on_change`, then the file is loaded on startup and either + * the user-specific configuration file base ($XDG_CONFIG_HOME or $HOME/.config), + * or the supplied path is monitored for changes. + * \remark MirAL 5.1 + */ +class ConfigFile +{ +public: + /// Loader functor is passed both the open stream and the actual path (for use in reporting problems) + using Loader = std::function; + + /// Mode of reloading + enum class Mode + { + no_reloading, + reload_on_change + }; + + ConfigFile(MirRunner& runner, std::filesystem::path file, Mode mode, Loader load_config); + ~ConfigFile(); + +private: + + class Self; + std::shared_ptr self; +}; +} + +#endif //MIRAL_CONFIG_FILE_H diff --git a/src/miral/CMakeLists.txt b/src/miral/CMakeLists.txt index 2a268148e0c..84351a2e81c 100644 --- a/src/miral/CMakeLists.txt +++ b/src/miral/CMakeLists.txt @@ -55,6 +55,7 @@ add_library(miral-external OBJECT application_authorizer.cpp ${miral_include}/miral/application_authorizer.h application_info.cpp ${miral_include}/miral/application_info.h canonical_window_manager.cpp ${miral_include}/miral/canonical_window_manager.h + config_file.cpp ${miral_include}/miral/config_file.h configuration_option.cpp ${miral_include}/miral/configuration_option.h ${miral_include}/miral/command_line_option.h cursor_theme.cpp ${miral_include}/miral/cursor_theme.h diff --git a/src/miral/config_file.cpp b/src/miral/config_file.cpp new file mode 100644 index 00000000000..de9bef0d8c2 --- /dev/null +++ b/src/miral/config_file.cpp @@ -0,0 +1,221 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include + +#define MIR_LOG_COMPONENT "ReloadingConfigFile" +#include + +#include + +#include +#include + +#include +#include +#include +#include + +using namespace std::filesystem; + +namespace +{ +auto config_directory(path const& file) -> std::optional +{ + if (file.has_parent_path()) + { + return file.parent_path(); + } + else if (auto config_home = getenv("XDG_CONFIG_HOME")) + { + return config_home; + } + else if (auto home = getenv("HOME")) + { + return path(home) / ".config"; + } + else + { + return std::nullopt; + } +} + +auto watch_descriptor(mir::Fd const& inotify_fd, std::optional const& path) -> std::optional +{ + if (!path.has_value()) + return std::nullopt; + + if (inotify_fd < 0) + BOOST_THROW_EXCEPTION((std::system_error{errno, std::system_category(), "Failed to initialize inotify_fd"})); + + return inotify_add_watch(inotify_fd, path.value().c_str(), IN_CLOSE_WRITE | IN_CREATE | IN_MOVED_TO); +} + +class Watcher +{ +public: + using Loader = miral::ConfigFile::Loader; + Watcher(miral::MirRunner& runner, path file, miral::ConfigFile::Loader load_config); + + mir::Fd const inotify_fd; + Loader const load_config; + path const filename; + std::optional const directory; + std::optional const directory_watch_descriptor; + + void register_handler(miral::MirRunner& runner); + std::unique_ptr fd_handle; +}; +} + +class miral::ConfigFile::Self +{ +public: + Self(MirRunner& runner, path file, Mode mode, Loader load_config); + +private: + std::unique_ptr watcher; +}; + +Watcher::Watcher(miral::MirRunner& runner, path file, miral::ConfigFile::Loader load_config) : + inotify_fd{inotify_init1(IN_CLOEXEC)}, + load_config{load_config}, + filename{file.filename()}, + directory{config_directory(file)}, + directory_watch_descriptor{watch_descriptor(inotify_fd, directory)} +{ + register_handler(runner); + + if (directory_watch_descriptor.has_value()) + { + mir::log_debug("Monitoring %s for configuration changes", (directory.value()/filename).c_str()); + } +} + +miral::ConfigFile::Self::Self(MirRunner& runner, path file, Mode mode, Loader load_config) +{ + auto const filename{file.filename()}; + auto const directory{config_directory(file)}; + + // With C++26 we should be able to use the optional directory as a range to + // initialize config_roots. Until then, we'll just do it the long way... + std::vector config_roots; + + if (directory) + { + config_roots.push_back(directory.value()); + } + + if (auto config_dirs = getenv("XDG_CONFIG_DIRS")) + { + std::istringstream config_stream{config_dirs}; + for (std::string config_root; getline(config_stream, config_root, ':');) + { + config_roots.push_back(config_root); + } + } + else + { + config_roots.push_back("/etc/xdg"); + } + + /* Read config file */ + for (auto const& config_root : config_roots) + { + auto filepath = config_root / filename; + if (std::ifstream config_file{filepath}) + { + load_config(config_file, filepath); + mir::log_debug("Loaded %s", filepath.c_str()); + break; + } + } + + switch (mode) + { + case Mode::no_reloading: + break; + + case Mode::reload_on_change: + watcher = std::make_unique(runner, file, std::move(load_config)); + break; + } +} + +miral::ConfigFile::ConfigFile(MirRunner& runner, path file, Mode mode, Loader load_config) : + self{std::make_shared(runner, file, mode, load_config)} +{ +} + +miral::ConfigFile::~ConfigFile() = default; + +void Watcher::register_handler(miral::MirRunner& runner) +{ + if (directory_watch_descriptor) + { + fd_handle = runner.register_fd_handler(inotify_fd, [this] (int) + { + static size_t const sizeof_inotify_event = sizeof(inotify_event); + + // A union ensures buffer is aligned for inotify_event + union + { + char buffer[sizeof_inotify_event + NAME_MAX + 1]; + inotify_event unused [[maybe_unused]]; + } inotify_magic; + + auto const readsize = read(inotify_fd, &inotify_magic, sizeof(inotify_magic)); + if (readsize < static_cast(sizeof_inotify_event)) + { + return; + } + + auto raw_buffer = inotify_magic.buffer; + while (raw_buffer != inotify_magic.buffer + readsize) + { + // This is safe because inotify_magic.buffer is aligned and event.len includes padding for alignment + auto& event = reinterpret_cast(*raw_buffer); + if (event.mask & (IN_CLOSE_WRITE | IN_MOVED_TO) && event.wd == directory_watch_descriptor.value()) + try + { + if (event.name == filename) + { + auto const& file = directory.value() / filename; + + if (std::ifstream config_file{file}) + { + load_config(config_file, file); + mir::log_debug("(Re)loaded %s", file.c_str()); + } + else + { + mir::log_debug("Failed to open %s", file.c_str()); + } + } + } + catch (...) + { + mir::log(mir::logging::Severity::warning, MIR_LOG_COMPONENT, std::current_exception(), + "Failed to reload configuration"); + } + + raw_buffer += sizeof_inotify_event+event.len; + } + }); + } +} diff --git a/src/miral/symbols.map b/src/miral/symbols.map index 79b4961cfd4..a2d1fd49702 100644 --- a/src/miral/symbols.map +++ b/src/miral/symbols.map @@ -456,6 +456,8 @@ local: *; MIRAL_5.1 { global: extern "C++" { + miral::ConfigFile::?ConfigFile*; + miral::ConfigFile::ConfigFile*; miral::Decorations::Decorations*; miral::Decorations::always_csd*; miral::Decorations::always_ssd*; @@ -469,7 +471,9 @@ global: miral::IdleListener::on_wake*; miral::IdleListener::operator*; miral::WindowManagerTools::move_cursor_to*; - typeinfo?for?miral::IdleListener; + typeinfo?for?miral::ConfigFile; typeinfo?for?miral::Decorations; + typeinfo?for?miral::IdleListener; }; } MIRAL_5.0; + diff --git a/tests/miral/CMakeLists.txt b/tests/miral/CMakeLists.txt index e7d52891c62..86865a67d0f 100644 --- a/tests/miral/CMakeLists.txt +++ b/tests/miral/CMakeLists.txt @@ -69,6 +69,7 @@ target_link_libraries(miral-test-internal mir_add_wrapped_executable(miral-test NOINSTALL external_client.cpp + config_file.cpp runner.cpp wayland_extensions.cpp zone.cpp diff --git a/tests/miral/config_file.cpp b/tests/miral/config_file.cpp new file mode 100644 index 00000000000..f56bd6ad474 --- /dev/null +++ b/tests/miral/config_file.cpp @@ -0,0 +1,614 @@ +/* +* Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "miral/test_server.h" +#include "miral/config_file.h" + +#include +#include + +#include +#include + +using miral::ConfigFile; + +namespace +{ +char const* const no_such_file = "no/such/file"; +char const* const a_file = "/tmp/test_reloading_config_file/a_file"; +std::filesystem::path const config_file = "test_reloading_config_file.config"; + +class PendingLoad +{ +public: + void mark_pending() + { + std::lock_guard lock{mutex}; + pending_loads = true; + } + + void wait_for_load() + { + std::unique_lock lock{mutex}; + + if (!cv.wait_for(lock, std::chrono::milliseconds{10}, [this] { return !pending_loads; })) + { + std::cerr << "wait_for_load() timed out" << std::endl; + } + } + + void notify_load() + { + { + std::lock_guard lock{mutex}; + pending_loads = false; + } + + cv.notify_one(); + } + +private: + std::mutex mutex; + std::condition_variable cv; + bool pending_loads = false; +}; + +struct TestConfigFile : miral::TestServer, PendingLoad +{ + TestConfigFile(); + MOCK_METHOD(void, load, (std::istream& in, std::filesystem::path path), ()); + + std::optional reloading_config_file; + + void write_a_file() + { + mark_pending(); + std::ofstream file(a_file); + file << "some content"; + } + + void write_config_in(std::filesystem::path path) + { + mark_pending(); + std::ofstream file(path/config_file); + file << "some content"; + } + + void SetUp() override + { + miral::TestServer::SetUp(); + ON_CALL(*this, load(testing::_, testing::_)).WillByDefault([this]{ notify_load(); }); + } +}; + +char const* const home = "/tmp/test_reloading_config_file/home"; +char const* const home_config = "/tmp/test_reloading_config_file/home/.config"; +char const* const xdg_conf_home = "/tmp/test_reloading_config_file/xdg_conf_dir_home"; +char const* const xdg_conf_dir0 = "/tmp/test_reloading_config_file/xdg_conf_dir_zero"; +char const* const xdg_conf_dir1 = "/tmp/test_reloading_config_file/xdg_conf_dir_one"; +char const* const xdg_conf_dir2 = "/tmp/test_reloading_config_file/xdg_conf_dir_two"; + +TestConfigFile::TestConfigFile() +{ + std::filesystem::remove_all("/tmp/test_reloading_config_file/"); + + for (auto dir : {home_config, xdg_conf_home, xdg_conf_dir0, xdg_conf_dir1, xdg_conf_dir2}) + { + std::filesystem::create_directories(dir); + } + + add_to_environment("HOME", home); + add_to_environment("XDG_CONFIG_HOME", xdg_conf_home); + add_to_environment("XDG_CONFIG_DIRS", std::format("{}:{}:{}", xdg_conf_dir0, xdg_conf_dir1, xdg_conf_dir2).c_str()); +} + +} + +TEST_F(TestConfigFile, with_reload_on_change_and_no_file_nothing_is_loaded) +{ + EXPECT_CALL(*this, load).Times(0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + no_such_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); +} + +TEST_F(TestConfigFile, with_reload_on_change_and_a_file_something_is_loaded) +{ + EXPECT_CALL(*this, load).Times(1); + + write_a_file(); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + a_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_when_a_file_is_written_something_is_loaded) +{ + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + a_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + EXPECT_CALL(*this, load).Times(1); + + write_a_file(); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_each_time_a_file_is_rewritten_something_is_loaded) +{ + auto const times = 42; + + EXPECT_CALL(*this, load).Times(times+1); + + write_a_file(); // Initial write + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + a_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + for (auto i = 0; i != times; ++i) + { + wait_for_load(); + write_a_file(); + } + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_when_config_home_unset_a_file_in_home_config_is_loaded) +{ + add_to_environment("XDG_CONFIG_HOME", nullptr); + using testing::_; + EXPECT_CALL(*this, load(_, home_config/config_file)).Times(1); + + write_config_in(xdg_conf_home); + write_config_in(home_config); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_a_file_in_xdg_config_home_is_loaded) +{ + using testing::_; + EXPECT_CALL(*this, load(_, xdg_conf_home/config_file)).Times(1); + + write_config_in(xdg_conf_dir0); + write_config_in(xdg_conf_home); + write_config_in(home_config); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_a_file_in_xdg_config_home_is_reloaded) +{ + using testing::_; + EXPECT_CALL(*this, load(_, xdg_conf_home/config_file)).Times(2); + + write_config_in(xdg_conf_dir0); + write_config_in(xdg_conf_home); + write_config_in(home_config); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); + + write_config_in(xdg_conf_dir0); + write_config_in(xdg_conf_home); + write_config_in(home_config); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_a_config_in_xdg_conf_dir0_is_loaded) +{ + using testing::_; + EXPECT_CALL(*this, load(_, xdg_conf_dir0/config_file)).Times(1); + + write_config_in(xdg_conf_dir0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_after_a_config_in_xdg_conf_dir0_is_loaded_a_new_config_in_xdg_conf_home_is_loaded) +{ + using testing::_; + + testing::InSequence sequence; + EXPECT_CALL(*this, load(_, xdg_conf_dir0/config_file)).Times(1); + EXPECT_CALL(*this, load(_, xdg_conf_home/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + write_config_in(xdg_conf_dir1); + write_config_in(xdg_conf_dir0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); + + write_config_in(xdg_conf_home); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_a_config_in_xdg_conf_dir0_is_loaded_in_preference_to_dir1_or_2) +{ + using testing::_; + + EXPECT_CALL(*this, load(_, xdg_conf_dir0/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + write_config_in(xdg_conf_dir1); + write_config_in(xdg_conf_dir0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_a_config_in_xdg_conf_dir1_is_loaded_in_preference_to_dir2) +{ + using testing::_; + + EXPECT_CALL(*this, load(_, xdg_conf_dir1/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + write_config_in(xdg_conf_dir1); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_reload_on_change_a_config_in_xdg_conf_dir2_is_loaded) +{ + using testing::_; + + EXPECT_CALL(*this, load(_, xdg_conf_dir2/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::reload_on_change, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_and_no_file_nothing_is_loaded) +{ + EXPECT_CALL(*this, load).Times(0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + no_such_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); +} + +TEST_F(TestConfigFile, with_no_reloading_and_a_file_something_is_loaded) +{ + EXPECT_CALL(*this, load).Times(1); + + write_a_file(); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + a_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_when_a_file_is_written_nothing_is_loaded) +{ + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + a_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + EXPECT_CALL(*this, load).Times(0); + + write_a_file(); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_when_a_file_is_rewritten_nothing_is_reloaded) +{ + EXPECT_CALL(*this, load).Times(1); + + write_a_file(); // Initial write + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + a_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); + write_a_file(); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_when_config_home_unset_a_file_in_home_config_is_loaded) +{ + add_to_environment("XDG_CONFIG_HOME", nullptr); + using testing::_; + EXPECT_CALL(*this, load(_, home_config/config_file)).Times(1); + + write_config_in(xdg_conf_home); + write_config_in(home_config); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_a_file_in_xdg_config_home_is_loaded) +{ + using testing::_; + EXPECT_CALL(*this, load(_, xdg_conf_home/config_file)).Times(1); + + write_config_in(xdg_conf_dir0); + write_config_in(xdg_conf_home); + write_config_in(home_config); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_a_file_in_xdg_config_home_is_not_reloaded) +{ + using testing::_; + EXPECT_CALL(*this, load(_, xdg_conf_home/config_file)).Times(1); + + write_config_in(xdg_conf_dir0); + write_config_in(xdg_conf_home); + write_config_in(home_config); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); + + write_config_in(xdg_conf_dir0); + write_config_in(xdg_conf_home); + write_config_in(home_config); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_a_config_in_xdg_conf_dir0_is_loaded) +{ + using testing::_; + EXPECT_CALL(*this, load(_, xdg_conf_dir0/config_file)).Times(1); + + write_config_in(xdg_conf_dir0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_after_a_config_in_xdg_conf_dir0_is_loaded_a_new_config_in_xdg_conf_home_is_not_loaded) +{ + using testing::_; + + testing::InSequence sequence; + EXPECT_CALL(*this, load(_, xdg_conf_dir0/config_file)).Times(1); + EXPECT_CALL(*this, load(_, xdg_conf_home/config_file)).Times(0); + + write_config_in(xdg_conf_dir2); + write_config_in(xdg_conf_dir1); + write_config_in(xdg_conf_dir0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); + + write_config_in(xdg_conf_home); + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_a_config_in_xdg_conf_dir0_is_loaded_in_preference_to_dir1_or_2) +{ + using testing::_; + + EXPECT_CALL(*this, load(_, xdg_conf_dir0/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + write_config_in(xdg_conf_dir1); + write_config_in(xdg_conf_dir0); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_a_config_in_xdg_conf_dir1_is_loaded_in_preference_to_dir2) +{ + using testing::_; + + EXPECT_CALL(*this, load(_, xdg_conf_dir1/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + write_config_in(xdg_conf_dir1); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +} + +TEST_F(TestConfigFile, with_no_reloading_a_config_in_xdg_conf_dir2_is_loaded) +{ + using testing::_; + + EXPECT_CALL(*this, load(_, xdg_conf_dir2/config_file)).Times(1); + + write_config_in(xdg_conf_dir2); + + invoke_runner([this](miral::MirRunner& runner) + { + reloading_config_file = ConfigFile{ + runner, + config_file, + ConfigFile::Mode::no_reloading, + [this](std::istream& in, std::filesystem::path path) { load(in, path); }}; + }); + + wait_for_load(); +}