LCOV - code coverage report
Current view: top level - tests/unit - test_session_store.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 94.4 % 267 252
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 16 16

            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 : }
        

Generated by: LCOV version 2.0-1