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:
Levi Neely 2026-03-11 07:52:35 +01:00 committed by GitHub
parent 79a4a5620f
commit ae77d3e2f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 2665 additions and 465 deletions

1
.gitignore vendored
View File

@ -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_*

View File

@ -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 $@ $^

View File

@ -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 "

166
pclsync/pfstasks_tree.c Normal file
View File

@ -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);
}

38
pclsync/pfstasks_tree.h Normal file
View File

@ -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 */

View File

@ -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,

72
pclsync/pfsupload_send.c Normal file
View File

@ -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;
}

48
pclsync/pfsupload_send.h Normal file
View File

@ -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 */

View File

@ -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)))

View File

@ -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;

72
pclsync/ptask_free.c Normal file
View File

@ -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);
}
}

View File

@ -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 */

View File

@ -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;
}

View File

@ -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 */

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}