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>
This commit is contained in:
parent
67c99c5e06
commit
82aaf83d9a
20
Makefile
20
Makefile
|
|
@ -155,6 +155,8 @@ TESTS_DIR := tests
|
|||
TEST_CFLAGS := -D_POSIX_C_SOURCE=200809L
|
||||
TEST_CXXFLAGS := -D_POSIX_C_SOURCE=200809L
|
||||
|
||||
HELPERS_DIR := tests/helpers
|
||||
|
||||
TEST_BINS := \
|
||||
tests/test_pdbg_path \
|
||||
tests/test_ptools_params \
|
||||
|
|
@ -163,7 +165,11 @@ 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
|
||||
|
||||
.PHONY: test tests check clean-tests
|
||||
|
||||
|
|
@ -211,6 +217,18 @@ 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_read_response: $(UNIT_DIR)/test_read_response.cpp rpcclient.cpp tests/stubs/test_stubs_cpp.c
|
||||
$(CXX) $(TEST_CXXFLAGS) $(CXXFLAGS) -o $@ $^
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
#include "pfoldersync.h"
|
||||
#include "pfs.h"
|
||||
#include "pfstasks.h"
|
||||
#include "pfstasks_tree.h"
|
||||
#include "pfsupload.h"
|
||||
#include "plibs.h"
|
||||
#include "pmem.h"
|
||||
|
|
@ -192,150 +193,6 @@ void pfs_task_release_folder_tasks_locked(psync_fstask_folder_t *folder) {
|
|||
}
|
||||
}
|
||||
|
||||
static psync_tree *pfs_task_search_tree(psync_tree *tree, size_t nameoff,
|
||||
const char *name, uint64_t taskid,
|
||||
size_t taskidoff) {
|
||||
int c;
|
||||
while (tree) {
|
||||
c = strcmp(name, ((char *)tree) + nameoff);
|
||||
if (c < 0)
|
||||
tree = tree->left;
|
||||
else if (c > 0)
|
||||
tree = tree->right;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (!tree || !taskid || *((uint64_t *)(((char *)tree) + taskidoff)) == taskid)
|
||||
return tree;
|
||||
else {
|
||||
psync_tree *tn;
|
||||
tn = ptree_get_prev(tree);
|
||||
while (tn) {
|
||||
if (strcmp(name, ((char *)tn) + nameoff))
|
||||
break;
|
||||
if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid)
|
||||
return tn;
|
||||
tn = ptree_get_prev(tn);
|
||||
}
|
||||
tn = ptree_get_next(tree);
|
||||
while (tn) {
|
||||
if (strcmp(name, ((char *)tn) + nameoff))
|
||||
break;
|
||||
if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid)
|
||||
return tn;
|
||||
tn = ptree_get_next(tn);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static psync_tree *pfs_task_walk_tree(psync_tree *tree, uint64_t taskid,
|
||||
size_t taskidoff) {
|
||||
tree = ptree_get_first(tree);
|
||||
while (tree) {
|
||||
if (*((uint64_t *)(((char *)tree) + taskidoff)) == taskid)
|
||||
return tree;
|
||||
tree = ptree_get_next(tree);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void pfs_task_insert_into_tree(psync_tree **tree, size_t nameoff,
|
||||
psync_tree *element) {
|
||||
const char *name;
|
||||
psync_tree *node;
|
||||
int c;
|
||||
|
||||
if (!*tree) {
|
||||
ptree_add_after(tree, NULL, element);
|
||||
return;
|
||||
}
|
||||
|
||||
name = ((char *)element) + nameoff;
|
||||
node = *tree;
|
||||
|
||||
while (1) {
|
||||
c = strcmp(name, ((char *)node) + nameoff);
|
||||
|
||||
if (c < 0) {
|
||||
if (node->left)
|
||||
node = node->left;
|
||||
else {
|
||||
ptree_add_before(tree, node, element);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (c == 0)
|
||||
pdbg_logf(D_WARNING, "duplicate entry %s, should not happen", name);
|
||||
|
||||
if (node->right)
|
||||
node = node->right;
|
||||
else {
|
||||
ptree_add_after(tree, node, element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
psync_fstask_mkdir_t *pfs_task_find_mkdir(psync_fstask_folder_t *folder,
|
||||
const char *name,
|
||||
uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->mkdirs,
|
||||
offsetof(psync_fstask_mkdir_t, name), name,
|
||||
taskid, offsetof(psync_fstask_mkdir_t, taskid)),
|
||||
psync_fstask_mkdir_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_rmdir_t *pfs_task_find_rmdir(psync_fstask_folder_t *folder,
|
||||
const char *name,
|
||||
uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->rmdirs,
|
||||
offsetof(psync_fstask_rmdir_t, name), name,
|
||||
taskid, offsetof(psync_fstask_rmdir_t, taskid)),
|
||||
psync_fstask_rmdir_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_creat_t *pfs_task_find_creat(psync_fstask_folder_t *folder,
|
||||
const char *name,
|
||||
uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->creats,
|
||||
offsetof(psync_fstask_creat_t, name), name,
|
||||
taskid, offsetof(psync_fstask_creat_t, taskid)),
|
||||
psync_fstask_creat_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_unlink_t *pfs_task_find_unlink(psync_fstask_folder_t *folder,
|
||||
const char *name,
|
||||
uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->unlinks,
|
||||
offsetof(psync_fstask_unlink_t, name), name,
|
||||
taskid, offsetof(psync_fstask_unlink_t, taskid)),
|
||||
psync_fstask_unlink_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_mkdir_t *
|
||||
pfs_task_find_mkdir_by_folderid(psync_fstask_folder_t *folder,
|
||||
psync_fsfolderid_t folderid) {
|
||||
return ptree_element(
|
||||
pfs_task_walk_tree(folder->mkdirs, folderid,
|
||||
offsetof(psync_fstask_mkdir_t, folderid)),
|
||||
psync_fstask_mkdir_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_creat_t *
|
||||
pfs_task_find_creat_by_fileid(psync_fstask_folder_t *folder,
|
||||
psync_fsfileid_t fileid) {
|
||||
return ptree_element(
|
||||
pfs_task_walk_tree(folder->creats, fileid,
|
||||
offsetof(psync_fstask_creat_t, fileid)),
|
||||
psync_fstask_creat_t, tree);
|
||||
}
|
||||
|
||||
static void pfs_task_depend(uint64_t taskid, uint64_t dependontaskid) {
|
||||
psync_sql_res *res;
|
||||
res = psql_prepare("INSERT OR IGNORE INTO fstaskdepend "
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* pfstasks_tree.c — pure tree operations for the fstask subsystem.
|
||||
*
|
||||
* All functions here are psql-free and operate only on in-memory psync_tree
|
||||
* structures. Extracted so that unit tests can link this file alone without
|
||||
* pulling in pfstasks.c's heavy dependencies (psql, pcrypto, pfs*, …).
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pdbg.h"
|
||||
#include "pfstasks_tree.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Static helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/*
|
||||
* BST search by name string at `nameoff`.
|
||||
* If `taskid` != 0 and a name-equal node has a different taskid,
|
||||
* walks prev/next siblings to find the matching one.
|
||||
*/
|
||||
static psync_tree *pfs_task_search_tree(psync_tree *tree, size_t nameoff,
|
||||
const char *name, uint64_t taskid,
|
||||
size_t taskidoff) {
|
||||
int c;
|
||||
while (tree) {
|
||||
c = strcmp(name, ((char *)tree) + nameoff);
|
||||
if (c < 0) tree = tree->left;
|
||||
else if (c > 0) tree = tree->right;
|
||||
else break;
|
||||
}
|
||||
if (!tree || !taskid ||
|
||||
*((uint64_t *)(((char *)tree) + taskidoff)) == taskid)
|
||||
return tree;
|
||||
/* Walk siblings with the same name to find the matching taskid */
|
||||
psync_tree *tn = ptree_get_prev(tree);
|
||||
while (tn) {
|
||||
if (strcmp(name, ((char *)tn) + nameoff)) break;
|
||||
if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid) return tn;
|
||||
tn = ptree_get_prev(tn);
|
||||
}
|
||||
tn = ptree_get_next(tree);
|
||||
while (tn) {
|
||||
if (strcmp(name, ((char *)tn) + nameoff)) break;
|
||||
if (*((uint64_t *)(((char *)tn) + taskidoff)) == taskid) return tn;
|
||||
tn = ptree_get_next(tn);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Linear in-order walk to find the first node whose uint64_t field at
|
||||
* `taskidoff` equals `taskid`.
|
||||
*/
|
||||
static psync_tree *pfs_task_walk_tree(psync_tree *tree, uint64_t taskid,
|
||||
size_t taskidoff) {
|
||||
tree = ptree_get_first(tree);
|
||||
while (tree) {
|
||||
if (*((uint64_t *)(((char *)tree) + taskidoff)) == taskid) return tree;
|
||||
tree = ptree_get_next(tree);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported: tree insertion */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void pfs_task_insert_into_tree(psync_tree **tree, size_t nameoff,
|
||||
psync_tree *element) {
|
||||
const char *name;
|
||||
psync_tree *node;
|
||||
int c;
|
||||
|
||||
if (!*tree) {
|
||||
ptree_add_after(tree, NULL, element);
|
||||
return;
|
||||
}
|
||||
|
||||
name = ((char *)element) + nameoff;
|
||||
node = *tree;
|
||||
|
||||
while (1) {
|
||||
c = strcmp(name, ((char *)node) + nameoff);
|
||||
if (c < 0) {
|
||||
if (node->left)
|
||||
node = node->left;
|
||||
else {
|
||||
ptree_add_before(tree, node, element);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (c == 0)
|
||||
pdbg_logf(D_WARNING, "duplicate entry %s, should not happen",
|
||||
name);
|
||||
if (node->right)
|
||||
node = node->right;
|
||||
else {
|
||||
ptree_add_after(tree, node, element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported: find by name (+ optional taskid discriminator) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
psync_fstask_mkdir_t *pfs_task_find_mkdir(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->mkdirs,
|
||||
offsetof(psync_fstask_mkdir_t, name), name,
|
||||
taskid, offsetof(psync_fstask_mkdir_t, taskid)),
|
||||
psync_fstask_mkdir_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_rmdir_t *pfs_task_find_rmdir(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->rmdirs,
|
||||
offsetof(psync_fstask_rmdir_t, name), name,
|
||||
taskid, offsetof(psync_fstask_rmdir_t, taskid)),
|
||||
psync_fstask_rmdir_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_creat_t *pfs_task_find_creat(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->creats,
|
||||
offsetof(psync_fstask_creat_t, name), name,
|
||||
taskid, offsetof(psync_fstask_creat_t, taskid)),
|
||||
psync_fstask_creat_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_unlink_t *pfs_task_find_unlink(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid) {
|
||||
return ptree_element(
|
||||
pfs_task_search_tree(folder->unlinks,
|
||||
offsetof(psync_fstask_unlink_t, name), name,
|
||||
taskid, offsetof(psync_fstask_unlink_t, taskid)),
|
||||
psync_fstask_unlink_t, tree);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported: find by numeric ID */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
psync_fstask_mkdir_t *pfs_task_find_mkdir_by_folderid(
|
||||
psync_fstask_folder_t *folder, psync_fsfolderid_t folderid) {
|
||||
return ptree_element(
|
||||
pfs_task_walk_tree(folder->mkdirs, folderid,
|
||||
offsetof(psync_fstask_mkdir_t, folderid)),
|
||||
psync_fstask_mkdir_t, tree);
|
||||
}
|
||||
|
||||
psync_fstask_creat_t *pfs_task_find_creat_by_fileid(
|
||||
psync_fstask_folder_t *folder, psync_fsfileid_t fileid) {
|
||||
return ptree_element(
|
||||
pfs_task_walk_tree(folder->creats, fileid,
|
||||
offsetof(psync_fstask_creat_t, fileid)),
|
||||
psync_fstask_creat_t, tree);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* pfstasks_tree.h — pure tree layer for the fstask subsystem.
|
||||
*
|
||||
* Declares functions that operate solely on in-memory psync_tree structures
|
||||
* with zero psql calls. This header is included by pfstasks.c and by the
|
||||
* unit test test_pfstasks_tree.c.
|
||||
*
|
||||
* Do NOT add functions with psql dependencies here.
|
||||
*/
|
||||
#ifndef PFSTASKS_TREE_H
|
||||
#define PFSTASKS_TREE_H
|
||||
|
||||
#include "pfstasks.h"
|
||||
|
||||
/*
|
||||
* Insert `element` into the AVL tree `*tree`, ordering by the NUL-terminated
|
||||
* string at offset `nameoff` within each element.
|
||||
*/
|
||||
void pfs_task_insert_into_tree(psync_tree **tree, size_t nameoff,
|
||||
psync_tree *element);
|
||||
|
||||
/* Find by name (and optional taskid discriminator; pass 0 to match any) */
|
||||
psync_fstask_mkdir_t *pfs_task_find_mkdir(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid);
|
||||
psync_fstask_rmdir_t *pfs_task_find_rmdir(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid);
|
||||
psync_fstask_creat_t *pfs_task_find_creat(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid);
|
||||
psync_fstask_unlink_t *pfs_task_find_unlink(psync_fstask_folder_t *folder,
|
||||
const char *name, uint64_t taskid);
|
||||
|
||||
/* Find by numeric ID via linear walk */
|
||||
psync_fstask_mkdir_t *pfs_task_find_mkdir_by_folderid(
|
||||
psync_fstask_folder_t *folder, psync_fsfolderid_t folderid);
|
||||
psync_fstask_creat_t *pfs_task_find_creat_by_fileid(
|
||||
psync_fstask_folder_t *folder, psync_fsfileid_t fileid);
|
||||
|
||||
#endif /* PFSTASKS_TREE_H */
|
||||
|
|
@ -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_STATIC);
|
||||
else
|
||||
sqlite3_bind_null(stmt, 5);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) return -1;
|
||||
return (int64_t)sqlite3_last_insert_rowid(g_db);
|
||||
}
|
||||
|
||||
int psql_test_count_fstask(const char *where_clause) {
|
||||
char sql[512];
|
||||
if (where_clause && *where_clause)
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT COUNT(*) FROM fstask WHERE %s", where_clause);
|
||||
else
|
||||
snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM fstask");
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
int count = -1;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return count;
|
||||
}
|
||||
|
||||
int psql_test_count_fstaskdepend(const char *where_clause) {
|
||||
char sql[512];
|
||||
if (where_clause && *where_clause)
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT COUNT(*) FROM fstaskdepend WHERE %s", where_clause);
|
||||
else
|
||||
snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM fstaskdepend");
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
int count = -1;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return count;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* psql_test_helpers.h — in-memory SQLite harness for unit tests.
|
||||
*
|
||||
* Provides psql_test_open() / psql_test_close() which open a ":memory:"
|
||||
* database and apply the full PSYNC_DATABASE_STRUCTURE schema from
|
||||
* pdatabase.h. Use psql_test_db() to obtain the sqlite3* connection for
|
||||
* direct SQL operations in tests.
|
||||
*
|
||||
* Intentionally bypasses psql.c so tests can link this file alone without
|
||||
* pulling in psql.c's heavyweight transitive dependencies (pcache, psignal,
|
||||
* psys, pnetlibs, …).
|
||||
*/
|
||||
#ifndef PSQL_TEST_HELPERS_H
|
||||
#define PSQL_TEST_HELPERS_H
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
/*
|
||||
* Open an in-memory SQLite database and apply the full PSYNC_DATABASE_STRUCTURE
|
||||
* schema. Returns 0 on success, -1 on failure.
|
||||
* Call this once at the start of each test that needs the DB.
|
||||
*/
|
||||
int psql_test_open(void);
|
||||
|
||||
/* Close and discard the in-memory database. */
|
||||
void psql_test_close(void);
|
||||
|
||||
/* Return the active sqlite3* connection (valid between open/close). */
|
||||
sqlite3 *psql_test_db(void);
|
||||
|
||||
/* Execute a SQL statement and return the sqlite3 result code. */
|
||||
int psql_test_exec(const char *sql);
|
||||
|
||||
/* Insert one fstask row; returns the inserted rowid on success, -1 on error. */
|
||||
int64_t psql_test_insert_fstask(int type, int status, int64_t folderid,
|
||||
const char *text1);
|
||||
|
||||
/* Count rows matching a simple WHERE clause on fstask. */
|
||||
int psql_test_count_fstask(const char *where_clause);
|
||||
|
||||
/* Count rows matching a simple WHERE clause on fstaskdepend. */
|
||||
int psql_test_count_fstaskdepend(const char *where_clause);
|
||||
|
||||
#endif /* PSQL_TEST_HELPERS_H */
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* Test: pfstasks DB layer via psql_test_helpers
|
||||
*
|
||||
* Opens an :memory: SQLite database with the full PSYNC_DATABASE_STRUCTURE
|
||||
* schema, then exercises:
|
||||
* 1. Schema creation (tables and indices exist)
|
||||
* 2. fstask row insertion and retrieval
|
||||
* 3. Dependency insertion via fstaskdepend
|
||||
* 4. CASCADE DELETE: deleting an fstask removes its fstaskdepend rows
|
||||
* 5. FK enforcement: fstaskdepend row with unknown fstaskid is rejected
|
||||
* 6. rmdir blocking: SQL query pattern that pfstasks.c uses to detect
|
||||
* non-empty folders (file/folder rows in DB)
|
||||
* 7. creat-after-unlink: insert UNLINK then CREAT tasks for the same name
|
||||
* in the same folder, verify both tasks exist and can be queried by type
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "psql_test_helpers.h"
|
||||
|
||||
/* Task type constants (mirrors pfstasks.h / PSYNC_FS_TASK_*) */
|
||||
#define TASK_MKDIR 1
|
||||
#define TASK_RMDIR 2
|
||||
#define TASK_CREAT 3
|
||||
#define TASK_UNLINK 4
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
static int passes = 0, failures = 0;
|
||||
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0)
|
||||
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0)
|
||||
|
||||
static int table_exists(const char *name) {
|
||||
char sql[256];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='%s'",
|
||||
name);
|
||||
sqlite3_stmt *stmt;
|
||||
sqlite3 *db = psql_test_db();
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
|
||||
int ok = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
ok = sqlite3_column_int(stmt, 0) > 0;
|
||||
sqlite3_finalize(stmt);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 1: Schema creation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_schema_created(void) {
|
||||
const char *required[] = {
|
||||
"fstask", "fstaskdepend", "folder", "file", "setting",
|
||||
"syncfolder", "localfolder", "localfile", NULL
|
||||
};
|
||||
int ok = 1;
|
||||
for (int i = 0; required[i]; i++) {
|
||||
if (!table_exists(required[i])) {
|
||||
ok = 0;
|
||||
FAIL("schema: table exists", "table '%s' missing", required[i]);
|
||||
}
|
||||
}
|
||||
if (ok)
|
||||
PASS("schema: all required tables created");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 2: fstask insert and query */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_fstask_insert_query(void) {
|
||||
int64_t id = psql_test_insert_fstask(TASK_MKDIR, 0, 100, "mydir");
|
||||
if (id <= 0)
|
||||
{ FAIL("fstask insert", "rowid=%lld", (long long)id); return; }
|
||||
|
||||
int cnt = psql_test_count_fstask("type=1 AND folderid=100");
|
||||
if (cnt == 1)
|
||||
PASS("fstask insert: row present with correct type/folderid");
|
||||
else
|
||||
FAIL("fstask insert/query", "count=%d expected 1", cnt);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 3: fstaskdepend insert */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_fstaskdepend_insert(void) {
|
||||
int64_t task1 = psql_test_insert_fstask(TASK_MKDIR, 0, 200, "parent");
|
||||
int64_t task2 = psql_test_insert_fstask(TASK_MKDIR, 0, 200, "child");
|
||||
if (task1 <= 0 || task2 <= 0)
|
||||
{ FAIL("fstaskdepend setup", "tasks=%lld/%lld", (long long)task1,
|
||||
(long long)task2); return; }
|
||||
|
||||
char sql[256];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"INSERT INTO fstaskdepend (fstaskid, dependfstaskid) VALUES (%lld, %lld)",
|
||||
(long long)task2, (long long)task1);
|
||||
if (psql_test_exec(sql) != SQLITE_OK)
|
||||
{ FAIL("fstaskdepend insert", "exec failed"); return; }
|
||||
|
||||
char where[128];
|
||||
snprintf(where, sizeof(where), "fstaskid=%lld AND dependfstaskid=%lld",
|
||||
(long long)task2, (long long)task1);
|
||||
int cnt = psql_test_count_fstaskdepend(where);
|
||||
if (cnt == 1)
|
||||
PASS("fstaskdepend: dependency row inserted");
|
||||
else
|
||||
FAIL("fstaskdepend insert/count", "count=%d expected 1", cnt);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 4: CASCADE DELETE — deleting fstask removes fstaskdepend rows */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_cascade_delete(void) {
|
||||
int64_t parent = psql_test_insert_fstask(TASK_MKDIR, 0, 300, "cascade_p");
|
||||
int64_t child = psql_test_insert_fstask(TASK_MKDIR, 0, 300, "cascade_c");
|
||||
if (parent <= 0 || child <= 0)
|
||||
{ FAIL("cascade setup", "ids=%lld/%lld", (long long)parent,
|
||||
(long long)child); return; }
|
||||
|
||||
char sql[256];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"INSERT INTO fstaskdepend (fstaskid, dependfstaskid) VALUES (%lld, %lld)",
|
||||
(long long)child, (long long)parent);
|
||||
psql_test_exec(sql);
|
||||
|
||||
/* Delete the parent task */
|
||||
snprintf(sql, sizeof(sql), "DELETE FROM fstask WHERE id=%lld",
|
||||
(long long)parent);
|
||||
psql_test_exec(sql);
|
||||
|
||||
/* 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);
|
||||
psql_test_exec(sql);
|
||||
|
||||
/* 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'");
|
||||
int dep_cnt = psql_test_count_fstaskdepend(NULL);
|
||||
|
||||
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);
|
||||
|
||||
(void)dep_cnt;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 8: psql_test_open / psql_test_close idempotence */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void test_open_close_idempotent(void) {
|
||||
/* Close and re-open; db should be usable again */
|
||||
psql_test_close();
|
||||
if (psql_test_db() != NULL)
|
||||
{ FAIL("open/close: db NULL after close", "still non-null"); return; }
|
||||
if (psql_test_open() != 0)
|
||||
{ FAIL("open/close: re-open", "failed"); return; }
|
||||
if (psql_test_db() == NULL)
|
||||
{ FAIL("open/close: db non-null after re-open", "NULL"); return; }
|
||||
if (!table_exists("fstask"))
|
||||
FAIL("open/close: fstask table after re-open", "missing");
|
||||
else
|
||||
PASS("psql_test_open/close idempotent: re-open restores fresh schema");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
int main(void) {
|
||||
if (psql_test_open() != 0) {
|
||||
fprintf(stderr, "FATAL: psql_test_open() failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
test_schema_created();
|
||||
test_fstask_insert_query();
|
||||
test_fstaskdepend_insert();
|
||||
test_cascade_delete();
|
||||
test_fk_enforcement();
|
||||
test_rmdir_blocking_sql();
|
||||
test_creat_after_unlink();
|
||||
test_open_close_idempotent();
|
||||
|
||||
psql_test_close();
|
||||
|
||||
printf("\n%d passed, %d failed\n", passes, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Test: pfstasks_tree.c — pure tree layer for fstask structs
|
||||
*
|
||||
* Builds psync_fstask_folder_t fixtures directly (no DB, no psql) and
|
||||
* verifies pfs_task_find_mkdir, pfs_task_find_rmdir, pfs_task_find_creat,
|
||||
* pfs_task_find_unlink, pfs_task_find_mkdir_by_folderid, and
|
||||
* pfs_task_find_creat_by_fileid.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "pfstasks_tree.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Harness */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int passes = 0, failures = 0;
|
||||
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0)
|
||||
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Node builders (stack / malloc, NOT pmem) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Allocate a psync_fstask_mkdir_t with a given name, taskid, folderid */
|
||||
static psync_fstask_mkdir_t *mk_mkdir(const char *name, uint64_t taskid,
|
||||
psync_fsfolderid_t folderid) {
|
||||
size_t len = strlen(name) + 1;
|
||||
psync_fstask_mkdir_t *n = (psync_fstask_mkdir_t *)
|
||||
calloc(1, offsetof(psync_fstask_mkdir_t, name) + len);
|
||||
n->taskid = taskid;
|
||||
n->folderid = folderid;
|
||||
memcpy(n->name, name, len);
|
||||
return n;
|
||||
}
|
||||
|
||||
static psync_fstask_rmdir_t *mk_rmdir(const char *name, uint64_t taskid,
|
||||
psync_fsfolderid_t folderid) {
|
||||
size_t len = strlen(name) + 1;
|
||||
psync_fstask_rmdir_t *n = (psync_fstask_rmdir_t *)
|
||||
calloc(1, offsetof(psync_fstask_rmdir_t, name) + len);
|
||||
n->taskid = taskid;
|
||||
n->folderid = folderid;
|
||||
memcpy(n->name, name, len);
|
||||
return n;
|
||||
}
|
||||
|
||||
static psync_fstask_creat_t *mk_creat(const char *name, uint64_t taskid,
|
||||
psync_fsfileid_t fileid) {
|
||||
size_t len = strlen(name) + 1;
|
||||
psync_fstask_creat_t *n = (psync_fstask_creat_t *)
|
||||
calloc(1, offsetof(psync_fstask_creat_t, name) + len);
|
||||
n->taskid = taskid;
|
||||
n->fileid = fileid;
|
||||
memcpy(n->name, name, len);
|
||||
return n;
|
||||
}
|
||||
|
||||
static psync_fstask_unlink_t *mk_unlink(const char *name, uint64_t taskid,
|
||||
psync_fsfileid_t fileid) {
|
||||
size_t len = strlen(name) + 1;
|
||||
psync_fstask_unlink_t *n = (psync_fstask_unlink_t *)
|
||||
calloc(1, offsetof(psync_fstask_unlink_t, name) + len);
|
||||
n->taskid = taskid;
|
||||
n->fileid = fileid;
|
||||
memcpy(n->name, name, len);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Build a clean empty folder */
|
||||
static void folder_init(psync_fstask_folder_t *f, psync_fsfolderid_t folderid) {
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->folderid = folderid;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Tests */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* find_mkdir: basic hit and miss */
|
||||
static void test_find_mkdir_basic(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 1);
|
||||
|
||||
psync_fstask_mkdir_t *a = mk_mkdir("alpha", 10, -10);
|
||||
psync_fstask_mkdir_t *b = mk_mkdir("beta", 20, -20);
|
||||
psync_fstask_mkdir_t *c = mk_mkdir("gamma", 30, -30);
|
||||
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &a->tree);
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &b->tree);
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &c->tree);
|
||||
|
||||
psync_fstask_mkdir_t *found = pfs_task_find_mkdir(&f, "beta", 0);
|
||||
if (!found || found->taskid != 20)
|
||||
FAIL("find_mkdir basic: hit", "found=%p taskid=%llu",
|
||||
(void*)found, found ? (unsigned long long)found->taskid : 0);
|
||||
else
|
||||
PASS("find_mkdir: finds 'beta' by name");
|
||||
|
||||
if (pfs_task_find_mkdir(&f, "delta", 0) != NULL)
|
||||
FAIL("find_mkdir basic: miss", "found non-NULL for absent key");
|
||||
else
|
||||
PASS("find_mkdir: returns NULL for absent name");
|
||||
|
||||
free(a); free(b); free(c);
|
||||
}
|
||||
|
||||
/* find_mkdir: taskid discriminator with same-name duplicates */
|
||||
static void test_find_mkdir_taskid(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 2);
|
||||
|
||||
/* Two mkdirs with same name "foo" but different taskids */
|
||||
psync_fstask_mkdir_t *x = mk_mkdir("foo", 100, -100);
|
||||
psync_fstask_mkdir_t *y = mk_mkdir("foo", 200, -200);
|
||||
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &x->tree);
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &y->tree);
|
||||
|
||||
psync_fstask_mkdir_t *found100 = pfs_task_find_mkdir(&f, "foo", 100);
|
||||
psync_fstask_mkdir_t *found200 = pfs_task_find_mkdir(&f, "foo", 200);
|
||||
|
||||
if (!found100 || found100->taskid != 100)
|
||||
FAIL("find_mkdir taskid: find taskid=100", "got %p", (void*)found100);
|
||||
else
|
||||
PASS("find_mkdir: taskid discriminator finds correct node (100)");
|
||||
|
||||
if (!found200 || found200->taskid != 200)
|
||||
FAIL("find_mkdir taskid: find taskid=200", "got %p", (void*)found200);
|
||||
else
|
||||
PASS("find_mkdir: taskid discriminator finds correct node (200)");
|
||||
|
||||
free(x); free(y);
|
||||
}
|
||||
|
||||
/* find_rmdir: basic */
|
||||
static void test_find_rmdir_basic(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 3);
|
||||
|
||||
psync_fstask_rmdir_t *a = mk_rmdir("dirA", 11, 101);
|
||||
psync_fstask_rmdir_t *b = mk_rmdir("dirB", 22, 102);
|
||||
|
||||
pfs_task_insert_into_tree(&f.rmdirs, offsetof(psync_fstask_rmdir_t, name), &a->tree);
|
||||
pfs_task_insert_into_tree(&f.rmdirs, offsetof(psync_fstask_rmdir_t, name), &b->tree);
|
||||
|
||||
psync_fstask_rmdir_t *r = pfs_task_find_rmdir(&f, "dirA", 0);
|
||||
if (!r || r->taskid != 11)
|
||||
FAIL("find_rmdir basic", "taskid=%llu", r ? (unsigned long long)r->taskid : 0);
|
||||
else
|
||||
PASS("find_rmdir: finds 'dirA'");
|
||||
|
||||
if (pfs_task_find_rmdir(&f, "dirC", 0) != NULL)
|
||||
FAIL("find_rmdir miss", "non-NULL for absent");
|
||||
else
|
||||
PASS("find_rmdir: NULL for absent name");
|
||||
|
||||
free(a); free(b);
|
||||
}
|
||||
|
||||
/* find_creat: basic */
|
||||
static void test_find_creat_basic(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 4);
|
||||
|
||||
psync_fstask_creat_t *a = mk_creat("file.txt", 55, 1001);
|
||||
psync_fstask_creat_t *b = mk_creat("photo.jpg", 66, 1002);
|
||||
|
||||
pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &a->tree);
|
||||
pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &b->tree);
|
||||
|
||||
psync_fstask_creat_t *r = pfs_task_find_creat(&f, "photo.jpg", 0);
|
||||
if (!r || r->fileid != 1002)
|
||||
FAIL("find_creat basic", "fileid=%lld", r ? (long long)r->fileid : 0);
|
||||
else
|
||||
PASS("find_creat: finds 'photo.jpg'");
|
||||
|
||||
free(a); free(b);
|
||||
}
|
||||
|
||||
/* find_unlink: basic */
|
||||
static void test_find_unlink_basic(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 5);
|
||||
|
||||
psync_fstask_unlink_t *a = mk_unlink("old.txt", 77, 2001);
|
||||
psync_fstask_unlink_t *b = mk_unlink("tmp.log", 88, 2002);
|
||||
|
||||
pfs_task_insert_into_tree(&f.unlinks, offsetof(psync_fstask_unlink_t, name), &a->tree);
|
||||
pfs_task_insert_into_tree(&f.unlinks, offsetof(psync_fstask_unlink_t, name), &b->tree);
|
||||
|
||||
psync_fstask_unlink_t *r = pfs_task_find_unlink(&f, "old.txt", 0);
|
||||
if (!r || r->fileid != 2001)
|
||||
FAIL("find_unlink basic", "fileid=%lld", r ? (long long)r->fileid : 0);
|
||||
else
|
||||
PASS("find_unlink: finds 'old.txt'");
|
||||
|
||||
if (pfs_task_find_unlink(&f, "missing.txt", 0) != NULL)
|
||||
FAIL("find_unlink miss", "non-NULL");
|
||||
else
|
||||
PASS("find_unlink: NULL for absent name");
|
||||
|
||||
free(a); free(b);
|
||||
}
|
||||
|
||||
/* find_mkdir_by_folderid: walk tree by folderid */
|
||||
static void test_find_mkdir_by_folderid(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 6);
|
||||
|
||||
psync_fstask_mkdir_t *a = mk_mkdir("one", 1, -101);
|
||||
psync_fstask_mkdir_t *b = mk_mkdir("two", 2, -202);
|
||||
psync_fstask_mkdir_t *c = mk_mkdir("three", 3, -303);
|
||||
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &a->tree);
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &b->tree);
|
||||
pfs_task_insert_into_tree(&f.mkdirs, offsetof(psync_fstask_mkdir_t, name), &c->tree);
|
||||
|
||||
psync_fstask_mkdir_t *r = pfs_task_find_mkdir_by_folderid(&f, -202);
|
||||
if (!r || strcmp(r->name, "two") != 0)
|
||||
FAIL("find_mkdir_by_folderid", "name=%s", r ? r->name : "(null)");
|
||||
else
|
||||
PASS("find_mkdir_by_folderid: finds node with folderid=-202");
|
||||
|
||||
if (pfs_task_find_mkdir_by_folderid(&f, -999) != NULL)
|
||||
FAIL("find_mkdir_by_folderid miss", "non-NULL for absent folderid");
|
||||
else
|
||||
PASS("find_mkdir_by_folderid: NULL for absent folderid");
|
||||
|
||||
free(a); free(b); free(c);
|
||||
}
|
||||
|
||||
/* find_creat_by_fileid: walk tree by fileid */
|
||||
static void test_find_creat_by_fileid(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 7);
|
||||
|
||||
psync_fstask_creat_t *a = mk_creat("a.txt", 10, 5001);
|
||||
psync_fstask_creat_t *b = mk_creat("b.txt", 20, 5002);
|
||||
psync_fstask_creat_t *c = mk_creat("c.txt", 30, 5003);
|
||||
|
||||
pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &a->tree);
|
||||
pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &b->tree);
|
||||
pfs_task_insert_into_tree(&f.creats, offsetof(psync_fstask_creat_t, name), &c->tree);
|
||||
|
||||
psync_fstask_creat_t *r = pfs_task_find_creat_by_fileid(&f, 5002);
|
||||
if (!r || strcmp(r->name, "b.txt") != 0)
|
||||
FAIL("find_creat_by_fileid", "name=%s", r ? r->name : "(null)");
|
||||
else
|
||||
PASS("find_creat_by_fileid: finds node with fileid=5002");
|
||||
|
||||
free(a); free(b); free(c);
|
||||
}
|
||||
|
||||
/* Empty folder: all finds return NULL */
|
||||
static void test_empty_folder(void) {
|
||||
psync_fstask_folder_t f;
|
||||
folder_init(&f, 8);
|
||||
int ok = 1;
|
||||
if (pfs_task_find_mkdir(&f, "x", 0) != NULL) ok = 0;
|
||||
if (pfs_task_find_rmdir(&f, "x", 0) != NULL) ok = 0;
|
||||
if (pfs_task_find_creat(&f, "x", 0) != NULL) ok = 0;
|
||||
if (pfs_task_find_unlink(&f, "x", 0) != NULL) ok = 0;
|
||||
if (ok)
|
||||
PASS("empty folder: all finds return NULL");
|
||||
else
|
||||
FAIL("empty folder", "unexpected non-NULL");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
int main(void) {
|
||||
test_find_mkdir_basic();
|
||||
test_find_mkdir_taskid();
|
||||
test_find_rmdir_basic();
|
||||
test_find_creat_basic();
|
||||
test_find_unlink_basic();
|
||||
test_find_mkdir_by_folderid();
|
||||
test_find_creat_by_fileid();
|
||||
test_empty_folder();
|
||||
|
||||
printf("\n%d passed, %d failed\n", passes, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* Test: pintervaltree.c — interval tree (add, remove, merge, split, cut_end)
|
||||
*
|
||||
* Exercises psync_interval_tree_add, _remove, _cut_end, _free, and the
|
||||
* inline helpers first_interval_containing_or_after / get_first / get_next.
|
||||
* Memory is managed by pintervaltree.c via pmem; tests call _free() at the
|
||||
* end of each case to avoid leaks.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "pintervaltree.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int passes = 0, failures = 0;
|
||||
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0)
|
||||
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0)
|
||||
|
||||
static int count_intervals(psync_interval_tree_t *tree) {
|
||||
int n = 0;
|
||||
psync_interval_tree_t *it;
|
||||
psync_interval_tree_for_each(it, tree) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Find an interval with exact [from, to] */
|
||||
static int has_interval(psync_interval_tree_t *tree, uint64_t from, uint64_t to) {
|
||||
psync_interval_tree_t *it;
|
||||
psync_interval_tree_for_each(it, tree)
|
||||
if (it->from == from && it->to == to) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Tests */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Add a single interval; verify it's stored correctly */
|
||||
static void test_add_single(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
if (!tree)
|
||||
{ FAIL("add single: tree non-null", "tree is NULL"); return; }
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("add single: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 20))
|
||||
FAIL("add single: [10,20] present", "not found");
|
||||
else
|
||||
PASS("add single interval [10,20]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Two non-overlapping, non-adjacent intervals: no merging */
|
||||
static void test_add_non_overlapping(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 30, 40);
|
||||
if (count_intervals(tree) != 2)
|
||||
FAIL("non-overlapping: count", "expected 2 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 20) || !has_interval(tree, 30, 40))
|
||||
FAIL("non-overlapping: both present", "one missing");
|
||||
else
|
||||
PASS("two non-overlapping intervals stored separately");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Overlapping: [10,20] then [15,30] → merged to [10,30] */
|
||||
static void test_add_overlapping_merge(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 15, 30);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("overlapping merge: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 30))
|
||||
FAIL("overlapping merge: [10,30]", "not found");
|
||||
else
|
||||
PASS("overlapping intervals merged to [10,30]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Adjacent: [10,20] then [20,30] → merged to [10,30] */
|
||||
static void test_add_adjacent_merge(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 20, 30);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("adjacent merge: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 30))
|
||||
FAIL("adjacent merge: [10,30]", "not found");
|
||||
else
|
||||
PASS("adjacent intervals [10,20]+[20,30] merged to [10,30]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Contained: add [10,30], then add [15,20] → no change (subset already covered) */
|
||||
static void test_add_contained(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 30);
|
||||
psync_interval_tree_add(&tree, 15, 20);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("contained: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 30))
|
||||
FAIL("contained: [10,30] unchanged", "not found");
|
||||
else
|
||||
PASS("adding contained interval is a no-op");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Spanning: add [15,25], then add [10,30] → becomes [10,30] */
|
||||
static void test_add_spanning(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 15, 25);
|
||||
psync_interval_tree_add(&tree, 10, 30);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("spanning: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 30))
|
||||
FAIL("spanning: [10,30]", "not found");
|
||||
else
|
||||
PASS("spanning interval replaces smaller existing interval");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Merge multiple intervals: [5,10]+[10,15]+[15,20] → [5,20] */
|
||||
static void test_add_chain_merge(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 5, 10);
|
||||
psync_interval_tree_add(&tree, 10, 15);
|
||||
psync_interval_tree_add(&tree, 15, 20);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("chain merge: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 5, 20))
|
||||
FAIL("chain merge: [5,20]", "not found");
|
||||
else
|
||||
PASS("three adjacent intervals merged into [5,20]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Remove middle: [10,30] → remove [15,20] → [10,15] and [20,30] */
|
||||
static void test_remove_middle_split(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 30);
|
||||
psync_interval_tree_remove(&tree, 15, 20);
|
||||
if (count_intervals(tree) != 2)
|
||||
FAIL("remove middle: count", "expected 2 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 15) || !has_interval(tree, 20, 30))
|
||||
FAIL("remove middle: split halves",
|
||||
"[10,15]=%d [20,30]=%d",
|
||||
has_interval(tree, 10, 15), has_interval(tree, 20, 30));
|
||||
else
|
||||
PASS("remove middle splits [10,30] into [10,15] and [20,30]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Remove exact interval: [10,20] → remove [10,20] → empty */
|
||||
static void test_remove_exact(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_remove(&tree, 10, 20);
|
||||
if (count_intervals(tree) != 0)
|
||||
FAIL("remove exact: empty", "expected 0 got %d", count_intervals(tree));
|
||||
else
|
||||
PASS("remove exact interval leaves tree empty");
|
||||
/* tree may be NULL here; free handles NULL */
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Remove left overlap: [10,30] → remove [5,15] → [15,30] */
|
||||
static void test_remove_left_overlap(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 30);
|
||||
psync_interval_tree_remove(&tree, 5, 15);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("remove left: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 15, 30))
|
||||
FAIL("remove left: [15,30]", "not found");
|
||||
else
|
||||
PASS("remove left overlap: [10,30] clipped to [15,30]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Remove right overlap: [10,30] → remove [25,35] → [10,25] */
|
||||
static void test_remove_right_overlap(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 30);
|
||||
psync_interval_tree_remove(&tree, 25, 35);
|
||||
if (count_intervals(tree) != 1)
|
||||
FAIL("remove right: count", "expected 1 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 25))
|
||||
FAIL("remove right: [10,25]", "not found");
|
||||
else
|
||||
PASS("remove right overlap: [10,30] clipped to [10,25]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* Remove spanning: [10,20]+[30,40] → remove [5,45] → empty */
|
||||
static void test_remove_spanning(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 30, 40);
|
||||
psync_interval_tree_remove(&tree, 5, 45);
|
||||
if (count_intervals(tree) != 0)
|
||||
FAIL("remove spanning: empty", "expected 0 got %d", count_intervals(tree));
|
||||
else
|
||||
PASS("remove spanning erases all intervals");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* cut_end: [10,20]+[30,40]+[50,60] → cut_end(35) → [10,20]+[30,35] */
|
||||
static void test_cut_end(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 30, 40);
|
||||
psync_interval_tree_add(&tree, 50, 60);
|
||||
psync_interval_tree_cut_end(&tree, 35);
|
||||
if (count_intervals(tree) != 2)
|
||||
FAIL("cut_end: count", "expected 2 got %d", count_intervals(tree));
|
||||
else if (!has_interval(tree, 10, 20) || !has_interval(tree, 30, 35))
|
||||
FAIL("cut_end: [10,20] and [30,35]",
|
||||
"[10,20]=%d [30,35]=%d",
|
||||
has_interval(tree, 10, 20), has_interval(tree, 30, 35));
|
||||
else
|
||||
PASS("cut_end(35) leaves [10,20]+[30,35]");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* cut_end at 0: all intervals removed */
|
||||
static void test_cut_end_all(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 30, 40);
|
||||
psync_interval_tree_cut_end(&tree, 0);
|
||||
if (count_intervals(tree) != 0)
|
||||
FAIL("cut_end(0): all removed", "expected 0 got %d", count_intervals(tree));
|
||||
else
|
||||
PASS("cut_end(0) removes all intervals");
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* first_interval_containing_or_after: find first interval containing a point */
|
||||
static void test_first_containing_or_after(void) {
|
||||
psync_interval_tree_t *tree = NULL;
|
||||
psync_interval_tree_add(&tree, 10, 20);
|
||||
psync_interval_tree_add(&tree, 30, 40);
|
||||
psync_interval_tree_add(&tree, 50, 60);
|
||||
|
||||
/* point within first interval */
|
||||
psync_interval_tree_t *r = psync_interval_tree_first_interval_containing_or_after(tree, 15);
|
||||
if (!r || r->from != 10 || r->to != 20)
|
||||
FAIL("containing_or_after: point inside", "from=%llu to=%llu",
|
||||
r ? (unsigned long long)r->from : 0, r ? (unsigned long long)r->to : 0);
|
||||
else
|
||||
PASS("first_containing_or_after: point inside interval");
|
||||
|
||||
/* point between intervals → returns next interval */
|
||||
r = psync_interval_tree_first_interval_containing_or_after(tree, 25);
|
||||
if (!r || r->from != 30 || r->to != 40)
|
||||
FAIL("containing_or_after: gap → next", "from=%llu to=%llu",
|
||||
r ? (unsigned long long)r->from : 0, r ? (unsigned long long)r->to : 0);
|
||||
else
|
||||
PASS("first_containing_or_after: gap returns next interval");
|
||||
|
||||
/* point past all intervals → NULL */
|
||||
r = psync_interval_tree_first_interval_containing_or_after(tree, 70);
|
||||
if (r != NULL)
|
||||
FAIL("containing_or_after: past end → NULL", "got non-null");
|
||||
else
|
||||
PASS("first_containing_or_after: past end returns NULL");
|
||||
|
||||
psync_interval_tree_free(tree);
|
||||
}
|
||||
|
||||
/* free on NULL is safe */
|
||||
static void test_free_null(void) {
|
||||
psync_interval_tree_free(NULL);
|
||||
PASS("free(NULL) does not crash");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
int main(void) {
|
||||
test_add_single();
|
||||
test_add_non_overlapping();
|
||||
test_add_overlapping_merge();
|
||||
test_add_adjacent_merge();
|
||||
test_add_contained();
|
||||
test_add_spanning();
|
||||
test_add_chain_merge();
|
||||
test_remove_middle_split();
|
||||
test_remove_exact();
|
||||
test_remove_left_overlap();
|
||||
test_remove_right_overlap();
|
||||
test_remove_spanning();
|
||||
test_cut_end();
|
||||
test_cut_end_all();
|
||||
test_first_containing_or_after();
|
||||
test_free_null();
|
||||
|
||||
printf("\n%d passed, %d failed\n", passes, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Test: ptree.c — AVL balanced BST
|
||||
*
|
||||
* Tests insert, lookup, delete, and in-order traversal using a simple
|
||||
* integer-keyed node type. ptree itself manages no memory; nodes are
|
||||
* stack-allocated here so no cleanup is needed.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ptree.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Node type */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
psync_tree tree;
|
||||
int key;
|
||||
} inode_t;
|
||||
|
||||
static int icmp(const psync_tree *a, const psync_tree *b) {
|
||||
int ka = ptree_element(a, inode_t, tree)->key;
|
||||
int kb = ptree_element(b, inode_t, tree)->key;
|
||||
return (ka > kb) - (ka < kb);
|
||||
}
|
||||
|
||||
static void inode_init(inode_t *n, int key) {
|
||||
memset(&n->tree, 0, sizeof(n->tree));
|
||||
n->key = key;
|
||||
}
|
||||
|
||||
/* BST lookup by key */
|
||||
static inode_t *find_key(psync_tree *root, int key) {
|
||||
while (root) {
|
||||
inode_t *n = ptree_element(root, inode_t, tree);
|
||||
if (key < n->key) root = root->left;
|
||||
else if (key > n->key) root = root->right;
|
||||
else return n;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test harness */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int passes = 0, failures = 0;
|
||||
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while (0)
|
||||
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while (0)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Tests */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Single node: first == last == the node, next/prev return NULL */
|
||||
static void test_single_node(void) {
|
||||
inode_t n;
|
||||
psync_tree *root = NULL;
|
||||
inode_init(&n, 42);
|
||||
ptree_add(&root, &n.tree, icmp);
|
||||
if (root == NULL)
|
||||
{ FAIL("single insert: root non-null", "root is NULL"); return; }
|
||||
psync_tree *f = ptree_get_first(root);
|
||||
psync_tree *l = ptree_get_last(root);
|
||||
if (f != &n.tree || l != &n.tree)
|
||||
FAIL("single: first == last == node", "first=%p last=%p node=%p",
|
||||
(void*)f, (void*)l, (void*)&n.tree);
|
||||
else if (ptree_get_next(f) != NULL)
|
||||
FAIL("single: next of only node is NULL", "got non-null");
|
||||
else if (ptree_get_prev(l) != NULL)
|
||||
FAIL("single: prev of only node is NULL", "got non-null");
|
||||
else
|
||||
PASS("single node insert/first/last/next/prev");
|
||||
}
|
||||
|
||||
/* In-order traversal gives keys in ascending order for any insert order */
|
||||
static void test_traversal_sorted(void) {
|
||||
int keys[] = { 5, 3, 8, 1, 4, 7, 9, 2 };
|
||||
int n = (int)(sizeof(keys) / sizeof(keys[0]));
|
||||
inode_t nodes[8];
|
||||
psync_tree *root = NULL;
|
||||
int i;
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_init(&nodes[i], keys[i]);
|
||||
ptree_add(&root, &nodes[i].tree, icmp);
|
||||
}
|
||||
/* collect traversal */
|
||||
psync_tree *tr = ptree_get_first(root);
|
||||
int prev_key = -1, count = 0, ok = 1;
|
||||
while (tr) {
|
||||
int k = ptree_element(tr, inode_t, tree)->key;
|
||||
if (k <= prev_key) { ok = 0; break; }
|
||||
prev_key = k;
|
||||
count++;
|
||||
tr = ptree_get_next(tr);
|
||||
}
|
||||
if (!ok)
|
||||
FAIL("traversal sorted", "out-of-order key %d after %d", prev_key, prev_key);
|
||||
else if (count != n)
|
||||
FAIL("traversal sorted: count", "expected %d, got %d", n, count);
|
||||
else
|
||||
PASS("traversal in sorted order after arbitrary inserts");
|
||||
}
|
||||
|
||||
/* Reverse-order insert stresses AVL rebalancing */
|
||||
static void test_reverse_insert_traversal(void) {
|
||||
int n = 7;
|
||||
inode_t nodes[7];
|
||||
psync_tree *root = NULL;
|
||||
int i;
|
||||
for (i = n; i >= 1; i--) {
|
||||
inode_init(&nodes[i-1], i);
|
||||
ptree_add(&root, &nodes[i-1].tree, icmp);
|
||||
}
|
||||
psync_tree *tr = ptree_get_first(root);
|
||||
int prev = 0, count = 0, ok = 1;
|
||||
while (tr) {
|
||||
int k = ptree_element(tr, inode_t, tree)->key;
|
||||
if (k != prev + 1) { ok = 0; break; }
|
||||
prev = k;
|
||||
count++;
|
||||
tr = ptree_get_next(tr);
|
||||
}
|
||||
if (!ok || count != n)
|
||||
FAIL("reverse insert traversal", "ok=%d count=%d expected %d", ok, count, n);
|
||||
else
|
||||
PASS("reverse insert: AVL rebalanced, traversal still sorted");
|
||||
}
|
||||
|
||||
/* Lookup by key finds the right node (or NULL for missing) */
|
||||
static void test_lookup(void) {
|
||||
int keys[] = { 10, 5, 15, 3, 7 };
|
||||
int n = (int)(sizeof(keys) / sizeof(keys[0]));
|
||||
inode_t nodes[5];
|
||||
psync_tree *root = NULL;
|
||||
int i;
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_init(&nodes[i], keys[i]);
|
||||
ptree_add(&root, &nodes[i].tree, icmp);
|
||||
}
|
||||
int ok = 1;
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_t *found = find_key(root, keys[i]);
|
||||
if (!found || found->key != keys[i]) { ok = 0; break; }
|
||||
}
|
||||
if (!ok)
|
||||
FAIL("lookup: existing keys", "key not found");
|
||||
else if (find_key(root, 99) != NULL)
|
||||
FAIL("lookup: absent key returns NULL", "got non-null for key 99");
|
||||
else
|
||||
PASS("lookup: finds all inserted keys, NULL for missing");
|
||||
}
|
||||
|
||||
/* Delete a leaf node */
|
||||
static void test_delete_leaf(void) {
|
||||
int keys[] = { 5, 3, 7 };
|
||||
inode_t nodes[3];
|
||||
psync_tree *root = NULL;
|
||||
int i;
|
||||
for (i = 0; i < 3; i++) {
|
||||
inode_init(&nodes[i], keys[i]);
|
||||
ptree_add(&root, &nodes[i].tree, icmp);
|
||||
}
|
||||
/* delete leaf (key=3) */
|
||||
ptree_del(&root, &nodes[1].tree);
|
||||
if (find_key(root, 3) != NULL)
|
||||
FAIL("delete leaf: key gone", "key 3 still found");
|
||||
else if (find_key(root, 5) == NULL || find_key(root, 7) == NULL)
|
||||
FAIL("delete leaf: others remain", "5 or 7 missing");
|
||||
else {
|
||||
/* traversal gives 5, 7 */
|
||||
psync_tree *tr = ptree_get_first(root);
|
||||
int a = ptree_element(tr, inode_t, tree)->key;
|
||||
tr = ptree_get_next(tr);
|
||||
int b = ptree_element(tr, inode_t, tree)->key;
|
||||
tr = ptree_get_next(tr);
|
||||
if (a == 5 && b == 7 && tr == NULL)
|
||||
PASS("delete leaf: correct traversal afterward");
|
||||
else
|
||||
FAIL("delete leaf: traversal wrong", "a=%d b=%d next=%p", a, b, (void*)tr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete root when root has two children */
|
||||
static void test_delete_root(void) {
|
||||
inode_t nodes[5];
|
||||
psync_tree *root = NULL;
|
||||
int keys[] = { 10, 5, 15, 3, 8 };
|
||||
int n = 5, i;
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_init(&nodes[i], keys[i]);
|
||||
ptree_add(&root, &nodes[i].tree, icmp);
|
||||
}
|
||||
inode_t *old_root = ptree_element(root, inode_t, tree);
|
||||
ptree_del(&root, &old_root->tree);
|
||||
/* old root key must be absent */
|
||||
if (find_key(root, old_root->key) != NULL)
|
||||
{ FAIL("delete root: root key gone", "still present"); return; }
|
||||
/* remaining keys must all be present */
|
||||
int missing = 0;
|
||||
for (i = 0; i < n; i++)
|
||||
if (keys[i] != old_root->key && find_key(root, keys[i]) == NULL)
|
||||
{ missing = keys[i]; break; }
|
||||
if (missing)
|
||||
FAIL("delete root: remaining keys", "key %d missing", missing);
|
||||
else {
|
||||
/* traversal still sorted */
|
||||
psync_tree *tr = ptree_get_first(root);
|
||||
int prev = -1, cnt = 0, ok = 1;
|
||||
while (tr) {
|
||||
int k = ptree_element(tr, inode_t, tree)->key;
|
||||
if (k <= prev) { ok = 0; break; }
|
||||
prev = k; cnt++;
|
||||
tr = ptree_get_next(tr);
|
||||
}
|
||||
if (!ok || cnt != n - 1)
|
||||
FAIL("delete root: traversal", "ok=%d cnt=%d expected %d", ok, cnt, n-1);
|
||||
else
|
||||
PASS("delete root: correct traversal after root deletion");
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete all nodes one by one; tree must be empty at the end */
|
||||
static void test_delete_all(void) {
|
||||
int keys[] = { 4, 2, 6, 1, 3, 5, 7 };
|
||||
int n = (int)(sizeof(keys) / sizeof(keys[0]));
|
||||
inode_t nodes[7];
|
||||
psync_tree *root = NULL;
|
||||
int i;
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_init(&nodes[i], keys[i]);
|
||||
ptree_add(&root, &nodes[i].tree, icmp);
|
||||
}
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_t *nd = find_key(root, keys[i]);
|
||||
if (!nd) { FAIL("delete all: find before delete", "key %d missing", keys[i]); return; }
|
||||
ptree_del(&root, &nd->tree);
|
||||
}
|
||||
if (root != NULL)
|
||||
FAIL("delete all: root is NULL after all deletes", "root=%p", (void*)root);
|
||||
else if (ptree_get_first(root) != NULL)
|
||||
FAIL("delete all: first is NULL", "non-null");
|
||||
else
|
||||
PASS("delete all: tree empty after deleting all nodes");
|
||||
}
|
||||
|
||||
/* ptree_for_each visits every node exactly once */
|
||||
static void test_for_each_macro(void) {
|
||||
int keys[] = { 9, 2, 5, 1, 8, 4 };
|
||||
int n = (int)(sizeof(keys) / sizeof(keys[0]));
|
||||
inode_t nodes[6];
|
||||
psync_tree *root = NULL;
|
||||
int i;
|
||||
for (i = 0; i < n; i++) {
|
||||
inode_init(&nodes[i], keys[i]);
|
||||
ptree_add(&root, &nodes[i].tree, icmp);
|
||||
}
|
||||
int seen[6] = {0};
|
||||
psync_tree *tr;
|
||||
ptree_for_each(tr, root) {
|
||||
inode_t *nd = ptree_element(tr, inode_t, tree);
|
||||
for (i = 0; i < n; i++)
|
||||
if (keys[i] == nd->key) { seen[i]++; break; }
|
||||
}
|
||||
int ok = 1;
|
||||
for (i = 0; i < n; i++)
|
||||
if (seen[i] != 1) { ok = 0; break; }
|
||||
if (ok)
|
||||
PASS("ptree_for_each visits every node exactly once");
|
||||
else
|
||||
FAIL("ptree_for_each", "some node visited wrong number of times");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
int main(void) {
|
||||
test_single_node();
|
||||
test_traversal_sorted();
|
||||
test_reverse_insert_traversal();
|
||||
test_lookup();
|
||||
test_delete_leaf();
|
||||
test_delete_root();
|
||||
test_delete_all();
|
||||
test_for_each_macro();
|
||||
|
||||
printf("\n%d passed, %d failed\n", passes, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Loading…
Reference in New Issue