Line data Source code
1 : /**
2 : * @file test_session_store.c
3 : * @brief Unit tests for session persistence.
4 : *
5 : * Overrides HOME so the test doesn't touch the developer's real
6 : * ~/.config/tg-cli/session.bin.
7 : */
8 :
9 : #include "test_helpers.h"
10 : #include "app/session_store.h"
11 : #include "mtproto_session.h"
12 :
13 : #include <errno.h>
14 : #include <fcntl.h>
15 : #include <stdio.h>
16 : #include <stdlib.h>
17 : #include <string.h>
18 : #include <sys/file.h>
19 : #include <sys/stat.h>
20 : #include <sys/wait.h>
21 : #include <unistd.h>
22 :
23 12 : static void with_tmp_home(const char *subdir) {
24 : char tmp[256];
25 12 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-session-test-%s", subdir);
26 : /* Best-effort cleanup of stale leftovers */
27 : char path[512];
28 12 : snprintf(path, sizeof(path), "%s/.config/tg-cli/session.bin", tmp);
29 12 : (void)unlink(path);
30 12 : setenv("HOME", tmp, 1);
31 12 : }
32 :
33 1 : static void test_save_load_roundtrip(void) {
34 1 : with_tmp_home("roundtrip");
35 :
36 : MtProtoSession s;
37 1 : mtproto_session_init(&s);
38 : uint8_t key[256];
39 257 : for (int i = 0; i < 256; i++) key[i] = (uint8_t)(i * 7 + 3);
40 1 : mtproto_session_set_auth_key(&s, key);
41 1 : mtproto_session_set_salt(&s, 0xDEADBEEFCAFEBABEULL);
42 1 : s.session_id = 0x1122334455667788ULL;
43 :
44 1 : ASSERT(session_store_save(&s, 4) == 0, "save succeeds");
45 :
46 : MtProtoSession r;
47 1 : mtproto_session_init(&r);
48 1 : int dc = 0;
49 1 : ASSERT(session_store_load(&r, &dc) == 0, "load succeeds");
50 1 : ASSERT(dc == 4, "dc_id restored");
51 1 : ASSERT(r.has_auth_key == 1, "has_auth_key set");
52 1 : ASSERT(r.server_salt == 0xDEADBEEFCAFEBABEULL, "salt restored");
53 1 : ASSERT(r.session_id == 0x1122334455667788ULL, "session_id restored");
54 1 : ASSERT(memcmp(r.auth_key, key, 256) == 0, "auth_key matches");
55 :
56 1 : session_store_clear();
57 : }
58 :
59 1 : static void test_load_missing_file(void) {
60 1 : with_tmp_home("missing");
61 : /* Ensure no file */
62 1 : session_store_clear();
63 :
64 : MtProtoSession s;
65 1 : mtproto_session_init(&s);
66 1 : int dc = 0;
67 1 : ASSERT(session_store_load(&s, &dc) == -1, "load returns -1 if no file");
68 : }
69 :
70 1 : static void test_save_without_key_fails(void) {
71 1 : with_tmp_home("no-key");
72 :
73 : MtProtoSession s;
74 1 : mtproto_session_init(&s); /* has_auth_key=0 */
75 1 : ASSERT(session_store_save(&s, 2) == -1, "save without key fails");
76 : }
77 :
78 1 : static void test_load_wrong_magic(void) {
79 1 : with_tmp_home("bad-magic");
80 :
81 : char path[256];
82 1 : snprintf(path, sizeof(path),
83 : "%s/.config/tg-cli/session.bin", getenv("HOME"));
84 : char dir[256];
85 1 : snprintf(dir, sizeof(dir), "%s/.config/tg-cli", getenv("HOME"));
86 1 : mkdir("/tmp/tg-cli-session-test-bad-magic", 0700);
87 : char base[256];
88 1 : snprintf(base, sizeof(base), "%s/.config",
89 : "/tmp/tg-cli-session-test-bad-magic");
90 1 : mkdir(base, 0700);
91 1 : mkdir(dir, 0700);
92 :
93 1 : FILE *f = fopen(path, "wb");
94 1 : if (f) {
95 1 : char junk[284] = "XXXX";
96 1 : fwrite(junk, 1, sizeof(junk), f);
97 1 : fclose(f);
98 : }
99 1 : MtProtoSession s; mtproto_session_init(&s);
100 1 : int dc = 0;
101 1 : ASSERT(session_store_load(&s, &dc) == -1, "bad magic rejected");
102 :
103 1 : session_store_clear();
104 : }
105 :
106 : /*
107 : * Test: test_load_wrong_version
108 : *
109 : * Writes a session file with the correct "TGCS" magic but version = 3
110 : * (STORE_VERSION + 1). session_store_load must return -1 and must not
111 : * modify the file.
112 : */
113 1 : static void test_load_wrong_version(void) {
114 1 : with_tmp_home("wrong-version");
115 :
116 : /* Build the directory tree manually (mirrors test_load_wrong_magic). */
117 1 : mkdir("/tmp/tg-cli-session-test-wrong-version", 0700);
118 : char base[256];
119 1 : snprintf(base, sizeof(base),
120 : "%s/.config", "/tmp/tg-cli-session-test-wrong-version");
121 1 : mkdir(base, 0700);
122 : char dir[256];
123 1 : snprintf(dir, sizeof(dir), "%s/.config/tg-cli",
124 : "/tmp/tg-cli-session-test-wrong-version");
125 1 : mkdir(dir, 0700);
126 :
127 : char path[256];
128 1 : snprintf(path, sizeof(path), "%s/.config/tg-cli/session.bin",
129 : getenv("HOME"));
130 :
131 : /* Build a minimal header: "TGCS" + version=3 + home_dc_id=0 + count=0. */
132 : uint8_t header[16];
133 1 : memset(header, 0, sizeof(header));
134 1 : memcpy(header, "TGCS", 4);
135 1 : int32_t bad_ver = 3; /* STORE_VERSION (2) + 1 */
136 1 : memcpy(header + 4, &bad_ver, 4);
137 :
138 1 : FILE *f = fopen(path, "wb");
139 1 : ASSERT(f != NULL, "created session file for wrong-version test");
140 1 : if (f) {
141 1 : fwrite(header, 1, sizeof(header), f);
142 1 : fclose(f);
143 : }
144 :
145 : /* Record mtime/size before calling load so we can verify no modification. */
146 : struct stat st_before;
147 1 : ASSERT(stat(path, &st_before) == 0, "stat before load");
148 :
149 : MtProtoSession s;
150 1 : mtproto_session_init(&s);
151 1 : int dc = 0;
152 1 : ASSERT(session_store_load(&s, &dc) == -1, "wrong version rejected");
153 :
154 : struct stat st_after;
155 1 : ASSERT(stat(path, &st_after) == 0, "stat after load");
156 1 : ASSERT(st_before.st_size == st_after.st_size, "file size unchanged");
157 1 : ASSERT(st_before.st_mtime == st_after.st_mtime, "file mtime unchanged");
158 :
159 1 : (void)unlink(path);
160 : }
161 :
162 : /*
163 : * Test: test_load_truncated_file
164 : *
165 : * Writes a session file whose content is shorter than the required 16-byte
166 : * header. session_store_load must return -1.
167 : */
168 1 : static void test_load_truncated_file(void) {
169 1 : with_tmp_home("truncated");
170 :
171 1 : mkdir("/tmp/tg-cli-session-test-truncated", 0700);
172 : char base[256];
173 1 : snprintf(base, sizeof(base),
174 : "%s/.config", "/tmp/tg-cli-session-test-truncated");
175 1 : mkdir(base, 0700);
176 : char dir[256];
177 1 : snprintf(dir, sizeof(dir), "%s/.config/tg-cli",
178 : "/tmp/tg-cli-session-test-truncated");
179 1 : mkdir(dir, 0700);
180 :
181 : char path[256];
182 1 : snprintf(path, sizeof(path), "%s/.config/tg-cli/session.bin",
183 : getenv("HOME"));
184 :
185 : /* Only 8 bytes — less than the 16-byte STORE_HEADER. */
186 : uint8_t short_buf[8];
187 1 : memcpy(short_buf, "TGCS", 4);
188 1 : memset(short_buf + 4, 0, 4);
189 :
190 1 : FILE *f = fopen(path, "wb");
191 1 : ASSERT(f != NULL, "created truncated session file");
192 1 : if (f) {
193 1 : fwrite(short_buf, 1, sizeof(short_buf), f);
194 1 : fclose(f);
195 : }
196 :
197 : MtProtoSession s;
198 1 : mtproto_session_init(&s);
199 1 : int dc = 0;
200 1 : ASSERT(session_store_load(&s, &dc) == -1, "truncated file rejected");
201 :
202 1 : (void)unlink(path);
203 : }
204 :
205 : /* Multi-DC: save two DCs, each round-trips cleanly. */
206 1 : static void test_multi_dc_save_load(void) {
207 1 : with_tmp_home("multi-dc");
208 :
209 1 : MtProtoSession home; mtproto_session_init(&home);
210 257 : uint8_t k_home[256]; for (int i = 0; i < 256; i++) k_home[i] = (uint8_t)i;
211 1 : mtproto_session_set_auth_key(&home, k_home);
212 1 : mtproto_session_set_salt(&home, 0xA1A1A1A1A1A1A1A1ULL);
213 1 : home.session_id = 0x0101010101010101ULL;
214 :
215 1 : MtProtoSession media; mtproto_session_init(&media);
216 257 : uint8_t k_media[256]; for (int i = 0; i < 256; i++) k_media[i] = (uint8_t)(255 - i);
217 1 : mtproto_session_set_auth_key(&media, k_media);
218 1 : mtproto_session_set_salt(&media, 0xB2B2B2B2B2B2B2B2ULL);
219 1 : media.session_id = 0x0202020202020202ULL;
220 :
221 1 : ASSERT(session_store_save(&home, 2) == 0, "home save DC2");
222 1 : ASSERT(session_store_save_dc(4, &media) == 0, "secondary save DC4");
223 :
224 : /* load home → DC2 */
225 1 : MtProtoSession r; mtproto_session_init(&r);
226 1 : int dc = 0;
227 1 : ASSERT(session_store_load(&r, &dc) == 0, "load home ok");
228 1 : ASSERT(dc == 2, "home stayed DC2 after secondary save");
229 1 : ASSERT(r.server_salt == 0xA1A1A1A1A1A1A1A1ULL, "home salt");
230 1 : ASSERT(memcmp(r.auth_key, k_home, 256) == 0, "home auth_key");
231 :
232 : /* load secondary DC4 */
233 1 : MtProtoSession r2; mtproto_session_init(&r2);
234 1 : ASSERT(session_store_load_dc(4, &r2) == 0, "load DC4 ok");
235 1 : ASSERT(r2.server_salt == 0xB2B2B2B2B2B2B2B2ULL, "DC4 salt");
236 1 : ASSERT(memcmp(r2.auth_key, k_media, 256) == 0, "DC4 auth_key");
237 :
238 : /* load a DC we never saved */
239 1 : MtProtoSession r3; mtproto_session_init(&r3);
240 1 : ASSERT(session_store_load_dc(5, &r3) == -1, "unknown DC rejected");
241 :
242 1 : session_store_clear();
243 : }
244 :
245 : /* Upserting the same DC overwrites in place and does not grow count. */
246 1 : static void test_upsert_in_place(void) {
247 1 : with_tmp_home("upsert");
248 :
249 1 : MtProtoSession s1; mtproto_session_init(&s1);
250 257 : uint8_t k1[256]; for (int i = 0; i < 256; i++) k1[i] = (uint8_t)(i ^ 0x55);
251 1 : mtproto_session_set_auth_key(&s1, k1);
252 1 : mtproto_session_set_salt(&s1, 0x1111111111111111ULL);
253 1 : s1.session_id = 0xAAAAAAAAAAAAAAAAULL;
254 1 : ASSERT(session_store_save(&s1, 2) == 0, "first save");
255 :
256 1 : MtProtoSession s2; mtproto_session_init(&s2);
257 257 : uint8_t k2[256]; for (int i = 0; i < 256; i++) k2[i] = (uint8_t)(i ^ 0xAA);
258 1 : mtproto_session_set_auth_key(&s2, k2);
259 1 : mtproto_session_set_salt(&s2, 0x2222222222222222ULL);
260 1 : s2.session_id = 0xBBBBBBBBBBBBBBBBULL;
261 1 : ASSERT(session_store_save(&s2, 2) == 0, "overwrite save");
262 :
263 1 : MtProtoSession r; mtproto_session_init(&r);
264 1 : int dc = 0;
265 1 : ASSERT(session_store_load(&r, &dc) == 0, "load after upsert");
266 1 : ASSERT(dc == 2, "home still DC2");
267 1 : ASSERT(r.server_salt == 0x2222222222222222ULL, "salt overwritten");
268 1 : ASSERT(memcmp(r.auth_key, k2, 256) == 0, "auth_key overwritten");
269 :
270 1 : session_store_clear();
271 : }
272 :
273 : /* save_dc on an empty store still sets home (bootstrap convenience). */
274 1 : static void test_save_dc_on_empty_sets_home(void) {
275 1 : with_tmp_home("save-dc-empty");
276 1 : session_store_clear();
277 :
278 1 : MtProtoSession s; mtproto_session_init(&s);
279 1 : uint8_t k[256] = {0};
280 1 : mtproto_session_set_auth_key(&s, k);
281 1 : mtproto_session_set_salt(&s, 42);
282 1 : ASSERT(session_store_save_dc(4, &s) == 0, "save_dc on empty");
283 :
284 1 : MtProtoSession r; mtproto_session_init(&r);
285 1 : int dc = 0;
286 1 : ASSERT(session_store_load(&r, &dc) == 0, "load picks up the only DC");
287 1 : ASSERT(dc == 4, "home promoted to DC4");
288 :
289 1 : session_store_clear();
290 : }
291 :
292 1 : static void test_null_args(void) {
293 1 : ASSERT(session_store_save(NULL, 1) == -1, "null session rejected");
294 1 : int dc = 0;
295 1 : ASSERT(session_store_load(NULL, &dc) == -1, "null session (load)");
296 : }
297 :
298 : /* Helper: build a valid session with a recognisable key pattern. */
299 4 : static void make_session(MtProtoSession *s, uint8_t fill) {
300 4 : mtproto_session_init(s);
301 : uint8_t key[256];
302 1028 : for (int i = 0; i < 256; i++) key[i] = (uint8_t)(fill + i);
303 4 : mtproto_session_set_auth_key(s, key);
304 4 : mtproto_session_set_salt(s, (uint64_t)fill * 0x0101010101010101ULL);
305 4 : s->session_id = (uint64_t)fill;
306 4 : }
307 :
308 : /*
309 : * Test: write_under_lock
310 : *
311 : * Verifies that session_store_save() succeeds and the resulting file can be
312 : * loaded back correctly — i.e. the lock-acquire + atomic rename path works
313 : * end-to-end in the normal (non-contended) case.
314 : */
315 1 : static void test_write_under_lock(void) {
316 1 : with_tmp_home("write-under-lock");
317 1 : session_store_clear();
318 :
319 : MtProtoSession s;
320 1 : make_session(&s, 0xAB);
321 :
322 1 : ASSERT(session_store_save(&s, 3) == 0, "save under lock succeeds");
323 :
324 : MtProtoSession r;
325 1 : mtproto_session_init(&r);
326 1 : int dc = 0;
327 1 : ASSERT(session_store_load(&r, &dc) == 0, "load after locked write ok");
328 1 : ASSERT(dc == 3, "dc_id correct");
329 1 : ASSERT(r.has_auth_key == 1, "auth key present");
330 1 : ASSERT(r.server_salt == s.server_salt, "salt matches");
331 :
332 1 : session_store_clear();
333 : }
334 :
335 : /*
336 : * Test: concurrent_write_conflict
337 : *
338 : * Forks a child that holds the exclusive lock on session.bin while the parent
339 : * tries to save. The parent's save must fail with -1 (lock busy), not
340 : * corrupt the file or hang.
341 : *
342 : * Mechanism:
343 : * 1. Parent creates the session file.
344 : * 2. Parent opens a write-end pipe and forks.
345 : * 3. Child: open + flock(LOCK_EX) the file, close write-end of pipe
346 : * (signals "lock held"), then blocks reading from a second pipe until
347 : * signalled to exit.
348 : * 4. Parent: reads from the pipe (waits until child holds lock), then
349 : * tries session_store_save() — should return -1.
350 : * 5. Parent closes "go" pipe so child exits, waits for child, then verifies
351 : * the file is still intact.
352 : *
353 : * Skip gracefully on non-POSIX (Windows) where flock is unavailable; on those
354 : * platforms concurrent writes are not guarded and the test would be vacuous.
355 : */
356 1 : static void test_concurrent_write_conflict(void) {
357 : #if defined(_WIN32)
358 : /* Advisory locking not implemented on Windows; skip. */
359 : g_tests_run++;
360 : return;
361 : #else
362 1 : with_tmp_home("concurrent-write");
363 1 : session_store_clear();
364 :
365 : /* Create an initial valid store that the child can lock. */
366 : MtProtoSession s0;
367 1 : make_session(&s0, 0x10);
368 1 : ASSERT(session_store_save(&s0, 1) == 0, "initial write ok");
369 :
370 : /* pipe[0]=read, pipe[1]=write — child closes [1] to signal lock held. */
371 : int lock_ready[2]; /* child → parent: "I have the lock" */
372 : int release_lock[2]; /* parent → child: "you may release now" */
373 1 : ASSERT(pipe(lock_ready) == 0, "pipe lock_ready created");
374 1 : ASSERT(pipe(release_lock) == 0, "pipe release_lock created");
375 :
376 1 : const char *home = getenv("HOME");
377 1 : ASSERT(home != NULL, "HOME is set");
378 : char path[512];
379 1 : snprintf(path, sizeof(path), "%s/.config/tg-cli/session.bin", home);
380 :
381 1 : pid_t child = fork();
382 1 : ASSERT(child >= 0, "fork succeeded");
383 :
384 1 : if (child == 0) {
385 : /* ---- child ---- */
386 : /* Close parent-side ends. */
387 0 : close(lock_ready[0]);
388 0 : close(release_lock[1]);
389 :
390 : /* Acquire exclusive lock. */
391 0 : int fd = open(path, O_RDWR, 0600);
392 0 : if (fd == -1) { close(lock_ready[1]); close(release_lock[0]); _exit(1); }
393 0 : if (flock(fd, LOCK_EX) != 0) {
394 0 : close(fd); close(lock_ready[1]); close(release_lock[0]); _exit(2);
395 : }
396 :
397 : /* Signal parent: lock acquired. */
398 0 : close(lock_ready[1]); /* write-end close → parent read returns EOF */
399 :
400 : /* Block until parent says "done". */
401 : char rbuf[1];
402 0 : ssize_t nr = read(release_lock[0], rbuf, 1);
403 : (void)nr;
404 :
405 0 : flock(fd, LOCK_UN);
406 0 : close(fd);
407 0 : close(release_lock[0]);
408 0 : _exit(0);
409 : }
410 :
411 : /* ---- parent ---- */
412 1 : close(lock_ready[1]);
413 1 : close(release_lock[0]);
414 :
415 : /* Wait for child to hold the lock. */
416 : char buf[1];
417 1 : ssize_t nr = read(lock_ready[0], buf, 1);
418 : (void)nr;
419 1 : close(lock_ready[0]);
420 :
421 : /* Attempt a write while child holds the lock — must fail. */
422 : MtProtoSession s1;
423 1 : make_session(&s1, 0x20);
424 1 : int rc = session_store_save(&s1, 2);
425 1 : ASSERT(rc == -1, "save returns -1 when lock is held by another process");
426 :
427 : /* Release child and reap. */
428 1 : close(release_lock[1]);
429 1 : int status = 0;
430 1 : waitpid(child, &status, 0);
431 1 : ASSERT(WIFEXITED(status) && WEXITSTATUS(status) == 0, "child exited cleanly");
432 :
433 : /* The original file must still be readable and intact. */
434 : MtProtoSession r;
435 1 : mtproto_session_init(&r);
436 1 : int dc = 0;
437 1 : ASSERT(session_store_load(&r, &dc) == 0, "file intact after failed concurrent write");
438 1 : ASSERT(dc == 1, "original home DC preserved");
439 1 : ASSERT(r.server_salt == s0.server_salt, "original salt intact");
440 :
441 1 : session_store_clear();
442 : #endif /* _WIN32 */
443 : }
444 :
445 : /*
446 : * Test: concurrent_write_stress
447 : *
448 : * Forks N_WRITERS children that all try to session_store_save() at the same
449 : * time with distinct payloads. After all children exit, the parent loads the
450 : * file and verifies:
451 : * a) load succeeds (file is not corrupt / zero-length).
452 : * b) The resulting session matches exactly one of the N payload patterns
453 : * (no torn / interleaved data).
454 : *
455 : * Because session_store_save() uses a non-blocking flock(LOCK_EX | LOCK_NB),
456 : * all children that lose the race return -1 — that is expected and counted.
457 : * The winner commits atomically via rename(2), so the final file must be
458 : * exactly one of the N valid payloads.
459 : *
460 : * POSIX-only: skipped on Windows where flock is not available.
461 : */
462 1 : static void test_concurrent_write_stress(void) {
463 : #if defined(_WIN32)
464 : g_tests_run++;
465 : return;
466 : #else
467 : #define N_WRITERS 8
468 :
469 1 : with_tmp_home("stress-write");
470 1 : session_store_clear();
471 :
472 : /* Write an initial valid store so the file exists before the storm. */
473 : MtProtoSession s0;
474 1 : make_session(&s0, 0x01);
475 1 : ASSERT(session_store_save(&s0, 1) == 0, "stress: initial write ok");
476 :
477 : /* Each child uses fill = (child_index + 2) so fill 0x01 is the seed only. */
478 : pid_t pids[N_WRITERS];
479 :
480 9 : for (int i = 0; i < N_WRITERS; i++) {
481 8 : pid_t p = fork();
482 8 : ASSERT(p >= 0, "stress: fork succeeded");
483 8 : if (p == 0) {
484 : /* Child: attempt one save and exit with 0 (success) or 1 (lock busy). */
485 : MtProtoSession cs;
486 0 : make_session(&cs, (uint8_t)(i + 2));
487 0 : int rc = session_store_save(&cs, i + 2);
488 0 : _exit(rc == 0 ? 0 : 1);
489 : }
490 8 : pids[i] = p;
491 : }
492 :
493 : /* Reap all children; at least one must have succeeded. */
494 1 : int any_succeeded = 0;
495 9 : for (int i = 0; i < N_WRITERS; i++) {
496 8 : int st = 0;
497 8 : waitpid(pids[i], &st, 0);
498 8 : if (WIFEXITED(st) && WEXITSTATUS(st) == 0)
499 1 : any_succeeded = 1;
500 : }
501 1 : ASSERT(any_succeeded, "stress: at least one writer succeeded");
502 :
503 : /* Load must succeed and the payload must belong to exactly one writer. */
504 : MtProtoSession r;
505 1 : mtproto_session_init(&r);
506 1 : int dc = 0;
507 1 : ASSERT(session_store_load(&r, &dc) == 0, "stress: load after concurrent writes ok");
508 :
509 : /*
510 : * Verify the loaded key is coherent: all bytes in the auth_key must follow
511 : * the pattern used by make_session — key[j] = fill + j — for some fill
512 : * between 1 and N_WRITERS+1 inclusive. A torn write would produce a key
513 : * whose bytes do NOT all follow any single fill value.
514 : */
515 1 : int pattern_matched = 0;
516 3 : for (int fill = 1; fill <= N_WRITERS + 1 && !pattern_matched; fill++) {
517 2 : int ok = 1;
518 259 : for (int j = 0; j < 256 && ok; j++) {
519 257 : if (r.auth_key[j] != (uint8_t)(fill + j))
520 1 : ok = 0;
521 : }
522 2 : if (ok) pattern_matched = 1;
523 : }
524 1 : ASSERT(pattern_matched, "stress: loaded auth_key matches exactly one writer's payload");
525 :
526 1 : session_store_clear();
527 : #undef N_WRITERS
528 : #endif /* _WIN32 */
529 : }
530 :
531 1 : void run_session_store_tests(void) {
532 1 : RUN_TEST(test_save_load_roundtrip);
533 1 : RUN_TEST(test_load_missing_file);
534 1 : RUN_TEST(test_save_without_key_fails);
535 1 : RUN_TEST(test_load_wrong_magic);
536 1 : RUN_TEST(test_load_wrong_version);
537 1 : RUN_TEST(test_load_truncated_file);
538 1 : RUN_TEST(test_null_args);
539 1 : RUN_TEST(test_multi_dc_save_load);
540 1 : RUN_TEST(test_upsert_in_place);
541 1 : RUN_TEST(test_save_dc_on_empty_sets_home);
542 1 : RUN_TEST(test_write_under_lock);
543 1 : RUN_TEST(test_concurrent_write_conflict);
544 1 : RUN_TEST(test_concurrent_write_stress);
545 1 : }
|