Line data Source code
1 : // Copyright (c) 2018-2024 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/snapdev
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 : /** \file
20 : * \brief Verify that the lockfile classes work as expected.
21 : *
22 : * This file implements tests to verify the lockfile and lockfd functionality.
23 : */
24 :
25 : // file being tested
26 : //
27 : #include <snapdev/lockfile.h>
28 :
29 :
30 : // self
31 : //
32 : #include "catch_main.h"
33 :
34 :
35 : // snapdev
36 : //
37 : #include <snapdev/timespec_ex.h>
38 :
39 :
40 : // C++
41 : //
42 : #include <thread>
43 :
44 :
45 : // last include
46 : //
47 : #include <snapdev/poison.h>
48 :
49 :
50 :
51 : namespace
52 : {
53 :
54 :
55 : class lockfile_thread
56 : {
57 : public:
58 7 : lockfile_thread(
59 : std::string const & filename
60 : , snapdev::operation_t operation = snapdev::operation_t::OPERATION_EXCLUSIVE)
61 7 : : f_filename(filename)
62 7 : , f_operation(operation)
63 : {
64 7 : }
65 :
66 : lockfile_thread(lockfile_thread const &) = delete;
67 : lockfile_thread & operator = (lockfile_thread const &) = delete;
68 :
69 7 : ~lockfile_thread()
70 : {
71 7 : if(f_thread != nullptr)
72 : {
73 7 : f_thread->join();
74 7 : delete f_thread;
75 : }
76 7 : }
77 :
78 7 : void start_thread(int try_lock = 0)
79 : {
80 7 : f_running = true;
81 7 : if(try_lock)
82 : {
83 4 : f_thread = new std::thread(&lockfile_thread::run_try_lock, this, try_lock);
84 : }
85 : else
86 : {
87 3 : f_thread = new std::thread(&lockfile_thread::run, this);
88 : }
89 7 : }
90 :
91 3 : void run()
92 : {
93 3 : snapdev::lockfile lock(f_filename, f_operation);
94 3 : CATCH_REQUIRE_FALSE(lock.is_locked());
95 3 : lock.lock();
96 3 : CATCH_REQUIRE(lock.is_locked());
97 :
98 3 : std::lock_guard<std::mutex> guard(f_mutex);
99 3 : f_running = false;
100 6 : }
101 :
102 4 : void run_try_lock(int try_lock)
103 : {
104 4 : snapdev::lockfile lock(f_filename, f_operation);
105 4 : CATCH_REQUIRE_FALSE(lock.is_locked());
106 4 : lock.try_lock();
107 4 : if(try_lock == 1)
108 : {
109 1 : CATCH_REQUIRE(lock.is_locked());
110 : }
111 : else
112 : {
113 3 : CATCH_REQUIRE_FALSE(lock.is_locked());
114 : }
115 :
116 4 : std::lock_guard<std::mutex> guard(f_mutex);
117 4 : f_running = false;
118 8 : }
119 :
120 15 : bool is_running() const
121 : {
122 15 : std::lock_guard<std::mutex> guard(f_mutex);
123 15 : return f_running;
124 15 : }
125 :
126 : private:
127 : mutable std::mutex f_mutex = std::mutex();
128 : std::string f_filename = std::string();
129 : snapdev::operation_t f_operation = snapdev::operation_t::OPERATION_EXCLUSIVE;
130 : std::thread * f_thread = nullptr;
131 : bool f_running = false;
132 : };
133 :
134 :
135 : } // no name namespace
136 :
137 :
138 :
139 9 : CATCH_TEST_CASE("lockfile", "[lock][file]")
140 : {
141 9 : CATCH_START_SECTION("lockfile: simple lock/unlock test")
142 : {
143 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
144 1 : std::string const filename(path + "/test-1.lock");
145 1 : snapdev::lockfile lock(filename);
146 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
147 1 : lock.lock();
148 1 : CATCH_REQUIRE(lock.is_locked());
149 1 : lock.lock();
150 1 : CATCH_REQUIRE(lock.is_locked());
151 1 : lock.unlock();
152 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
153 1 : lock.unlock();
154 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
155 :
156 1 : CATCH_REQUIRE(lock.get_path() == filename);
157 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
158 1 : }
159 9 : CATCH_END_SECTION()
160 :
161 9 : CATCH_START_SECTION("lockfile: test a lock with a thread")
162 : {
163 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
164 1 : std::string const filename(path + "/test-2.lock");
165 :
166 1 : snapdev::lockfile lock(filename);
167 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
168 1 : lock.lock();
169 1 : CATCH_REQUIRE(lock.is_locked());
170 :
171 1 : lockfile_thread t(filename);
172 1 : t.start_thread();
173 1 : CATCH_REQUIRE(t.is_running());
174 :
175 : // still locked
176 : //
177 1 : CATCH_REQUIRE(lock.is_locked());
178 1 : CATCH_REQUIRE(t.is_running());
179 :
180 : // sleep a bit to make sure things stay locked
181 : //
182 1 : sleep(2);
183 1 : CATCH_REQUIRE(lock.is_locked());
184 1 : CATCH_REQUIRE(t.is_running());
185 :
186 1 : lock.unlock();
187 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
188 :
189 : for(;;)
190 : {
191 2 : if(!t.is_running())
192 : {
193 1 : break;
194 : }
195 1 : snapdev::timespec_ex wait(0.001); // 1ms
196 1 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
197 1 : }
198 :
199 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
200 1 : }
201 9 : CATCH_END_SECTION()
202 :
203 9 : CATCH_START_SECTION("lockfile: verify the automatic unlock with a thread")
204 : {
205 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
206 1 : std::string const filename(path + "/test-3.lock");
207 :
208 1 : snapdev::lockfile * lock(new snapdev::lockfile(filename));
209 1 : CATCH_REQUIRE_FALSE(lock->is_locked());
210 1 : lock->lock();
211 1 : CATCH_REQUIRE(lock->is_locked());
212 :
213 1 : lockfile_thread t(filename);
214 1 : t.start_thread();
215 1 : CATCH_REQUIRE(t.is_running());
216 :
217 : // still locked
218 : //
219 1 : CATCH_REQUIRE(lock->is_locked());
220 1 : CATCH_REQUIRE(t.is_running());
221 :
222 : // sleep a bit to make sure things stay locked
223 : //
224 1 : sleep(2);
225 1 : CATCH_REQUIRE(lock->is_locked());
226 1 : CATCH_REQUIRE(t.is_running());
227 :
228 : // verify that the delete removes the lock
229 : //
230 1 : delete lock;
231 :
232 : for(;;)
233 : {
234 2 : if(!t.is_running())
235 : {
236 1 : break;
237 : }
238 1 : snapdev::timespec_ex wait(0.001); // 1ms
239 1 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
240 1 : }
241 1 : }
242 9 : CATCH_END_SECTION()
243 :
244 9 : CATCH_START_SECTION("lockfile: a shared lock does not prevent the thread from finishing")
245 : {
246 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
247 1 : std::string const filename(path + "/test-4.lock");
248 :
249 1 : snapdev::lockfile lock(filename, snapdev::operation_t::OPERATION_SHARED);
250 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
251 1 : lock.lock();
252 1 : CATCH_REQUIRE(lock.is_locked());
253 :
254 1 : lockfile_thread t(filename, snapdev::operation_t::OPERATION_SHARED);
255 1 : t.start_thread();
256 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
257 :
258 : // still locked
259 : //
260 1 : CATCH_REQUIRE(lock.is_locked());
261 :
262 : // sleep a bit to make sure things stay locked
263 : //
264 1 : sleep(2);
265 1 : CATCH_REQUIRE(lock.is_locked());
266 :
267 : // the thread will die on its own without us having to unlock()
268 : //
269 : for(;;)
270 : {
271 1 : if(!t.is_running())
272 : {
273 1 : break;
274 : }
275 : //snapdev::timespec_ex wait(0.001); // 1ms
276 : //CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
277 0 : sleep(1);
278 : }
279 :
280 : // still locked and the thread returned as expected
281 : //
282 1 : CATCH_REQUIRE(lock.is_locked());
283 1 : }
284 9 : CATCH_END_SECTION()
285 :
286 9 : CATCH_START_SECTION("lockfile: test a try_lock() with a thread")
287 : {
288 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
289 1 : std::string const filename(path + "/test-5.lock");
290 :
291 1 : snapdev::lockfile lock(filename);
292 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
293 1 : lock.lock();
294 1 : CATCH_REQUIRE(lock.is_locked());
295 :
296 1 : lockfile_thread t(filename);
297 1 : t.start_thread(2); // the try_lock will fail
298 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
299 :
300 : // still locked
301 : //
302 1 : CATCH_REQUIRE(lock.is_locked());
303 :
304 : // sleep a bit to make sure things stay locked
305 : //
306 1 : sleep(2);
307 1 : CATCH_REQUIRE(lock.is_locked());
308 :
309 : for(;;)
310 : {
311 1 : if(!t.is_running())
312 : {
313 1 : break;
314 : }
315 0 : snapdev::timespec_ex wait(0.001); // 1ms
316 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
317 0 : }
318 :
319 : // still locked and the thread returned as expected
320 : //
321 1 : CATCH_REQUIRE(lock.is_locked());
322 1 : }
323 9 : CATCH_END_SECTION()
324 :
325 9 : CATCH_START_SECTION("lockfile: test a shared lock and try_lock() with a thread")
326 : {
327 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
328 1 : std::string const filename(path + "/test-6.lock");
329 :
330 1 : snapdev::lockfile lock(filename, snapdev::operation_t::OPERATION_SHARED);
331 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
332 1 : lock.lock();
333 1 : CATCH_REQUIRE(lock.is_locked());
334 :
335 1 : lockfile_thread t(filename, snapdev::operation_t::OPERATION_SHARED);
336 1 : t.start_thread(1); // the try_lock will succeed
337 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
338 :
339 : // still locked
340 : //
341 1 : CATCH_REQUIRE(lock.is_locked());
342 :
343 : // sleep a bit to make sure things stay locked
344 : //
345 1 : sleep(2);
346 1 : CATCH_REQUIRE(lock.is_locked());
347 :
348 : for(;;)
349 : {
350 1 : if(!t.is_running())
351 : {
352 1 : break;
353 : }
354 0 : snapdev::timespec_ex wait(0.001); // 1ms
355 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
356 0 : }
357 :
358 : // still locked and the thread returned as expected
359 : //
360 1 : CATCH_REQUIRE(lock.is_locked());
361 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_SHARED);
362 1 : }
363 9 : CATCH_END_SECTION()
364 :
365 9 : CATCH_START_SECTION("lockfile: test an exclusive lock and a shared try_lock() lock with a thread")
366 : {
367 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
368 1 : std::string const filename(path + "/test-7.lock");
369 :
370 1 : snapdev::lockfile lock(filename);
371 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
372 1 : lock.lock();
373 1 : CATCH_REQUIRE(lock.is_locked());
374 :
375 1 : lockfile_thread t(filename, snapdev::operation_t::OPERATION_SHARED);
376 1 : t.start_thread(2); // the try_lock will fail anyway
377 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
378 :
379 : // still locked
380 : //
381 1 : CATCH_REQUIRE(lock.is_locked());
382 :
383 : // sleep a bit to make sure things stay locked
384 : //
385 1 : sleep(2);
386 1 : CATCH_REQUIRE(lock.is_locked());
387 :
388 : for(;;)
389 : {
390 1 : if(!t.is_running())
391 : {
392 1 : break;
393 : }
394 0 : snapdev::timespec_ex wait(0.001); // 1ms
395 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
396 0 : }
397 :
398 : // still locked and the thread returned as expected
399 : //
400 1 : CATCH_REQUIRE(lock.is_locked());
401 1 : }
402 9 : CATCH_END_SECTION()
403 :
404 9 : CATCH_START_SECTION("lockfile: test a shared lock and an exclusive try_lock() lock with a thread")
405 : {
406 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
407 1 : std::string const filename(path + "/test-8.lock");
408 :
409 1 : snapdev::lockfile lock(filename, snapdev::operation_t::OPERATION_SHARED);
410 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
411 1 : lock.lock();
412 1 : CATCH_REQUIRE(lock.is_locked());
413 :
414 1 : lockfile_thread t(filename);
415 1 : t.start_thread(2); // the try_lock will fail anyway
416 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
417 :
418 : // still locked
419 : //
420 1 : CATCH_REQUIRE(lock.is_locked());
421 :
422 : // sleep a bit to make sure things stay locked
423 : //
424 1 : sleep(2);
425 1 : CATCH_REQUIRE(lock.is_locked());
426 :
427 : for(;;)
428 : {
429 1 : if(!t.is_running())
430 : {
431 1 : break;
432 : }
433 0 : snapdev::timespec_ex wait(0.001); // 1ms
434 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
435 0 : }
436 :
437 : // still locked and the thread returned as expected
438 : //
439 1 : CATCH_REQUIRE(lock.is_locked());
440 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_SHARED);
441 1 : }
442 9 : CATCH_END_SECTION()
443 :
444 9 : CATCH_START_SECTION("lockfile: test copying a lockfile")
445 : {
446 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
447 1 : std::string const filename(path + "/test-9.lock");
448 1 : snapdev::lockfile lock(filename);
449 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
450 1 : lock.lock();
451 1 : CATCH_REQUIRE(lock.is_locked());
452 1 : lock.lock();
453 1 : CATCH_REQUIRE(lock.is_locked());
454 1 : lock.unlock();
455 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
456 1 : lock.unlock();
457 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
458 :
459 : {
460 1 : snapdev::lockfile copy(lock);
461 :
462 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
463 1 : CATCH_REQUIRE_FALSE(copy.is_locked());
464 1 : lock.lock();
465 1 : CATCH_REQUIRE(lock.is_locked());
466 1 : CATCH_REQUIRE(copy.is_locked());
467 :
468 1 : CATCH_REQUIRE(copy.get_path() == filename);
469 1 : CATCH_REQUIRE(copy.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
470 1 : }
471 :
472 : {
473 1 : snapdev::lockfile copy(lock);
474 :
475 1 : CATCH_REQUIRE(lock.is_locked());
476 1 : CATCH_REQUIRE(copy.is_locked());
477 1 : lock.unlock();
478 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
479 1 : CATCH_REQUIRE_FALSE(copy.is_locked());
480 :
481 1 : CATCH_REQUIRE(copy.get_path() == filename);
482 1 : CATCH_REQUIRE(copy.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
483 1 : }
484 :
485 1 : CATCH_REQUIRE(lock.get_path() == filename);
486 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
487 1 : }
488 9 : CATCH_END_SECTION()
489 9 : }
490 :
491 :
492 1 : CATCH_TEST_CASE("lockfile_error", "[lock][file]")
493 : {
494 1 : CATCH_START_SECTION("lockfile_error: no path generates an error trying to create the lock file")
495 : {
496 1 : int const e(ENOENT);
497 3 : CATCH_REQUIRE_THROWS_MATCHES(
498 : snapdev::lockfile(std::string())
499 : , snapdev::file_error
500 : , Catch::Matchers::ExceptionMessage(
501 : "lockfile_error: Error creating lock file \"\" (errno: "
502 : + std::to_string(e)
503 : + ", "
504 : + strerror(e)
505 : + ")."));
506 : }
507 1 : CATCH_END_SECTION()
508 1 : }
509 :
510 :
511 3 : CATCH_TEST_CASE("lockfd", "[lock][file]")
512 : {
513 3 : CATCH_START_SECTION("lockfd: test exclusive lock with -1, nothing happens")
514 : {
515 1 : snapdev::lockfd lock(-1);
516 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
517 1 : CATCH_REQUIRE_FALSE(lock.lock());
518 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
519 1 : lock.unlock();
520 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
521 1 : }
522 3 : CATCH_END_SECTION()
523 :
524 3 : CATCH_START_SECTION("lockfd: test shared lock with -1, nothing happens")
525 : {
526 1 : snapdev::lockfd lock(-1, snapdev::operation_t::OPERATION_SHARED);
527 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
528 1 : CATCH_REQUIRE_FALSE(lock.lock());
529 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
530 1 : lock.unlock();
531 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
532 1 : }
533 3 : CATCH_END_SECTION()
534 :
535 3 : CATCH_START_SECTION("lockfd: test shared lock with actual file")
536 : {
537 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
538 1 : std::string const filename(path + "/fd-test-1.lock");
539 1 : int const fd(::open(filename.c_str(), O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH));
540 : {
541 1 : snapdev::lockfd lock(fd, snapdev::operation_t::OPERATION_SHARED);
542 1 : CATCH_REQUIRE(lock.is_locked());
543 1 : lock.unlock();
544 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
545 1 : CATCH_REQUIRE(lock.lock());
546 1 : CATCH_REQUIRE(lock.is_locked());
547 1 : }
548 1 : close(fd);
549 1 : }
550 3 : CATCH_END_SECTION()
551 3 : }
552 :
553 :
554 :
555 : // vim: ts=4 sw=4 et
|