+/* run some tests against dbindex */
+
+#define _GNU_SOURCE
+
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include <libavformat/avformat.h>
+
+#include <regex.h>
+
+#define obstack_chunk_alloc malloc
+#define obstack_chunk_free free
+#include <obstack.h>
+
+#include "dbindex.c"
+
+void dbindex_dump(dbindex *db);
+void dump_lists(dbindex *db, dbtxn *txn);
+int dbindex_validate_lists(dbindex *db);
+
+static dbindex *db;
+static dbtxn *tx;
+static char dbpath[] = "/tmp/dbtestXXXXXX";
+static const char *this_test = NULL;
+
+#define check_ret(test) do { if (!(test)) { fprintf(stderr, "fail: %s: " #test "\n", this_test); return 0; } } while(0)
+#define check(test) (test) ? (1) : (fprintf(stderr, "fail: %s: " #test "\n", this_test), 0)
+
+static int check_disk_equals(const dbdisk *a, const dbdisk *b) {
+ check_ret(a != NULL && b != NULL);
+ check_ret(a->id == b->id);
+ check_ret(a->uuid != NULL && b->uuid != NULL);
+ check_ret(strcmp(a->uuid, b->uuid) == 0);
+ check_ret(a->mount != NULL && b->mount != NULL);
+ check_ret(strcmp(a->mount, b->mount) == 0);
+ return 1;
+}
+
+static int check_file_equals(const dbfile *a, const dbfile *b) {
+ check_ret(a != NULL && b != NULL);
+ check_ret(a->id == b->id);
+ check_ret(a->diskid == b->diskid);
+ check_ret(a->size == b->size);
+ check_ret(a->mtime == b->mtime);
+ check_ret(a->duration == b->duration);
+ check_ret(a->path != NULL && b->path != NULL);
+ check_ret(strcmp(a->path, b->path) == 0);
+
+ check_ret((a->title == NULL && b->title == NULL)
+ || (a->title != NULL && b->title != NULL && strcmp(a->title, b->title) == 0));
+ check_ret((a->artist == NULL && b->artist == NULL)
+ || (a->artist != NULL && b->artist != NULL && strcmp(a->artist, b->artist) == 0));
+ return 1;
+}
+
+static int check_list_equals(const dblist *a, const dblist *b) {
+ check_ret(a != NULL && b != NULL);
+ check_ret(a->id == b->id);
+ check_ret(a->name != NULL && b->name != NULL);
+ check_ret(strcmp(a->name, b->name) == 0);
+
+ check_ret((a->desc != NULL && b->desc != NULL) || (a->desc == NULL && b->desc == NULL));
+ if (a->desc)
+ check_ret(strcmp(a->desc, b->desc) == 0);
+ return 1;
+}
+
+static int lookup_string_secondary(MDB_txn *tx, MDB_dbi dbi, char *keyval, dbid_t datval) {
+ MDB_val key = { .mv_data = keyval, .mv_size = strlen(keyval) };
+ MDB_val dat = { .mv_data = &datval, .mv_size = sizeof(datval) };
+ int res;
+ MDB_cursor *cursor;
+
+ res = mdb_cursor_open(tx, dbi, &cursor);
+ res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_BOTH);
+ mdb_cursor_close(cursor);
+
+ return res;
+}
+
+static int lookup_dbid_secondary(MDB_txn *tx, MDB_dbi dbi, dbid_t keyval, dbid_t datval) {
+ MDB_val key = { .mv_data = &keyval, .mv_size = sizeof(keyval) };
+ MDB_val dat = { .mv_data = &datval, .mv_size = sizeof(datval) };
+ int res;
+ MDB_cursor *cursor;
+
+ res = mdb_cursor_open(tx, dbi, &cursor);
+ res = mdb_cursor_get(cursor, &key, &dat, MDB_GET_BOTH);
+ mdb_cursor_close(cursor);
+
+ return res;
+}
+
+/* ********************************************************************** */
+
+static int test_add_disk(void) {
+ dbdisk disk = {
+ .flags = 0,
+ .uuid = "disk-uuid-0",
+ .label = "disk-label",
+ .mount = "/some/path"
+ };
+ int ok;
+ int res;
+
+ this_test = __FUNCTION__;
+
+ ok = (res = dbdisk_add(tx, db, &disk)) == 0;
+ if (ok) {
+ dbdisk *diska = dbdisk_get(tx, db, disk.id);
+ dbdisk *diskb = dbdisk_get_uuid(tx, db, disk.uuid);
+
+ ok &= check_disk_equals(diska, &disk);
+ ok &= check_disk_equals(diskb, &disk);
+
+ dbdisk_free(diska);
+ dbdisk_free(diskb);
+ }
+
+ return ok;
+}
+
+static int test_del_disk(void) {
+ dbdisk disk = {
+ .flags = 0,
+ .uuid = "disk-uuid-0",
+ .label = "disk-label",
+ .mount = "/some/path"
+ };
+ int ok;
+ int res;
+
+ this_test = __FUNCTION__;
+
+ ok = (res = dbdisk_add(tx, db, &disk)) == 0;
+ if (ok) {
+ res = dbdisk_del(tx, db, &disk);
+
+ check(res == 0);
+
+ dbdisk *diska = dbdisk_get(tx, db, disk.id);
+ dbdisk *diskb = dbdisk_get_uuid(tx, db, disk.uuid);
+
+ check(diska == NULL);
+ check(diskb == NULL);
+
+ dbdisk_free(diska);
+ dbdisk_free(diskb);
+ }
+
+ return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_add_file(void) {
+ dbdisk disk = {
+ .flags = 0,
+ .uuid = "disk-uuid-0",
+ .label = "disk-label",
+ .mount = "/some/path"
+ };
+
+ dbfile file = {
+ .diskid = 200,
+ .size = 10,
+ .mtime = 12,
+ .duration = 100,
+ .path = "/files/1",
+ .title = "music 1",
+ .artist = "artist 1"
+ };
+ int ok;
+
+ this_test = __FUNCTION__;
+
+ ok = check(dbdisk_add(tx, db, &disk) == 0);
+
+ // basic, no disk id
+ ok &= check(dbfile_add(tx, db, &file) == MDB_NOTFOUND);
+
+ file.diskid = disk.id;
+ ok &= check(dbfile_add(tx, db, &file) == 0);
+
+ // check keys
+ dbfile *filea = dbfile_get(tx, db, file.id);
+ dbfile *fileb = dbfile_get_path(tx, db, disk.id, file.path);
+
+ ok &= check_file_equals(&file, filea);
+ ok &= check_file_equals(&file, fileb);
+
+ dbfile_free(filea);
+ dbfile_free(fileb);
+
+ char path[strlen(file.path) + 8 + 1];
+ sprintf(path, "%08x%s", disk.id, file.path);
+
+ *strrchr(path, '/') = 0;
+
+ ok &= check(lookup_dbid_secondary(tx, db->file_by_disk, file.diskid, file.id) == 0);
+ ok &= check(lookup_string_secondary(tx, db->file_by_dir, path, file.id) == 0);
+ ok &= check(lookup_string_secondary(tx, db->file_by_title, file.title, file.id) == 0);
+ ok &= check(lookup_string_secondary(tx, db->file_by_artist, file.artist, file.id) == 0);
+
+ return ok;
+}
+
+static int test_del_file(void) {
+ dbdisk disk = {
+ .flags = 0,
+ .uuid = "disk-uuid-0",
+ .label = "disk-label",
+ .mount = "/some/path"
+ };
+
+ dbfile file = {
+ .diskid = 200,
+ .size = 10,
+ .mtime = 12,
+ .duration = 100,
+ .path = "/files/1",
+ .title = "music 1",
+ .artist = "artist 1"
+ };
+ int ok;
+
+ this_test = __FUNCTION__;
+
+ ok = check(dbdisk_add(tx, db, &disk) == 0);
+ file.diskid = disk.id;
+ ok &= check(dbfile_add(tx, db, &file) == 0);
+
+ ok &= dbfile_del(tx, db, &file) == 0;
+
+ // check keys
+ dbfile *filea = dbfile_get(tx, db, file.id);
+ dbfile *fileb = dbfile_get_path(tx, db, disk.id, file.path);
+
+ ok &= check(filea == NULL);
+ ok &= check(fileb == NULL);
+
+ dbfile_free(filea);
+ dbfile_free(fileb);
+
+ char path[strlen(file.path) + 8 + 1];
+ sprintf(path, "%08x%s", disk.id, file.path);
+
+ *strrchr(path, '/') = 0;
+
+ ok &= check(lookup_dbid_secondary(tx, db->file_by_disk, file.diskid, file.id) == MDB_NOTFOUND);
+ ok &= check(lookup_string_secondary(tx, db->file_by_dir, path, file.id) == MDB_NOTFOUND);
+ ok &= check(lookup_string_secondary(tx, db->file_by_title, file.title, file.id) == MDB_NOTFOUND);
+ ok &= check(lookup_string_secondary(tx, db->file_by_artist, file.artist, file.id) == MDB_NOTFOUND);
+
+ // TODO: playlists
+
+ return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_add_files(void) {
+ dbdisk disk0 = {
+ .flags = 0,
+ .uuid = "disk-uuid-0",
+ .label = "disk-label",
+ .mount = "/some/path"
+ };
+ dbdisk disk1 = {
+ .flags = 0,
+ .uuid = "disk-uuid-1",
+ .label = "disk-label",
+ .mount = "/other/path"
+ };
+
+ char path[256];
+ char title[256];
+ char artist[256];
+ dbfile file = {
+ .diskid = 200,
+ .size = 10,
+ .mtime = 12,
+ .duration = 100,
+ .path = path,
+ .title = title,
+ .artist = artist
+ };
+ int ok, res;
+ int ntotal = 0;
+ this_test = __FUNCTION__;
+
+ ok = check(dbdisk_add(tx, db, &disk0) == 0);
+ ok &= check(dbdisk_add(tx, db, &disk1) == 0);
+
+ file.diskid = disk0.id;
+ for (int i=0;i<100;i++) {
+ sprintf(title, "title %d", i % 10);
+ sprintf(artist, "artist %d", i / 9);
+ sprintf(path, "/dir%d/file%d", i % 7, i);
+ ok &= check((res = dbfile_add(tx, db, &file)) == 0);
+ ntotal++;
+ }
+
+ file.diskid = disk1.id;
+ for (int i=0;i<100;i++) {
+ sprintf(title, "title %d", i % 10);
+ sprintf(artist, "artist %d", i / 9);
+ sprintf(path, "/dir%d/file%d", i % 7, i);
+ ok &= check((res = dbfile_add(tx, db, &file)) == 0);
+ ntotal++;
+ }
+
+ // validate will check the secondary indices
+
+ return ok;
+}
+
+static int test_del_files(void) {
+ int ok = 1, res;
+
+ test_add_files();
+
+ // delete some of the files
+ ez_array array = EZ_INIT_ARRAY(array);
+
+ secondary_list_all(tx, db->file_by_disk, &array);
+
+ size_t count = array.ea_size / sizeof(dbid_t);
+ dbid_t *fids = array.ea_data;
+
+ for (int i=0;i<count;i+=4) {
+ ok &= (res = dbfile_del_id(tx, db, fids[i])) == 0;
+ }
+ // validate will check the secondary indices
+
+ ez_array_clear(&array);
+
+ return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_add_list1(void) {
+ dblist list = {
+ .name = "list",
+ .desc = "list desc"
+ };
+ int ok;
+ int res;
+
+ this_test = __FUNCTION__;
+
+ ok = (res = dblist_add(tx, db, &list)) == 0;
+ if (ok) {
+ dblist *lista = dblist_get(tx, db, list.id);
+ dblist *listb = dblist_get_name(tx, db, list.name);
+
+ ok &= check_list_equals(lista, &list);
+ ok &= check_list_equals(listb, &list);
+
+ dblist_free(lista);
+ dblist_free(listb);
+ }
+
+ return ok;
+}
+
+static int test_add_list2(void) {
+ dblist list = {
+ .name = "list",
+ .desc = NULL
+ };
+ int ok;
+ int res;
+
+ this_test = __FUNCTION__;
+
+ ok = (res = dblist_add(tx, db, &list)) == 0;
+ if (ok) {
+ dblist *lista = dblist_get(tx, db, list.id);
+ dblist *listb = dblist_get_name(tx, db, list.name);
+
+ ok &= check_list_equals(lista, &list);
+ ok &= check_list_equals(listb, &list);
+
+ dblist_free(lista);
+ dblist_free(listb);
+ }
+
+ return ok;
+}
+
+/* ********************************************************************** */
+
+static int test_del_list1(void) {
+ dblist list = {
+ .name = "list",
+ };
+ int ok;
+ int res;
+
+ this_test = __FUNCTION__;
+
+ ok = (res = dblist_add(tx, db, &list)) == 0;
+ if (ok) {
+ check_ret(dblist_del(tx, db, &list) == 0);
+ check_ret(dblist_del_id(tx, db, list.id) == MDB_NOTFOUND);
+ check_ret(dblist_del(tx, db, &list) == MDB_NOTFOUND);
+
+ dblist *lista = dblist_get(tx, db, list.id);
+ dblist *listb = dblist_get_name(tx, db, list.name);
+
+ check(lista == NULL);
+ check(listb == NULL);
+
+ dblist_free(lista);
+ dblist_free(listb);
+ }
+
+ return ok;
+}
+
+static int test_del_list2(void) {
+ dblist list = {
+ .name = "list",
+ };
+ int ok;
+ int res;
+
+ this_test = __FUNCTION__;
+
+ ok = (res = dblist_add(tx, db, &list)) == 0;
+ if (ok) {
+ check_ret(dblist_del_id(tx, db, list.id) == 0);
+ check_ret(dblist_del_id(tx, db, list.id) == MDB_NOTFOUND);
+ check_ret(dblist_del(tx, db, &list) == MDB_NOTFOUND);
+
+ dblist *lista = dblist_get(tx, db, list.id);
+ dblist *listb = dblist_get_name(tx, db, list.name);
+
+ check(lista == NULL);
+ check(listb == NULL);
+
+ dblist_free(lista);
+ dblist_free(listb);
+ }
+
+ return ok;
+}
+
+/* ********************************************************************** */
+
+#define MIN(a, b) ((a)<(b)?(a):(b))
+
+static int test_add_lists(void) {
+ dblist list = {
+ .name = "list",
+ .desc = "list desc"
+ };
+
+ if (!test_add_files()) {
+ this_test = __FUNCTION__;
+ return 0;
+ }
+
+ this_test = __FUNCTION__;
+
+ check_ret(dblist_add(tx, db, &list) == 0);
+
+ ez_array files = EZ_INIT_ARRAY(files);
+ secondary_list_all(tx, db->file_by_disk, &files);
+
+ size_t count = files.ea_size / sizeof(dbid_t);
+ dbid_t *fids = files.ea_data;
+
+ // check adding non-existant file
+ {
+ dblistcursor curr = { .listid = list.id, .fileid = fids[count-1] + 1 };
+
+ check_ret(dblist_add_file(tx, db, &curr) == MDB_NOTFOUND);
+ }
+
+ for (int i=0,seq=1;i<count;i+=3,seq+=1) {
+ dbid_t fid = fids[i];
+ dblist *lista;
+ dblistcursor curr = { .listid = list.id, .fileid = fid };
+
+ check_ret(dblist_add_file(tx, db, &curr) == 0);
+ check_ret(curr.seq = seq);
+
+ lista = dblist_get(tx, db, list.id);
+ check_ret(lista->size == seq);
+ check_ret(lista->seq == seq);
+ dblist_free(lista);
+ }
+
+ ez_array_clear(&files);
+
+ return 1;
+}
+
+static int test_del_lists_all(void) {
+ if (!test_add_lists())
+ return 0;
+
+ this_test = __FUNCTION__;
+
+ dblist *list = dblist_get_name(tx, db, "list");
+
+ ez_array files = EZ_INIT_ARRAY(files);
+ secondary_list_all(tx, db->file_by_disk, &files);
+
+ size_t count = files.ea_size / sizeof(dbid_t);
+ dbid_t *fids = files.ea_data;
+
+ // check deleting non-existant seq
+ {
+ dblistcursor curr = { .listid = list->id, .seq = 1000 };
+
+ check_ret(dblist_del_file(tx, db, &curr) == MDB_NOTFOUND);
+ }
+
+ for (int i=0,seq=1;i<count;i+=3,seq+=1) {
+ dbid_t fid = fids[i];
+ dblist *lista;
+ dblistcursor curr = { .listid = list->id, .seq = seq };
+
+ check_ret(dblist_del_file(tx, db, &curr) == 0);
+ check_ret(curr.fileid == fid);
+
+ lista = dblist_get(tx, db, list->id);
+ check_ret(lista->size == list->size - seq);
+ dblist_free(lista);
+ }
+
+ ez_array_clear(&files);
+
+ return 1;
+}
+
+static int test_del_lists_some(void) {
+ if (!test_add_lists())
+ return 0;
+
+ this_test = __FUNCTION__;
+
+ dblist *list = dblist_get_name(tx, db, "list");
+
+ ez_array files = EZ_INIT_ARRAY(files);
+ secondary_list_all(tx, db->file_by_disk, &files);
+
+ size_t count = files.ea_size / sizeof(dbid_t);
+ dbid_t *fids = files.ea_data;
+
+ // check deleting non-existant seq
+ {
+ dblistcursor curr = { .listid = list->id, .seq = 1000 };
+
+ check_ret(dblist_del_file(tx, db, &curr) == MDB_NOTFOUND);
+ }
+
+ for (int i=0,seq=1;i<count;i+=6,seq+=2) {
+ dbid_t fid = fids[i];
+ dblist *lista;
+ dblistcursor curr = { .listid = list->id, .seq = seq };
+
+ check_ret(dblist_del_file(tx, db, &curr) == 0);
+ check_ret(curr.fileid == fid);
+
+ lista = dblist_get(tx, db, list->id);
+ check_ret(lista->size == list->size - (seq+1)/2);
+ dblist_free(lista);
+ }
+
+ ez_array_clear(&files);
+
+ return 1;
+}
+
+/* ********************************************************************** */
+
+static int test_add_lists_dup(void) {
+ dblist list = {
+ .name = "list-dup",
+ .desc = "list with duplicates"
+ };
+
+ if (!test_add_files())
+ return 0;
+
+ this_test = __FUNCTION__;
+
+ check_ret(dblist_add(tx, db, &list) == 0);
+
+ ez_array files = EZ_INIT_ARRAY(files);
+ secondary_list_all(tx, db->file_by_disk, &files);
+
+ size_t count = files.ea_size / sizeof(dbid_t);
+ dbid_t *fids = files.ea_data;
+
+ for (int i=0,seq=1;i<count/2;i+=3,seq+=1) {
+ dbid_t fid = fids[i];
+ dblist *lista;
+ dblistcursor curr = { .listid = list.id, .fileid = fid };
+
+ check_ret(dblist_add_file(tx, db, &curr) == 0);
+ check_ret(curr.seq = seq * 2 - 1);
+
+ lista = dblist_get(tx, db, list.id);
+ check_ret(lista->size == seq * 2 - 1);
+ check_ret(lista->seq == seq * 2 - 1);
+ dblist_free(lista);
+
+ check_ret(dblist_add_file(tx, db, &curr) == 0);
+ check_ret(curr.seq = seq * 2);
+
+ lista = dblist_get(tx, db, list.id);
+ check_ret(lista->size == seq * 2);
+ check_ret(lista->seq == seq * 2);
+ dblist_free(lista);
+ }
+
+ ez_array_clear(&files);
+
+ return 1;
+}
+
+static int test_del_lists_dup(void) {
+ if (!test_add_lists_dup())
+ return 0;
+
+ this_test = __FUNCTION__;
+
+ dblist *list = dblist_get_name(tx, db, "list-dup");
+
+ ez_array files = EZ_INIT_ARRAY(files);
+ secondary_list_all(tx, db->file_by_disk, &files);
+
+ size_t count = files.ea_size / sizeof(dbid_t);
+ dbid_t *fids = files.ea_data;
+
+ for (int i=0,seq=1;i<count/2;i+=3,seq+=1) {
+ dbid_t fid = fids[i];
+ dblist *lista;
+ dblistcursor curr = { .listid = list->id, .fileid = fid };
+
+ check_ret(dblist_del_file(tx, db, &curr) == 0);
+ check_ret(curr.seq == seq * 2 - 1);
+
+ lista = dblist_get(tx, db, list->id);
+ check_ret(lista->size == list->size - seq * 2 + 1);
+ dblist_free(lista);
+
+ curr.seq = 0;
+ check_ret(dblist_del_file(tx, db, &curr) == 0);
+ check_ret(curr.seq == seq * 2);
+
+ lista = dblist_get(tx, db, list->id);
+ check_ret(lista->size == list->size - seq * 2);
+ dblist_free(lista);
+
+ }
+
+ ez_array_clear(&files);
+
+ return 1;
+}
+
+
+/* ********************************************************************** */
+
+static int test_del_list_files(void) {
+ if (!test_add_lists())
+ return 0;
+
+ this_test = __FUNCTION__;
+
+ dblist *list = dblist_get_name(tx, db, "list");
+
+ ez_array files = EZ_INIT_ARRAY(files);
+ secondary_list_all(tx, db->file_by_disk, &files);
+
+ size_t count = files.ea_size / sizeof(dbid_t);
+ dbid_t *fids = files.ea_data;
+
+ for (int i=0,seq=1;i<count/2;i+=1,seq++) {
+ dbid_t fid = fids[i];
+
+ dbfile_del_id(tx, db, fid);
+
+ dblistcursor curr = { .listid = list->id, .fileid = fid };
+ dbscan scan;
+ dbfile *file = dbscan_list_entry(tx, &scan, db, &curr);
+
+ check_ret((file == NULL && scan.res == MDB_NOTFOUND));
+
+ dbscan_free(&scan);
+ }
+
+ return 1;
+}
+
+/* ********************************************************************** */
+
+// TODO: the various scan interfaces
+
+/* ********************************************************************** */
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+static int(*tests[])(void) = {
+ test_add_disk,
+ test_del_disk,
+ test_add_file,
+ test_del_file,
+
+ test_add_files,
+ test_del_files,
+
+ test_add_list1,
+ test_add_list2,
+ test_del_list1,
+ test_del_list2,
+ test_add_lists,
+
+ test_del_lists_all,
+ test_del_lists_some,
+
+ test_add_lists_dup,
+ test_del_lists_dup,
+
+ test_del_list_files,
+ NULL
+};
+
+static void setup(void) {
+ db = dbindex_open(dbpath);
+ this_test = NULL;
+}
+
+static void cleanup(void) {
+ char tmp[strlen(dbpath) + 16];
+ dbindex_close(db);
+
+ sprintf(tmp, "%s/data.mdb", dbpath);
+ unlink(tmp);
+ sprintf(tmp, "%s/lock.mdb", dbpath);
+ unlink(tmp);
+}
+
+int main(int argc, char **argv) {
+ int pass = 0;
+ int fail = 0;
+ struct obstack log;
+
+ if (!mkdtemp(dbpath))
+ return 1;
+
+ obstack_init(&log);
+ obstack_printf(&log, "\nSUMMARY\n-------\n");
+ printf("using db: %s\n", dbpath);
+ for (int i=0;tests[i];i++) {
+ int ok;
+
+ setup();
+
+ tx = dbindex_begin(db, NULL, 0);
+
+ ok = tests[i]();
+
+ if (ok)
+ dbindex_commit(tx);
+ else
+ dbindex_abort(tx);
+
+ printf("%s: %s\n", this_test, ok ? "pass" : "fail");
+ obstack_printf(&log, "%s: %s\n", ok ? "PASS" : "FAIL", this_test);
+
+ if (ok) {
+ ok &= check(dbindex_validate(db) == 1);
+ ok &= check(dbindex_validate_lists(db) == 0);
+ }
+
+ if (ok)
+ pass++;
+ else
+ fail++;
+
+ cleanup();
+ }
+
+ rmdir(dbpath);
+
+ obstack_printf(&log, "----------------\nTOTAL: pass: %d, fail: %d\n", pass, fail);
+ printf("\n");
+ fputs(obstack_finish(&log), stdout);
+ obstack_free(&log, NULL);
+ return fail;
+}