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