Line data Source code
1 : // Copyright (c) 2016-2024 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/cluck
4 : // contact@m2osw.com
5 : //
6 : // This program is free software: you can redistribute it and/or modify
7 : // it under the terms of the GNU General Public License as published by
8 : // the Free Software Foundation, either version 3 of the License, or
9 : // (at your option) any later version.
10 : //
11 : // This program is distributed in the hope that it will be useful,
12 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : // GNU General Public License for more details.
15 : //
16 : // You should have received a copy of the GNU General Public License
17 : // along with this program. If not, see <https://www.gnu.org/licenses/>.
18 :
19 : // self
20 : //
21 : #include "catch_main.h"
22 :
23 :
24 :
25 : // daemon
26 : //
27 : #define private public
28 : #include <daemon/ticket.h>
29 : #undef private
30 :
31 : #include <daemon/cluckd.h>
32 :
33 :
34 : // cluck
35 : //
36 : #include <cluck/exception.h>
37 : #include <cluck/version.h>
38 :
39 :
40 : // cppthread
41 : //
42 : #include <cppthread/thread.h>
43 :
44 :
45 : // snapdev
46 : //
47 : #include <snapdev/gethostname.h>
48 : #include <snapdev/stringize.h>
49 :
50 :
51 : // last include
52 : //
53 : #include <snapdev/poison.h>
54 :
55 :
56 :
57 : namespace
58 : {
59 :
60 :
61 :
62 : char const * g_argv[2] = {
63 : "catch_daemon_ticket",
64 : nullptr
65 : };
66 :
67 :
68 : class cluckd_mock
69 : : public cluck_daemon::cluckd
70 : {
71 : public:
72 : cluckd_mock();
73 :
74 : private:
75 : };
76 :
77 :
78 6 : cluckd_mock::cluckd_mock()
79 6 : : cluckd(1, const_cast<char **>(g_argv))
80 : {
81 6 : }
82 :
83 :
84 : class messenger_mock
85 : : public cluck_daemon::messenger
86 : {
87 : public:
88 : typedef std::shared_ptr<messenger_mock> pointer_t;
89 :
90 : messenger_mock(cluck_daemon::cluckd * c, advgetopt::getopt & opts);
91 :
92 : // connection_with_send_message implementation
93 : //
94 : virtual bool send_message(ed::message & msg, bool cache = false) override;
95 : void add_expected_message(ed::message & msg);
96 :
97 : private:
98 : ed::message::list_t f_expected_message = ed::message::list_t();
99 : };
100 :
101 :
102 1 : messenger_mock::messenger_mock(cluck_daemon::cluckd * c, advgetopt::getopt & opts)
103 1 : : messenger(c, opts)
104 : {
105 1 : }
106 :
107 :
108 1 : bool messenger_mock::send_message(ed::message & msg, bool cache)
109 : {
110 1 : SNAP_LOG_INFO
111 : << "mock messenger captured message \""
112 : << msg
113 : << "\"."
114 : << SNAP_LOG_SEND;
115 :
116 1 : CATCH_REQUIRE_FALSE(cache);
117 :
118 1 : CATCH_REQUIRE_FALSE(f_expected_message.empty());
119 :
120 1 : CATCH_REQUIRE(f_expected_message.front().get_sent_from_server() == msg.get_sent_from_server());
121 1 : CATCH_REQUIRE(f_expected_message.front().get_sent_from_service() == msg.get_sent_from_service());
122 1 : CATCH_REQUIRE(f_expected_message.front().get_server() == msg.get_server());
123 1 : CATCH_REQUIRE(f_expected_message.front().get_service() == msg.get_service());
124 1 : CATCH_REQUIRE(f_expected_message.front().get_command() == msg.get_command());
125 :
126 6 : for(auto p : f_expected_message.front().get_all_parameters())
127 : {
128 5 : CATCH_REQUIRE(msg.has_parameter(p.first));
129 5 : CATCH_REQUIRE(msg.get_parameter(p.first) == p.second);
130 5 : }
131 :
132 1 : f_expected_message.pop_front();
133 :
134 1 : return true;
135 : }
136 :
137 :
138 1 : void messenger_mock::add_expected_message(ed::message & msg)
139 : {
140 1 : f_expected_message.push_back(msg);
141 1 : }
142 :
143 :
144 :
145 : } // no name namespace
146 :
147 :
148 :
149 2 : CATCH_TEST_CASE("daemon_ticket", "[cluckd][ticket][daemon]")
150 : {
151 4 : CATCH_START_SECTION("daemon_ticket: verify defaults")
152 : {
153 1 : cluck::timeout_t obtention_timeout(snapdev::now());
154 1 : obtention_timeout += cluck::timeout_t(5, 0);
155 1 : cluckd_mock d;
156 1 : cluck_daemon::ticket t(
157 : &d
158 : , nullptr
159 : , "ticket_test"
160 : , 123
161 : , "rc/5003"
162 : , obtention_timeout
163 2 : , cluck::timeout_t(10, 0)
164 : , "rc"
165 11 : , "website");
166 :
167 1 : CATCH_REQUIRE(t.get_owner() == snapdev::gethostname());
168 1 : t.set_owner("rc3");
169 1 : CATCH_REQUIRE(t.get_owner() == "rc3");
170 :
171 1 : CATCH_REQUIRE(t.get_client_pid() == 5003);
172 :
173 1 : CATCH_REQUIRE(t.get_serial() == cluck_daemon::ticket::NO_SERIAL);
174 1 : t.set_serial(93);
175 1 : CATCH_REQUIRE(t.get_serial() == 93);
176 :
177 1 : CATCH_REQUIRE(t.get_unlock_duration() == cluck::timeout_t(10, 0));
178 1 : t.set_unlock_duration(cluck::timeout_t(3, 500000000));
179 1 : CATCH_REQUIRE(t.get_unlock_duration() == cluck::timeout_t(3, 500000000));
180 :
181 1 : CATCH_REQUIRE(t.get_ticket_number() == cluck_daemon::ticket::NO_TICKET);
182 1 : t.set_ticket_number(435);
183 1 : CATCH_REQUIRE(t.get_ticket_number() == 435);
184 :
185 1 : CATCH_REQUIRE_FALSE(t.is_locked());
186 :
187 : //CATCH_REQUIRE(t.one_leader()); -- TBD: this requires cluckd info
188 :
189 1 : CATCH_REQUIRE(t.get_obtention_timeout() == obtention_timeout);
190 :
191 1 : CATCH_REQUIRE(t.get_lock_duration() == cluck::timeout_t(10, 0));
192 :
193 : // the result of this function varies depending on the current
194 : // state (i.e. ALIVE, LOCKED, WAITING) -- at the moment we are
195 : // waiting so it returns the obtention timeout
196 : //
197 1 : CATCH_REQUIRE(t.get_current_timeout_date() == obtention_timeout);
198 :
199 : // the following can fail if your computer is really really slow
200 : //
201 1 : CATCH_REQUIRE_FALSE(t.timed_out());
202 :
203 1 : CATCH_REQUIRE(t.get_object_name() == "ticket_test");
204 1 : CATCH_REQUIRE(t.get_tag() == 123);
205 1 : CATCH_REQUIRE(t.get_server_name() == "rc");
206 1 : CATCH_REQUIRE(t.get_service_name() == "website");
207 1 : CATCH_REQUIRE(t.get_entering_key() == "rc/5003");
208 1 : CATCH_REQUIRE(t.get_ticket_key() == "000001b3/rc/5003");
209 :
210 1 : std::string const blob(t.serialize());
211 :
212 1 : cluck_daemon::ticket t2(
213 : &d
214 : , nullptr
215 : , "ticket_test"
216 : , ed::dispatcher_match::DISPATCHER_MATCH_NO_TAG
217 : , "rc/5003"
218 2 : , cluck::CLUCK_DEFAULT_TIMEOUT + snapdev::now()
219 : , cluck::CLUCK_DEFAULT_TIMEOUT
220 : , ""
221 12 : , "");
222 1 : t2.unserialize(blob);
223 :
224 : // now t2 is (mostly) equal to t1
225 : //
226 1 : CATCH_REQUIRE(t2.get_owner() == "rc3");
227 :
228 1 : CATCH_REQUIRE(t2.get_client_pid() == 5003);
229 :
230 1 : CATCH_REQUIRE(t2.get_serial() == 93);
231 :
232 1 : CATCH_REQUIRE(t2.get_unlock_duration() == cluck::timeout_t(3, 500000000));
233 :
234 1 : CATCH_REQUIRE(t2.get_ticket_number() == 435);
235 :
236 1 : CATCH_REQUIRE_FALSE(t2.is_locked());
237 :
238 1 : CATCH_REQUIRE(t2.get_obtention_timeout() == obtention_timeout);
239 :
240 1 : CATCH_REQUIRE(t2.get_lock_duration() == cluck::timeout_t(10, 0));
241 :
242 1 : CATCH_REQUIRE(t2.get_current_timeout_date() == obtention_timeout);
243 :
244 1 : CATCH_REQUIRE_FALSE(t2.timed_out());
245 :
246 1 : CATCH_REQUIRE(t2.get_object_name() == "ticket_test");
247 1 : CATCH_REQUIRE(t2.get_tag() == 123);
248 1 : CATCH_REQUIRE(t2.get_server_name() == "rc");
249 1 : CATCH_REQUIRE(t2.get_service_name() == "website");
250 1 : CATCH_REQUIRE(t2.get_entering_key() == "rc/5003");
251 1 : CATCH_REQUIRE(t2.get_ticket_key() == "000001b3/rc/5003");
252 1 : }
253 3 : CATCH_END_SECTION()
254 :
255 4 : CATCH_START_SECTION("daemon_ticket: test set_alive_timeout()")
256 : {
257 1 : cluck::timeout_t const now(snapdev::now());
258 1 : cluck::timeout_t const obtention_timeout = now + cluck::timeout_t(5, 0);
259 1 : cluckd_mock d;
260 1 : cluck_daemon::ticket t(
261 : &d
262 : , nullptr
263 : , "ticket_test"
264 : , 123
265 : , "rc/5003"
266 : , obtention_timeout
267 2 : , cluck::timeout_t(10, 0)
268 : , "rc"
269 11 : , "website");
270 :
271 1 : CATCH_REQUIRE(t.get_current_timeout_date() == obtention_timeout);
272 :
273 : // set a valid date between now & obtention timeout
274 : //
275 1 : cluck::timeout_t const alive_timeout(now + cluck::timeout_t(2, 500'000'000));
276 1 : t.set_alive_timeout(alive_timeout);
277 1 : CATCH_REQUIRE(t.get_current_timeout_date() == alive_timeout);
278 :
279 : // if negative, use 0 instead--this resets back to the obtention timeout
280 : //
281 1 : t.set_alive_timeout(cluck::timeout_t(-10, 345'637'291));
282 1 : CATCH_REQUIRE(t.get_current_timeout_date() == obtention_timeout);
283 :
284 : // reuse the valid date to make sure the next version works
285 : //
286 1 : t.set_alive_timeout(alive_timeout);
287 1 : CATCH_REQUIRE(t.get_current_timeout_date() == alive_timeout);
288 :
289 : // explicitly set to 0
290 : //
291 1 : t.set_alive_timeout(cluck::timeout_t(0, 0));
292 1 : CATCH_REQUIRE(t.get_current_timeout_date() == obtention_timeout);
293 :
294 : // reset one more time
295 : //
296 1 : t.set_alive_timeout(alive_timeout);
297 1 : CATCH_REQUIRE(t.get_current_timeout_date() == alive_timeout);
298 :
299 : // if the alive timeout is after obtention, the set uses the obtention
300 : // timeout instead
301 : //
302 1 : t.set_alive_timeout(obtention_timeout + cluck::timeout_t(3, 409'453'112));
303 1 : CATCH_REQUIRE(t.get_current_timeout_date() == obtention_timeout);
304 1 : }
305 3 : CATCH_END_SECTION()
306 2 : }
307 :
308 :
309 4 : CATCH_TEST_CASE("daemon_ticket_errors", "[cluckd][ticket][daemon][error]")
310 : {
311 6 : CATCH_START_SECTION("daemon_ticket_errors: call set_ticket_number() twice")
312 : {
313 1 : cluck::timeout_t obtention_timeout(snapdev::now());
314 1 : obtention_timeout += cluck::timeout_t(5, 0);
315 1 : cluckd_mock d;
316 1 : cluck_daemon::ticket t(
317 : &d
318 : , nullptr
319 : , "ticket_test"
320 : , 123
321 : , "rc/5003"
322 : , obtention_timeout
323 2 : , cluck::timeout_t(10, 0)
324 : , "rc"
325 11 : , "website");
326 :
327 : // the first time, it goes through as expected
328 : //
329 1 : t.set_ticket_number(123);
330 1 : CATCH_REQUIRE(t.get_ticket_number() == 123);
331 1 : CATCH_REQUIRE(t.get_ticket_key() == "0000007b/rc/5003");
332 :
333 : // the second time, we detect that it is already defined
334 : //
335 1 : CATCH_REQUIRE_THROWS_MATCHES(
336 : t.set_ticket_number(123)
337 : , cluck::logic_error
338 : , Catch::Matchers::ExceptionMessage(
339 : "logic_error: ticket::set_ticket_number() called with "
340 : + std::to_string(123)
341 : + " when f_our_ticket is already set to "
342 : + std::to_string(123)
343 : + "."));
344 :
345 : // reset back to zero is also not possible
346 : //
347 1 : CATCH_REQUIRE_THROWS_MATCHES(
348 : t.set_ticket_number(cluck_daemon::ticket::NO_TICKET)
349 : , cluck::logic_error
350 : , Catch::Matchers::ExceptionMessage(
351 : "logic_error: ticket::set_ticket_number() called with "
352 : + std::to_string(cluck_daemon::ticket::NO_TICKET)
353 : + " when f_our_ticket is already set to "
354 : + std::to_string(123)
355 : + "."));
356 1 : }
357 5 : CATCH_END_SECTION()
358 :
359 6 : CATCH_START_SECTION("daemon_ticket_errors: ticket number wrap around in max_ticket()")
360 : {
361 1 : cluck::timeout_t obtention_timeout(snapdev::now());
362 1 : obtention_timeout += cluck::timeout_t(5, 0);
363 1 : cluckd_mock d;
364 1 : cluck_daemon::ticket t(
365 : &d
366 : , nullptr
367 : , "ticket_test"
368 : , 123
369 : , "rc/5003"
370 : , obtention_timeout
371 2 : , cluck::timeout_t(10, 0)
372 : , "rc"
373 11 : , "website");
374 :
375 1 : CATCH_REQUIRE_THROWS_MATCHES(
376 : t.max_ticket(std::numeric_limits<cluck_daemon::ticket::ticket_id_t>::max())
377 : , cluck::out_of_range
378 : , Catch::Matchers::ExceptionMessage(
379 : "out_of_range: ticket::max_ticket() tried to generate the next ticket and got a wrapping around number."));
380 1 : }
381 5 : CATCH_END_SECTION()
382 :
383 6 : CATCH_START_SECTION("daemon_ticket_errors: ticket with bad entering key")
384 : {
385 1 : cluck::timeout_t obtention_timeout(snapdev::now());
386 1 : obtention_timeout += cluck::timeout_t(5, 0);
387 1 : cluckd_mock d;
388 1 : cluck_daemon::ticket t(
389 : &d
390 : , nullptr
391 : , "ticket_test"
392 : , 123
393 : , "bad_entering_key"
394 : , obtention_timeout
395 2 : , cluck::timeout_t(10, 0)
396 : , "rc"
397 11 : , "website");
398 :
399 1 : CATCH_REQUIRE_THROWS_MATCHES(
400 : t.get_client_pid()
401 : , cluck::invalid_parameter
402 : , Catch::Matchers::ExceptionMessage(
403 : "cluck_exception: ticket::get_client_pid() split f_entering_key "
404 : "\"bad_entering_key\" and did not get exactly two segments."));
405 1 : }
406 5 : CATCH_END_SECTION()
407 :
408 6 : CATCH_START_SECTION("daemon_ticket_errors: lock_failed()")
409 : {
410 1 : advgetopt::option const options[] =
411 : {
412 : advgetopt::define_option(
413 : advgetopt::Name("candidate-priority")
414 : , advgetopt::ShortName('p')
415 : , advgetopt::Flags(advgetopt::all_flags<
416 : advgetopt::GETOPT_FLAG_REQUIRED
417 : , advgetopt::GETOPT_FLAG_GROUP_OPTIONS>())
418 : , advgetopt::Help("Define the priority of this candidate (1 to 14) to gain a leader position or \"off\".")
419 : , advgetopt::DefaultValue("14")
420 : ),
421 : advgetopt::define_option(
422 : advgetopt::Name("server-name")
423 : , advgetopt::ShortName('n')
424 : , advgetopt::Flags(advgetopt::all_flags<
425 : advgetopt::GETOPT_FLAG_DYNAMIC_CONFIGURATION
426 : , advgetopt::GETOPT_FLAG_REQUIRED
427 : , advgetopt::GETOPT_FLAG_GROUP_OPTIONS>())
428 : , advgetopt::Help("Set the name of this server instance.")
429 : ),
430 : advgetopt::end_options()
431 : };
432 :
433 1 : advgetopt::options_environment const options_environment =
434 : {
435 : .f_project_name = "cluckd",
436 : .f_group_name = "cluck",
437 : .f_options = options,
438 : .f_environment_variable_name = "CLUCKD_OPTIONS",
439 : .f_environment_flags = advgetopt::GETOPT_ENVIRONMENT_FLAG_SYSTEM_PARAMETERS
440 : | advgetopt::GETOPT_ENVIRONMENT_FLAG_PROCESS_SYSTEM_PARAMETERS,
441 : .f_help_header = "Usage: %p [-<opt>]\n"
442 : "where -<opt> is one or more of:",
443 : .f_help_footer = "%c",
444 : .f_version = CLUCK_VERSION_STRING,
445 : .f_license = "GNU GPL v3",
446 : .f_copyright = "Copyright (c) 2013-"
447 : SNAPDEV_STRINGIZE(UTC_BUILD_YEAR)
448 : " by Made to Order Software Corporation -- All Rights Reserved",
449 1 : };
450 :
451 :
452 1 : cluck::timeout_t obtention_timeout(snapdev::now());
453 1 : obtention_timeout += cluck::timeout_t(5, 0);
454 1 : cluckd_mock d;
455 1 : char const * argv[] = { "test-ticket" };
456 1 : advgetopt::getopt opts(options_environment, 1, const_cast<char **>(argv));
457 1 : messenger_mock::pointer_t m(std::make_shared<messenger_mock>(&d, opts));
458 1 : cluck_daemon::ticket t(
459 : &d
460 : , m
461 : , "ticket_test"
462 : , 123
463 : , "rc/5003"
464 : , obtention_timeout
465 2 : , cluck::timeout_t(10, 0)
466 : , "rc"
467 11 : , "website");
468 :
469 1 : ed::message lock_failed;
470 1 : lock_failed.set_server("rc");
471 1 : lock_failed.set_service("website");
472 1 : lock_failed.set_command("LOCK_FAILED");
473 1 : lock_failed.add_parameter("description", "ticket failed before or after the lock was obtained (no reason)");
474 1 : lock_failed.add_parameter("error", "failed");
475 1 : lock_failed.add_parameter("key", "rc/5003");
476 1 : lock_failed.add_parameter("object_name", "ticket_test");
477 1 : lock_failed.add_parameter("tag", 123);
478 1 : m->add_expected_message(lock_failed);
479 1 : t.lock_failed("no reason");
480 :
481 : {
482 1 : std::string const blob(t.serialize());
483 1 : cluck_daemon::ticket t2(
484 : &d
485 : , nullptr
486 : , "ticket_test"
487 : , ed::dispatcher_match::DISPATCHER_MATCH_NO_TAG
488 : , "rc/5003"
489 2 : , cluck::CLUCK_DEFAULT_TIMEOUT + snapdev::now()
490 : , cluck::CLUCK_DEFAULT_TIMEOUT
491 : , ""
492 12 : , "");
493 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_NONE);
494 1 : t2.unserialize(blob);
495 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_LOCK);
496 1 : }
497 :
498 1 : t.lock_failed("ignore");
499 :
500 : {
501 1 : std::string const blob(t.serialize());
502 1 : cluck_daemon::ticket t2(
503 : &d
504 : , nullptr
505 : , "ticket_test"
506 : , ed::dispatcher_match::DISPATCHER_MATCH_NO_TAG
507 : , "rc/5003"
508 2 : , cluck::CLUCK_DEFAULT_TIMEOUT + snapdev::now()
509 : , cluck::CLUCK_DEFAULT_TIMEOUT
510 : , ""
511 12 : , "");
512 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_NONE);
513 1 : t2.unserialize(blob);
514 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_UNLOCKING);
515 1 : }
516 :
517 1 : t.lock_failed("further");
518 :
519 : {
520 1 : std::string const blob(t.serialize());
521 1 : cluck_daemon::ticket t2(
522 : &d
523 : , nullptr
524 : , "ticket_test"
525 : , ed::dispatcher_match::DISPATCHER_MATCH_NO_TAG
526 : , "rc/5003"
527 2 : , cluck::CLUCK_DEFAULT_TIMEOUT + snapdev::now()
528 : , cluck::CLUCK_DEFAULT_TIMEOUT
529 : , ""
530 12 : , "");
531 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_NONE);
532 1 : t2.unserialize(blob);
533 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_UNLOCKING);
534 1 : }
535 :
536 1 : t.lock_failed("calls");
537 :
538 : {
539 1 : std::string const blob(t.serialize());
540 1 : cluck_daemon::ticket t2(
541 : &d
542 : , nullptr
543 : , "ticket_test"
544 : , ed::dispatcher_match::DISPATCHER_MATCH_NO_TAG
545 : , "rc/5003"
546 2 : , cluck::CLUCK_DEFAULT_TIMEOUT + snapdev::now()
547 : , cluck::CLUCK_DEFAULT_TIMEOUT
548 : , ""
549 12 : , "");
550 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_NONE);
551 1 : t2.unserialize(blob);
552 1 : CATCH_REQUIRE(t2.f_lock_failed == cluck_daemon::ticket::lock_failure_t::LOCK_FAILURE_UNLOCKING);
553 1 : }
554 1 : }
555 5 : CATCH_END_SECTION()
556 4 : }
557 :
558 :
559 :
560 : // vim: ts=4 sw=4 et
|