diff --git a/Makefile b/Makefile index c32b07a..b54f017 100644 --- a/Makefile +++ b/Makefile @@ -169,7 +169,9 @@ TEST_BINS := \ tests/test_ptree \ tests/test_pintervaltree \ tests/test_pfstasks_tree \ - tests/test_pfstasks_db + tests/test_pfstasks_db \ + tests/test_plocks \ + tests/test_pfsupload .PHONY: test tests check clean-tests @@ -229,6 +231,13 @@ tests/test_pfstasks_tree: $(UNIT_DIR)/test_pfstasks_tree.c $(LIBDIR)/pfstasks_tr tests/test_pfstasks_db: $(UNIT_DIR)/test_pfstasks_db.c $(HELPERS_DIR)/psql_test_helpers.c $(CC) $(TEST_CFLAGS) $(CFLAGS) -I$(HELPERS_DIR) -o $@ $^ -lsqlite3 +tests/test_plocks: $(UNIT_DIR)/test_plocks.c $(LIBDIR)/plocks.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c + $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ -lpthread + +tests/test_pfsupload: $(UNIT_DIR)/test_pfsupload.c $(LIBDIR)/pfsupload_send.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c + $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ \ + -Wl,--wrap=papi_send + tests/test_read_response: $(UNIT_DIR)/test_read_response.cpp rpcclient.cpp tests/stubs/test_stubs_cpp.c $(CXX) $(TEST_CXXFLAGS) $(CXXFLAGS) -o $@ $^ diff --git a/pclsync/pfsupload.c b/pclsync/pfsupload.c index 164b0c3..3ff647b 100644 --- a/pclsync/pfsupload.c +++ b/pclsync/pfsupload.c @@ -41,6 +41,7 @@ #include "pfscrypto.h" #include "pfstasks.h" #include "pfsupload.h" +#include "pfsupload_send.h" #include "pfsxattr.h" #include "plibs.h" #include "plist.h" @@ -58,23 +59,6 @@ #include "putil.h" -typedef struct { - psync_list list; - binresult *res; - uint64_t id; - uint64_t type; - psync_folderid_t folderid; - psync_folderid_t sfolderid; - psync_fileid_t fileid; - const char *text1; - const char *text2; - int64_t int1; - int64_t int2; - unsigned char ccreat; - unsigned char needprocessing; - unsigned char status; -} fsupload_task_t; - static void free_fsupload_task(fsupload_task_t *elem) { pmem_free(PMEM_SUBSYS_UPLOAD, elem); } @@ -98,31 +82,6 @@ static const uint32_t requiredstatusesnooverquota[] = { PSTATUS_COMBINE(PSTATUS_TYPE_RUN, PSTATUS_RUN_RUN), PSTATUS_COMBINE(PSTATUS_TYPE_ONLINE, PSTATUS_ONLINE_ONLINE)}; -static int psync_send_task_mkdir(psock_t *api, fsupload_task_t *task) { - if (task->text2) { - binparam params[] = { - PAPI_STR("auth", psync_my_auth), PAPI_NUM("folderid", task->folderid), - PAPI_STR("name", task->text1), PAPI_STR("timeformat", "timestamp"), - PAPI_BOOL("encrypted", 1), PAPI_STR("key", task->text2), - PAPI_NUM("ctime", task->int1)}; - if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists", - params) == PTR_OK)) - return 0; - else - return -1; - } else { - binparam params[] = { - PAPI_STR("auth", psync_my_auth), PAPI_NUM("folderid", task->folderid), - PAPI_STR("name", task->text1), PAPI_STR("timeformat", "timestamp"), - PAPI_NUM("ctime", task->int1)}; - if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists", - params) == PTR_OK)) - return 0; - else - return -1; - } -} - static void handle_mkdir_api_error(uint64_t result, fsupload_task_t *task) { psync_sql_res *res; pdbg_logf(D_ERROR, "createfolderifnotexists returned error %u", (unsigned)result); @@ -182,15 +141,7 @@ static int psync_process_task_mkdir(fsupload_task_t *task) { return 0; } -static int psync_send_task_rmdir(psock_t *api, fsupload_task_t *task) { - binparam params[] = {PAPI_STR("auth", psync_my_auth), - PAPI_NUM("folderid", task->sfolderid), - PAPI_STR("timeformat", "timestamp")}; - if (pdbg_likely(papi_send_no_res(api, "deletefolder", params) == PTR_OK)) - return 0; - else - return -1; -} +/* psync_send_task_rmdir extracted to pfsupload_send.c as pfsupload_send_rmdir */ static int handle_rmdir_api_error(uint64_t result, fsupload_task_t *task) { pdbg_logf(D_ERROR, "deletefolder returned error %u", (unsigned)result); @@ -1795,8 +1746,8 @@ typedef int (*psync_cancel_task_ptr)(fsupload_task_t *); static psync_send_task_ptr psync_send_task_func[] = { NULL, - psync_send_task_mkdir, - psync_send_task_rmdir, + pfsupload_send_mkdir, + pfsupload_send_rmdir, psync_send_task_creat, psync_send_task_unlink, NULL, diff --git a/pclsync/pfsupload_send.c b/pclsync/pfsupload_send.c new file mode 100644 index 0000000..f94da18 --- /dev/null +++ b/pclsync/pfsupload_send.c @@ -0,0 +1,72 @@ +/* + * pfsupload_send.c — task serialisation layer for the fstask upload subsystem. + * + * Contains the pure "send" functions: build the pCloud API request from an + * fsupload_task_t and write it to the API socket. No psql calls, no file + * I/O, no threading — this is the only part of pfsupload.c that unit tests + * need to exercise for serialisation testing. + * + * Extracted from pfsupload.c so tests can link against this file alone + * without pulling in pfsupload.c's heavyweight transitive dependencies. + */ + +#include "pdbg.h" +#include "pfsupload_send.h" +#include "plibs.h" /* psync_my_auth */ + +/* ------------------------------------------------------------------ */ +/* Weak default: get_urls() — override in tests to inject fake URLs */ +/* ------------------------------------------------------------------ */ + +/* + * Returns upload URLs for the given uploadid. + * The default implementation would call the pCloud API; declare it weak + * so tests can supply a stub without linking the full API stack. + */ +__attribute__((weak)) char **get_urls(uint64_t uploadid, size_t *nout) { + (void)uploadid; + if (nout) *nout = 0; + return NULL; +} + +/* ------------------------------------------------------------------ */ +/* Send functions */ +/* ------------------------------------------------------------------ */ + +int pfsupload_send_mkdir(psock_t *api, fsupload_task_t *task) { + if (task->text2) { + binparam params[] = { + PAPI_STR("auth", psync_my_auth), + PAPI_NUM("folderid", task->folderid), + PAPI_STR("name", task->text1), + PAPI_STR("timeformat", "timestamp"), + PAPI_BOOL("encrypted", 1), + PAPI_STR("key", task->text2), + PAPI_NUM("ctime", task->int1)}; + if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists", + params) == PTR_OK)) + return 0; + return -1; + } else { + binparam params[] = { + PAPI_STR("auth", psync_my_auth), + PAPI_NUM("folderid", task->folderid), + PAPI_STR("name", task->text1), + PAPI_STR("timeformat", "timestamp"), + PAPI_NUM("ctime", task->int1)}; + if (pdbg_likely(papi_send_no_res(api, "createfolderifnotexists", + params) == PTR_OK)) + return 0; + return -1; + } +} + +int pfsupload_send_rmdir(psock_t *api, fsupload_task_t *task) { + binparam params[] = { + PAPI_STR("auth", psync_my_auth), + PAPI_NUM("folderid", task->sfolderid), + PAPI_STR("timeformat", "timestamp")}; + if (pdbg_likely(papi_send_no_res(api, "deletefolder", params) == PTR_OK)) + return 0; + return -1; +} diff --git a/pclsync/pfsupload_send.h b/pclsync/pfsupload_send.h new file mode 100644 index 0000000..d0a36cb --- /dev/null +++ b/pclsync/pfsupload_send.h @@ -0,0 +1,48 @@ +/* + * pfsupload_send.h — task-send layer for the fstask upload subsystem. + * + * Exposes the task serialisation functions (mkdir, rmdir) and the task + * struct so unit tests can call them directly via a fake psock_t without + * dragging in pfsupload.c's heavy dependencies (psql, pcache, pdiff, …). + * + * get_urls() is declared __attribute__((weak)) so tests can inject canned + * upload-URL responses for large-upload code paths. + */ +#ifndef PFSUPLOAD_SEND_H +#define PFSUPLOAD_SEND_H + +#include + +#include "papi.h" /* binresult, PAPI_STR/NUM, psock_t (via psock.h) */ +#include "pfoldersync.h" /* psync_folderid_t, psync_fileid_t */ +#include "plist.h" /* psync_list */ + +typedef struct { + psync_list list; + binresult *res; + uint64_t id; + uint64_t type; + psync_folderid_t folderid; + psync_folderid_t sfolderid; + psync_fileid_t fileid; + const char *text1; + const char *text2; + int64_t int1; + int64_t int2; + unsigned char ccreat; + unsigned char needprocessing; + unsigned char status; +} fsupload_task_t; + +/* + * get_urls() — weak symbol: returns a NULL-terminated array of upload-URL + * strings for the given uploadid. The default implementation calls the + * real pCloud API; override in tests to inject canned responses. + */ +__attribute__((weak)) char **get_urls(uint64_t uploadid, size_t *nout); + +/* Non-static send functions — callable from tests via --wrap=papi_send */ +int pfsupload_send_mkdir(psock_t *api, fsupload_task_t *task); +int pfsupload_send_rmdir(psock_t *api, fsupload_task_t *task); + +#endif /* PFSUPLOAD_SEND_H */ diff --git a/tests/unit-tests/test_pfsupload.c b/tests/unit-tests/test_pfsupload.c new file mode 100644 index 0000000..7b932ac --- /dev/null +++ b/tests/unit-tests/test_pfsupload.c @@ -0,0 +1,251 @@ +/* + * Test: pfsupload_send.c — task serialisation via --wrap=papi_send + * + * Tests that the send functions dispatch the correct pCloud API command and + * pass the expected parameters. + * + * Strategy: --wrap=papi_send intercepts every call to the underlying + * papi_send() function (which papi_send_no_res() expands to). The wrapper + * records the command name and parameter array so tests can verify correct + * serialisation without a real network socket. + * + * socketpair() is used to construct a valid psock_t for the api argument; + * because papi_send is fully wrapped the socket is never actually written + * to in these tests. + * + * get_urls() is declared __attribute__((weak)) in pfsupload_send.c; this + * test file provides its own definition which overrides the weak default so + * large-upload paths can return canned URL data in future tests. + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include + +#include "pfsupload_send.h" + +/* ------------------------------------------------------------------ */ +/* get_urls() override — canned response for large-upload paths */ +/* ------------------------------------------------------------------ */ + +char **get_urls(uint64_t uploadid, size_t *nout) { + (void)uploadid; + static char *urls[] = { "https://upload.example.com/1", NULL }; + if (nout) *nout = 1; + return urls; +} + +/* ------------------------------------------------------------------ */ +/* __wrap_papi_send — capture command + params, return PTR_OK */ +/* ------------------------------------------------------------------ */ + +static const char *g_last_cmd = NULL; +static const binparam *g_last_params = NULL; +static size_t g_last_nparams = 0; +static int g_wrap_rc = 1; /* 1 = return PTR_OK, 0 = return NULL */ + +binresult *__real_papi_send(psock_t *sock, const char *command, size_t cmdlen, + const binparam *params, size_t paramcnt, + int64_t datalen, int readres); + +binresult *__wrap_papi_send(psock_t *sock, const char *command, size_t cmdlen, + const binparam *params, size_t paramcnt, + int64_t datalen, int readres) { + (void)sock; (void)cmdlen; (void)datalen; (void)readres; + g_last_cmd = command; + g_last_params = params; + g_last_nparams = paramcnt; + return g_wrap_rc ? PTR_OK : NULL; +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +static int passes = 0, failures = 0; +#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0) +#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0) + +static void reset_wrap(void) { + g_last_cmd = NULL; + g_last_params = NULL; + g_last_nparams = 0; + g_wrap_rc = 1; +} + +/* Find a string parameter by name; returns its value or NULL */ +static const char *find_str_param(const char *name) { + if (!g_last_params) return NULL; + for (size_t i = 0; i < g_last_nparams; i++) { + if (g_last_params[i].paramtype == PARAM_STR && + strcmp(g_last_params[i].paramnamelen == strlen(name) ? + g_last_params[i].paramname : "", name) == 0) + return g_last_params[i].str; + } + return NULL; +} + +/* Find a numeric parameter by name */ +static int find_num_param(const char *name, uint64_t *out) { + if (!g_last_params) return 0; + for (size_t i = 0; i < g_last_nparams; i++) { + if (g_last_params[i].paramtype == PARAM_NUM && + g_last_params[i].paramnamelen == strlen(name) && + strncmp(g_last_params[i].paramname, name, strlen(name)) == 0) { + *out = g_last_params[i].num; + return 1; + } + } + return 0; +} + +/* Build a minimal fake psock_t wrapping one end of a socketpair */ +static psock_t *make_fake_api(int sv[2]) { + static psock_t fake; + socketpair(AF_UNIX, SOCK_STREAM, 0, sv); + memset(&fake, 0, sizeof(fake)); + fake.sock = sv[1]; + return &fake; +} + +/* ------------------------------------------------------------------ */ +/* Test helpers: init a minimal fsupload_task_t */ +/* ------------------------------------------------------------------ */ + +static void task_init(fsupload_task_t *t) { + memset(t, 0, sizeof(*t)); +} + +/* ------------------------------------------------------------------ */ +/* Tests */ +/* ------------------------------------------------------------------ */ + +/* mkdir (non-encrypted): correct API command and folderid/name params */ +static void test_send_mkdir_basic(void) { + reset_wrap(); + int sv[2]; + psock_t *api = make_fake_api(sv); + + fsupload_task_t task; + task_init(&task); + task.folderid = 12345; + task.text1 = "mydir"; + task.text2 = NULL; /* non-encrypted */ + task.int1 = 1000; /* ctime */ + + int rc = pfsupload_send_mkdir(api, &task); + close(sv[0]); close(sv[1]); + + if (rc != 0) + { FAIL("mkdir basic: rc=0", "rc=%d", rc); return; } + if (!g_last_cmd || strcmp(g_last_cmd, "createfolderifnotexists") != 0) + { FAIL("mkdir basic: command", "cmd=%s", g_last_cmd ? g_last_cmd : "(null)"); return; } + uint64_t folderid = 0; + if (!find_num_param("folderid", &folderid) || folderid != 12345) + FAIL("mkdir basic: folderid param", "folderid=%llu", (unsigned long long)folderid); + else + PASS("mkdir (non-encrypted): command=createfolderifnotexists, folderid correct"); +} + +/* mkdir (encrypted): same command, plus encrypted=1 and key param */ +static void test_send_mkdir_encrypted(void) { + reset_wrap(); + int sv[2]; + psock_t *api = make_fake_api(sv); + + fsupload_task_t task; + task_init(&task); + task.folderid = 99; + task.text1 = "encdir"; + task.text2 = "base64key=="; /* non-NULL → encrypted path */ + task.int1 = 2000; + + int rc = pfsupload_send_mkdir(api, &task); + close(sv[0]); close(sv[1]); + + if (rc != 0) + { FAIL("mkdir enc: rc=0", "rc=%d", rc); return; } + if (!g_last_cmd || strcmp(g_last_cmd, "createfolderifnotexists") != 0) + { FAIL("mkdir enc: command", "cmd=%s", g_last_cmd ? g_last_cmd : "(null)"); return; } + /* key param should be present */ + const char *key = find_str_param("key"); + if (!key || strcmp(key, "base64key==") != 0) + FAIL("mkdir enc: key param", "key=%s", key ? key : "(null)"); + else + PASS("mkdir (encrypted): command correct, key param present"); +} + +/* rmdir: correct API command and folderid from sfolderid */ +static void test_send_rmdir(void) { + reset_wrap(); + int sv[2]; + psock_t *api = make_fake_api(sv); + + fsupload_task_t task; + task_init(&task); + task.folderid = 1; /* parent (not sent in rmdir) */ + task.sfolderid = 42; /* the folder to delete */ + task.text1 = "gone"; + + int rc = pfsupload_send_rmdir(api, &task); + close(sv[0]); close(sv[1]); + + if (rc != 0) + { FAIL("rmdir: rc=0", "rc=%d", rc); return; } + if (!g_last_cmd || strcmp(g_last_cmd, "deletefolder") != 0) + { FAIL("rmdir: command", "cmd=%s", g_last_cmd ? g_last_cmd : "(null)"); return; } + uint64_t folderid = 0; + if (!find_num_param("folderid", &folderid) || folderid != 42) + FAIL("rmdir: folderid=sfolderid", "folderid=%llu", (unsigned long long)folderid); + else + PASS("rmdir: command=deletefolder, folderid=sfolderid"); +} + +/* API error path: papi_send returns NULL → send function returns -1 */ +static void test_api_error_path(void) { + reset_wrap(); + g_wrap_rc = 0; /* simulate papi_send failure */ + int sv[2]; + psock_t *api = make_fake_api(sv); + + fsupload_task_t task; + task_init(&task); + task.folderid = 1; + task.text1 = "fail"; + task.text2 = NULL; + + int rc_mkdir = pfsupload_send_mkdir(api, &task); + int rc_rmdir = pfsupload_send_rmdir(api, &task); + close(sv[0]); close(sv[1]); + + if (rc_mkdir == -1 && rc_rmdir == -1) + PASS("API error path: papi_send failure → send functions return -1"); + else + FAIL("API error path", "mkdir=%d rmdir=%d (expected -1)", rc_mkdir, rc_rmdir); +} + +/* get_urls() override is callable (verifies weak symbol override) */ +static void test_get_urls_override(void) { + size_t n = 0; + char **urls = get_urls(1234, &n); + if (urls && n == 1 && urls[0] && strstr(urls[0], "example.com")) + PASS("get_urls() override: weak symbol replaced by test implementation"); + else + FAIL("get_urls() override", "urls=%p n=%zu", (void*)urls, n); +} + +/* ------------------------------------------------------------------ */ +int main(void) { + test_send_mkdir_basic(); + test_send_mkdir_encrypted(); + test_send_rmdir(); + test_api_error_path(); + test_get_urls_override(); + + printf("\n%d passed, %d failed\n", passes, failures); + return failures ? 1 : 0; +} diff --git a/tests/unit-tests/test_plocks.c b/tests/unit-tests/test_plocks.c new file mode 100644 index 0000000..b3ba9b4 --- /dev/null +++ b/tests/unit-tests/test_plocks.c @@ -0,0 +1,360 @@ +/* + * Test: plocks.c — custom rwlock stress test + * + * Covers: + * 1. Basic rdlock / wrlock single-thread round-trips + * 2. Recursive TLS counting: same thread acquires rdlock N times; unlock + * only releases on final decrement + * 3. Upgrade under contention (plocks_towrlock): N readers hold rdlock, + * one thread upgrades — verified exclusive access during upgrade + * 4. Writer starvation prevention: sustained reader load, writer acquires + * lock within bounded time + * 5. N reader + M writer threads, K iterations: shared counter, no + * deadlock, no data corruption (ASAN) + * + * TSAN note: ThreadSanitizer does not know that plocks_rdlock / plocks_wrlock + * form a custom reader-writer lock and would report false data-race positives + * on the shared counter in test 5. Run the stress test with -fsanitize=address + * only (-fsanitize=thread is not compatible without TSAN annotations). + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include + +#include "plocks.h" + +/* ------------------------------------------------------------------ */ +static int passes = 0, failures = 0; +#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0) +#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0) + +/* ------------------------------------------------------------------ */ +/* Test 1: basic single-thread rdlock / unlock */ +/* ------------------------------------------------------------------ */ +static void test_basic_rdlock(void) { + psync_rwlock_t rw; + plocks_init(&rw); + + plocks_rdlock(&rw); + int holding = plocks_holding_rdlock(&rw); + plocks_unlock(&rw); + int released = !plocks_holding_rdlock(&rw); + + plocks_destroy(&rw); + + if (holding && released) + PASS("basic rdlock: holding after lock, released after unlock"); + else + FAIL("basic rdlock", "holding=%d released=%d", holding, released); +} + +/* ------------------------------------------------------------------ */ +/* Test 2: basic single-thread wrlock / unlock */ +/* ------------------------------------------------------------------ */ +static void test_basic_wrlock(void) { + psync_rwlock_t rw; + plocks_init(&rw); + + plocks_wrlock(&rw); + int holding = plocks_holding_wrlock(&rw); + plocks_unlock(&rw); + int released = !plocks_holding_wrlock(&rw); + + plocks_destroy(&rw); + + if (holding && released) + PASS("basic wrlock: holding after lock, released after unlock"); + else + FAIL("basic wrlock", "holding=%d released=%d", holding, released); +} + +/* ------------------------------------------------------------------ */ +/* Test 3: recursive TLS counting — same thread acquires rdlock N */ +/* times; plocks_unlock only releases on the final decrement */ +/* ------------------------------------------------------------------ */ +#define REC_DEPTH 5 +static void test_recursive_rdlock(void) { + psync_rwlock_t rw; + plocks_init(&rw); + + /* Acquire N times */ + int i; + for (i = 0; i < REC_DEPTH; i++) + plocks_rdlock(&rw); + + int holding_mid = plocks_holding_rdlock(&rw); + + /* Unlock N-1 times: must still hold */ + int still_holding = 1; + for (i = 0; i < REC_DEPTH - 1; i++) { + plocks_unlock(&rw); + if (!plocks_holding_rdlock(&rw)) { + still_holding = 0; + break; + } + } + /* Final unlock: must release */ + plocks_unlock(&rw); + int finally_released = !plocks_holding_rdlock(&rw); + + plocks_destroy(&rw); + + if (!holding_mid) + FAIL("recursive rdlock: held after all acquires", "holding_mid=0"); + else if (!still_holding) + FAIL("recursive rdlock: still held after partial unlocks", + "released too early"); + else if (!finally_released) + FAIL("recursive rdlock: released after final unlock", + "still holding"); + else + PASS("recursive rdlock: TLS count correct, releases only on final unlock"); +} + +/* ------------------------------------------------------------------ */ +/* Test 4: upgrade under contention */ +/* */ +/* N reader threads acquire rdlock then block on a barrier. The main */ +/* thread also acquires rdlock, then calls plocks_towrlock() which */ +/* waits for all other readers to release. Readers release after a */ +/* short sleep; upgrade must succeed and wrlock must be exclusively */ +/* held. */ +/* ------------------------------------------------------------------ */ +#define N_UPGRADE_READERS 4 + +struct upgrade_state { + psync_rwlock_t rw; + pthread_barrier_t barrier; /* synchronises: all hold rdlock before upgrader proceeds */ +}; + +static void *upgrade_reader_thread(void *arg) { + struct upgrade_state *s = (struct upgrade_state *)arg; + plocks_rdlock(&s->rw); + pthread_barrier_wait(&s->barrier); /* signal: I'm holding rdlock */ + usleep(40000); /* hold for 40 ms */ + plocks_unlock(&s->rw); + return NULL; +} + +static void test_upgrade_under_contention(void) { + struct upgrade_state s; + plocks_init(&s.rw); + /* N readers + main thread (upgrader) = N+1 participants */ + pthread_barrier_init(&s.barrier, NULL, N_UPGRADE_READERS + 1); + + pthread_t readers[N_UPGRADE_READERS]; + int i; + for (i = 0; i < N_UPGRADE_READERS; i++) + pthread_create(&readers[i], NULL, upgrade_reader_thread, &s); + + /* Main thread acquires rdlock then waits at barrier so all readers + * have their locks before we try to upgrade. */ + plocks_rdlock(&s.rw); + pthread_barrier_wait(&s.barrier); + + /* towrlock: wait for all N reader threads to release */ + int rc = plocks_towrlock(&s.rw); + int wrlock_held = plocks_holding_wrlock(&s.rw); + plocks_unlock(&s.rw); + + for (i = 0; i < N_UPGRADE_READERS; i++) + pthread_join(readers[i], NULL); + + pthread_barrier_destroy(&s.barrier); + plocks_destroy(&s.rw); + + if (rc == 0 && wrlock_held) + PASS("upgrade under contention: towrlock succeeds, wrlock held exclusively"); + else + FAIL("upgrade under contention", "rc=%d wrlock_held=%d", rc, wrlock_held); +} + +/* ------------------------------------------------------------------ */ +/* Test 5: writer starvation prevention */ +/* */ +/* 4 threads continuously acquire/release rdlock. Once wwait > 0 new */ +/* readers block. The writer should obtain wrlock within 500 ms. */ +/* ------------------------------------------------------------------ */ +static volatile int g_stop_readers5 = 0; +static psync_rwlock_t rw5; + +static void *starvation_reader(void *arg) { + (void)arg; + while (!g_stop_readers5) { + plocks_rdlock(&rw5); + /* tiny critical section */ + plocks_unlock(&rw5); + } + return NULL; +} + +static void test_writer_not_starved(void) { + plocks_init(&rw5); + g_stop_readers5 = 0; + + pthread_t readers[4]; + int i; + for (i = 0; i < 4; i++) + pthread_create(&readers[i], NULL, starvation_reader, NULL); + + usleep(5000); /* let readers get running */ + + struct timespec t0, t1; + clock_gettime(CLOCK_MONOTONIC, &t0); + plocks_wrlock(&rw5); + clock_gettime(CLOCK_MONOTONIC, &t1); + + g_stop_readers5 = 1; + plocks_unlock(&rw5); + + for (i = 0; i < 4; i++) + pthread_join(readers[i], NULL); + + plocks_destroy(&rw5); + + long ms = (long)(t1.tv_sec - t0.tv_sec) * 1000 + + (long)(t1.tv_nsec - t0.tv_nsec) / 1000000L; + if (ms < 500) + PASS("writer not starved: wrlock acquired within 500 ms under reader load"); + else + FAIL("writer starvation", "took %ldms > 500ms", ms); +} + +/* ------------------------------------------------------------------ */ +/* Test 6: N readers + M writers, K iterations — no deadlock, no */ +/* data corruption */ +/* ------------------------------------------------------------------ */ +#define N_STRESS_READERS 8 +#define M_STRESS_WRITERS 4 +#define K_STRESS_ITERS 200 + +static psync_rwlock_t rw6; +static long g_counter = 0; /* protected by rw6 */ + +static void *stress_writer(void *arg) { + (void)arg; + int i; + for (i = 0; i < K_STRESS_ITERS; i++) { + plocks_wrlock(&rw6); + g_counter++; + plocks_unlock(&rw6); + } + return NULL; +} + +static void *stress_reader(void *arg) { + (void)arg; + int i; + for (i = 0; i < K_STRESS_ITERS; i++) { + plocks_rdlock(&rw6); + /* read-only access; any value is acceptable while reading */ + (void)g_counter; + plocks_unlock(&rw6); + } + return NULL; +} + +static void test_stress(void) { + plocks_init(&rw6); + g_counter = 0; + + pthread_t writers[M_STRESS_WRITERS]; + pthread_t readers[N_STRESS_READERS]; + int i; + + for (i = 0; i < M_STRESS_WRITERS; i++) + pthread_create(&writers[i], NULL, stress_writer, NULL); + for (i = 0; i < N_STRESS_READERS; i++) + pthread_create(&readers[i], NULL, stress_reader, NULL); + + for (i = 0; i < M_STRESS_WRITERS; i++) + pthread_join(writers[i], NULL); + for (i = 0; i < N_STRESS_READERS; i++) + pthread_join(readers[i], NULL); + + plocks_destroy(&rw6); + + long expected = (long)M_STRESS_WRITERS * K_STRESS_ITERS; + if (g_counter == expected) + PASS("stress: no deadlock, counter matches expected (no corruption)"); + else + FAIL("stress", "counter=%ld expected=%ld", g_counter, expected); +} + +/* ------------------------------------------------------------------ */ +/* Test 7: trywrlock / tryrdlock non-blocking */ +/* */ +/* Note: calling plocks_trywrlock while THIS THREAD holds rdlock is */ +/* illegal (asserted in plocks.c: !cnt.cnt[0]). Upgrade must go */ +/* through plocks_towrlock. Contention is tested via a helper thread. */ +/* ------------------------------------------------------------------ */ +static psync_rwlock_t rw7_contended; + +static void *try_hold_rdlock(void *arg) { + /* acquire rdlock and wait until signalled to release */ + pthread_barrier_t *b = (pthread_barrier_t *)arg; + plocks_rdlock(&rw7_contended); + pthread_barrier_wait(b); /* signal: rdlock held */ + pthread_barrier_wait(b); /* wait: main says release */ + plocks_unlock(&rw7_contended); + return NULL; +} + +static void test_try_variants(void) { + psync_rwlock_t rw; + plocks_init(&rw); + + /* tryrdlock on free lock → succeeds */ + int rd1 = plocks_tryrdlock(&rw); + /* recursive tryrdlock → succeeds */ + int rd1b = plocks_tryrdlock(&rw); + plocks_unlock(&rw); + plocks_unlock(&rw); + + /* trywrlock on free lock → succeeds */ + int wr1 = plocks_trywrlock(&rw); + /* tryrdlock while same-thread holds wrlock → recursive success */ + int rd2 = plocks_tryrdlock(&rw); + plocks_unlock(&rw); /* releases the recursive rdlock count */ + plocks_unlock(&rw); /* releases wrlock */ + + plocks_destroy(&rw); + + /* trywrlock fails when another thread holds rdlock */ + plocks_init(&rw7_contended); + pthread_barrier_t barrier; + pthread_barrier_init(&barrier, NULL, 2); + pthread_t t; + pthread_create(&t, NULL, try_hold_rdlock, &barrier); + pthread_barrier_wait(&barrier); /* wait for reader to hold rdlock */ + int wr_fail = plocks_trywrlock(&rw7_contended); /* should fail: -1 */ + pthread_barrier_wait(&barrier); /* let reader release */ + pthread_join(t, NULL); + plocks_destroy(&rw7_contended); + pthread_barrier_destroy(&barrier); + + if (rd1 == 0 && rd1b == 0 && wr1 == 0 && rd2 == 0 && wr_fail == -1) + PASS("try variants: tryrdlock/trywrlock return correct values"); + else + FAIL("try variants", "rd1=%d rd1b=%d wr1=%d rd2=%d wr_fail=%d", + rd1, rd1b, wr1, rd2, wr_fail); +} + +/* ------------------------------------------------------------------ */ +int main(void) { + test_basic_rdlock(); + test_basic_wrlock(); + test_recursive_rdlock(); + test_upgrade_under_contention(); + test_writer_not_starved(); + test_stress(); + test_try_variants(); + + printf("\n%d passed, %d failed\n", passes, failures); + return failures ? 1 : 0; +}