From 691c2a8d8ab1ec580292d7149ace73e9311e432f Mon Sep 17 00:00:00 2001 From: gmega Date: Fri, 30 Jan 2026 11:13:08 -0300 Subject: [PATCH] Implement easylibstorage wrapper with tests, wire up console commands Co-Authored-By: Claude Opus 4.5 --- CMakeLists.txt | 19 +++- PROMPT.md | 4 +- easylibstorage.c | 240 ++++++++++++++++++++++++++++++++++++++++ easylibstorage.h | 19 ++-- main.c | 139 +++++++++++++++++++---- tests/mock_libstorage.c | 90 +++++++++++++++ tests/test_runner.c | 155 ++++++++++++++++++++++++++ 7 files changed, 633 insertions(+), 33 deletions(-) create mode 100644 easylibstorage.c create mode 100644 tests/mock_libstorage.c create mode 100644 tests/test_runner.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 7960985..b7e055c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.14) project(storageconsole C) set(CMAKE_C_STANDARD 11) -add_executable(storageconsole main.c +add_executable(storageconsole + main.c + easylibstorage.c easylibstorage.h ) @@ -28,3 +30,18 @@ else() message(WARNING "libstorage not found. Build or provide it before linking.") endif() +# --- Tests --- +enable_testing() + +add_executable(test_runner + tests/test_runner.c + easylibstorage.c + tests/mock_libstorage.c +) + +target_include_directories(test_runner PRIVATE + "${CMAKE_SOURCE_DIR}" + "${LOGOS_STORAGE_NIM_ROOT}/library" +) + +add_test(NAME easylibstorage_tests COMMAND test_runner) diff --git a/PROMPT.md b/PROMPT.md index 5a46c98..da1c5fb 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -7,7 +7,7 @@ The headers for `libstorage` are located at `/home/giuliano/Work/Status/logos-st The code for libstorage is located at `home/giuliano/Work/Status/logos-storage-nim/library`. -There are examples for how libstorage can be used in `/home/giuliano/Work/Status/logos-storage-nim/examples/c/examples.c`. +There are examples for how libstorage can be used in `/home/giuliano/Work/Status/logos-storage-nim/examples/c/storage.c`. # Development Process (TDD) You MUST follow a Test Driven Development approach. Since no test environment exists yet, your FIRST task is to: @@ -22,6 +22,8 @@ Then, for *each* function in `easylibstorage.h`: **CRITICAL**: This refactoring step is VERY IMPORTANT. You MUST look for ways to simplify, deduplicate, and coalesce code here, WITHOUT OVERCOMPLICATING. **SIMPLICITY IS KEY AND YOUR GUIDING PRINCIPLE.** # API Implementation Details +- **Configuration JSON.** The keys described in the node_config struct should be passed as kebab-case into the config JSON string. See an example +in the `storage.c` file. `bootstrap-node` must be passed as a string array. - **Memory Management**: Clearly document who owns returned pointers (e.g., CIDs). Ensure no memory leaks. - **Log Levels**: Map the `char *log_level` in the config to the internal `enum log_level`. - **Download Return**: Note that `e_storage_download` currently returns `STORAGE_NODE` in the header. If this is a mistake, change it to return `int` (status code) and update the header. diff --git a/easylibstorage.c b/easylibstorage.c new file mode 100644 index 0000000..8f56021 --- /dev/null +++ b/easylibstorage.c @@ -0,0 +1,240 @@ +#include "easylibstorage.h" +#include "libstorage.h" + +#include +#include +#include +#include +#include + +#define MAX_RETRIES 250 +#define POLL_INTERVAL_US (100 * 1000) +#define DEFAULT_CHUNK_SIZE (64 * 1024) + +typedef struct { + int ret; + char *msg; + size_t len; + progress_callback pcb; + int bytes_done; +} resp; + +static resp *resp_alloc(void) { + resp *r = calloc(1, sizeof(resp)); + r->ret = -1; + return r; +} + +static void resp_free(resp *r) { + if (!r) return; + free(r->msg); + free(r); +} + +static void resp_wait(resp *r) { + for (int i = 0; i < MAX_RETRIES && r->ret == -1; i++) { + usleep(POLL_INTERVAL_US); + } +} + +// Callback for simple (non-progress) async operations. +static void on_complete(int ret, const char *msg, size_t len, void *userData) { + resp *r = userData; + if (!r) return; + + free(r->msg); + r->msg = NULL; + r->len = 0; + + if (msg && len > 0) { + r->msg = malloc(len + 1); + if (r->msg) { + memcpy(r->msg, msg, len); + r->msg[len] = '\0'; + r->len = len; + } + } + + r->ret = ret; +} + +// Callback for operations that report progress before completing. +static void on_progress(int ret, const char *msg, size_t len, void *userData) { + resp *r = userData; + if (!r) return; + + if (ret == RET_PROGRESS) { + r->bytes_done += (int) len; + if (r->pcb) { + r->pcb(0, r->bytes_done, ret); + } + return; // don't set r->ret yet — still in progress + } + + // Final callback (RET_OK or RET_ERR) + free(r->msg); + r->msg = NULL; + r->len = 0; + + if (msg && len > 0) { + r->msg = malloc(len + 1); + if (r->msg) { + memcpy(r->msg, msg, len); + r->msg[len] = '\0'; + r->len = len; + } + } + + r->ret = ret; +} + +// Dispatches an async call, waits for completion, extracts the result. +// Returns RET_OK/RET_ERR. If dispatch_ret != RET_OK, returns RET_ERR immediately. +// If out is non-NULL and the response has a message, *out receives a strdup'd copy. +static int call_wait(int dispatch_ret, resp *r, char **out) { + if (dispatch_ret != RET_OK) { + resp_free(r); + return RET_ERR; + } + + resp_wait(r); + + int result = (r->ret == RET_OK) ? RET_OK : RET_ERR; + + if (out) { + *out = r->msg ? strdup(r->msg) : NULL; + } + + resp_free(r); + return result; +} + +static int nim_initialized = 0; + +STORAGE_NODE e_storage_new(node_config config) { + if (!nim_initialized) { + extern void libstorageNimMain(void); + libstorageNimMain(); + nim_initialized = 1; + } + + // Build JSON config string. + // Format: {"api-port":N,"disc-port":N,"data-dir":"...","log-level":"...","bootstrap-node":["..."]} + char json[2048]; + int pos = 0; + + pos += snprintf(json + pos, sizeof(json) - pos, "{\"api-port\":%d,\"disc-port\":%d", config.api_port, + config.disc_port); + + if (config.data_dir) { + pos += snprintf(json + pos, sizeof(json) - pos, ",\"data-dir\":\"%s\"", config.data_dir); + } + + if (config.log_level) { + pos += snprintf(json + pos, sizeof(json) - pos, ",\"log-level\":\"%s\"", config.log_level); + } + + if (config.bootstrap_node) { + pos += snprintf(json + pos, sizeof(json) - pos, ",\"bootstrap-node\":[\"%s\"]", config.bootstrap_node); + } + + snprintf(json + pos, sizeof(json) - pos, "}"); + + resp *r = resp_alloc(); + void *ctx = storage_new(json, (StorageCallback) on_complete, r); + + if (!ctx) { + resp_free(r); + return NULL; + } + + resp_wait(r); + + if (r->ret != RET_OK) { + resp_free(r); + return NULL; + } + + resp_free(r); + return ctx; +} + +int e_storage_start(STORAGE_NODE node) { + if (!node) return RET_ERR; + resp *r = resp_alloc(); + return call_wait(storage_start(node, (StorageCallback) on_complete, r), r, NULL); +} + +int e_storage_stop(STORAGE_NODE node) { + if (!node) return RET_ERR; + resp *r = resp_alloc(); + return call_wait(storage_stop(node, (StorageCallback) on_complete, r), r, NULL); +} + +int e_storage_destroy(STORAGE_NODE node) { + if (!node) return RET_ERR; + + // Close first (tolerate failure) + resp *r = resp_alloc(); + call_wait(storage_close(node, (StorageCallback) on_complete, r), r, NULL); + + // Destroy + r = resp_alloc(); + return call_wait(storage_destroy(node, (StorageCallback) on_complete, r), r, NULL); +} + +char *e_storage_upload(STORAGE_NODE node, const char *filepath, progress_callback cb) { + if (!node || !filepath) return NULL; + + // Init upload session + resp *r = resp_alloc(); + char *session_id = NULL; + int ret = call_wait(storage_upload_init(node, filepath, DEFAULT_CHUNK_SIZE, (StorageCallback) on_complete, r), r, + &session_id); + if (ret != RET_OK || !session_id) { + free(session_id); + return NULL; + } + + // Upload file with progress + r = resp_alloc(); + r->pcb = cb; + char *cid = NULL; + ret = call_wait(storage_upload_file(node, session_id, (StorageCallback) on_progress, r), r, &cid); + free(session_id); + + if (ret != RET_OK) { + free(cid); + return NULL; + } + + if (cb) { + printf("\n"); // newline after progress output + } + + return cid; +} + +int e_storage_download(STORAGE_NODE node, const char *cid, const char *filepath, progress_callback cb) { + if (!node || !cid || !filepath) return RET_ERR; + + // Init download + resp *r = resp_alloc(); + int ret = + call_wait(storage_download_init(node, cid, DEFAULT_CHUNK_SIZE, false, (StorageCallback) on_complete, r), r, + NULL); + if (ret != RET_OK) return RET_ERR; + + // Stream to file with progress + r = resp_alloc(); + r->pcb = cb; + ret = call_wait( + storage_download_stream(node, cid, DEFAULT_CHUNK_SIZE, false, filepath, (StorageCallback) on_progress, r), + r, NULL); + + if (cb) { + printf("\n"); + } + + return ret; +} diff --git a/easylibstorage.h b/easylibstorage.h index bd6ebd7..f10df98 100644 --- a/easylibstorage.h +++ b/easylibstorage.h @@ -1,16 +1,7 @@ #ifndef STORAGECONSOLE_EASYLIBSTORAGE_H #define STORAGECONSOLE_EASYLIBSTORAGE_H -#include - #define STORAGE_NODE void * -#define CID char * - -enum log_level { - WARN, - INFO, - DEBUG, -}; typedef struct { int api_port; @@ -22,11 +13,17 @@ typedef struct { typedef void (*progress_callback)(int total, int complete, int status); +// Creates a new storage node. Returns opaque pointer, or NULL on failure. STORAGE_NODE e_storage_new(node_config config); + int e_storage_start(STORAGE_NODE node); int e_storage_stop(STORAGE_NODE node); int e_storage_destroy(STORAGE_NODE node); -CID e_storage_upload(STORAGE_NODE node, FILE *input, progress_callback cb); -STORAGE_NODE e_storage_download(STORAGE_NODE node, CID cid, FILE *output, progress_callback cb); + +// Uploads a file. Returns CID string on success (caller must free), or NULL on failure. +char *e_storage_upload(STORAGE_NODE node, const char *filepath, progress_callback cb); + +// Downloads content identified by cid to filepath. Returns 0 on success. +int e_storage_download(STORAGE_NODE node, const char *cid, const char *filepath, progress_callback cb); #endif // STORAGECONSOLE_EASYLIBSTORAGE_H diff --git a/main.c b/main.c index 3ee4882..1d06326 100644 --- a/main.c +++ b/main.c @@ -1,5 +1,6 @@ #include "easylibstorage.h" +#include #include #include #include @@ -8,7 +9,7 @@ typedef struct { void *ctx; } console; -typedef void (*fn)(void *, console *); +typedef void (*fn)(char *, console *); struct command { const char *name; @@ -16,20 +17,16 @@ struct command { fn command; }; -int n_commands(); +int n_commands(void); static const struct command commands[]; -void cmd_help(void *_, console *c) { +void cmd_help(char *args, console *c) { printf("Commands:\n"); for (int i = 0; i < n_commands(); i++) { printf(" [%s]: %s\n", commands[i].name, commands[i].desc); } } -void cmd_quit(void *_, console *c) { - -} - void progress_print(int total, int complete, int status) { if (total > 0) { printf("\r %d / %d bytes", complete, total); @@ -39,32 +36,127 @@ void progress_print(int total, int complete, int status) { fflush(stdout); } -void cmd_start(void *args, console *c) { +void cmd_start(char *args, console *c) { + if (c->ctx) { + printf("Node already running. Stop it first.\n"); + return; + } + int api_port = 0, disc_port = 0; + char bootstrap[2048] = {0}; + + if (!args || sscanf(args, "%d %d %2047s", &api_port, &disc_port, bootstrap) < 2) { + printf("Usage: start [API_PORT] [DISC_PORT] [BOOTSTRAP_NODE]\n"); + return; + } + + node_config cfg = {0}; + cfg.api_port = api_port; + cfg.disc_port = disc_port; + cfg.data_dir = "./data"; + cfg.log_level = "INFO"; + cfg.bootstrap_node = bootstrap[0] ? bootstrap : NULL; + + printf("Creating node...\n"); + STORAGE_NODE node = e_storage_new(cfg); + if (!node) { + printf("Failed to create node.\n"); + return; + } + + printf("Starting node...\n"); + if (e_storage_start(node) != 0) { + printf("Failed to start node.\n"); + e_storage_destroy(node); + return; + } + + c->ctx = node; + printf("Node started on API port %d, discovery port %d.\n", api_port, disc_port); } -void cmd_stop(void *args, console *c) { +void cmd_stop(char *args, console *c) { + if (!c->ctx) { + printf("No node running.\n"); + return; + } + printf("Stopping node...\n"); + e_storage_stop(c->ctx); + e_storage_destroy(c->ctx); + c->ctx = NULL; + printf("Node stopped.\n"); } -void cmd_upload(void *args, console *c) { +void cmd_upload(char *args, console *c) { + if (!c->ctx) { + printf("No node running. Start one first.\n"); + return; + } + if (!args || args[0] == '\0') { + printf("Usage: upload [PATH]\n"); + return; + } + + char resolved[PATH_MAX]; + if (!realpath(args, resolved)) { + printf("File not found: %s\n", args); + return; + } + + printf("Uploading %s...\n", resolved); + char *cid = e_storage_upload(c->ctx, resolved, progress_print); + if (cid) { + printf("CID: %s\n", cid); + free(cid); + } else { + printf("Upload failed.\n"); + } } -void cmd_download(void *args, console *c) { +void cmd_download(char *args, console *c) { + if (!c->ctx) { + printf("No node running. Start one first.\n"); + return; + } + char cid[256] = {0}; + char path[2048] = {0}; + + if (!args || sscanf(args, "%255s %2047s", cid, path) < 2) { + printf("Usage: download [CID] [PATH]\n"); + return; + } + + printf("Downloading %s to %s...\n", cid, path); + if (e_storage_download(c->ctx, cid, path, progress_print) == 0) { + printf("Download complete.\n"); + } else { + printf("Download failed.\n"); + } +} + +void cmd_quit(char *args, console *c) { + if (c->ctx) { + printf("Stopping node...\n"); + e_storage_stop(c->ctx); + e_storage_destroy(c->ctx); + c->ctx = NULL; + } + exit(0); } static const struct command commands[] = { {"help", "prints this help message", cmd_help}, {"quit", "quits this program", cmd_quit}, - {"start", "[API PORT] [DISC PORT] [BOOTSTRAP NODE] creates and starts a node", cmd_start}, + {"start", "[API_PORT] [DISC_PORT] [BOOTSTRAP_NODE] creates and starts a node", cmd_start}, {"stop", "stops and destroys the node", cmd_stop}, {"upload", "[PATH] uploads a file to the node", cmd_upload}, {"download", "[CID] [PATH] downloads content to a file", cmd_download}, }; -int n_commands() { return sizeof(commands) / sizeof(commands[0]); } +int n_commands(void) { return sizeof(commands) / sizeof(commands[0]); } int main(void) { char buf[4096]; @@ -82,22 +174,29 @@ int main(void) { } buf[strcspn(buf, "\n")] = 0; - char *cmd = strtok(buf, " "); - if (cmd == NULL) { - // user has input an empty string + if (buf[0] == '\0') { continue; } + // Split at first space: cmd points to command name, rest points to arguments + char *rest = strchr(buf, ' '); + if (rest) { + *rest = '\0'; + rest++; + // Skip leading spaces in arguments + while (*rest == ' ') rest++; + if (*rest == '\0') rest = NULL; + } + for (i = 0; i < n_commands(); i++) { - if (strcmp(cmd, commands[i].name) == 0) { - char *arg = strtok(NULL, " "); - commands[i].command(arg, &c); + if (strcmp(buf, commands[i].name) == 0) { + commands[i].command(rest, &c); break; } } if (i == n_commands()) { - printf("Invalid command %s\n", buf); + printf("Invalid command: %s\n", buf); } } } diff --git a/tests/mock_libstorage.c b/tests/mock_libstorage.c new file mode 100644 index 0000000..941e473 --- /dev/null +++ b/tests/mock_libstorage.c @@ -0,0 +1,90 @@ +#include "libstorage.h" + +#include +#include + +// A fake context to return from storage_new. +static int fake_ctx_data = 42; + +void libstorageNimMain(void) { + // no-op +} + +void *storage_new(const char *configJson, StorageCallback callback, void *userData) { + if (callback) { + callback(RET_OK, "ok", 2, userData); + } + return &fake_ctx_data; +} + +int storage_start(void *ctx, StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + if (callback) { + callback(RET_OK, "started", 7, userData); + } + return RET_OK; +} + +int storage_stop(void *ctx, StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + if (callback) { + callback(RET_OK, "stopped", 7, userData); + } + return RET_OK; +} + +int storage_close(void *ctx, StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + if (callback) { + callback(RET_OK, "closed", 6, userData); + } + return RET_OK; +} + +int storage_destroy(void *ctx, StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + if (callback) { + callback(RET_OK, "destroyed", 9, userData); + } + return RET_OK; +} + +int storage_upload_init(void *ctx, const char *filepath, size_t chunkSize, StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + // Return a fake session ID + const char *session_id = "mock-session-123"; + if (callback) { + callback(RET_OK, session_id, strlen(session_id), userData); + } + return RET_OK; +} + +int storage_upload_file(void *ctx, const char *sessionId, StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + // Fire a progress callback first, then final OK with CID + if (callback) { + callback(RET_PROGRESS, "chunk", 5, userData); + const char *cid = "zDvZRwzmAbCdEfGhIjKlMnOpQrStUvWxYz0123456789ABCD"; + callback(RET_OK, cid, strlen(cid), userData); + } + return RET_OK; +} + +int storage_download_init(void *ctx, const char *cid, size_t chunkSize, bool local, StorageCallback callback, + void *userData) { + if (!ctx) return RET_ERR; + if (callback) { + callback(RET_OK, "init", 4, userData); + } + return RET_OK; +} + +int storage_download_stream(void *ctx, const char *cid, size_t chunkSize, bool local, const char *filepath, + StorageCallback callback, void *userData) { + if (!ctx) return RET_ERR; + if (callback) { + callback(RET_PROGRESS, "data", 4, userData); + callback(RET_OK, "done", 4, userData); + } + return RET_OK; +} diff --git a/tests/test_runner.c b/tests/test_runner.c new file mode 100644 index 0000000..50f40f8 --- /dev/null +++ b/tests/test_runner.c @@ -0,0 +1,155 @@ +#include "easylibstorage.h" + +#include +#include +#include +#include + +#define RET_OK 0 +#define RET_ERR 1 + +static int tests_run = 0; +static int tests_passed = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_run++; \ + printf(" %-30s", #fn); \ + fn(); \ + tests_passed++; \ + printf(" OK\n"); \ + } while (0) + +static node_config default_config(void) { + node_config cfg = {0}; + cfg.api_port = 8080; + cfg.disc_port = 8090; + cfg.data_dir = "./test-data"; + cfg.log_level = "WARN"; + cfg.bootstrap_node = NULL; + return cfg; +} + +// --- Tests --- + +static void test_new(void) { + node_config cfg = default_config(); + cfg.bootstrap_node = "spr:abc123"; + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); +} + +static void test_new_defaults(void) { + node_config cfg = {0}; + cfg.api_port = 9000; + cfg.disc_port = 9010; + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); +} + +static void test_start(void) { + node_config cfg = default_config(); + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); + int ret = e_storage_start(node); + assert(ret == RET_OK); +} + +static void test_start_null(void) { + int ret = e_storage_start(NULL); + assert(ret == RET_ERR); +} + +static void test_stop(void) { + node_config cfg = default_config(); + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); + e_storage_start(node); + int ret = e_storage_stop(node); + assert(ret == RET_OK); +} + +static void test_destroy(void) { + node_config cfg = default_config(); + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); + int ret = e_storage_destroy(node); + assert(ret == RET_OK); +} + +static void test_destroy_null(void) { + int ret = e_storage_destroy(NULL); + assert(ret == RET_ERR); +} + +static void test_upload(void) { + node_config cfg = default_config(); + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); + e_storage_start(node); + + char *cid = e_storage_upload(node, "/tmp/test.txt", NULL); + assert(cid != NULL); + assert(strlen(cid) > 0); + free(cid); +} + +static void test_upload_null(void) { + char *cid = e_storage_upload(NULL, "/tmp/test.txt", NULL); + assert(cid == NULL); +} + +static void test_download(void) { + node_config cfg = default_config(); + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); + e_storage_start(node); + + int ret = e_storage_download(node, "zDvZRwzmSomeCid", "/tmp/out.dat", NULL); + assert(ret == RET_OK); +} + +static void test_download_null(void) { + int ret = e_storage_download(NULL, "zDvZRwzmSomeCid", "/tmp/out.dat", NULL); + assert(ret == RET_ERR); +} + +static void test_full_lifecycle(void) { + node_config cfg = default_config(); + cfg.bootstrap_node = "spr:node1"; + + STORAGE_NODE node = e_storage_new(cfg); + assert(node != NULL); + + assert(e_storage_start(node) == RET_OK); + + char *cid = e_storage_upload(node, "/tmp/lifecycle.txt", NULL); + assert(cid != NULL); + assert(strlen(cid) > 0); + + assert(e_storage_download(node, cid, "/tmp/lifecycle_out.dat", NULL) == RET_OK); + free(cid); + + assert(e_storage_stop(node) == RET_OK); + assert(e_storage_destroy(node) == RET_OK); +} + +int main(void) { + printf("Running easylibstorage tests...\n"); + + RUN_TEST(test_new); + RUN_TEST(test_new_defaults); + RUN_TEST(test_start); + RUN_TEST(test_start_null); + RUN_TEST(test_stop); + RUN_TEST(test_destroy); + RUN_TEST(test_destroy_null); + RUN_TEST(test_upload); + RUN_TEST(test_upload_null); + RUN_TEST(test_download); + RUN_TEST(test_download_null); + RUN_TEST(test_full_lifecycle); + + printf("\n%d/%d tests passed.\n", tests_passed, tests_run); + return (tests_passed == tests_run) ? 0 : 1; +}