301 lines
12 KiB
C
301 lines
12 KiB
C
/*
|
|
* 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;
|
|
}
|