From ae77d3e2f2531d5fdb969d442aea67ecd1a29c3e Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:52:35 +0100 Subject: [PATCH] Add testability infrastructure: unit tests, extracted modules, CI integration (#381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add unit test for psync_task_free refcount fix (#377) Adds tests/unit-tests/test_ptask_free.c to verify all code paths of the psync_task_free fix from #377: single-owner free, last-ref destroy, non-last-ref decrement, READY task signaling, and lock-before-refcnt ordering. All 5 tests pass. Also adds compiled binary to .gitignore. Co-Authored-By: Claude Sonnet 4.6 * Refactor test_ptask_free to link production code via --wrap Extract psync_task_free + psync_task_destroy (and their static helpers psync_task_dec_refcnt, psync_task_entry) from ptask.c into a new separately-compilable unit pclsync/ptask_free.c. Add pclsync/ptask_free_internal.h to expose the internal struct layout (struct psync_task_manager_t_ / struct psync_task_t_) for test use without pulling in ptask.c's heavyweight transitive dependencies. Rewrite tests/unit-tests/test_ptask_free.c to: - Include ptask_free_internal.h instead of duplicating structs inline - Call the real psync_task_free() rather than a local replica - Intercept pthread_mutex_lock/unlock and pmem_free via --wrap linker flags to observe lock discipline and detect destroy invocations Update the Makefile test_ptask_free target to link pclsync/ptask_free.c and pass the required --wrap flags. Production build unchanged: ptask_free.o is picked up automatically by the existing wildcard COBJ rule. Co-Authored-By: Claude Sonnet 4.6 * Implement Tasks #3, #4, #5: tree tests, pfstasks tree layer, DB harness Task #3 — Unit tests for ptree and pintervaltree tests/unit-tests/test_ptree.c: 8 tests covering single-node insert, in-order traversal after arbitrary and reverse inserts, BST lookup, leaf/root/all-node deletion, and ptree_for_each visitation. tests/unit-tests/test_pintervaltree.c: 18 tests covering single add, non-overlapping, overlapping/adjacent/contained/spanning merges, chain merge, remove middle split, remove exact/left/right/spanning, cut_end, first_interval_containing_or_after, and free(NULL). Task #4 — Extract pfstasks tree layer pclsync/pfstasks_tree.h + pclsync/pfstasks_tree.c: pure tree layer (zero psql calls) extracted from pfstasks.c — pfs_task_search_tree, pfs_task_walk_tree (static helpers), pfs_task_insert_into_tree, pfs_task_find_mkdir/rmdir/creat/unlink, pfs_task_find_mkdir_by_folderid, pfs_task_find_creat_by_fileid. pclsync/pfstasks.c: #includes pfstasks_tree.h; all moved functions removed; all callers unchanged. tests/unit-tests/test_pfstasks_tree.c: 13 tests using direct tree construction (no DB) to verify find-by-name, taskid discrimination, find-by-numeric-id, and empty-folder edge cases. Task #5 — psql in-memory harness + pfstasks DB tests tests/helpers/psql_test_helpers.h + .c: lightweight harness that opens :memory: via sqlite3_open, enables PRAGMA foreign_keys=ON, and applies the full PSYNC_DATABASE_STRUCTURE schema. Exposes psql_test_db(), psql_test_exec(), psql_test_insert_fstask(), psql_test_count_fstask/fstaskdepend(). No dependency on psql.c. tests/unit-tests/test_pfstasks_db.c: 10 tests verifying schema creation, fstask insertion/query, fstaskdepend insertion, CASCADE DELETE propagation, FK enforcement, rmdir-blocking SQL pattern, creat-after-unlink sequencing, and open/close idempotence. All 11 new tests pass; production build clean. Co-Authored-By: Claude Sonnet 4.6 * Fix P1 review findings in pfstasks_db test and helpers 1. Check psql_test_exec() return values in test_cascade_delete() and test_creat_after_unlink() consistently with test_fstaskdepend_insert(). 2. Remove dead dep_cnt variable and (void)dep_cnt suppressor from test_creat_after_unlink(). 3. Change SQLITE_STATIC → SQLITE_TRANSIENT for text1 binding in psql_test_insert_fstask() to avoid dangling-pointer footgun on future reuse. Co-Authored-By: Claude Sonnet 4.6 * Fix ASAN/LSAN failures: ppath_home stack-use-after-scope + intentional leak pclsync/ppath.c: Move buff[4096] to function scope in ppath_home() so the pointer stored in dir via result->pw_dir remains live through the putil_strdup(dir) call. Previously buff went out of scope at the if-block close, causing a stack-use-after-scope ASAN report on every call that fell through the getpwuid_r path. tests/unit-tests/test_ptools_errptr.c: run_unfixed() intentionally leaks errPtr to demonstrate the pre-fix bug. Wrap the allocation with LSAN_DISABLE() / LSAN_ENABLE() so LSAN does not abort the process at exit before stdio flushes, which was causing a non-zero exit code. The guard uses nested #ifdef/__has_feature to remain compatible with both GCC (__SANITIZE_ADDRESS__) and Clang (__has_feature(address_sanitizer)) without triggering "missing binary operator" errors on GCC. Co-Authored-By: Claude Sonnet 4.6 * Implement Tasks #11 and #12: plocks stress test + pfsupload send tests Task #11 — plocks.c stress test (test_plocks.c) 7 tests: basic rdlock/wrlock round-trip, recursive TLS counting (same thread acquires rdlock N times; unlock only releases on final decrement), upgrade under contention (N readers + towrlock; barrier-synchronized), writer starvation prevention (sustained reader load; writer acquires within 500ms), N-reader + M-writer counter-integrity stress test (ASAN), and try-variant contention (trywrlock fails when another thread holds rdlock). TSAN note documented: custom lock internals require ASAN-only when ThreadSanitizer annotations are absent. Task #12 — pfsupload send-function tests (pfsupload_send.c/h + test_pfsupload.c) Extract psync_send_task_mkdir and psync_send_task_rmdir from pfsupload.c into pclsync/pfsupload_send.c as non-static pfsupload_send_mkdir/rmdir. Expose fsupload_task_t struct via pclsync/pfsupload_send.h. Add __attribute__((weak)) get_urls() as an injectable URL seam for large- upload paths. pfsupload.c updated to include pfsupload_send.h and use the renamed functions in its dispatch table; pfsupload_send.o is automatically picked up by the production wildcard build. 5 tests: mkdir (non-encrypted) command + folderid param, mkdir (encrypted) key param present, rmdir command + sfolderid, API error path (papi_send failure → -1), get_urls() weak override. Uses --wrap=papi_send to intercept API calls and socketpair() to provide a valid psock_t without real network I/O. Co-Authored-By: Claude Sonnet 4.6 * P2 cleanup: comments, make_fake_api stack alloc, find_str_param fix 1. test_plocks.c: add comment to test_upgrade_under_contention clarifying it verifies towrlock completion and holding_wrlock; notes that concurrent exclusivity is covered by test_stress(). 2. test_pfsupload.c / make_fake_api: replace static-local psock_t with caller-supplied stack allocation (out parameter) to eliminate the multiple-calls-per-test footgun. 3. test_pfsupload.c / find_str_param: replace ternary `paramnamelen == strlen ? paramname : ""` with explicit length check + strncmp, matching the cleaner pattern used in find_num_param. 4. Makefile: add comment next to -Wl,--wrap=papi_send noting it redirects papi_send to __wrap_papi_send and is GNU ld only (not macOS Apple ld). Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Levi Neely Co-authored-by: Claude Sonnet 4.6 --- .gitignore | 1 - Makefile | 37 ++- pclsync/pfstasks.c | 145 +--------- pclsync/pfstasks_tree.c | 166 ++++++++++++ pclsync/pfstasks_tree.h | 38 +++ pclsync/pfsupload.c | 57 +--- pclsync/pfsupload_send.c | 72 +++++ pclsync/pfsupload_send.h | 48 ++++ pclsync/ppath.c | 4 +- pclsync/ptask.c | 77 +----- pclsync/ptask_free.c | 72 +++++ pclsync/ptask_free_internal.h | 75 ++++++ tests/helpers/psql_test_helpers.c | 130 +++++++++ tests/helpers/psql_test_helpers.h | 44 +++ tests/unit-tests/test_pfstasks_db.c | 300 +++++++++++++++++++++ tests/unit-tests/test_pfstasks_tree.c | 289 ++++++++++++++++++++ tests/unit-tests/test_pfsupload.c | 252 ++++++++++++++++++ tests/unit-tests/test_pintervaltree.c | 304 +++++++++++++++++++++ tests/unit-tests/test_plocks.c | 367 ++++++++++++++++++++++++++ tests/unit-tests/test_ptask_free.c | 336 +++++++++++------------ tests/unit-tests/test_ptools_errptr.c | 26 ++ tests/unit-tests/test_ptree.c | 290 ++++++++++++++++++++ 22 files changed, 2665 insertions(+), 465 deletions(-) create mode 100644 pclsync/pfstasks_tree.c create mode 100644 pclsync/pfstasks_tree.h create mode 100644 pclsync/pfsupload_send.c create mode 100644 pclsync/pfsupload_send.h create mode 100644 pclsync/ptask_free.c create mode 100644 pclsync/ptask_free_internal.h create mode 100644 tests/helpers/psql_test_helpers.c create mode 100644 tests/helpers/psql_test_helpers.h create mode 100644 tests/unit-tests/test_pfstasks_db.c create mode 100644 tests/unit-tests/test_pfstasks_tree.c create mode 100644 tests/unit-tests/test_pfsupload.c create mode 100644 tests/unit-tests/test_pintervaltree.c create mode 100644 tests/unit-tests/test_plocks.c create mode 100644 tests/unit-tests/test_ptree.c diff --git a/.gitignore b/.gitignore index 6074675..5bfbd97 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,4 @@ tests/test_prun tests/test_ptools_errptr tests/test_ptools_params tests/test_read_response -tests/test_ptask_free tests/test_* diff --git a/Makefile b/Makefile index b80a8f6..bf2338d 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,8 @@ TESTS_DIR := tests TEST_CFLAGS := -D_POSIX_C_SOURCE=200809L TEST_CXXFLAGS := -D_POSIX_C_SOURCE=200809L +HELPERS_DIR := tests/helpers + TEST_BINS := \ tests/test_pdbg_path \ tests/test_ptools_params \ @@ -163,7 +165,13 @@ TEST_BINS := \ tests/test_prun \ tests/test_ptools_errptr \ tests/test_read_response \ - tests/test_signal_safety + tests/test_signal_safety \ + tests/test_ptree \ + tests/test_pintervaltree \ + tests/test_pfstasks_tree \ + tests/test_pfstasks_db \ + tests/test_plocks \ + tests/test_pfsupload .PHONY: test tests check clean-tests @@ -191,8 +199,12 @@ tests/test_ptools_params: $(UNIT_DIR)/test_ptools_params.c $(LIBDIR)/ptools.c $( tests/test_pfs_lock_ordering: $(UNIT_DIR)/test_pfs_lock_ordering.c $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $< -lpthread -tests/test_ptask_free: $(UNIT_DIR)/test_ptask_free.c - $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $< -lpthread +tests/test_ptask_free: $(UNIT_DIR)/test_ptask_free.c $(LIBDIR)/ptask_free.c + $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ \ + -Wl,--wrap=pthread_mutex_lock \ + -Wl,--wrap=pthread_mutex_unlock \ + -Wl,--wrap=pmem_free \ + -lpthread tests/test_prun: $(UNIT_DIR)/test_prun.c $(LIBDIR)/prun.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c $(CC) -D_POSIX_C_SOURCE=199309L $(CFLAGS) -o $@ $^ \ @@ -207,6 +219,25 @@ tests/test_ptools_errptr: $(UNIT_DIR)/test_ptools_errptr.c $(LIBDIR)/ptools.c $( -Wl,--wrap=malloc \ -Wl,--wrap=free +tests/test_ptree: $(UNIT_DIR)/test_ptree.c $(LIBDIR)/ptree.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c + $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ + +tests/test_pintervaltree: $(UNIT_DIR)/test_pintervaltree.c $(LIBDIR)/pintervaltree.c $(LIBDIR)/ptree.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c + $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ + +tests/test_pfstasks_tree: $(UNIT_DIR)/test_pfstasks_tree.c $(LIBDIR)/pfstasks_tree.c $(LIBDIR)/ptree.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c $(LIBDIR)/ppath.c tests/stubs/test_stubs.c + $(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ + +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 # redirect papi_send → __wrap_papi_send; GNU ld only (not macOS Apple ld) + 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/pfstasks.c b/pclsync/pfstasks.c index fd1cd14..48ece77 100644 --- a/pclsync/pfstasks.c +++ b/pclsync/pfstasks.c @@ -40,6 +40,7 @@ #include "pfoldersync.h" #include "pfs.h" #include "pfstasks.h" +#include "pfstasks_tree.h" #include "pfsupload.h" #include "plibs.h" #include "pmem.h" @@ -192,150 +193,6 @@ void pfs_task_release_folder_tasks_locked(psync_fstask_folder_t *folder) { } } -static psync_tree *pfs_task_search_tree(psync_tree *tree, size_t nameoff, - const char *name, uint64_t taskid, - size_t taskidoff) { - int c; - while (tree) { - c = strcmp(name, ((char *)tree) + nameoff); - if (c < 0) - tree = tree->left; - else if (c > 0) - tree = tree->right; - else - break; - } - if (!tree || !taskid || *((uint64_t *)(((char *)tree) + taskidoff)) == taskid) - return tree; - else { - psync_tree *tn; - tn = ptree_get_prev(tree); - while (tn) { - if (strcmp(name, ((char *)tn) + nameoff)) - break; - if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid) - return tn; - tn = ptree_get_prev(tn); - } - tn = ptree_get_next(tree); - while (tn) { - if (strcmp(name, ((char *)tn) + nameoff)) - break; - if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid) - return tn; - tn = ptree_get_next(tn); - } - return NULL; - } -} - -static psync_tree *pfs_task_walk_tree(psync_tree *tree, uint64_t taskid, - size_t taskidoff) { - tree = ptree_get_first(tree); - while (tree) { - if (*((uint64_t *)(((char *)tree) + taskidoff)) == taskid) - return tree; - tree = ptree_get_next(tree); - } - return NULL; -} - -static void pfs_task_insert_into_tree(psync_tree **tree, size_t nameoff, - psync_tree *element) { - const char *name; - psync_tree *node; - int c; - - if (!*tree) { - ptree_add_after(tree, NULL, element); - return; - } - - name = ((char *)element) + nameoff; - node = *tree; - - while (1) { - c = strcmp(name, ((char *)node) + nameoff); - - if (c < 0) { - if (node->left) - node = node->left; - else { - ptree_add_before(tree, node, element); - return; - } - } else { - if (c == 0) - pdbg_logf(D_WARNING, "duplicate entry %s, should not happen", name); - - if (node->right) - node = node->right; - else { - ptree_add_after(tree, node, element); - return; - } - } - } -} - -psync_fstask_mkdir_t *pfs_task_find_mkdir(psync_fstask_folder_t *folder, - const char *name, - uint64_t taskid) { - return ptree_element( - pfs_task_search_tree(folder->mkdirs, - offsetof(psync_fstask_mkdir_t, name), name, - taskid, offsetof(psync_fstask_mkdir_t, taskid)), - psync_fstask_mkdir_t, tree); -} - -psync_fstask_rmdir_t *pfs_task_find_rmdir(psync_fstask_folder_t *folder, - const char *name, - uint64_t taskid) { - return ptree_element( - pfs_task_search_tree(folder->rmdirs, - offsetof(psync_fstask_rmdir_t, name), name, - taskid, offsetof(psync_fstask_rmdir_t, taskid)), - psync_fstask_rmdir_t, tree); -} - -psync_fstask_creat_t *pfs_task_find_creat(psync_fstask_folder_t *folder, - const char *name, - uint64_t taskid) { - return ptree_element( - pfs_task_search_tree(folder->creats, - offsetof(psync_fstask_creat_t, name), name, - taskid, offsetof(psync_fstask_creat_t, taskid)), - psync_fstask_creat_t, tree); -} - -psync_fstask_unlink_t *pfs_task_find_unlink(psync_fstask_folder_t *folder, - const char *name, - uint64_t taskid) { - return ptree_element( - pfs_task_search_tree(folder->unlinks, - offsetof(psync_fstask_unlink_t, name), name, - taskid, offsetof(psync_fstask_unlink_t, taskid)), - psync_fstask_unlink_t, tree); -} - -psync_fstask_mkdir_t * -pfs_task_find_mkdir_by_folderid(psync_fstask_folder_t *folder, - psync_fsfolderid_t folderid) { - return ptree_element( - pfs_task_walk_tree(folder->mkdirs, folderid, - offsetof(psync_fstask_mkdir_t, folderid)), - psync_fstask_mkdir_t, tree); -} - -psync_fstask_creat_t * -pfs_task_find_creat_by_fileid(psync_fstask_folder_t *folder, - psync_fsfileid_t fileid) { - return ptree_element( - pfs_task_walk_tree(folder->creats, fileid, - offsetof(psync_fstask_creat_t, fileid)), - psync_fstask_creat_t, tree); -} - static void pfs_task_depend(uint64_t taskid, uint64_t dependontaskid) { psync_sql_res *res; res = psql_prepare("INSERT OR IGNORE INTO fstaskdepend " diff --git a/pclsync/pfstasks_tree.c b/pclsync/pfstasks_tree.c new file mode 100644 index 0000000..e9b273c --- /dev/null +++ b/pclsync/pfstasks_tree.c @@ -0,0 +1,166 @@ +/* + * pfstasks_tree.c — pure tree operations for the fstask subsystem. + * + * All functions here are psql-free and operate only on in-memory psync_tree + * structures. Extracted so that unit tests can link this file alone without + * pulling in pfstasks.c's heavy dependencies (psql, pcrypto, pfs*, …). + */ + +#include +#include + +#include "pdbg.h" +#include "pfstasks_tree.h" + +/* ------------------------------------------------------------------ */ +/* Static helpers */ +/* ------------------------------------------------------------------ */ + +/* + * BST search by name string at `nameoff`. + * If `taskid` != 0 and a name-equal node has a different taskid, + * walks prev/next siblings to find the matching one. + */ +static psync_tree *pfs_task_search_tree(psync_tree *tree, size_t nameoff, + const char *name, uint64_t taskid, + size_t taskidoff) { + int c; + while (tree) { + c = strcmp(name, ((char *)tree) + nameoff); + if (c < 0) tree = tree->left; + else if (c > 0) tree = tree->right; + else break; + } + if (!tree || !taskid || + *((uint64_t *)(((char *)tree) + taskidoff)) == taskid) + return tree; + /* Walk siblings with the same name to find the matching taskid */ + psync_tree *tn = ptree_get_prev(tree); + while (tn) { + if (strcmp(name, ((char *)tn) + nameoff)) break; + if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid) return tn; + tn = ptree_get_prev(tn); + } + tn = ptree_get_next(tree); + while (tn) { + if (strcmp(name, ((char *)tn) + nameoff)) break; + if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid) return tn; + tn = ptree_get_next(tn); + } + return NULL; +} + +/* + * Linear in-order walk to find the first node whose uint64_t field at + * `taskidoff` equals `taskid`. + */ +static psync_tree *pfs_task_walk_tree(psync_tree *tree, uint64_t taskid, + size_t taskidoff) { + tree = ptree_get_first(tree); + while (tree) { + if (*((uint64_t *)(((char *)tree) + taskidoff)) == taskid) return tree; + tree = ptree_get_next(tree); + } + return NULL; +} + +/* ------------------------------------------------------------------ */ +/* Exported: tree insertion */ +/* ------------------------------------------------------------------ */ + +void pfs_task_insert_into_tree(psync_tree **tree, size_t nameoff, + psync_tree *element) { + const char *name; + psync_tree *node; + int c; + + if (!*tree) { + ptree_add_after(tree, NULL, element); + return; + } + + name = ((char *)element) + nameoff; + node = *tree; + + while (1) { + c = strcmp(name, ((char *)node) + nameoff); + if (c < 0) { + if (node->left) + node = node->left; + else { + ptree_add_before(tree, node, element); + return; + } + } else { + if (c == 0) + pdbg_logf(D_WARNING, "duplicate entry %s, should not happen", + name); + if (node->right) + node = node->right; + else { + ptree_add_after(tree, node, element); + return; + } + } + } +} + +/* ------------------------------------------------------------------ */ +/* Exported: find by name (+ optional taskid discriminator) */ +/* ------------------------------------------------------------------ */ + +psync_fstask_mkdir_t *pfs_task_find_mkdir(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid) { + return ptree_element( + pfs_task_search_tree(folder->mkdirs, + offsetof(psync_fstask_mkdir_t, name), name, + taskid, offsetof(psync_fstask_mkdir_t, taskid)), + psync_fstask_mkdir_t, tree); +} + +psync_fstask_rmdir_t *pfs_task_find_rmdir(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid) { + return ptree_element( + pfs_task_search_tree(folder->rmdirs, + offsetof(psync_fstask_rmdir_t, name), name, + taskid, offsetof(psync_fstask_rmdir_t, taskid)), + psync_fstask_rmdir_t, tree); +} + +psync_fstask_creat_t *pfs_task_find_creat(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid) { + return ptree_element( + pfs_task_search_tree(folder->creats, + offsetof(psync_fstask_creat_t, name), name, + taskid, offsetof(psync_fstask_creat_t, taskid)), + psync_fstask_creat_t, tree); +} + +psync_fstask_unlink_t *pfs_task_find_unlink(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid) { + return ptree_element( + pfs_task_search_tree(folder->unlinks, + offsetof(psync_fstask_unlink_t, name), name, + taskid, offsetof(psync_fstask_unlink_t, taskid)), + psync_fstask_unlink_t, tree); +} + +/* ------------------------------------------------------------------ */ +/* Exported: find by numeric ID */ +/* ------------------------------------------------------------------ */ + +psync_fstask_mkdir_t *pfs_task_find_mkdir_by_folderid( + psync_fstask_folder_t *folder, psync_fsfolderid_t folderid) { + return ptree_element( + pfs_task_walk_tree(folder->mkdirs, folderid, + offsetof(psync_fstask_mkdir_t, folderid)), + psync_fstask_mkdir_t, tree); +} + +psync_fstask_creat_t *pfs_task_find_creat_by_fileid( + psync_fstask_folder_t *folder, psync_fsfileid_t fileid) { + return ptree_element( + pfs_task_walk_tree(folder->creats, fileid, + offsetof(psync_fstask_creat_t, fileid)), + psync_fstask_creat_t, tree); +} diff --git a/pclsync/pfstasks_tree.h b/pclsync/pfstasks_tree.h new file mode 100644 index 0000000..4c7dd28 --- /dev/null +++ b/pclsync/pfstasks_tree.h @@ -0,0 +1,38 @@ +/* + * pfstasks_tree.h — pure tree layer for the fstask subsystem. + * + * Declares functions that operate solely on in-memory psync_tree structures + * with zero psql calls. This header is included by pfstasks.c and by the + * unit test test_pfstasks_tree.c. + * + * Do NOT add functions with psql dependencies here. + */ +#ifndef PFSTASKS_TREE_H +#define PFSTASKS_TREE_H + +#include "pfstasks.h" + +/* + * Insert `element` into the AVL tree `*tree`, ordering by the NUL-terminated + * string at offset `nameoff` within each element. + */ +void pfs_task_insert_into_tree(psync_tree **tree, size_t nameoff, + psync_tree *element); + +/* Find by name (and optional taskid discriminator; pass 0 to match any) */ +psync_fstask_mkdir_t *pfs_task_find_mkdir(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid); +psync_fstask_rmdir_t *pfs_task_find_rmdir(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid); +psync_fstask_creat_t *pfs_task_find_creat(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid); +psync_fstask_unlink_t *pfs_task_find_unlink(psync_fstask_folder_t *folder, + const char *name, uint64_t taskid); + +/* Find by numeric ID via linear walk */ +psync_fstask_mkdir_t *pfs_task_find_mkdir_by_folderid( + psync_fstask_folder_t *folder, psync_fsfolderid_t folderid); +psync_fstask_creat_t *pfs_task_find_creat_by_fileid( + psync_fstask_folder_t *folder, psync_fsfileid_t fileid); + +#endif /* PFSTASKS_TREE_H */ 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/pclsync/ppath.c b/pclsync/ppath.c index e4a053b..672928f 100644 --- a/pclsync/ppath.c +++ b/pclsync/ppath.c @@ -58,12 +58,14 @@ int64_t ppath_free_space(const char *path) { char *ppath_home() { struct stat st; const char *dir; + /* buff must be function-scoped: dir may point into it (via pw_dir) and + * must remain valid through the putil_strdup(dir) call below. */ + char buff[4096]; dir = getenv("HOME"); if (pdbg_unlikely(!dir) || pdbg_unlikely(stat(dir, &st)) || pdbg_unlikely(!pfile_stat_mode_ok(&st, 7))) { struct passwd pwd; struct passwd *result; - char buff[4096]; if (pdbg_unlikely(getpwuid_r(getuid(), &pwd, buff, sizeof(buff), &result)) || pdbg_unlikely(stat(result->pw_dir, &st)) || pdbg_unlikely(!pfile_stat_mode_ok(&st, 7))) diff --git a/pclsync/ptask.c b/pclsync/ptask.c index 07a32e9..12918e6 100644 --- a/pclsync/ptask.c +++ b/pclsync/ptask.c @@ -48,20 +48,12 @@ #include "pstatus.h" #include "psys.h" #include "ptask.h" +#include "ptask_free_internal.h" #include "ptree.h" #include "pupload.h" #define get_len(t) (sizeof(t) - offsetof(t, request)) -#define PSYNC_TASK_STATUS_RUNNING 0 -#define PSYNC_TASK_STATUS_READY 1 -#define PSYNC_TASK_STATUS_DONE 2 -#define PSYNC_TASK_STATUS_RETURNED 3 - -// #define PSYNC_WAIT_ANYBODY -1 // unused, but may be important later -#define PSYNC_WAIT_NOBODY -2 -#define PSYNC_WAIT_FREED -3 - #define TASK_TYPE_EXIT 0 #define TASK_TYPE_FILE_DWL 1 #define TASK_TYPE_FILE_DWL_NM 2 @@ -163,22 +155,6 @@ typedef struct { int fd; } download_context_t; -struct psync_task_t_ { - psync_task_callback_t callback; - void *param; - pthread_cond_t cond; - int id; - int status; -}; - -struct psync_task_manager_t_ { - pthread_mutex_t mutex; - int taskcnt; - int refcnt; - int waitfor; - struct psync_task_t_ tasks[]; -}; - static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static int running = 0; static int sockd = INVALID_SOCKET; @@ -845,35 +821,6 @@ static int task_send_async(const void *task, size_t len) { return ret; } -static void psync_task_destroy(psync_task_manager_t tm) { - int i; - for (i = 0; i < tm->taskcnt; i++) - pthread_cond_destroy(&tm->tasks[i].cond); - pthread_mutex_destroy(&tm->mutex); - pmem_free(PMEM_SUBSYS_OTHER, tm); -} - -static void psync_task_dec_refcnt(psync_task_manager_t tm) { - int refcnt; - pthread_mutex_lock(&tm->mutex); - refcnt = --tm->refcnt; - pthread_mutex_unlock(&tm->mutex); - if (!refcnt) - psync_task_destroy(tm); -} - -static psync_task_manager_t psync_get_manager_of_task(struct psync_task_t_ *t) { - return (psync_task_manager_t)(((char *)(t - t->id)) - - offsetof(struct psync_task_manager_t_, tasks)); -} - -static void psync_task_entry(void *ptr) { - struct psync_task_t_ *t; - t = (struct psync_task_t_ *)ptr; - t->callback(ptr, t->param); - psync_task_dec_refcnt(psync_get_manager_of_task(t)); -} - void ptask_ldir_mk(psync_syncid_t syncid, psync_folderid_t folderid, psync_folderid_t localfolderid) { @@ -1181,28 +1128,6 @@ void *psync_task_papi_result(psync_task_manager_t tm, int id) { return ret; } -void psync_task_free(psync_task_manager_t tm) { - int refcnt, i; - pthread_mutex_lock(&tm->mutex); - if (tm->refcnt == 1) { - pthread_mutex_unlock(&tm->mutex); - psync_task_destroy(tm); - } - else { - tm->waitfor = PSYNC_WAIT_FREED; - for (i = 0; i < tm->taskcnt; i++) - if (tm->tasks[i].status == PSYNC_TASK_STATUS_READY) { - tm->tasks[i].status = PSYNC_TASK_STATUS_RETURNED; - pthread_cond_signal(&tm->tasks[i].cond); - } - refcnt = --tm->refcnt; - pthread_mutex_unlock(&tm->mutex); - if (!refcnt) { - psync_task_destroy(tm); - } - } -} - int psync_task_complete(void *h, void *data) { psync_task_manager_t tm; struct psync_task_t_ *t; diff --git a/pclsync/ptask_free.c b/pclsync/ptask_free.c new file mode 100644 index 0000000..463065c --- /dev/null +++ b/pclsync/ptask_free.c @@ -0,0 +1,72 @@ +/* + * ptask_free.c — psync_task_manager_t lifecycle: destroy and free. + * + * Extracted from ptask.c as a separately compilable unit so that + * tests/unit-tests/test_ptask_free.c can link against this file alone, + * without dragging in ptask.c's heavyweight dependencies (papi, psql, + * pdeflate, …). + * + * Fix: b92a389 — mutex is held during the refcnt check in all paths. + */ + +#include +#include + +#include "pmem.h" +#include "ptask_free_internal.h" + +/* ------------------------------------------------------------------ */ +/* Static helpers */ +/* ------------------------------------------------------------------ */ + +static void psync_task_destroy(psync_task_manager_t tm) { + int i; + for (i = 0; i < tm->taskcnt; i++) + pthread_cond_destroy(&tm->tasks[i].cond); + pthread_mutex_destroy(&tm->mutex); + pmem_free(PMEM_SUBSYS_OTHER, tm); +} + +static void psync_task_dec_refcnt(psync_task_manager_t tm) { + int refcnt; + pthread_mutex_lock(&tm->mutex); + refcnt = --tm->refcnt; + pthread_mutex_unlock(&tm->mutex); + if (!refcnt) + psync_task_destroy(tm); +} + +/* ------------------------------------------------------------------ */ +/* Thread entry-point (non-static: referenced from psync_task_run_tasks + * in ptask.c via the forward declaration in ptask_free_internal.h) */ +/* ------------------------------------------------------------------ */ + +void psync_task_entry(void *ptr) { + struct psync_task_t_ *t = (struct psync_task_t_ *)ptr; + t->callback(ptr, t->param); + psync_task_dec_refcnt(psync_get_manager_of_task(t)); +} + +/* ------------------------------------------------------------------ */ +/* Public API */ +/* ------------------------------------------------------------------ */ + +void psync_task_free(psync_task_manager_t tm) { + int refcnt, i; + pthread_mutex_lock(&tm->mutex); + if (tm->refcnt == 1) { + pthread_mutex_unlock(&tm->mutex); + psync_task_destroy(tm); + } else { + tm->waitfor = PSYNC_WAIT_FREED; + for (i = 0; i < tm->taskcnt; i++) + if (tm->tasks[i].status == PSYNC_TASK_STATUS_READY) { + tm->tasks[i].status = PSYNC_TASK_STATUS_RETURNED; + pthread_cond_signal(&tm->tasks[i].cond); + } + refcnt = --tm->refcnt; + pthread_mutex_unlock(&tm->mutex); + if (!refcnt) + psync_task_destroy(tm); + } +} diff --git a/pclsync/ptask_free_internal.h b/pclsync/ptask_free_internal.h new file mode 100644 index 0000000..80ab024 --- /dev/null +++ b/pclsync/ptask_free_internal.h @@ -0,0 +1,75 @@ +/* + * ptask_free_internal.h — internal layout of psync_task_manager_t. + * + * Included by: + * - pclsync/ptask_free.c (production lifecycle code) + * - pclsync/ptask.c (rest of the task subsystem) + * - tests/unit-tests/test_ptask_free.c (struct-level access in tests) + * + * NOT part of the public API. Do not include from general application code. + */ +#ifndef PTASK_FREE_INTERNAL_H +#define PTASK_FREE_INTERNAL_H + +#include +#include + +/* + * Provide minimal forward declarations when this header is included + * standalone (e.g. from the test). When ptask.h has already been + * included its include guard (_PSYNC_TASKS_H) suppresses the duplicates. + */ +#ifndef _PSYNC_TASKS_H +typedef void (*psync_task_callback_t)(void *, void *); +struct psync_task_manager_t_; +typedef struct psync_task_manager_t_ *psync_task_manager_t; +#endif + +/* Task status values */ +#define PSYNC_TASK_STATUS_RUNNING 0 +#define PSYNC_TASK_STATUS_READY 1 +#define PSYNC_TASK_STATUS_DONE 2 +#define PSYNC_TASK_STATUS_RETURNED 3 + +/* waitfor sentinel values */ +// #define PSYNC_WAIT_ANYBODY -1 /* unused, but may be useful later */ +#define PSYNC_WAIT_NOBODY -2 +#define PSYNC_WAIT_FREED -3 + +struct psync_task_t_ { + psync_task_callback_t callback; + void *param; + pthread_cond_t cond; + int id; + int status; +}; + +struct psync_task_manager_t_ { + pthread_mutex_t mutex; + int taskcnt; + int refcnt; + int waitfor; + struct psync_task_t_ tasks[]; +}; + +/* + * Helper: given a pointer to an individual task, return the owning manager. + * Declared static inline so both ptask.c and ptask_free.c can use it + * without any linkage conflict. + */ +static inline psync_task_manager_t +psync_get_manager_of_task(struct psync_task_t_ *t) { + return (psync_task_manager_t)(((char *)(t - t->id)) - + offsetof(struct psync_task_manager_t_, tasks)); +} + +/* Defined in ptask_free.c; declared in ptask.h for normal callers. */ +void psync_task_free(psync_task_manager_t tm); + +/* + * psync_task_entry is defined in ptask_free.c and used as a thread + * entry-point in psync_task_run_tasks (ptask.c). + */ +void psync_task_entry(void *ptr); + +#endif /* PTASK_FREE_INTERNAL_H */ diff --git a/tests/helpers/psql_test_helpers.c b/tests/helpers/psql_test_helpers.c new file mode 100644 index 0000000..ba0fd99 --- /dev/null +++ b/tests/helpers/psql_test_helpers.c @@ -0,0 +1,130 @@ +/* + * psql_test_helpers.c — in-memory SQLite harness for unit tests. + * + * Opens ":memory:" and applies the PSYNC_DATABASE_STRUCTURE schema from + * pdatabase.h. All tests that need a live database use this file. + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include + +#include + +#include "pdatabase.h" +#include "psql_test_helpers.h" + +static sqlite3 *g_db = NULL; + +int psql_test_open(void) { + if (g_db) { + sqlite3_close(g_db); + g_db = NULL; + } + if (sqlite3_open(":memory:", &g_db) != SQLITE_OK) { + fprintf(stderr, "psql_test_open: sqlite3_open failed: %s\n", + sqlite3_errmsg(g_db)); + sqlite3_close(g_db); + g_db = NULL; + return -1; + } + /* + * Enable foreign key enforcement — sqlite3 disables it by default. + * Must be set before the schema is applied so CASCADE/FK constraints + * take effect from the start (mirrors psql_connect's DATABASE_CONFIG). + */ + sqlite3_exec(g_db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL); + + /* + * PSYNC_DATABASE_STRUCTURE begins with "PRAGMA page_size=4096;…" and ends + * with "COMMIT;\n". For :memory: the pragmas are silently ignored and the + * DDL is applied inside a BEGIN/COMMIT block. + */ + char *err = NULL; + int rc = sqlite3_exec(g_db, PSYNC_DATABASE_STRUCTURE, NULL, NULL, &err); + if (rc != SQLITE_OK) { + fprintf(stderr, "psql_test_open: schema error: %s\n", + err ? err : "(null)"); + sqlite3_free(err); + sqlite3_close(g_db); + g_db = NULL; + return -1; + } + return 0; +} + +void psql_test_close(void) { + if (g_db) { + sqlite3_close(g_db); + g_db = NULL; + } +} + +sqlite3 *psql_test_db(void) { + return g_db; +} + +int psql_test_exec(const char *sql) { + char *err = NULL; + int rc = sqlite3_exec(g_db, sql, NULL, NULL, &err); + if (rc != SQLITE_OK) { + fprintf(stderr, "psql_test_exec: %s\n SQL: %s\n", + err ? err : "(null)", sql); + sqlite3_free(err); + } + return rc; +} + +int64_t psql_test_insert_fstask(int type, int status, int64_t folderid, + const char *text1) { + sqlite3_stmt *stmt; + const char *sql = "INSERT INTO fstask (type, status, folderid, sfolderid," + " text1) VALUES (?, ?, ?, ?, ?)"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_int(stmt, 1, type); + sqlite3_bind_int(stmt, 2, status); + sqlite3_bind_int64(stmt, 3, folderid); + sqlite3_bind_int64(stmt, 4, folderid); + if (text1) + sqlite3_bind_text(stmt, 5, text1, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(stmt, 5); + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) return -1; + return (int64_t)sqlite3_last_insert_rowid(g_db); +} + +int psql_test_count_fstask(const char *where_clause) { + char sql[512]; + if (where_clause && *where_clause) + snprintf(sql, sizeof(sql), + "SELECT COUNT(*) FROM fstask WHERE %s", where_clause); + else + snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM fstask"); + sqlite3_stmt *stmt; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + int count = -1; + if (sqlite3_step(stmt) == SQLITE_ROW) + count = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + return count; +} + +int psql_test_count_fstaskdepend(const char *where_clause) { + char sql[512]; + if (where_clause && *where_clause) + snprintf(sql, sizeof(sql), + "SELECT COUNT(*) FROM fstaskdepend WHERE %s", where_clause); + else + snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM fstaskdepend"); + sqlite3_stmt *stmt; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + int count = -1; + if (sqlite3_step(stmt) == SQLITE_ROW) + count = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + return count; +} diff --git a/tests/helpers/psql_test_helpers.h b/tests/helpers/psql_test_helpers.h new file mode 100644 index 0000000..425a4bb --- /dev/null +++ b/tests/helpers/psql_test_helpers.h @@ -0,0 +1,44 @@ +/* + * psql_test_helpers.h — in-memory SQLite harness for unit tests. + * + * Provides psql_test_open() / psql_test_close() which open a ":memory:" + * database and apply the full PSYNC_DATABASE_STRUCTURE schema from + * pdatabase.h. Use psql_test_db() to obtain the sqlite3* connection for + * direct SQL operations in tests. + * + * Intentionally bypasses psql.c so tests can link this file alone without + * pulling in psql.c's heavyweight transitive dependencies (pcache, psignal, + * psys, pnetlibs, …). + */ +#ifndef PSQL_TEST_HELPERS_H +#define PSQL_TEST_HELPERS_H + +#include + +/* + * Open an in-memory SQLite database and apply the full PSYNC_DATABASE_STRUCTURE + * schema. Returns 0 on success, -1 on failure. + * Call this once at the start of each test that needs the DB. + */ +int psql_test_open(void); + +/* Close and discard the in-memory database. */ +void psql_test_close(void); + +/* Return the active sqlite3* connection (valid between open/close). */ +sqlite3 *psql_test_db(void); + +/* Execute a SQL statement and return the sqlite3 result code. */ +int psql_test_exec(const char *sql); + +/* Insert one fstask row; returns the inserted rowid on success, -1 on error. */ +int64_t psql_test_insert_fstask(int type, int status, int64_t folderid, + const char *text1); + +/* Count rows matching a simple WHERE clause on fstask. */ +int psql_test_count_fstask(const char *where_clause); + +/* Count rows matching a simple WHERE clause on fstaskdepend. */ +int psql_test_count_fstaskdepend(const char *where_clause); + +#endif /* PSQL_TEST_HELPERS_H */ diff --git a/tests/unit-tests/test_pfstasks_db.c b/tests/unit-tests/test_pfstasks_db.c new file mode 100644 index 0000000..ae36a01 --- /dev/null +++ b/tests/unit-tests/test_pfstasks_db.c @@ -0,0 +1,300 @@ +/* + * Test: pfstasks DB layer via psql_test_helpers + * + * Opens an :memory: SQLite database with the full PSYNC_DATABASE_STRUCTURE + * schema, then exercises: + * 1. Schema creation (tables and indices exist) + * 2. fstask row insertion and retrieval + * 3. Dependency insertion via fstaskdepend + * 4. CASCADE DELETE: deleting an fstask removes its fstaskdepend rows + * 5. FK enforcement: fstaskdepend row with unknown fstaskid is rejected + * 6. rmdir blocking: SQL query pattern that pfstasks.c uses to detect + * non-empty folders (file/folder rows in DB) + * 7. creat-after-unlink: insert UNLINK then CREAT tasks for the same name + * in the same folder, verify both tasks exist and can be queried by type + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#include "psql_test_helpers.h" + +/* Task type constants (mirrors pfstasks.h / PSYNC_FS_TASK_*) */ +#define TASK_MKDIR 1 +#define TASK_RMDIR 2 +#define TASK_CREAT 3 +#define TASK_UNLINK 4 + +/* ------------------------------------------------------------------ */ +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 int table_exists(const char *name) { + char sql[256]; + snprintf(sql, sizeof(sql), + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='%s'", + name); + sqlite3_stmt *stmt; + sqlite3 *db = psql_test_db(); + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; + int ok = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) + ok = sqlite3_column_int(stmt, 0) > 0; + sqlite3_finalize(stmt); + return ok; +} + +/* ------------------------------------------------------------------ */ +/* Test 1: Schema creation */ +/* ------------------------------------------------------------------ */ +static void test_schema_created(void) { + const char *required[] = { + "fstask", "fstaskdepend", "folder", "file", "setting", + "syncfolder", "localfolder", "localfile", NULL + }; + int ok = 1; + for (int i = 0; required[i]; i++) { + if (!table_exists(required[i])) { + ok = 0; + FAIL("schema: table exists", "table '%s' missing", required[i]); + } + } + if (ok) + PASS("schema: all required tables created"); +} + +/* ------------------------------------------------------------------ */ +/* Test 2: fstask insert and query */ +/* ------------------------------------------------------------------ */ +static void test_fstask_insert_query(void) { + int64_t id = psql_test_insert_fstask(TASK_MKDIR, 0, 100, "mydir"); + if (id <= 0) + { FAIL("fstask insert", "rowid=%lld", (long long)id); return; } + + int cnt = psql_test_count_fstask("type=1 AND folderid=100"); + if (cnt == 1) + PASS("fstask insert: row present with correct type/folderid"); + else + FAIL("fstask insert/query", "count=%d expected 1", cnt); +} + +/* ------------------------------------------------------------------ */ +/* Test 3: fstaskdepend insert */ +/* ------------------------------------------------------------------ */ +static void test_fstaskdepend_insert(void) { + int64_t task1 = psql_test_insert_fstask(TASK_MKDIR, 0, 200, "parent"); + int64_t task2 = psql_test_insert_fstask(TASK_MKDIR, 0, 200, "child"); + if (task1 <= 0 || task2 <= 0) + { FAIL("fstaskdepend setup", "tasks=%lld/%lld", (long long)task1, + (long long)task2); return; } + + char sql[256]; + snprintf(sql, sizeof(sql), + "INSERT INTO fstaskdepend (fstaskid, dependfstaskid) VALUES (%lld, %lld)", + (long long)task2, (long long)task1); + if (psql_test_exec(sql) != SQLITE_OK) + { FAIL("fstaskdepend insert", "exec failed"); return; } + + char where[128]; + snprintf(where, sizeof(where), "fstaskid=%lld AND dependfstaskid=%lld", + (long long)task2, (long long)task1); + int cnt = psql_test_count_fstaskdepend(where); + if (cnt == 1) + PASS("fstaskdepend: dependency row inserted"); + else + FAIL("fstaskdepend insert/count", "count=%d expected 1", cnt); +} + +/* ------------------------------------------------------------------ */ +/* Test 4: CASCADE DELETE — deleting fstask removes fstaskdepend rows */ +/* ------------------------------------------------------------------ */ +static void test_cascade_delete(void) { + int64_t parent = psql_test_insert_fstask(TASK_MKDIR, 0, 300, "cascade_p"); + int64_t child = psql_test_insert_fstask(TASK_MKDIR, 0, 300, "cascade_c"); + if (parent <= 0 || child <= 0) + { FAIL("cascade setup", "ids=%lld/%lld", (long long)parent, + (long long)child); return; } + + char sql[256]; + snprintf(sql, sizeof(sql), + "INSERT INTO fstaskdepend (fstaskid, dependfstaskid) VALUES (%lld, %lld)", + (long long)child, (long long)parent); + if (psql_test_exec(sql) != SQLITE_OK) + { FAIL("cascade: insert depend", "exec failed"); return; } + + /* Delete the parent task */ + snprintf(sql, sizeof(sql), "DELETE FROM fstask WHERE id=%lld", + (long long)parent); + if (psql_test_exec(sql) != SQLITE_OK) + { FAIL("cascade: delete parent", "exec failed"); return; } + + /* Both the parent row AND the dependency row should be gone */ + char where[128]; + snprintf(where, sizeof(where), "dependfstaskid=%lld", (long long)parent); + int dep_cnt = psql_test_count_fstaskdepend(where); + snprintf(where, sizeof(where), "id=%lld", (long long)parent); + int task_cnt = psql_test_count_fstask(where); + + if (task_cnt == 0 && dep_cnt == 0) + PASS("CASCADE DELETE: fstask + fstaskdepend rows removed together"); + else + FAIL("CASCADE DELETE", "task_cnt=%d dep_cnt=%d (expected 0,0)", + task_cnt, dep_cnt); +} + +/* ------------------------------------------------------------------ */ +/* Test 5: FK enforcement — fstaskdepend rejects unknown fstaskid */ +/* ------------------------------------------------------------------ */ +static void test_fk_enforcement(void) { + /* + * Attempt to insert an fstaskdepend row referencing a non-existent + * fstask id. With foreign_keys=ON this should fail. + */ + int rc = psql_test_exec( + "INSERT INTO fstaskdepend (fstaskid, dependfstaskid) " + "VALUES (9999999, 9999998)"); + if (rc != SQLITE_OK) + PASS("FK enforcement: fstaskdepend rejects unknown fstaskid"); + else + FAIL("FK enforcement", "insert should have failed with FK violation"); +} + +/* ------------------------------------------------------------------ */ +/* Test 6: rmdir blocking — SQL query pattern used by pfstasks.c */ +/* */ +/* pfstasks.c checks non-empty folder via: */ +/* SELECT name FROM file WHERE parentfolderid= */ +/* SELECT name FROM folder WHERE parentfolderid= */ +/* ------------------------------------------------------------------ */ +static void test_rmdir_blocking_sql(void) { + /* + * Insert a parent folder (id=500) and a child file inside it. + * Then simulate the pfstasks.c non-empty check. + */ + sqlite3 *db = psql_test_db(); + + /* Insert parent folder */ + psql_test_exec("INSERT INTO folder (id, parentfolderid, name) " + "VALUES (500, 0, 'testfolder')"); + + /* Insert a file inside it */ + psql_test_exec("INSERT INTO file (id, parentfolderid, name, size, hash," + " ctime, mtime) VALUES (5001, 500, 'child.txt', 0, 0, 0, 0)"); + + /* pfstasks.c query: files in folder */ + sqlite3_stmt *stmt; + int file_cnt = 0; + sqlite3_prepare_v2(db, + "SELECT COUNT(*) FROM file WHERE parentfolderid=500", + -1, &stmt, NULL); + if (sqlite3_step(stmt) == SQLITE_ROW) + file_cnt = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + + if (file_cnt > 0) + PASS("rmdir blocking: non-empty folder detected via file query"); + else + FAIL("rmdir blocking", "file_cnt=%d expected >0", file_cnt); + + /* Now remove the file and verify folder appears empty */ + psql_test_exec("DELETE FROM file WHERE id=5001"); + sqlite3_prepare_v2(db, + "SELECT COUNT(*) FROM file WHERE parentfolderid=500", + -1, &stmt, NULL); + file_cnt = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) + file_cnt = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + if (file_cnt == 0) + PASS("rmdir blocking: folder appears empty after file deletion"); + else + FAIL("rmdir empty check", "file_cnt=%d expected 0", file_cnt); +} + +/* ------------------------------------------------------------------ */ +/* Test 7: creat-after-unlink — insert UNLINK + CREAT for same name, */ +/* query both tasks exist and can be distinguished by type */ +/* ------------------------------------------------------------------ */ +static void test_creat_after_unlink(void) { + int64_t unlink_id = psql_test_insert_fstask(TASK_UNLINK, 0, 600, "data.bin"); + int64_t creat_id = psql_test_insert_fstask(TASK_CREAT, 1, 600, "data.bin"); + if (unlink_id <= 0 || creat_id <= 0) + { FAIL("creat-after-unlink setup", "ids=%lld/%lld", + (long long)unlink_id, (long long)creat_id); return; } + + /* Insert CREAT depends on UNLINK (same file, must sequence) */ + char sql[256]; + snprintf(sql, sizeof(sql), + "INSERT INTO fstaskdepend (fstaskid, dependfstaskid) VALUES (%lld, %lld)", + (long long)creat_id, (long long)unlink_id); + if (psql_test_exec(sql) != SQLITE_OK) + { FAIL("creat-after-unlink: insert depend", "exec failed"); return; } + + /* Verify both tasks exist for this folderid+name */ + int unlink_cnt = psql_test_count_fstask( + "type=4 AND folderid=600 AND text1='data.bin'"); + int creat_cnt = psql_test_count_fstask( + "type=3 AND folderid=600 AND text1='data.bin'"); + + if (unlink_cnt == 1 && creat_cnt == 1) + PASS("creat-after-unlink: UNLINK and CREAT tasks both recorded"); + else + FAIL("creat-after-unlink tasks", "unlink=%d creat=%d", + unlink_cnt, creat_cnt); + + /* CREAT depends on UNLINK */ + char where[256]; + snprintf(where, sizeof(where), + "fstaskid=%lld AND dependfstaskid=%lld", + (long long)creat_id, (long long)unlink_id); + int d = psql_test_count_fstaskdepend(where); + if (d == 1) + PASS("creat-after-unlink: CREAT depends on UNLINK in fstaskdepend"); + else + FAIL("creat-after-unlink dependency", "dep_count=%d expected 1", d); +} + +/* ------------------------------------------------------------------ */ +/* Test 8: psql_test_open / psql_test_close idempotence */ +/* ------------------------------------------------------------------ */ +static void test_open_close_idempotent(void) { + /* Close and re-open; db should be usable again */ + psql_test_close(); + if (psql_test_db() != NULL) + { FAIL("open/close: db NULL after close", "still non-null"); return; } + if (psql_test_open() != 0) + { FAIL("open/close: re-open", "failed"); return; } + if (psql_test_db() == NULL) + { FAIL("open/close: db non-null after re-open", "NULL"); return; } + if (!table_exists("fstask")) + FAIL("open/close: fstask table after re-open", "missing"); + else + PASS("psql_test_open/close idempotent: re-open restores fresh schema"); +} + +/* ------------------------------------------------------------------ */ +int main(void) { + if (psql_test_open() != 0) { + fprintf(stderr, "FATAL: psql_test_open() failed\n"); + return 1; + } + + test_schema_created(); + test_fstask_insert_query(); + test_fstaskdepend_insert(); + test_cascade_delete(); + test_fk_enforcement(); + test_rmdir_blocking_sql(); + test_creat_after_unlink(); + test_open_close_idempotent(); + + psql_test_close(); + + printf("\n%d passed, %d failed\n", passes, failures); + return failures ? 1 : 0; +} diff --git a/tests/unit-tests/test_pfstasks_tree.c b/tests/unit-tests/test_pfstasks_tree.c new file mode 100644 index 0000000..ad64b8c --- /dev/null +++ b/tests/unit-tests/test_pfstasks_tree.c @@ -0,0 +1,289 @@ +/* + * Test: pfstasks_tree.c — pure tree layer for fstask structs + * + * Builds psync_fstask_folder_t fixtures directly (no DB, no psql) and + * verifies pfs_task_find_mkdir, pfs_task_find_rmdir, pfs_task_find_creat, + * pfs_task_find_unlink, pfs_task_find_mkdir_by_folderid, and + * pfs_task_find_creat_by_fileid. + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#include "pfstasks_tree.h" + +/* ------------------------------------------------------------------ */ +/* Harness */ +/* ------------------------------------------------------------------ */ + +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) + +/* ------------------------------------------------------------------ */ +/* Node builders (stack / malloc, NOT pmem) */ +/* ------------------------------------------------------------------ */ + +/* Allocate a psync_fstask_mkdir_t with a given name, taskid, folderid */ +static psync_fstask_mkdir_t *mk_mkdir(const char *name, uint64_t taskid, + psync_fsfolderid_t folderid) { + size_t len = strlen(name) + 1; + psync_fstask_mkdir_t *n = (psync_fstask_mkdir_t *) + calloc(1, offsetof(psync_fstask_mkdir_t, name) + len); + n->taskid = taskid; + n->folderid = folderid; + memcpy(n->name, name, len); + return n; +} + +static psync_fstask_rmdir_t *mk_rmdir(const char *name, uint64_t taskid, + psync_fsfolderid_t folderid) { + size_t len = strlen(name) + 1; + psync_fstask_rmdir_t *n = (psync_fstask_rmdir_t *) + calloc(1, offsetof(psync_fstask_rmdir_t, name) + len); + n->taskid = taskid; + n->folderid = folderid; + memcpy(n->name, name, len); + return n; +} + +static psync_fstask_creat_t *mk_creat(const char *name, uint64_t taskid, + psync_fsfileid_t fileid) { + size_t len = strlen(name) + 1; + psync_fstask_creat_t *n = (psync_fstask_creat_t *) + calloc(1, offsetof(psync_fstask_creat_t, name) + len); + n->taskid = taskid; + n->fileid = fileid; + memcpy(n->name, name, len); + return n; +} + +static psync_fstask_unlink_t *mk_unlink(const char *name, uint64_t taskid, + psync_fsfileid_t fileid) { + size_t len = strlen(name) + 1; + psync_fstask_unlink_t *n = (psync_fstask_unlink_t *) + calloc(1, offsetof(psync_fstask_unlink_t, name) + len); + n->taskid = taskid; + n->fileid = fileid; + memcpy(n->name, name, len); + return n; +} + +/* Build a clean empty folder */ +static void folder_init(psync_fstask_folder_t *f, psync_fsfolderid_t folderid) { + memset(f, 0, sizeof(*f)); + f->folderid = folderid; +} + +/* ------------------------------------------------------------------ */ +/* Tests */ +/* ------------------------------------------------------------------ */ + +/* find_mkdir: basic hit and miss */ +static void test_find_mkdir_basic(void) { + psync_fstask_folder_t f; + folder_init(&f, 1); + + psync_fstask_mkdir_t *a = mk_mkdir("alpha", 10, -10); + psync_fstask_mkdir_t *b = mk_mkdir("beta", 20, -20); + psync_fstask_mkdir_t *c = mk_mkdir("gamma", 30, -30); + + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &a->tree); + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &b->tree); + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &c->tree); + + psync_fstask_mkdir_t *found = pfs_task_find_mkdir(&f, "beta", 0); + if (!found || found->taskid != 20) + FAIL("find_mkdir basic: hit", "found=%p taskid=%llu", + (void*)found, found ? (unsigned long long)found->taskid : 0); + else + PASS("find_mkdir: finds 'beta' by name"); + + if (pfs_task_find_mkdir(&f, "delta", 0) != NULL) + FAIL("find_mkdir basic: miss", "found non-NULL for absent key"); + else + PASS("find_mkdir: returns NULL for absent name"); + + free(a); free(b); free(c); +} + +/* find_mkdir: taskid discriminator with same-name duplicates */ +static void test_find_mkdir_taskid(void) { + psync_fstask_folder_t f; + folder_init(&f, 2); + + /* Two mkdirs with same name "foo" but different taskids */ + psync_fstask_mkdir_t *x = mk_mkdir("foo", 100, -100); + psync_fstask_mkdir_t *y = mk_mkdir("foo", 200, -200); + + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &x->tree); + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &y->tree); + + psync_fstask_mkdir_t *found100 = pfs_task_find_mkdir(&f, "foo", 100); + psync_fstask_mkdir_t *found200 = pfs_task_find_mkdir(&f, "foo", 200); + + if (!found100 || found100->taskid != 100) + FAIL("find_mkdir taskid: find taskid=100", "got %p", (void*)found100); + else + PASS("find_mkdir: taskid discriminator finds correct node (100)"); + + if (!found200 || found200->taskid != 200) + FAIL("find_mkdir taskid: find taskid=200", "got %p", (void*)found200); + else + PASS("find_mkdir: taskid discriminator finds correct node (200)"); + + free(x); free(y); +} + +/* find_rmdir: basic */ +static void test_find_rmdir_basic(void) { + psync_fstask_folder_t f; + folder_init(&f, 3); + + psync_fstask_rmdir_t *a = mk_rmdir("dirA", 11, 101); + psync_fstask_rmdir_t *b = mk_rmdir("dirB", 22, 102); + + pfs_task_insert_into_tree(&f.rmdirs, offsetof(psync_fstask_rmdir_t, name), &a->tree); + pfs_task_insert_into_tree(&f.rmdirs, offsetof(psync_fstask_rmdir_t, name), &b->tree); + + psync_fstask_rmdir_t *r = pfs_task_find_rmdir(&f, "dirA", 0); + if (!r || r->taskid != 11) + FAIL("find_rmdir basic", "taskid=%llu", r ? (unsigned long long)r->taskid : 0); + else + PASS("find_rmdir: finds 'dirA'"); + + if (pfs_task_find_rmdir(&f, "dirC", 0) != NULL) + FAIL("find_rmdir miss", "non-NULL for absent"); + else + PASS("find_rmdir: NULL for absent name"); + + free(a); free(b); +} + +/* find_creat: basic */ +static void test_find_creat_basic(void) { + psync_fstask_folder_t f; + folder_init(&f, 4); + + psync_fstask_creat_t *a = mk_creat("file.txt", 55, 1001); + psync_fstask_creat_t *b = mk_creat("photo.jpg", 66, 1002); + + pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &a->tree); + pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &b->tree); + + psync_fstask_creat_t *r = pfs_task_find_creat(&f, "photo.jpg", 0); + if (!r || r->fileid != 1002) + FAIL("find_creat basic", "fileid=%lld", r ? (long long)r->fileid : 0); + else + PASS("find_creat: finds 'photo.jpg'"); + + free(a); free(b); +} + +/* find_unlink: basic */ +static void test_find_unlink_basic(void) { + psync_fstask_folder_t f; + folder_init(&f, 5); + + psync_fstask_unlink_t *a = mk_unlink("old.txt", 77, 2001); + psync_fstask_unlink_t *b = mk_unlink("tmp.log", 88, 2002); + + pfs_task_insert_into_tree(&f.unlinks, offsetof(psync_fstask_unlink_t, name), &a->tree); + pfs_task_insert_into_tree(&f.unlinks, offsetof(psync_fstask_unlink_t, name), &b->tree); + + psync_fstask_unlink_t *r = pfs_task_find_unlink(&f, "old.txt", 0); + if (!r || r->fileid != 2001) + FAIL("find_unlink basic", "fileid=%lld", r ? (long long)r->fileid : 0); + else + PASS("find_unlink: finds 'old.txt'"); + + if (pfs_task_find_unlink(&f, "missing.txt", 0) != NULL) + FAIL("find_unlink miss", "non-NULL"); + else + PASS("find_unlink: NULL for absent name"); + + free(a); free(b); +} + +/* find_mkdir_by_folderid: walk tree by folderid */ +static void test_find_mkdir_by_folderid(void) { + psync_fstask_folder_t f; + folder_init(&f, 6); + + psync_fstask_mkdir_t *a = mk_mkdir("one", 1, -101); + psync_fstask_mkdir_t *b = mk_mkdir("two", 2, -202); + psync_fstask_mkdir_t *c = mk_mkdir("three", 3, -303); + + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &a->tree); + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &b->tree); + pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &c->tree); + + psync_fstask_mkdir_t *r = pfs_task_find_mkdir_by_folderid(&f, -202); + if (!r || strcmp(r->name, "two") != 0) + FAIL("find_mkdir_by_folderid", "name=%s", r ? r->name : "(null)"); + else + PASS("find_mkdir_by_folderid: finds node with folderid=-202"); + + if (pfs_task_find_mkdir_by_folderid(&f, -999) != NULL) + FAIL("find_mkdir_by_folderid miss", "non-NULL for absent folderid"); + else + PASS("find_mkdir_by_folderid: NULL for absent folderid"); + + free(a); free(b); free(c); +} + +/* find_creat_by_fileid: walk tree by fileid */ +static void test_find_creat_by_fileid(void) { + psync_fstask_folder_t f; + folder_init(&f, 7); + + psync_fstask_creat_t *a = mk_creat("a.txt", 10, 5001); + psync_fstask_creat_t *b = mk_creat("b.txt", 20, 5002); + psync_fstask_creat_t *c = mk_creat("c.txt", 30, 5003); + + pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &a->tree); + pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &b->tree); + pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &c->tree); + + psync_fstask_creat_t *r = pfs_task_find_creat_by_fileid(&f, 5002); + if (!r || strcmp(r->name, "b.txt") != 0) + FAIL("find_creat_by_fileid", "name=%s", r ? r->name : "(null)"); + else + PASS("find_creat_by_fileid: finds node with fileid=5002"); + + free(a); free(b); free(c); +} + +/* Empty folder: all finds return NULL */ +static void test_empty_folder(void) { + psync_fstask_folder_t f; + folder_init(&f, 8); + int ok = 1; + if (pfs_task_find_mkdir(&f, "x", 0) != NULL) ok = 0; + if (pfs_task_find_rmdir(&f, "x", 0) != NULL) ok = 0; + if (pfs_task_find_creat(&f, "x", 0) != NULL) ok = 0; + if (pfs_task_find_unlink(&f, "x", 0) != NULL) ok = 0; + if (ok) + PASS("empty folder: all finds return NULL"); + else + FAIL("empty folder", "unexpected non-NULL"); +} + +/* ------------------------------------------------------------------ */ +int main(void) { + test_find_mkdir_basic(); + test_find_mkdir_taskid(); + test_find_rmdir_basic(); + test_find_creat_basic(); + test_find_unlink_basic(); + test_find_mkdir_by_folderid(); + test_find_creat_by_fileid(); + test_empty_folder(); + + printf("\n%d passed, %d failed\n", passes, failures); + return failures ? 1 : 0; +} diff --git a/tests/unit-tests/test_pfsupload.c b/tests/unit-tests/test_pfsupload.c new file mode 100644 index 0000000..589a247 --- /dev/null +++ b/tests/unit-tests/test_pfsupload.c @@ -0,0 +1,252 @@ +/* + * 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; + size_t nlen = strlen(name); + for (size_t i = 0; i < g_last_nparams; i++) { + if (g_last_params[i].paramtype == PARAM_STR && + g_last_params[i].paramnamelen == nlen && + strncmp(g_last_params[i].paramname, name, nlen) == 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. + * The caller provides a stack-allocated psock_t; no static storage is used + * so multiple calls within the same test function are safe. */ +static void make_fake_api(psock_t *out, int sv[2]) { + socketpair(AF_UNIX, SOCK_STREAM, 0, sv); + memset(out, 0, sizeof(*out)); + out->sock = sv[1]; +} + +/* ------------------------------------------------------------------ */ +/* 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_s; make_fake_api(&api_s, sv); psock_t *api = &api_s; + + 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_s; make_fake_api(&api_s, sv); psock_t *api = &api_s; + + 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_s; make_fake_api(&api_s, sv); psock_t *api = &api_s; + + 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_s; make_fake_api(&api_s, sv); psock_t *api = &api_s; + + 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_pintervaltree.c b/tests/unit-tests/test_pintervaltree.c new file mode 100644 index 0000000..311254d --- /dev/null +++ b/tests/unit-tests/test_pintervaltree.c @@ -0,0 +1,304 @@ +/* + * Test: pintervaltree.c — interval tree (add, remove, merge, split, cut_end) + * + * Exercises psync_interval_tree_add, _remove, _cut_end, _free, and the + * inline helpers first_interval_containing_or_after / get_first / get_next. + * Memory is managed by pintervaltree.c via pmem; tests call _free() at the + * end of each case to avoid leaks. + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include + +#include "pintervaltree.h" + +/* ------------------------------------------------------------------ */ +/* 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 int count_intervals(psync_interval_tree_t *tree) { + int n = 0; + psync_interval_tree_t *it; + psync_interval_tree_for_each(it, tree) n++; + return n; +} + +/* Find an interval with exact [from, to] */ +static int has_interval(psync_interval_tree_t *tree, uint64_t from, uint64_t to) { + psync_interval_tree_t *it; + psync_interval_tree_for_each(it, tree) + if (it->from == from && it->to == to) return 1; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Tests */ +/* ------------------------------------------------------------------ */ + +/* Add a single interval; verify it's stored correctly */ +static void test_add_single(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + if (!tree) + { FAIL("add single: tree non-null", "tree is NULL"); return; } + if (count_intervals(tree) != 1) + FAIL("add single: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 20)) + FAIL("add single: [10,20] present", "not found"); + else + PASS("add single interval [10,20]"); + psync_interval_tree_free(tree); +} + +/* Two non-overlapping, non-adjacent intervals: no merging */ +static void test_add_non_overlapping(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 30, 40); + if (count_intervals(tree) != 2) + FAIL("non-overlapping: count", "expected 2 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 20) || !has_interval(tree, 30, 40)) + FAIL("non-overlapping: both present", "one missing"); + else + PASS("two non-overlapping intervals stored separately"); + psync_interval_tree_free(tree); +} + +/* Overlapping: [10,20] then [15,30] → merged to [10,30] */ +static void test_add_overlapping_merge(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 15, 30); + if (count_intervals(tree) != 1) + FAIL("overlapping merge: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 30)) + FAIL("overlapping merge: [10,30]", "not found"); + else + PASS("overlapping intervals merged to [10,30]"); + psync_interval_tree_free(tree); +} + +/* Adjacent: [10,20] then [20,30] → merged to [10,30] */ +static void test_add_adjacent_merge(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 20, 30); + if (count_intervals(tree) != 1) + FAIL("adjacent merge: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 30)) + FAIL("adjacent merge: [10,30]", "not found"); + else + PASS("adjacent intervals [10,20]+[20,30] merged to [10,30]"); + psync_interval_tree_free(tree); +} + +/* Contained: add [10,30], then add [15,20] → no change (subset already covered) */ +static void test_add_contained(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 30); + psync_interval_tree_add(&tree, 15, 20); + if (count_intervals(tree) != 1) + FAIL("contained: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 30)) + FAIL("contained: [10,30] unchanged", "not found"); + else + PASS("adding contained interval is a no-op"); + psync_interval_tree_free(tree); +} + +/* Spanning: add [15,25], then add [10,30] → becomes [10,30] */ +static void test_add_spanning(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 15, 25); + psync_interval_tree_add(&tree, 10, 30); + if (count_intervals(tree) != 1) + FAIL("spanning: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 30)) + FAIL("spanning: [10,30]", "not found"); + else + PASS("spanning interval replaces smaller existing interval"); + psync_interval_tree_free(tree); +} + +/* Merge multiple intervals: [5,10]+[10,15]+[15,20] → [5,20] */ +static void test_add_chain_merge(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 5, 10); + psync_interval_tree_add(&tree, 10, 15); + psync_interval_tree_add(&tree, 15, 20); + if (count_intervals(tree) != 1) + FAIL("chain merge: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 5, 20)) + FAIL("chain merge: [5,20]", "not found"); + else + PASS("three adjacent intervals merged into [5,20]"); + psync_interval_tree_free(tree); +} + +/* Remove middle: [10,30] → remove [15,20] → [10,15] and [20,30] */ +static void test_remove_middle_split(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 30); + psync_interval_tree_remove(&tree, 15, 20); + if (count_intervals(tree) != 2) + FAIL("remove middle: count", "expected 2 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 15) || !has_interval(tree, 20, 30)) + FAIL("remove middle: split halves", + "[10,15]=%d [20,30]=%d", + has_interval(tree, 10, 15), has_interval(tree, 20, 30)); + else + PASS("remove middle splits [10,30] into [10,15] and [20,30]"); + psync_interval_tree_free(tree); +} + +/* Remove exact interval: [10,20] → remove [10,20] → empty */ +static void test_remove_exact(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_remove(&tree, 10, 20); + if (count_intervals(tree) != 0) + FAIL("remove exact: empty", "expected 0 got %d", count_intervals(tree)); + else + PASS("remove exact interval leaves tree empty"); + /* tree may be NULL here; free handles NULL */ + psync_interval_tree_free(tree); +} + +/* Remove left overlap: [10,30] → remove [5,15] → [15,30] */ +static void test_remove_left_overlap(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 30); + psync_interval_tree_remove(&tree, 5, 15); + if (count_intervals(tree) != 1) + FAIL("remove left: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 15, 30)) + FAIL("remove left: [15,30]", "not found"); + else + PASS("remove left overlap: [10,30] clipped to [15,30]"); + psync_interval_tree_free(tree); +} + +/* Remove right overlap: [10,30] → remove [25,35] → [10,25] */ +static void test_remove_right_overlap(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 30); + psync_interval_tree_remove(&tree, 25, 35); + if (count_intervals(tree) != 1) + FAIL("remove right: count", "expected 1 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 25)) + FAIL("remove right: [10,25]", "not found"); + else + PASS("remove right overlap: [10,30] clipped to [10,25]"); + psync_interval_tree_free(tree); +} + +/* Remove spanning: [10,20]+[30,40] → remove [5,45] → empty */ +static void test_remove_spanning(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 30, 40); + psync_interval_tree_remove(&tree, 5, 45); + if (count_intervals(tree) != 0) + FAIL("remove spanning: empty", "expected 0 got %d", count_intervals(tree)); + else + PASS("remove spanning erases all intervals"); + psync_interval_tree_free(tree); +} + +/* cut_end: [10,20]+[30,40]+[50,60] → cut_end(35) → [10,20]+[30,35] */ +static void test_cut_end(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 30, 40); + psync_interval_tree_add(&tree, 50, 60); + psync_interval_tree_cut_end(&tree, 35); + if (count_intervals(tree) != 2) + FAIL("cut_end: count", "expected 2 got %d", count_intervals(tree)); + else if (!has_interval(tree, 10, 20) || !has_interval(tree, 30, 35)) + FAIL("cut_end: [10,20] and [30,35]", + "[10,20]=%d [30,35]=%d", + has_interval(tree, 10, 20), has_interval(tree, 30, 35)); + else + PASS("cut_end(35) leaves [10,20]+[30,35]"); + psync_interval_tree_free(tree); +} + +/* cut_end at 0: all intervals removed */ +static void test_cut_end_all(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 30, 40); + psync_interval_tree_cut_end(&tree, 0); + if (count_intervals(tree) != 0) + FAIL("cut_end(0): all removed", "expected 0 got %d", count_intervals(tree)); + else + PASS("cut_end(0) removes all intervals"); + psync_interval_tree_free(tree); +} + +/* first_interval_containing_or_after: find first interval containing a point */ +static void test_first_containing_or_after(void) { + psync_interval_tree_t *tree = NULL; + psync_interval_tree_add(&tree, 10, 20); + psync_interval_tree_add(&tree, 30, 40); + psync_interval_tree_add(&tree, 50, 60); + + /* point within first interval */ + psync_interval_tree_t *r = psync_interval_tree_first_interval_containing_or_after(tree, 15); + if (!r || r->from != 10 || r->to != 20) + FAIL("containing_or_after: point inside", "from=%llu to=%llu", + r ? (unsigned long long)r->from : 0, r ? (unsigned long long)r->to : 0); + else + PASS("first_containing_or_after: point inside interval"); + + /* point between intervals → returns next interval */ + r = psync_interval_tree_first_interval_containing_or_after(tree, 25); + if (!r || r->from != 30 || r->to != 40) + FAIL("containing_or_after: gap → next", "from=%llu to=%llu", + r ? (unsigned long long)r->from : 0, r ? (unsigned long long)r->to : 0); + else + PASS("first_containing_or_after: gap returns next interval"); + + /* point past all intervals → NULL */ + r = psync_interval_tree_first_interval_containing_or_after(tree, 70); + if (r != NULL) + FAIL("containing_or_after: past end → NULL", "got non-null"); + else + PASS("first_containing_or_after: past end returns NULL"); + + psync_interval_tree_free(tree); +} + +/* free on NULL is safe */ +static void test_free_null(void) { + psync_interval_tree_free(NULL); + PASS("free(NULL) does not crash"); +} + +/* ------------------------------------------------------------------ */ +int main(void) { + test_add_single(); + test_add_non_overlapping(); + test_add_overlapping_merge(); + test_add_adjacent_merge(); + test_add_contained(); + test_add_spanning(); + test_add_chain_merge(); + test_remove_middle_split(); + test_remove_exact(); + test_remove_left_overlap(); + test_remove_right_overlap(); + test_remove_spanning(); + test_cut_end(); + test_cut_end_all(); + test_first_containing_or_after(); + test_free_null(); + + 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..c41d7b8 --- /dev/null +++ b/tests/unit-tests/test_plocks.c @@ -0,0 +1,367 @@ +/* + * 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; +} + +/* + * test_upgrade_under_contention verifies that plocks_towrlock() completes + * (returns 0) and that the upgrading thread holds the write lock on return. + * Concurrent exclusivity — that no reader is simultaneously active once the + * write lock is granted — is covered by the counter-integrity check in + * test_stress(). + */ +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; +} diff --git a/tests/unit-tests/test_ptask_free.c b/tests/unit-tests/test_ptask_free.c index 1dcd921..95c867e 100644 --- a/tests/unit-tests/test_ptask_free.c +++ b/tests/unit-tests/test_ptask_free.c @@ -6,262 +6,224 @@ * 2. refcnt>1, last ref: lock acquired, refcnt decremented, destroy called * 3. refcnt>1, not last ref: refcnt decremented, destroy NOT called * 4. READY tasks get signaled (status→RETURNED) when freed with refcnt>1 + * 5. mutex is acquired before the refcnt check in all paths * - * The mutex is held during the refcnt check in all paths — the core fix. - * We verify this by intercepting pthread_mutex_lock/unlock with counters - * and confirming lock is held before destroy is invoked. + * Links against the real pclsync/ptask_free.c (production code). + * Uses --wrap linker flags to intercept pthread_mutex_lock/unlock and + * pmem_free so we can observe lock discipline and detect destroy calls + * without reimplementing any production logic inline. */ +#define _POSIX_C_SOURCE 200809L #include -#include #include #include #include +/* Internal struct layout — exposes psync_task_manager_t_ for make_tm() */ +#include "ptask_free_internal.h" +#include "pmem.h" + /* ------------------------------------------------------------------ */ -/* Intercept controls */ +/* __wrap / __real declarations */ /* ------------------------------------------------------------------ */ + +int __real_pthread_mutex_lock(pthread_mutex_t *m); +int __real_pthread_mutex_unlock(pthread_mutex_t *m); +void __real_pmem_free(pmem_subsystem_t subsystem, void *ptr); + +/* ------------------------------------------------------------------ */ +/* Intercept state */ +/* ------------------------------------------------------------------ */ + static int g_lock_calls = 0; static int g_unlock_calls = 0; -static int g_destroy_calls = 0; -static int g_free_calls = 0; -static int g_lock_held_at_destroy = 0; /* was lock held when destroy fired? */ +static int g_destroy_calls = 0; /* incremented by __wrap_pmem_free */ +static int g_lock_depth = 0; +static int g_lock_held_at_destroy = 0; /* 1 = mutex was unlocked when destroy fired */ /* ------------------------------------------------------------------ */ -/* Inline struct replica (mirrors ptask.c exactly) */ -/* ------------------------------------------------------------------ */ -#define PSYNC_TASK_STATUS_RUNNING 0 -#define PSYNC_TASK_STATUS_READY 1 -#define PSYNC_TASK_STATUS_DONE 2 -#define PSYNC_TASK_STATUS_RETURNED 3 - -#define PSYNC_WAIT_NOBODY -2 -#define PSYNC_WAIT_FREED -3 - -typedef void (*psync_task_callback_t)(void *, void *); - -struct psync_task_t_ { - psync_task_callback_t callback; - void *param; - pthread_cond_t cond; - int id; - int status; -}; - -struct psync_task_manager_t_ { - pthread_mutex_t mutex; - int taskcnt; - int refcnt; - int waitfor; - struct psync_task_t_ tasks[]; -}; - -typedef struct psync_task_manager_t_ *psync_task_manager_t; - -/* ------------------------------------------------------------------ */ -/* Mock implementations */ +/* Wrap implementations */ /* ------------------------------------------------------------------ */ -/* Track lock depth so we know if lock is held when destroy fires */ -static int g_lock_depth = 0; - -static int mock_mutex_lock(pthread_mutex_t *m) { - g_lock_calls++; - g_lock_depth++; - return pthread_mutex_lock(m); +int __wrap_pthread_mutex_lock(pthread_mutex_t *m) { + g_lock_calls++; + g_lock_depth++; + return __real_pthread_mutex_lock(m); } -static int mock_mutex_unlock(pthread_mutex_t *m) { - g_unlock_calls++; - g_lock_depth--; - return pthread_mutex_unlock(m); +int __wrap_pthread_mutex_unlock(pthread_mutex_t *m) { + g_unlock_calls++; + g_lock_depth--; + return __real_pthread_mutex_unlock(m); } -static void mock_pmem_free(void *p) { - g_free_calls++; - free(p); -} - -static void mock_psync_task_destroy(psync_task_manager_t tm) { - g_destroy_calls++; - g_lock_held_at_destroy = (g_lock_depth == 0); /* should be 0: unlocked before destroy */ - int i; - for (i = 0; i < tm->taskcnt; i++) - pthread_cond_destroy(&tm->tasks[i].cond); - pthread_mutex_destroy(&tm->mutex); - mock_pmem_free(tm); -} - -/* ------------------------------------------------------------------ */ -/* Replica of psync_task_free from the fixed branch */ -/* ------------------------------------------------------------------ */ -static void test_psync_task_free(psync_task_manager_t tm) { - int refcnt, i; - mock_mutex_lock(&tm->mutex); - if (tm->refcnt == 1) { - mock_mutex_unlock(&tm->mutex); - mock_psync_task_destroy(tm); - } else { - tm->waitfor = PSYNC_WAIT_FREED; - for (i = 0; i < tm->taskcnt; i++) - if (tm->tasks[i].status == PSYNC_TASK_STATUS_READY) { - tm->tasks[i].status = PSYNC_TASK_STATUS_RETURNED; - pthread_cond_signal(&tm->tasks[i].cond); - } - refcnt = --tm->refcnt; - mock_mutex_unlock(&tm->mutex); - if (!refcnt) - mock_psync_task_destroy(tm); - } +/* + * psync_task_destroy() calls pmem_free() as its last act. + * We intercept it to count destroy invocations and capture lock state. + * We call free() directly because make_tm() allocates with malloc(). + */ +void __wrap_pmem_free(pmem_subsystem_t subsystem, void *ptr) { + (void)subsystem; + g_destroy_calls++; + g_lock_held_at_destroy = (g_lock_depth == 0); /* should be 0 = unlocked */ + free(ptr); } /* ------------------------------------------------------------------ */ /* 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(void) { - g_lock_calls = 0; - g_unlock_calls = 0; - g_destroy_calls = 0; - g_free_calls = 0; - g_lock_depth = 0; - g_lock_held_at_destroy = 0; + g_lock_calls = 0; + g_unlock_calls = 0; + g_destroy_calls = 0; + g_lock_depth = 0; + g_lock_held_at_destroy = 0; } -/* Allocate and initialize a task manager with `cnt` tasks */ +/* Allocate and initialise a task manager with `cnt` tasks, refcnt=`refcnt` */ static psync_task_manager_t make_tm(int cnt, int refcnt) { - size_t sz = sizeof(struct psync_task_manager_t_) + - cnt * sizeof(struct psync_task_t_); - psync_task_manager_t tm = malloc(sz); - memset(tm, 0, sz); - pthread_mutex_init(&tm->mutex, NULL); - tm->taskcnt = cnt; - tm->refcnt = refcnt; - tm->waitfor = PSYNC_WAIT_NOBODY; - int i; - for (i = 0; i < cnt; i++) { - pthread_cond_init(&tm->tasks[i].cond, NULL); - tm->tasks[i].id = i; - tm->tasks[i].status = PSYNC_TASK_STATUS_RUNNING; - } - return tm; + size_t sz = sizeof(struct psync_task_manager_t_) + + cnt * sizeof(struct psync_task_t_); + psync_task_manager_t tm = (psync_task_manager_t)malloc(sz); + memset(tm, 0, sz); + pthread_mutex_init(&tm->mutex, NULL); + tm->taskcnt = cnt; + tm->refcnt = refcnt; + tm->waitfor = PSYNC_WAIT_NOBODY; + int i; + for (i = 0; i < cnt; i++) { + pthread_cond_init(&tm->tasks[i].cond, NULL); + tm->tasks[i].id = i; + tm->tasks[i].status = PSYNC_TASK_STATUS_RUNNING; + } + return tm; } /* ------------------------------------------------------------------ */ -/* Tests */ +/* Tests — call the real psync_task_free() */ /* ------------------------------------------------------------------ */ -/* refcnt=1: destroy called, mutex unlocked before destroy */ +/* refcnt=1: destroy called once, mutex unlocked before destroy */ static void test_single_owner_free(void) { - reset(); - psync_task_manager_t tm = make_tm(2, 1); + reset(); + psync_task_manager_t tm = make_tm(2, 1); - test_psync_task_free(tm); /* tm is freed inside */ + psync_task_free(tm); /* tm freed inside via __wrap_pmem_free */ - if (g_destroy_calls != 1) - FAIL("single owner: destroy called once", "destroy_calls=%d", g_destroy_calls); - else if (!g_lock_held_at_destroy) - FAIL("single owner: mutex unlocked before destroy", "lock_depth was non-zero at destroy"); - else if (g_lock_calls != 1 || g_unlock_calls != 1) - FAIL("single owner: lock/unlock balanced", "lock=%d unlock=%d", g_lock_calls, g_unlock_calls); - else - PASS("single owner free: destroy called once, mutex unlocked before destroy"); + if (g_destroy_calls != 1) + FAIL("single owner: destroy called once", + "destroy_calls=%d", g_destroy_calls); + else if (!g_lock_held_at_destroy) + FAIL("single owner: mutex unlocked before destroy", + "lock_depth was non-zero at destroy"); + else if (g_lock_calls != 1 || g_unlock_calls != 1) + FAIL("single owner: lock/unlock balanced", + "lock=%d unlock=%d", g_lock_calls, g_unlock_calls); + else + PASS("single owner free: destroy called once, mutex unlocked before destroy"); } -/* refcnt=2, free last ref manually: destroy called after second decrement */ +/* refcnt already at 1 when we call free: same result as single-owner */ static void test_last_ref_destroys(void) { - reset(); - psync_task_manager_t tm = make_tm(1, 2); + reset(); + psync_task_manager_t tm = make_tm(1, 2); - /* Simulate first ref already released: lower refcnt to 1 without locking */ - tm->refcnt = 1; + /* Simulate the other ref already gone */ + tm->refcnt = 1; - test_psync_task_free(tm); /* this is now the last ref */ + psync_task_free(tm); - if (g_destroy_calls == 1 && g_lock_held_at_destroy) - PASS("last ref free (refcnt path 1): destroy called, mutex unlocked before destroy"); - else - FAIL("last ref free", "destroy_calls=%d lock_held_at_destroy=%d", - g_destroy_calls, g_lock_held_at_destroy); + if (g_destroy_calls == 1 && g_lock_held_at_destroy) + PASS("last ref free: destroy called, mutex unlocked before destroy"); + else + FAIL("last ref free", + "destroy_calls=%d lock_held_at_destroy=%d", + g_destroy_calls, g_lock_held_at_destroy); } /* refcnt=2, not last ref: refcnt decremented, destroy NOT called */ static void test_not_last_ref_no_destroy(void) { - reset(); - psync_task_manager_t tm = make_tm(1, 2); + reset(); + psync_task_manager_t tm = make_tm(1, 2); - test_psync_task_free(tm); + psync_task_free(tm); - if (g_destroy_calls != 0) - FAIL("not last ref: no destroy", "destroy_calls=%d", g_destroy_calls); - else if (tm->refcnt != 1) - FAIL("not last ref: refcnt decremented to 1", "refcnt=%d", tm->refcnt); - else - PASS("not last ref: no destroy, refcnt decremented to 1"); + if (g_destroy_calls != 0) + FAIL("not last ref: no destroy", + "destroy_calls=%d", g_destroy_calls); + else if (tm->refcnt != 1) + FAIL("not last ref: refcnt decremented to 1", + "refcnt=%d", tm->refcnt); + else + PASS("not last ref: no destroy, refcnt decremented to 1"); - /* Manual cleanup since we didn't destroy */ - pthread_cond_destroy(&tm->tasks[0].cond); - pthread_mutex_destroy(&tm->mutex); - free(tm); + /* Manual cleanup since psync_task_destroy was not called */ + pthread_cond_destroy(&tm->tasks[0].cond); + pthread_mutex_destroy(&tm->mutex); + free(tm); } -/* READY tasks get RETURNED status when freed with refcnt>1 */ +/* READY tasks get RETURNED status + cond signalled when freed with refcnt>1 */ static void test_ready_tasks_signaled(void) { - reset(); - psync_task_manager_t tm = make_tm(3, 2); - tm->tasks[0].status = PSYNC_TASK_STATUS_RUNNING; - tm->tasks[1].status = PSYNC_TASK_STATUS_READY; - tm->tasks[2].status = PSYNC_TASK_STATUS_DONE; + reset(); + psync_task_manager_t tm = make_tm(3, 2); + tm->tasks[0].status = PSYNC_TASK_STATUS_RUNNING; + tm->tasks[1].status = PSYNC_TASK_STATUS_READY; + tm->tasks[2].status = PSYNC_TASK_STATUS_DONE; - test_psync_task_free(tm); + psync_task_free(tm); - int ok = (tm->tasks[0].status == PSYNC_TASK_STATUS_RUNNING && - tm->tasks[1].status == PSYNC_TASK_STATUS_RETURNED && - tm->tasks[2].status == PSYNC_TASK_STATUS_DONE && - tm->waitfor == PSYNC_WAIT_FREED); + int ok = (tm->tasks[0].status == PSYNC_TASK_STATUS_RUNNING && + tm->tasks[1].status == PSYNC_TASK_STATUS_RETURNED && + tm->tasks[2].status == PSYNC_TASK_STATUS_DONE && + tm->waitfor == PSYNC_WAIT_FREED); - if (ok) - PASS("READY tasks signaled RETURNED, others unchanged, waitfor=FREED"); - else - FAIL("READY tasks signaled", - "statuses=[%d,%d,%d] waitfor=%d", - tm->tasks[0].status, tm->tasks[1].status, - tm->tasks[2].status, tm->waitfor); + if (ok) + PASS("READY tasks signaled RETURNED, others unchanged, waitfor=FREED"); + else + FAIL("READY tasks signaled", + "statuses=[%d,%d,%d] waitfor=%d", + tm->tasks[0].status, tm->tasks[1].status, + tm->tasks[2].status, tm->waitfor); - /* Cleanup */ - int i; - for (i = 0; i < 3; i++) pthread_cond_destroy(&tm->tasks[i].cond); - pthread_mutex_destroy(&tm->mutex); - free(tm); + /* Manual cleanup */ + int i; + for (i = 0; i < 3; i++) pthread_cond_destroy(&tm->tasks[i].cond); + pthread_mutex_destroy(&tm->mutex); + free(tm); } -/* Lock is acquired before refcnt is read (core fix) */ +/* Mutex acquired before refcnt is inspected (core fix) */ static void test_lock_before_refcnt_check(void) { - reset(); - psync_task_manager_t tm = make_tm(1, 1); + reset(); + psync_task_manager_t tm = make_tm(1, 1); - /* We can only verify indirectly: lock_calls >= 1 before destroy fires. - * g_lock_held_at_destroy==1 means lock was acquired and released before destroy. */ - test_psync_task_free(tm); + psync_task_free(tm); - if (g_lock_calls >= 1 && g_lock_held_at_destroy) - PASS("mutex acquired before refcnt check; unlocked cleanly before destroy"); - else - FAIL("lock before refcnt check", - "lock_calls=%d lock_held_at_destroy=%d", g_lock_calls, g_lock_held_at_destroy); + /* g_lock_calls >= 1 means lock was acquired; g_lock_held_at_destroy = 1 + * means it was released cleanly before destroy fired */ + if (g_lock_calls >= 1 && g_lock_held_at_destroy) + PASS("mutex acquired before refcnt check; unlocked cleanly before destroy"); + else + FAIL("lock before refcnt check", + "lock_calls=%d lock_held_at_destroy=%d", + g_lock_calls, g_lock_held_at_destroy); } /* ------------------------------------------------------------------ */ int main(void) { - test_single_owner_free(); - test_last_ref_destroys(); - test_not_last_ref_no_destroy(); - test_ready_tasks_signaled(); - test_lock_before_refcnt_check(); + test_single_owner_free(); + test_last_ref_destroys(); + test_not_last_ref_no_destroy(); + test_ready_tasks_signaled(); + test_lock_before_refcnt_check(); - printf("\n%d passed, %d failed\n", passes, failures); - return failures ? 1 : 0; + printf("\n%d passed, %d failed\n", passes, failures); + return failures ? 1 : 0; } diff --git a/tests/unit-tests/test_ptools_errptr.c b/tests/unit-tests/test_ptools_errptr.c index 42edb7e..4a0a2a3 100644 --- a/tests/unit-tests/test_ptools_errptr.c +++ b/tests/unit-tests/test_ptools_errptr.c @@ -18,6 +18,28 @@ #include #include +/* Suppress LSAN reports for intentional leaks used to verify bug behaviour. + * GCC sets __SANITIZE_ADDRESS__; Clang exposes __has_feature as a built-in. + * Use nested #if so the __has_feature() call is only evaluated when the + * compiler actually understands it (avoids "missing binary operator" on GCC). */ +#ifdef __SANITIZE_ADDRESS__ +# include +# define LSAN_DISABLE() __lsan_disable() +# define LSAN_ENABLE() __lsan_enable() +#elif defined(__has_feature) +# if __has_feature(address_sanitizer) +# include +# define LSAN_DISABLE() __lsan_disable() +# define LSAN_ENABLE() __lsan_enable() +# else +# define LSAN_DISABLE() do {} while (0) +# define LSAN_ENABLE() do {} while (0) +# endif +#else +# define LSAN_DISABLE() do {} while (0) +# define LSAN_ENABLE() do {} while (0) +#endif + /* ------------------------------------------------------------------ */ /* Allocation tracking via --wrap */ /* ------------------------------------------------------------------ */ @@ -119,6 +141,9 @@ static int run_unfixed(void) { char *errPtr = NULL; int callRes; + /* Intentional leak: suppress LSAN so the process exits cleanly and + * stdio flushes before the sanitizer reports it. */ + LSAN_DISABLE(); callRes = mock_backend_call_1(&errPtr); (void)callRes; @@ -129,6 +154,7 @@ static int run_unfixed(void) { if (errPtr) free(errPtr); + LSAN_ENABLE(); return 0; } diff --git a/tests/unit-tests/test_ptree.c b/tests/unit-tests/test_ptree.c new file mode 100644 index 0000000..135f081 --- /dev/null +++ b/tests/unit-tests/test_ptree.c @@ -0,0 +1,290 @@ +/* + * Test: ptree.c — AVL balanced BST + * + * Tests insert, lookup, delete, and in-order traversal using a simple + * integer-keyed node type. ptree itself manages no memory; nodes are + * stack-allocated here so no cleanup is needed. + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include + +#include "ptree.h" + +/* ------------------------------------------------------------------ */ +/* Node type */ +/* ------------------------------------------------------------------ */ + +typedef struct { + psync_tree tree; + int key; +} inode_t; + +static int icmp(const psync_tree *a, const psync_tree *b) { + int ka = ptree_element(a, inode_t, tree)->key; + int kb = ptree_element(b, inode_t, tree)->key; + return (ka > kb) - (ka < kb); +} + +static void inode_init(inode_t *n, int key) { + memset(&n->tree, 0, sizeof(n->tree)); + n->key = key; +} + +/* BST lookup by key */ +static inode_t *find_key(psync_tree *root, int key) { + while (root) { + inode_t *n = ptree_element(root, inode_t, tree); + if (key < n->key) root = root->left; + else if (key > n->key) root = root->right; + else return n; + } + return NULL; +} + +/* ------------------------------------------------------------------ */ +/* Test harness */ +/* ------------------------------------------------------------------ */ + +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) + +/* ------------------------------------------------------------------ */ +/* Tests */ +/* ------------------------------------------------------------------ */ + +/* Single node: first == last == the node, next/prev return NULL */ +static void test_single_node(void) { + inode_t n; + psync_tree *root = NULL; + inode_init(&n, 42); + ptree_add(&root, &n.tree, icmp); + if (root == NULL) + { FAIL("single insert: root non-null", "root is NULL"); return; } + psync_tree *f = ptree_get_first(root); + psync_tree *l = ptree_get_last(root); + if (f != &n.tree || l != &n.tree) + FAIL("single: first == last == node", "first=%p last=%p node=%p", + (void*)f, (void*)l, (void*)&n.tree); + else if (ptree_get_next(f) != NULL) + FAIL("single: next of only node is NULL", "got non-null"); + else if (ptree_get_prev(l) != NULL) + FAIL("single: prev of only node is NULL", "got non-null"); + else + PASS("single node insert/first/last/next/prev"); +} + +/* In-order traversal gives keys in ascending order for any insert order */ +static void test_traversal_sorted(void) { + int keys[] = { 5, 3, 8, 1, 4, 7, 9, 2 }; + int n = (int)(sizeof(keys) / sizeof(keys[0])); + inode_t nodes[8]; + psync_tree *root = NULL; + int i; + for (i = 0; i < n; i++) { + inode_init(&nodes[i], keys[i]); + ptree_add(&root, &nodes[i].tree, icmp); + } + /* collect traversal */ + psync_tree *tr = ptree_get_first(root); + int prev_key = -1, count = 0, ok = 1; + while (tr) { + int k = ptree_element(tr, inode_t, tree)->key; + if (k <= prev_key) { ok = 0; break; } + prev_key = k; + count++; + tr = ptree_get_next(tr); + } + if (!ok) + FAIL("traversal sorted", "out-of-order key %d after %d", prev_key, prev_key); + else if (count != n) + FAIL("traversal sorted: count", "expected %d, got %d", n, count); + else + PASS("traversal in sorted order after arbitrary inserts"); +} + +/* Reverse-order insert stresses AVL rebalancing */ +static void test_reverse_insert_traversal(void) { + int n = 7; + inode_t nodes[7]; + psync_tree *root = NULL; + int i; + for (i = n; i >= 1; i--) { + inode_init(&nodes[i-1], i); + ptree_add(&root, &nodes[i-1].tree, icmp); + } + psync_tree *tr = ptree_get_first(root); + int prev = 0, count = 0, ok = 1; + while (tr) { + int k = ptree_element(tr, inode_t, tree)->key; + if (k != prev + 1) { ok = 0; break; } + prev = k; + count++; + tr = ptree_get_next(tr); + } + if (!ok || count != n) + FAIL("reverse insert traversal", "ok=%d count=%d expected %d", ok, count, n); + else + PASS("reverse insert: AVL rebalanced, traversal still sorted"); +} + +/* Lookup by key finds the right node (or NULL for missing) */ +static void test_lookup(void) { + int keys[] = { 10, 5, 15, 3, 7 }; + int n = (int)(sizeof(keys) / sizeof(keys[0])); + inode_t nodes[5]; + psync_tree *root = NULL; + int i; + for (i = 0; i < n; i++) { + inode_init(&nodes[i], keys[i]); + ptree_add(&root, &nodes[i].tree, icmp); + } + int ok = 1; + for (i = 0; i < n; i++) { + inode_t *found = find_key(root, keys[i]); + if (!found || found->key != keys[i]) { ok = 0; break; } + } + if (!ok) + FAIL("lookup: existing keys", "key not found"); + else if (find_key(root, 99) != NULL) + FAIL("lookup: absent key returns NULL", "got non-null for key 99"); + else + PASS("lookup: finds all inserted keys, NULL for missing"); +} + +/* Delete a leaf node */ +static void test_delete_leaf(void) { + int keys[] = { 5, 3, 7 }; + inode_t nodes[3]; + psync_tree *root = NULL; + int i; + for (i = 0; i < 3; i++) { + inode_init(&nodes[i], keys[i]); + ptree_add(&root, &nodes[i].tree, icmp); + } + /* delete leaf (key=3) */ + ptree_del(&root, &nodes[1].tree); + if (find_key(root, 3) != NULL) + FAIL("delete leaf: key gone", "key 3 still found"); + else if (find_key(root, 5) == NULL || find_key(root, 7) == NULL) + FAIL("delete leaf: others remain", "5 or 7 missing"); + else { + /* traversal gives 5, 7 */ + psync_tree *tr = ptree_get_first(root); + int a = ptree_element(tr, inode_t, tree)->key; + tr = ptree_get_next(tr); + int b = ptree_element(tr, inode_t, tree)->key; + tr = ptree_get_next(tr); + if (a == 5 && b == 7 && tr == NULL) + PASS("delete leaf: correct traversal afterward"); + else + FAIL("delete leaf: traversal wrong", "a=%d b=%d next=%p", a, b, (void*)tr); + } +} + +/* Delete root when root has two children */ +static void test_delete_root(void) { + inode_t nodes[5]; + psync_tree *root = NULL; + int keys[] = { 10, 5, 15, 3, 8 }; + int n = 5, i; + for (i = 0; i < n; i++) { + inode_init(&nodes[i], keys[i]); + ptree_add(&root, &nodes[i].tree, icmp); + } + inode_t *old_root = ptree_element(root, inode_t, tree); + ptree_del(&root, &old_root->tree); + /* old root key must be absent */ + if (find_key(root, old_root->key) != NULL) + { FAIL("delete root: root key gone", "still present"); return; } + /* remaining keys must all be present */ + int missing = 0; + for (i = 0; i < n; i++) + if (keys[i] != old_root->key && find_key(root, keys[i]) == NULL) + { missing = keys[i]; break; } + if (missing) + FAIL("delete root: remaining keys", "key %d missing", missing); + else { + /* traversal still sorted */ + psync_tree *tr = ptree_get_first(root); + int prev = -1, cnt = 0, ok = 1; + while (tr) { + int k = ptree_element(tr, inode_t, tree)->key; + if (k <= prev) { ok = 0; break; } + prev = k; cnt++; + tr = ptree_get_next(tr); + } + if (!ok || cnt != n - 1) + FAIL("delete root: traversal", "ok=%d cnt=%d expected %d", ok, cnt, n-1); + else + PASS("delete root: correct traversal after root deletion"); + } +} + +/* Delete all nodes one by one; tree must be empty at the end */ +static void test_delete_all(void) { + int keys[] = { 4, 2, 6, 1, 3, 5, 7 }; + int n = (int)(sizeof(keys) / sizeof(keys[0])); + inode_t nodes[7]; + psync_tree *root = NULL; + int i; + for (i = 0; i < n; i++) { + inode_init(&nodes[i], keys[i]); + ptree_add(&root, &nodes[i].tree, icmp); + } + for (i = 0; i < n; i++) { + inode_t *nd = find_key(root, keys[i]); + if (!nd) { FAIL("delete all: find before delete", "key %d missing", keys[i]); return; } + ptree_del(&root, &nd->tree); + } + if (root != NULL) + FAIL("delete all: root is NULL after all deletes", "root=%p", (void*)root); + else if (ptree_get_first(root) != NULL) + FAIL("delete all: first is NULL", "non-null"); + else + PASS("delete all: tree empty after deleting all nodes"); +} + +/* ptree_for_each visits every node exactly once */ +static void test_for_each_macro(void) { + int keys[] = { 9, 2, 5, 1, 8, 4 }; + int n = (int)(sizeof(keys) / sizeof(keys[0])); + inode_t nodes[6]; + psync_tree *root = NULL; + int i; + for (i = 0; i < n; i++) { + inode_init(&nodes[i], keys[i]); + ptree_add(&root, &nodes[i].tree, icmp); + } + int seen[6] = {0}; + psync_tree *tr; + ptree_for_each(tr, root) { + inode_t *nd = ptree_element(tr, inode_t, tree); + for (i = 0; i < n; i++) + if (keys[i] == nd->key) { seen[i]++; break; } + } + int ok = 1; + for (i = 0; i < n; i++) + if (seen[i] != 1) { ok = 0; break; } + if (ok) + PASS("ptree_for_each visits every node exactly once"); + else + FAIL("ptree_for_each", "some node visited wrong number of times"); +} + +/* ------------------------------------------------------------------ */ +int main(void) { + test_single_node(); + test_traversal_sorted(); + test_reverse_insert_traversal(); + test_lookup(); + test_delete_leaf(); + test_delete_root(); + test_delete_all(); + test_for_each_macro(); + + printf("\n%d passed, %d failed\n", passes, failures); + return failures ? 1 : 0; +}