1 #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
2 #include <iostream>
3 #include <doctest/doctest.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <sys/file.h>
7 #include <iostream>
8 #include <fstream>
9 
10 #include <wayfire/config/file.hpp>
11 #include <wayfire/util/log.hpp>
12 #include <wayfire/config/types.hpp>
13 #include "wayfire/config/compound-option.hpp"
14 #include "../src/option-impl.hpp"
15 
16 const std::string contents =
17     R"(
18 illegal_option = value
19 
20 [section1]
21 option1 = value1
22 option2=3
23 #Comment
24 option3         = value value value      # Comment
25 
26 hey_a = 15
27 bey_a = 1.2
28 
29 [section2]
30 option1 = value 4 \
31 value # Ignore
32 option2 = value \\
33 # Ignore
34 option3 = \#No way
35 
36 [wrongsection
37 option1
38 )";
39 
40 #include "expect_line.hpp"
41 
42 
43 TEST_CASE("wf::config::load_configuration_options_from_string")
44 {
45     std::stringstream log;
46     wf::log::initialize_logging(log, wf::log::LOG_LEVEL_DEBUG,
47         wf::log::LOG_COLOR_MODE_OFF);
48 
49     using wf::config::compound_option_t;
50     using wf::config::compound_option_entry_t;
51 
52     compound_option_t::entries_t entries;
53     entries.push_back(std::make_unique<compound_option_entry_t<int>>("hey_"));
54     entries.push_back(std::make_unique<compound_option_entry_t<double>>("bey_"));
55     auto opt = new compound_option_t{"option_list", std::move(entries)};
56 
57     using namespace wf;
58     using namespace wf::config;
59     config_manager_t config;
60 
61     /* Create the first section and add an option there */
62     auto section = std::make_shared<section_t>("section1");
63     section->register_new_option(
64         std::make_shared<option_t<int>>("option1", 10));
65     section->register_new_option(
66         std::make_shared<option_t<int>>("option2", 5));
67     section->register_new_option(
68         std::make_shared<option_t<std::string>>("option4", std::string("option4")));
69     section->register_new_option(std::shared_ptr<option_base_t>(opt));
70 
71     config.merge_section(section);
72     load_configuration_options_from_string(config, contents, "test");
73 
74     REQUIRE(config.get_section("section1"));
75     REQUIRE(config.get_section("section2"));
76     CHECK(config.get_section("wrongsection") == nullptr);
77 
78     auto s1 = config.get_section("section1");
79     auto s2 = config.get_section("section2");
80 
81     CHECK(s1->get_option("option1")->get_value_str() == "10");
82     CHECK(s1->get_option("option2")->get_value_str() == "3");
83     CHECK(s1->get_option("option3")->get_value_str() == "value value value");
84     CHECK(s1->get_option("option4")->get_value_str() == "option4");
85 
86     CHECK(s2->get_option("option1")->get_value_str() == "value 4 value");
87     CHECK(s2->get_option("option2")->get_value_str() == "value \\");
88     CHECK(s2->get_option("option3")->get_value_str() == "#No way");
89     CHECK(!s2->get_option_or("Ignored"));
90 
91     CHECK(opt->get_value<int, double>().size() == 1);
92 
93     EXPECT_LINE(log, "Error in file test:2");
94     EXPECT_LINE(log, "Error in file test:5");
95     EXPECT_LINE(log, "Error in file test:20");
96     EXPECT_LINE(log, "Error in file test:21");
97 }
98 
99 TEST_CASE("wf::config::load_configuration_options_from_string - "
100           "ignore compound entries not in string")
101 {
102     using namespace wf;
103     using namespace wf::config;
104 
105     // Compound option with prefix `test_`
106     compound_option_t::entries_t entries;
107     entries.push_back(std::make_unique<compound_option_entry_t<std::string>>("test_"));
108     auto opt = new compound_option_t{"option_list", std::move(entries)};
109 
110     config_manager_t config;
111 
112     // Register compound option
113     auto section = std::make_shared<section_t>("section");
114     section->register_new_option(std::shared_ptr<option_base_t>(opt));
115 
116     // Register an option which does not come from the config file
117     section->register_new_option(
118         std::make_shared<option_t<std::string>>("test_nofile", ""));
119     config.merge_section(section);
120 
121     // Update with an emtpy string. The compoud option should remain empty, because
122     // there are no values from the config file.
123     load_configuration_options_from_string(config, "");
124     CHECK(opt->get_value_untyped().empty());
125 }
126 
127 const std::string minimal_config_with_opt = R"(
128 [section]
129 option = value
130 )";
131 
132 TEST_CASE("wf::config::load_configuration_options_from_string - lock & reload")
133 {
134     using namespace wf;
135     using namespace wf::config;
136 
137     config_manager_t cfg;
138     load_configuration_options_from_string(cfg, minimal_config_with_opt);
139 
140     SUBCASE("locked")
141     {
142         cfg.get_option("section/option")->set_locked();
143         load_configuration_options_from_string(cfg, "");
144         CHECK(cfg.get_option("section/option")->get_value_str() == "value");
145     }
146 
147     SUBCASE("unlocked")
148     {
149         load_configuration_options_from_string(cfg, "");
150         CHECK(cfg.get_option("section/option")->get_value_str() == "");
151     }
152 }
153 
build_simple_config()154 wf::config::config_manager_t build_simple_config()
155 {
156     using namespace wf;
157     using namespace wf::config;
158     auto section1 = std::make_shared<section_t>("section1");
159     auto section2 = std::make_shared<section_t>("section2");
160 
161     section1->register_new_option(std::make_shared<option_t<int>>("option1", 4));
162     section1->register_new_option(std::make_shared<option_t<std::string>>("option2",
163         std::string("45 # 46 \\")));
164     section2->register_new_option(std::make_shared<option_t<double>>("option1",
165         4.25));
166 
167     compound_option_t::entries_t entries;
168     entries.push_back(std::make_unique<compound_option_entry_t<int>>("hey_"));
169     entries.push_back(std::make_unique<compound_option_entry_t<double>>("bey_"));
170     auto opt = new compound_option_t{"option_list", std::move(entries)};
171     opt->set_value(compound_list_t<int, double>{{"k1", 1, 1.2}});
172     section2->register_new_option(std::shared_ptr<compound_option_t>(opt));
173 
174     config_manager_t config;
175     config.merge_section(section1);
176     config.merge_section(section2);
177 
178     return config;
179 }
180 
181 std::string simple_config_source =
182     R"([section1]
183 option1 = 4
184 option2 = 45 \# 46 \\
185 
186 [section2]
187 bey_k1 = 1.200000
188 hey_k1 = 1
189 option1 = 4.250000
190 
191 )";
192 
193 
194 TEST_CASE("wf::config::save_configuration_options_to_string")
195 {
196     auto config = build_simple_config();
197     auto stringified = save_configuration_options_to_string(config);
198     CHECK(stringified == simple_config_source);
199 }
200 
201 TEST_CASE("wf::config::save_configuration_options_to_string - compound options erase")
202 {
203     using namespace wf;
204     using namespace wf::config;
205 
206     compound_option_t::entries_t entries;
207     entries.push_back(std::make_unique<compound_option_entry_t<int>>("hey_"));
208     entries.push_back(std::make_unique<compound_option_entry_t<double>>("bey_"));
209     auto opt = new compound_option_t{"option_list", std::move(entries)};
210     opt->set_value(compound_list_t<int, double>{{"k1", 1, 1.2}});
211 
212     auto section = std::make_shared<section_t>("Section");
213     section->register_new_option(std::shared_ptr<compound_option_t>(opt));
214 
215     // Add the same entries as in the compound option
216     section->register_new_option(std::make_shared<option_t<std::string>>("hey_k1",
217         "1"));
218     section->register_new_option(std::make_shared<option_t<std::string>>("bey_k1",
219         "1.2"));
220 
221     // However, make sure that XML-created options are saved even if they match
222     // the prefix of a compound option.
223     auto special_opt = std::make_shared<option_t<int>>("hey_you", 1);
224     special_opt->priv->xml = (xmlNode*)0x123;
225     section->register_new_option(special_opt);
226 
227     config_manager_t cfg;
228     cfg.merge_section(section);
229 
230     // Now, clear the value from the compound option
231     opt->set_value(compound_list_t<int, double>{});
232 
233     auto str = save_configuration_options_to_string(cfg);
234     // We expect that after deleting the values from the compound option,
235     // the values for k1 are not saved to the string.
236     const std::string expected =
237         R"([Section]
238 hey_you = 1
239 
240 )";
241 
242     CHECK(str == expected);
243 }
244 
245 TEST_CASE("wf::config::load_configuration_options_from_file - no such file")
246 {
247     std::string test_config = std::string("FileDoesNotExist");
248     wf::config::config_manager_t manager;
249     CHECK(!load_configuration_options_from_file(manager, test_config));
250 }
251 
252 TEST_CASE("wf::config::load_configuration_options_from_file - locking fails")
253 {
254     std::string test_config = std::string("../test/config_lock.ini");
255 
256     const int delay = 100e3; /** 100ms */
257 
258     int pid = fork();
259     if (pid == 0)
260     {
261         /* Lock config file before parent tries to lock it */
262         int fd = open(test_config.c_str(), O_RDWR);
263         flock(fd, LOCK_EX);
264 
265         /* Obtained a lock. Now wait until parent tries to lock */
266         usleep(2 * delay);
267 
268         /* By now, parent should have failed. */
269         flock(fd, LOCK_UN);
270         close(fd);
271     }
272 
273     /* Wait for other process to lock the file */
274     usleep(delay);
275 
276     wf::config::config_manager_t manager;
277     CHECK(!load_configuration_options_from_file(manager, test_config));
278 }
279 
check_int_test_config(const wf::config::config_manager_t & manager,std::string value_opt1="12")280 void check_int_test_config(const wf::config::config_manager_t& manager,
281     std::string value_opt1 = "12")
282 {
283     auto s1 = manager.get_section("section1");
284     auto s2 = manager.get_section("section2");
285     REQUIRE(s1 != nullptr);
286     REQUIRE(s2 != nullptr);
287 
288     auto o1 = manager.get_option("section1/option1");
289     auto o2 = manager.get_option("section2/option2");
290     auto o3 = manager.get_option("section2/option3");
291 
292     REQUIRE(o1);
293     REQUIRE(o2);
294     REQUIRE(o3);
295     CHECK(o1->get_value_str() == value_opt1);
296     CHECK(o2->get_value_str() == "opt2");
297     CHECK(o3->get_value_str() == "DoesNotExistInXML # \\");
298 }
299 
300 TEST_CASE("wf::config::load_configuration_options_from_file - success")
301 {
302     std::string test_config = std::string(TEST_SOURCE "/int_test/config.ini");
303 
304     /* Init with one section */
305     wf::config::config_manager_t manager;
306     auto s = std::make_shared<wf::config::section_t>("section1");
307     s->register_new_option(
308         std::make_shared<wf::config::option_t<int>>("option1", 1));
309     manager.merge_section(s);
310 
311     CHECK(load_configuration_options_from_file(manager, test_config));
312     REQUIRE(manager.get_section("section1") == s);
313     check_int_test_config(manager);
314 }
315 
316 TEST_CASE("wf::config::save_configuration_to_file - success")
317 {
318     std::string test_config = std::string(TEST_SOURCE "/dummy.ini");
319 
320     {
321         std::ofstream clr(test_config, std::ios::trunc | std::ios::ate);
322         clr << "Dummy";
323     }
324 
325     wf::config::save_configuration_to_file(build_simple_config(), test_config);
326 
327     /* Read file contents */
328     std::ifstream infile(test_config);
329     std::string file_contents((std::istreambuf_iterator<char>(infile)),
330         std::istreambuf_iterator<char>());
331 
332     CHECK(file_contents == simple_config_source);
333 
334     /* Check lock is released */
335     int fd = open(test_config.c_str(), O_RDWR);
336     CHECK(flock(fd, LOCK_EX | LOCK_NB) == 0);
337     flock(fd, LOCK_UN);
338     close(fd);
339 }
340 
341 TEST_CASE("wf::config::build_configuration")
342 {
343     wf::log::initialize_logging(std::cout, wf::log::LOG_LEVEL_DEBUG,
344         wf::log::LOG_COLOR_MODE_ON);
345     std::string xmldir   = std::string(TEST_SOURCE "/int_test/xml");
346     std::string sysconf  = std::string(TEST_SOURCE "/int_test/sys.ini");
347     std::string userconf = std::string(TEST_SOURCE "/int_test/config.ini");
348 
349     std::vector xmldirs(1, xmldir);
350     auto config = wf::config::build_configuration(xmldirs, sysconf, userconf);
351     check_int_test_config(config, "10");
352 
353     auto o1 = config.get_option("section1/option1");
354     auto o2 = config.get_option("section2/option2");
355     auto o3 = config.get_option("section2/option3");
356     auto o4 = config.get_option("section2/option4");
357     auto o5 = config.get_option("section2/option5");
358     auto o6 = config.get_option("sectionobj:objtest/option6");
359 
360     REQUIRE(o4);
361     REQUIRE(o5);
362 
363     using namespace wf;
364     using namespace wf::config;
365     CHECK(std::dynamic_pointer_cast<option_t<int>>(o1) != nullptr);
366     CHECK(std::dynamic_pointer_cast<option_t<std::string>>(o2) != nullptr);
367     CHECK(std::dynamic_pointer_cast<option_t<std::string>>(o3) != nullptr);
368     CHECK(std::dynamic_pointer_cast<option_t<std::string>>(o4) != nullptr);
369     CHECK(std::dynamic_pointer_cast<option_t<std::string>>(o5) != nullptr);
370     CHECK(std::dynamic_pointer_cast<option_t<int>>(o6) != nullptr);
371 
372     CHECK(o4->get_value_str() == "DoesNotExistInConfig");
373     CHECK(o5->get_value_str() == "Option5Sys");
374     CHECK(o6->get_value_str() == "10"); // bounds from xml applied
375 
376     o1->reset_to_default();
377     o2->reset_to_default();
378     o3->reset_to_default();
379     o4->reset_to_default();
380     o5->reset_to_default();
381     o6->reset_to_default();
382 
383     CHECK(o1->get_value_str() == "4");
384     CHECK(o2->get_value_str() == "XMLDefault");
385     CHECK(o3->get_value_str() == "");
386     CHECK(o4->get_value_str() == "DoesNotExistInConfig");
387     CHECK(o5->get_value_str() == "Option5Sys");
388     CHECK(o6->get_value_str() == "1");
389 }
390