diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 600be11..a86caf1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,6 +48,11 @@ jobs: run: shell: bash + env: + CONNECTION_STRING: ${{ secrets.CONNECTION_STRING }} + APIKEY: ${{ secrets.APIKEY }} + WEBLITE: ${{ secrets.WEBLITE }} + steps: - uses: actions/checkout@v4.2.2 @@ -62,11 +67,11 @@ jobs: - name: windows build curl if: matrix.os == 'windows-latest' - run: make curl/windows/libcurl.a -j4 + run: make curl/windows/libcurl.a shell: msys2 {0} - name: build sqlite-sync - run: make extension ${{ matrix.make && matrix.make || ''}} -j4 + run: make extension ${{ matrix.make && matrix.make || ''}} - name: windows install sqlite3 if: matrix.os == 'windows-latest' @@ -96,10 +101,13 @@ jobs: echo "::endgroup::" echo "::group::prepare the test script" - make test PLATFORM=$PLATFORM ARCH=$ARCH -j4 || echo "It should fail. Running remaining commands in the emulator" + make test PLATFORM=$PLATFORM ARCH=$ARCH || echo "It should fail. Running remaining commands in the emulator" cat > commands.sh << EOF mv -f /data/local/tmp/sqlite3 /system/xbin cd /data/local/tmp + export CONNECTION_STRING="$CONNECTION_STRING" + export APIKEY="$APIKEY" + export WEBLITE="$WEBLITE" $(make test PLATFORM=$PLATFORM ARCH=$ARCH -n) EOF echo "::endgroup::" @@ -118,7 +126,7 @@ jobs: - name: test sqlite-sync if: matrix.name == 'linux' || matrix.name == 'windows' - run: make test -j4 + run: make test - name: test sqlite-sync + coverage if: matrix.name == 'macos' diff --git a/.gitignore b/.gitignore index 7c74d94..4b4f0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ *.a unittest /curl/src +.vscode \ No newline at end of file diff --git a/Makefile b/Makefile index ff433eb..0958776 100644 --- a/Makefile +++ b/Makefile @@ -11,16 +11,22 @@ CURL_VERSION ?= 8.12.1 # Set default platform if not specified ifeq ($(OS),Windows_NT) PLATFORM := windows - HOST:= windows + HOST := windows + CPUS := $(shell powershell -Command "[Environment]::ProcessorCount") else HOST = $(shell uname -s | tr '[:upper:]' '[:lower:]') ifeq ($(HOST),darwin) PLATFORM := macos + CPUS := $(shell sysctl -n hw.ncpu) else PLATFORM := $(HOST) + CPUS := $(shell nproc) endif endif +# Speed up builds by using all available CPU cores +MAKEFLAGS += -j$(CPUS) + # Compiler and flags CC = gcc CFLAGS = -Wall -Wextra -Wno-unused-parameter -I$(SRC_DIR) -I$(SQLITE_DIR) -I$(CURL_DIR)/include @@ -42,18 +48,14 @@ CURL_SRC = $(CURL_DIR)/src/curl-$(CURL_VERSION) COV_DIR = coverage CUSTOM_CSS = $(TEST_DIR)/sqliteai.css -# Files and objects -ifeq ($(PLATFORM),windows) - TEST_TARGET := $(DIST_DIR)/test.exe -else - TEST_TARGET := $(DIST_DIR)/test -endif SRC_FILES = $(wildcard $(SRC_DIR)/*.c) -TEST_FILES = $(SRC_FILES) $(wildcard $(TEST_DIR)/*.c) $(wildcard $(SQLITE_DIR)/*.c) +TEST_SRC = $(wildcard $(TEST_DIR)/*.c) +TEST_FILES = $(SRC_FILES) $(TEST_SRC) $(wildcard $(SQLITE_DIR)/*.c) RELEASE_OBJ = $(patsubst %.c, $(BUILD_RELEASE)/%.o, $(notdir $(SRC_FILES))) TEST_OBJ = $(patsubst %.c, $(BUILD_TEST)/%.o, $(notdir $(TEST_FILES))) COV_FILES = $(filter-out $(SRC_DIR)/lz4.c $(SRC_DIR)/network.c, $(SRC_FILES)) CURL_LIB = $(CURL_DIR)/$(PLATFORM)/libcurl.a +TEST_TARGET = $(patsubst %.c,$(DIST_DIR)/%$(EXE), $(notdir $(TEST_SRC))) # Platform-specific settings ifeq ($(PLATFORM),windows) @@ -64,6 +66,7 @@ ifeq ($(PLATFORM),windows) DEF_FILE := $(BUILD_RELEASE)/cloudsync.def CFLAGS += -DCURL_STATICLIB CURL_CONFIG = --with-schannel CFLAGS="-DCURL_STATICLIB" + EXE = .exe else ifeq ($(PLATFORM),macos) TARGET := $(DIST_DIR)/cloudsync.dylib LDFLAGS += -arch x86_64 -arch arm64 -framework Security -dynamiclib -undefined dynamic_lookup @@ -90,7 +93,7 @@ else ifeq ($(PLATFORM),android) OPENSSL := $(BIN)/../sysroot/usr/include/openssl CC = $(BIN)/$(ARCH)-linux-android26-clang - CURL_CONFIG = --host $(ARCH)-$(HOST)-android26 --with-openssl=$(BIN)/../sysroot/usr LIBS="-lssl -lcrypto" AR=$(BIN)/llvm-ar AS=$(BIN)/llvm-as CC=$(BIN)/$(ARCH)-linux-android26-clang CXX=$(BIN)/$(ARCH)-linux-android26-clang++ LD=$(BIN)/ld RANLIB=$(BIN)/llvm-ranlib STRIP=$(BIN)/llvm-strip + CURL_CONFIG = --host $(ARCH)-$(HOST)-android26 --with-openssl=$(BIN)/../sysroot/usr LIBS="-lssl -lcrypto" AR=$(BIN)/llvm-ar AS=$(BIN)/llvm-as CC=$(CC) CXX=$(BIN)/$(ARCH)-linux-android26-clang++ LD=$(BIN)/ld RANLIB=$(BIN)/llvm-ranlib STRIP=$(BIN)/llvm-strip TARGET := $(DIST_DIR)/cloudsync.so LDFLAGS += -shared -lcrypto -lssl else ifeq ($(PLATFORM),ios) @@ -146,20 +149,20 @@ endif # Test executable $(TEST_TARGET): $(TEST_OBJ) - $(CC) $(TEST_OBJ) -o $@ $(T_LDFLAGS) + $(CC) $(filter-out $(patsubst $(DIST_DIR)/%$(EXE),$(BUILD_TEST)/%.o, $(filter-out $@,$(TEST_TARGET))), $(TEST_OBJ)) -o $@ $(T_LDFLAGS) # Object files $(BUILD_RELEASE)/%.o: %.c $(CC) $(CFLAGS) -O3 -fPIC -c $< -o $@ $(BUILD_TEST)/sqlite3.o: $(SQLITE_DIR)/sqlite3.c - $(CC) $(CFLAGS) -DSQLITE_CORE=1 -c $< -o $@ + $(CC) $(CFLAGS) -DSQLITE_CORE -c $< -o $@ $(BUILD_TEST)/%.o: %.c $(CC) $(T_CFLAGS) -c $< -o $@ # Run code coverage (--css-file $(CUSTOM_CSS)) test: $(TARGET) $(TEST_TARGET) $(SQLITE3) ":memory:" -cmd ".bail on" ".load ./$<" "SELECT cloudsync_version();" - ./$(TEST_TARGET) + set -e; for t in $(TEST_TARGET); do ./$$t; done ifneq ($(COVERAGE),false) mkdir -p $(COV_DIR) lcov --capture --directory . --output-file $(COV_DIR)/coverage.info $(subst src, --include src,${COV_FILES}) @@ -265,7 +268,7 @@ endif # Clean up generated files clean: - rm -rf $(BUILD_DIRS) $(DIST_DIR)/* $(COV_DIR) *.gcda *.gcno *.gcov $(CURL_DIR)/src + rm -rf $(BUILD_DIRS) $(DIST_DIR)/* $(COV_DIR) *.gcda *.gcno *.gcov $(CURL_DIR)/src *.sqlite # Help message help: diff --git a/test/main.c b/test/main.c new file mode 100644 index 0000000..260b8c6 --- /dev/null +++ b/test/main.c @@ -0,0 +1,340 @@ +// +// main.c +// sqlite-sync +// +// Created by Gioele Cantoni on 05/06/25. +// Set CONNECTION_STRING, APIKEY and WEBLITE environment variables before running this test. +// + +#include +#include +#include +#include "sqlite3.h" +#ifdef _WIN32 +#include +#else +#include +#endif + +#define PEERS 5 +#define DB_PATH "health-track.sqlite" +#define EXT_PATH "./dist/cloudsync" +#define RCHECK if (rc != SQLITE_OK) goto abort_test; +#define ERROR_MSG if (rc != SQLITE_OK) printf("Error: %s\n", sqlite3_errmsg(db)); +#define ABORT_TEST abort_test: ERROR_MSG if (db) sqlite3_close(db); return rc; + +typedef enum { PRINT, NOPRINT, INTGR, GT0 } expected_type; + +typedef struct { + expected_type type; + union { + int i; + const char *s; // for future use, if needed + } value; +} expected_t; + +static int callback(void *data, int argc, char **argv, char **names) { + expected_t *expect = (expected_t *)data; + + switch(expect->type) { + case NOPRINT: break; + case PRINT: + for (int i = 0; i < argc; i++) { + printf("%s: %s ", names[i], argv[i] ? argv[i] : "NULL"); + } + printf("\n"); + return SQLITE_OK; + + case INTGR: + if(argc == 1){ + int res = atoi(argv[0]); + + if(res != expect->value.i){ + printf("Error: expected from %s: %d, got %d\n", names[0], expect->value.i, res); + return SQLITE_ERROR; + } + + } else goto multiple_columns; + break; + + case GT0: + if(argc == 1){ + int res = atoi(argv[0]); + + if(!(res > 0)){ + printf("Error: expected from %s: to be greater than 0, got %d\n", names[0], res); + return SQLITE_ERROR; + } + + } else goto multiple_columns; + break; + + default: + printf("Error: unknown expect type\n"); + return SQLITE_ERROR; + } + + return SQLITE_OK; + +multiple_columns: + printf("Error: expected 1 column, got %d\n", argc); + return SQLITE_ERROR; +} + +int db_exec (sqlite3 *db, const char *sql) { + expected_t data; + data.type = NOPRINT; + + int rc = sqlite3_exec(db, sql, callback, &data, NULL); + if (rc != SQLITE_OK) printf("Error while executing %s: %s\n", sql, sqlite3_errmsg(db)); + return rc; +} + +int db_print (sqlite3 *db, const char *sql) { + expected_t data; + data.type = PRINT; + + int rc = sqlite3_exec(db, sql, callback, &data, NULL); + if (rc != SQLITE_OK) printf("Error while executing %s: %s\n", sql, sqlite3_errmsg(db)); + return rc; +} + +int db_expect_int (sqlite3 *db, const char *sql, int expect) { + expected_t data; + data.type = INTGR; + data.value.i = expect; + + int rc = sqlite3_exec(db, sql, callback, &data, NULL); + if (rc != SQLITE_OK) printf("Error while executing %s: %s\n", sql, sqlite3_errmsg(db)); + return rc; +} + +int db_expect_gt0 (sqlite3 *db, const char *sql) { + expected_t data; + data.type = GT0; + + int rc = sqlite3_exec(db, sql, callback, &data, NULL); + if (rc != SQLITE_OK) printf("Error while executing %s: %s\n", sql, sqlite3_errmsg(db)); + return rc; +} + +int open_load_ext(const char *db_path, sqlite3 **out_db) { + sqlite3 *db = NULL; + int rc = sqlite3_open(db_path, &db); + RCHECK + + // enable load extension + rc = sqlite3_enable_load_extension(db, 1); + RCHECK + + rc = db_exec(db, "SELECT load_extension('"EXT_PATH"');"); + RCHECK + + *out_db = db; + return rc; + +ABORT_TEST +} + +// MARK: - + +int db_init (sqlite3 *db){ + + int rc = db_exec(db, "\ + CREATE TABLE IF NOT EXISTS users (\ + id TEXT PRIMARY KEY NOT NULL,\ + name TEXT UNIQUE NOT NULL DEFAULT ''\ + );\ + CREATE TABLE IF NOT EXISTS activities (\ + id TEXT PRIMARY KEY NOT NULL,\ + user_id TEXT,\ + km REAL,\ + bpm INTEGER,\ + time TEXT,\ + activity_type TEXT NOT NULL DEFAULT 'running',\ + FOREIGN KEY(user_id) REFERENCES users(id)\ + );\ + CREATE TABLE IF NOT EXISTS workouts (\ + id TEXT PRIMARY KEY NOT NULL,\ + assigned_user_id TEXT,\ + day_of_week TEXT,\ + km REAL,\ + max_time TEXT\ + );\ + "); + +ERROR_MSG + return rc; + +} + +int test_init (const char *db_path, int init) { + int rc = SQLITE_OK; + + sqlite3 *db = NULL; + rc = open_load_ext(db_path, &db); RCHECK + + if(init){ + rc = db_init(db); + RCHECK + } + + rc = db_exec(db, "SELECT cloudsync_init('users');"); RCHECK + rc = db_exec(db, "SELECT cloudsync_init('activities');"); RCHECK + rc = db_exec(db, "SELECT cloudsync_init('workouts');"); RCHECK + + // init network with connection string + apikey + char network_init[512]; + snprintf(network_init, sizeof(network_init), "SELECT cloudsync_network_init('%s?apikey=%s');", getenv("CONNECTION_STRING"), getenv("APIKEY")); + rc = db_exec(db, network_init); RCHECK + + rc = db_expect_int(db, "SELECT COUNT(*) as count FROM activities;", 0); RCHECK + rc = db_expect_int(db, "SELECT COUNT(*) as count FROM workouts;", 0); RCHECK + char value[UUID_STR_MAXLEN]; + cloudsync_uuid_v7_string(value, true); + char sql[256]; + snprintf(sql, sizeof(sql), "INSERT INTO users (id, name) VALUES ('%s', '%s');", value, value); + rc = db_exec(db, sql); RCHECK + rc = db_expect_int(db, "SELECT COUNT(*) as count FROM users;", 1); RCHECK + rc = db_expect_gt0(db, "SELECT cloudsync_network_sync();"); RCHECK + rc = db_expect_gt0(db, "SELECT COUNT(*) as count FROM users;"); RCHECK + rc = db_expect_gt0(db, "SELECT COUNT(*) as count FROM activities;"); RCHECK + rc = db_expect_int(db, "SELECT COUNT(*) as count FROM workouts;", 0); RCHECK + rc = db_exec(db, "SELECT cloudsync_terminate();"); + +ABORT_TEST +} + +int test_is_enabled(const char *db_path) { + sqlite3 *db = NULL; + int rc = open_load_ext(db_path, &db); + + rc = db_expect_int(db, "SELECT cloudsync_is_enabled('users');", 1); RCHECK + rc = db_expect_int(db, "SELECT cloudsync_is_enabled('activities');", 1); RCHECK + rc = db_expect_int(db, "SELECT cloudsync_is_enabled('workouts');", 1); + +ABORT_TEST +} + +int test_db_version(const char *db_path) { + sqlite3 *db = NULL; + int rc = open_load_ext(db_path, &db); + + rc = db_expect_gt0(db, "SELECT cloudsync_db_version();"); RCHECK + rc = db_expect_gt0(db, "SELECT cloudsync_db_version_next();"); + +ABORT_TEST +} + +int test_enable_disable(const char *db_path) { + sqlite3 *db = NULL; + int rc = open_load_ext(db_path, &db); + + rc = db_exec(db, "SELECT cloudsync_init('*');"); RCHECK + rc = db_exec(db, "SELECT cloudsync_disable('users');"); RCHECK + rc = db_exec(db, "INSERT INTO users (id, name) VALUES ('12afb', 'provaCmeaakbefa');"); RCHECK + rc = db_exec(db, "SELECT cloudsync_enable('users');"); RCHECK + + // init network with connection string + apikey + char network_init[512]; + snprintf(network_init, sizeof(network_init), "SELECT cloudsync_network_init('%s?apikey=%s');", getenv("CONNECTION_STRING"), getenv("APIKEY")); + rc = db_exec(db, network_init); RCHECK + + rc = db_exec(db, "SELECT cloudsync_network_sync();"); RCHECK + rc = db_exec(db, "SELECT cloudsync_cleanup('*');"); + +ABORT_TEST +} + +int version(){ + sqlite3 *db = NULL; + int rc = open_load_ext(":memory:", &db); + + rc = db_print(db, "SELECT cloudsync_version();"); + RCHECK + +ABORT_TEST +} + +// MARK: - + +int test_report(const char *description, int rc){ + printf("%-24s %s\n", description, rc ? "FAILED" : "OK"); + return rc; +} + +#ifdef _WIN32 +DWORD WINAPI worker(LPVOID arg) { +#else +void* worker(void* arg) { +#endif + int thread_id = *(int*)arg; + + char description[32]; + snprintf(description, sizeof(description), "%d/%d Peer Test", thread_id+1, PEERS); + if(test_report(description, test_init(":memory:", 1))){ + printf("PEER %d FAIL.\n", thread_id+1); + exit(thread_id+1); + } + + return NULL; +} + +int main (void) { + int rc = SQLITE_OK; + + printf("\n\nIntegration Test "); + rc += version(); + printf("===========================================\n"); + test_report("Version Test:", rc); + + sqlite3 *db = NULL; + rc += open_load_ext(DB_PATH, &db); + rc += db_init(db); + if (db) sqlite3_close(db); + + rc += test_report("Init+Sync Test:", test_init(DB_PATH, 0)); + rc += test_report("Is Enabled Test:", test_is_enabled(DB_PATH)); + rc += test_report("DB Version Test:", test_db_version(DB_PATH)); + rc += test_report("Enable Disable Test:", test_enable_disable(DB_PATH)); + + remove(DB_PATH); // remove the database file + + #ifdef _WIN32 + HANDLE threads[PEERS]; + #else + pthread_t threads[PEERS]; + #endif + int thread_ids[PEERS]; + + for (int i = 0; i < PEERS; i++) { + thread_ids[i] = i; + #ifdef _WIN32 + threads[i] = CreateThread(NULL, 0, worker, &thread_ids[i], 0, NULL); + if (threads[i] == NULL) { + fprintf(stderr, "CreateThread failed\n"); + return 1; + } + #else + if (pthread_create(&threads[i], NULL, worker, &thread_ids[i]) != 0) { + perror("pthread_create"); + exit(1); + } + #endif + } + + // Wait for all threads to finish + #ifdef _WIN32 + WaitForMultipleObjects(PEERS, threads, TRUE, INFINITE); + #endif + for (int i = 0; i < PEERS; i++) { + #ifdef _WIN32 + CloseHandle(threads[i]); + #else + pthread_join(threads[i], NULL); + #endif + } + + printf("\n"); + return rc; +}