Line data Source code
1 : // Copyright (c) 2018-2025 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 const wait(0.001); // 1ms
239 1 : int const r(nanosleep(&wait, nullptr));
240 1 : if(r != 0)
241 : {
242 0 : int const e(errno);
243 : std::cerr
244 0 : << "error: nanosleep() returned an error ("
245 : << e
246 : << ", "
247 0 : << strerror(e)
248 0 : << ")\n";
249 : }
250 1 : CATCH_REQUIRE(r == 0);
251 1 : }
252 1 : }
253 9 : CATCH_END_SECTION()
254 :
255 9 : CATCH_START_SECTION("lockfile: a shared lock does not prevent the thread from finishing")
256 : {
257 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
258 1 : std::string const filename(path + "/test-4.lock");
259 :
260 1 : snapdev::lockfile lock(filename, snapdev::operation_t::OPERATION_SHARED);
261 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
262 1 : lock.lock();
263 1 : CATCH_REQUIRE(lock.is_locked());
264 :
265 1 : lockfile_thread t(filename, snapdev::operation_t::OPERATION_SHARED);
266 1 : t.start_thread();
267 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
268 :
269 : // still locked
270 : //
271 1 : CATCH_REQUIRE(lock.is_locked());
272 :
273 : // sleep a bit to make sure things stay locked
274 : //
275 1 : sleep(2);
276 1 : CATCH_REQUIRE(lock.is_locked());
277 :
278 : // the thread will die on its own without us having to unlock()
279 : //
280 : for(;;)
281 : {
282 1 : if(!t.is_running())
283 : {
284 1 : break;
285 : }
286 : //snapdev::timespec_ex wait(0.001); // 1ms
287 : //CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
288 0 : sleep(1);
289 : }
290 :
291 : // still locked and the thread returned as expected
292 : //
293 1 : CATCH_REQUIRE(lock.is_locked());
294 1 : }
295 9 : CATCH_END_SECTION()
296 :
297 9 : CATCH_START_SECTION("lockfile: test a try_lock() with a thread")
298 : {
299 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
300 1 : std::string const filename(path + "/test-5.lock");
301 :
302 1 : snapdev::lockfile lock(filename);
303 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
304 1 : lock.lock();
305 1 : CATCH_REQUIRE(lock.is_locked());
306 :
307 1 : lockfile_thread t(filename);
308 1 : t.start_thread(2); // the try_lock will fail
309 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
310 :
311 : // still locked
312 : //
313 1 : CATCH_REQUIRE(lock.is_locked());
314 :
315 : // sleep a bit to make sure things stay locked
316 : //
317 1 : sleep(2);
318 1 : CATCH_REQUIRE(lock.is_locked());
319 :
320 : for(;;)
321 : {
322 1 : if(!t.is_running())
323 : {
324 1 : break;
325 : }
326 0 : snapdev::timespec_ex wait(0.001); // 1ms
327 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
328 0 : }
329 :
330 : // still locked and the thread returned as expected
331 : //
332 1 : CATCH_REQUIRE(lock.is_locked());
333 1 : }
334 9 : CATCH_END_SECTION()
335 :
336 9 : CATCH_START_SECTION("lockfile: test a shared lock and try_lock() with a thread")
337 : {
338 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
339 1 : std::string const filename(path + "/test-6.lock");
340 :
341 1 : snapdev::lockfile lock(filename, snapdev::operation_t::OPERATION_SHARED);
342 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
343 1 : lock.lock();
344 1 : CATCH_REQUIRE(lock.is_locked());
345 :
346 1 : lockfile_thread t(filename, snapdev::operation_t::OPERATION_SHARED);
347 1 : t.start_thread(1); // the try_lock will succeed
348 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
349 :
350 : // still locked
351 : //
352 1 : CATCH_REQUIRE(lock.is_locked());
353 :
354 : // sleep a bit to make sure things stay locked
355 : //
356 1 : sleep(2);
357 1 : CATCH_REQUIRE(lock.is_locked());
358 :
359 : for(;;)
360 : {
361 1 : if(!t.is_running())
362 : {
363 1 : break;
364 : }
365 0 : snapdev::timespec_ex wait(0.001); // 1ms
366 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
367 0 : }
368 :
369 : // still locked and the thread returned as expected
370 : //
371 1 : CATCH_REQUIRE(lock.is_locked());
372 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_SHARED);
373 1 : }
374 9 : CATCH_END_SECTION()
375 :
376 9 : CATCH_START_SECTION("lockfile: test an exclusive lock and a shared try_lock() lock with a thread")
377 : {
378 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
379 1 : std::string const filename(path + "/test-7.lock");
380 :
381 1 : snapdev::lockfile lock(filename);
382 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
383 1 : lock.lock();
384 1 : CATCH_REQUIRE(lock.is_locked());
385 :
386 1 : lockfile_thread t(filename, snapdev::operation_t::OPERATION_SHARED);
387 1 : t.start_thread(2); // the try_lock will fail anyway
388 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
389 :
390 : // still locked
391 : //
392 1 : CATCH_REQUIRE(lock.is_locked());
393 :
394 : // sleep a bit to make sure things stay locked
395 : //
396 1 : sleep(2);
397 1 : CATCH_REQUIRE(lock.is_locked());
398 :
399 : for(;;)
400 : {
401 1 : if(!t.is_running())
402 : {
403 1 : break;
404 : }
405 0 : snapdev::timespec_ex wait(0.001); // 1ms
406 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
407 0 : }
408 :
409 : // still locked and the thread returned as expected
410 : //
411 1 : CATCH_REQUIRE(lock.is_locked());
412 1 : }
413 9 : CATCH_END_SECTION()
414 :
415 9 : CATCH_START_SECTION("lockfile: test a shared lock and an exclusive try_lock() lock with a thread")
416 : {
417 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
418 1 : std::string const filename(path + "/test-8.lock");
419 :
420 1 : snapdev::lockfile lock(filename, snapdev::operation_t::OPERATION_SHARED);
421 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
422 1 : lock.lock();
423 1 : CATCH_REQUIRE(lock.is_locked());
424 :
425 1 : lockfile_thread t(filename);
426 1 : t.start_thread(2); // the try_lock will fail anyway
427 : //CATCH_REQUIRE(t.is_running()); -- the thread may return at any time, so we cannot check whether it is running
428 :
429 : // still locked
430 : //
431 1 : CATCH_REQUIRE(lock.is_locked());
432 :
433 : // sleep a bit to make sure things stay locked
434 : //
435 1 : sleep(2);
436 1 : CATCH_REQUIRE(lock.is_locked());
437 :
438 : for(;;)
439 : {
440 1 : if(!t.is_running())
441 : {
442 1 : break;
443 : }
444 0 : snapdev::timespec_ex wait(0.001); // 1ms
445 0 : CATCH_REQUIRE(nanosleep(&wait, nullptr) == 0);
446 0 : }
447 :
448 : // still locked and the thread returned as expected
449 : //
450 1 : CATCH_REQUIRE(lock.is_locked());
451 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_SHARED);
452 1 : }
453 9 : CATCH_END_SECTION()
454 :
455 9 : CATCH_START_SECTION("lockfile: test copying a lockfile")
456 : {
457 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
458 1 : std::string const filename(path + "/test-9.lock");
459 1 : snapdev::lockfile lock(filename);
460 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
461 1 : lock.lock();
462 1 : CATCH_REQUIRE(lock.is_locked());
463 1 : lock.lock();
464 1 : CATCH_REQUIRE(lock.is_locked());
465 1 : lock.unlock();
466 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
467 1 : lock.unlock();
468 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
469 :
470 : {
471 1 : snapdev::lockfile copy(lock);
472 :
473 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
474 1 : CATCH_REQUIRE_FALSE(copy.is_locked());
475 1 : lock.lock();
476 1 : CATCH_REQUIRE(lock.is_locked());
477 1 : CATCH_REQUIRE(copy.is_locked());
478 :
479 1 : CATCH_REQUIRE(copy.get_path() == filename);
480 1 : CATCH_REQUIRE(copy.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
481 1 : }
482 :
483 : {
484 1 : snapdev::lockfile copy(lock);
485 :
486 1 : CATCH_REQUIRE(lock.is_locked());
487 1 : CATCH_REQUIRE(copy.is_locked());
488 1 : lock.unlock();
489 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
490 1 : CATCH_REQUIRE_FALSE(copy.is_locked());
491 :
492 1 : CATCH_REQUIRE(copy.get_path() == filename);
493 1 : CATCH_REQUIRE(copy.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
494 1 : }
495 :
496 1 : CATCH_REQUIRE(lock.get_path() == filename);
497 1 : CATCH_REQUIRE(lock.get_operation() == snapdev::operation_t::OPERATION_EXCLUSIVE);
498 1 : }
499 9 : CATCH_END_SECTION()
500 9 : }
501 :
502 :
503 1 : CATCH_TEST_CASE("lockfile_error", "[lock][file]")
504 : {
505 1 : CATCH_START_SECTION("lockfile_error: no path generates an error trying to create the lock file")
506 : {
507 1 : int const e(ENOENT);
508 4 : CATCH_REQUIRE_THROWS_MATCHES(
509 : snapdev::lockfile(std::string())
510 : , snapdev::file_error
511 : , Catch::Matchers::ExceptionMessage(
512 : "lockfile_error: Error creating lock file \"\" (errno: "
513 : + std::to_string(e)
514 : + ", "
515 : + strerror(e)
516 : + ")."));
517 : }
518 1 : CATCH_END_SECTION()
519 1 : }
520 :
521 :
522 3 : CATCH_TEST_CASE("lockfd", "[lock][file]")
523 : {
524 3 : CATCH_START_SECTION("lockfd: test exclusive lock with -1, nothing happens")
525 : {
526 1 : snapdev::lockfd lock(-1);
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 -1, nothing happens")
536 : {
537 1 : snapdev::lockfd lock(-1, snapdev::operation_t::OPERATION_SHARED);
538 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
539 1 : CATCH_REQUIRE_FALSE(lock.lock());
540 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
541 1 : lock.unlock();
542 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
543 1 : }
544 3 : CATCH_END_SECTION()
545 :
546 3 : CATCH_START_SECTION("lockfd: test shared lock with actual file")
547 : {
548 1 : std::string const path(SNAP_CATCH2_NAMESPACE::g_tmp_dir());
549 1 : std::string const filename(path + "/fd-test-1.lock");
550 1 : int const fd(::open(filename.c_str(), O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH));
551 : {
552 1 : snapdev::lockfd lock(fd, snapdev::operation_t::OPERATION_SHARED);
553 1 : CATCH_REQUIRE(lock.is_locked());
554 1 : lock.unlock();
555 1 : CATCH_REQUIRE_FALSE(lock.is_locked());
556 1 : CATCH_REQUIRE(lock.lock());
557 1 : CATCH_REQUIRE(lock.is_locked());
558 1 : }
559 1 : close(fd);
560 1 : }
561 3 : CATCH_END_SECTION()
562 3 : }
563 :
564 :
565 :
566 : // vim: ts=4 sw=4 et
|