Add testability infrastructure: unit tests, extracted modules, CI integration (#381)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Levi Neely <lkn@darkstar.example.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79a4a5620f
commit
ae77d3e2f2
|
|
@ -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_*
|
||||
|
|
|
|||
37
Makefile
37
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 $@ $^
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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 <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 <stdint.h>
|
||||
|
||||
#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 */
|
||||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <pthread.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <pthread.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* 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 */
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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 <sqlite3.h>
|
||||
|
||||
/*
|
||||
* 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 */
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#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=<id> */
|
||||
/* SELECT name FROM folder WHERE parentfolderid=<id> */
|
||||
/* ------------------------------------------------------------------ */
|
||||
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;
|
||||
}
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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 <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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 <pthread.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,28 @@
|
|||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* 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 <sanitizer/lsan_interface.h>
|
||||
# define LSAN_DISABLE() __lsan_disable()
|
||||
# define LSAN_ENABLE() __lsan_enable()
|
||||
#elif defined(__has_feature)
|
||||
# if __has_feature(address_sanitizer)
|
||||
# include <sanitizer/lsan_interface.h>
|
||||
# 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Loading…
Reference in New Issue