diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index 77add47d6a7a..fa366c543edf 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -86,8 +86,15 @@ target_link_libraries(basic opendal_c_shared) add_executable(error_handle examples/error_handle.c) target_link_libraries(error_handle opendal_c_shared) +add_executable(async_stat examples/async_stat.c) +target_link_libraries(async_stat opendal_c_shared pthread) + +add_executable(compare_sync_async examples/compare_sync_async.c) +target_link_libraries(compare_sync_async opendal_c_shared pthread) + # test targets set(GTEST_SRCS + tests/async_stat_test.cpp tests/bdd.cpp tests/error_msg.cpp tests/list.cpp @@ -95,7 +102,7 @@ set(GTEST_SRCS tests/reader.cpp ) add_executable(tests ${GTEST_SRCS}) -target_link_libraries(tests opendal_c_shared gtest_main uuid) +target_link_libraries(tests opendal_c_shared gtest_main uuid pthread) if (TEST_ENABLE_ASAN) target_compile_options(tests PRIVATE -fsanitize=address) target_link_options(tests PRIVATE -fsanitize=address) diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index b0f791fa9dcd..89a6b15c4547 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -35,6 +35,7 @@ cbindgen = "0.29.0" [dependencies] bytes = "1.4.0" +futures = "0.3" # this crate won't be published, we always use the local version opendal = { version = ">=0", path = "../../core", features = ["blocking"] } tokio = { version = "1.27", features = ["fs", "macros", "rt-multi-thread"] } diff --git a/bindings/c/README.md b/bindings/c/README.md index 432a0cc4938e..28f42a6fe6e1 100644 --- a/bindings/c/README.md +++ b/bindings/c/README.md @@ -120,9 +120,73 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ```sh cd build - make basic error_handle + make basic error_handle async_stat compare_sync_async ``` +- The `compare_sync_async` example prints the same write/read/delete flow with both + blocking and async operators so you can see the API differences in one run. + +## Async APIs + +OpenDAL’s C binding mirrors the Rust async operator, but keeps all runtime management on the Rust side so C callers never need to embed Tokio. The design is intentionally future/await centric: + +- `opendal_async_operator_new` builds an async operator that internally holds a clone of the core `Operator` plus a handle to OpenDAL’s shared Tokio runtime. +- Each async method (`*_stat`, `*_write`, `*_read`, `*_delete`) immediately returns an opaque `opendal_future_*` handle. Creating the future is non-blocking—the runtime schedules the real work on its thread pool. +- You stay in control of when to pull the result. Call `opendal_future_*_await` to block the current thread until the operation finishes, or `opendal_future_*_poll` to integrate with your own event loop without blocking. +- If you abandon an operation, call `opendal_future_*_free` to cancel it. This aborts the underlying task and drops any pending output safely. + +Because futures carry ownership of the eventual metadata/error objects, the `*_await` helpers always transfer heap allocations using the same conventions as the blocking API (free metadata with `opendal_metadata_free`, free errors with `opendal_error_free`, etc.). + +### Usage example + +Below is a full async stat sequence that starts the request, performs other work, then awaits the result. The same pattern applies to read/write/delete by swapping the function names. + +```c +#include "opendal.h" +#include +#include + +static void sleep_ms(unsigned int ms) { usleep(ms * 1000); } + +int main(void) { + opendal_result_operator_new res = opendal_async_operator_new("memory", NULL); + if (res.error) { + fprintf(stderr, "create async op failed: %d\n", res.error->code); + opendal_error_free(res.error); + return 1; + } + const opendal_async_operator* op = (const opendal_async_operator*)res.op; + + opendal_result_future_stat fut = opendal_async_operator_stat(op, "missing.txt"); + if (fut.error) { + fprintf(stderr, "stat future failed: %d\n", fut.error->code); + opendal_error_free(fut.error); + opendal_async_operator_free(op); + return 1; + } + + printf("stat scheduled, doing other work...\n"); + sleep_ms(500); // keep UI/event loop responsive while I/O runs + + opendal_result_stat out = opendal_future_stat_await(fut.future); + if (out.error) { + printf("stat failed as expected: %d\n", out.error->code); + opendal_error_free(out.error); + } else { + printf("stat succeeded, size=%llu\n", + (unsigned long long)opendal_metadata_content_length(out.meta)); + opendal_metadata_free(out.meta); + } + + opendal_async_operator_free(op); + return 0; +} +``` + +Need non-blocking integration with your own loop? Call `opendal_future_stat_poll(fut.future, &out)` inside your loop. It returns `OPENDAL_FUTURE_PENDING` until the result is ready; once it reports `OPENDAL_FUTURE_READY`, call `opendal_future_stat_await` exactly once to consume the output. + +See `examples/async_stat.c` for a narrated walkthrough and `tests/async_stat_test.cpp` for GoogleTest-based assertions that cover both success and error paths. + ## Documentation The documentation index page source is under `./docs/doxygen/html/index.html`. diff --git a/bindings/c/cbindgen.toml b/bindings/c/cbindgen.toml index 66296943c8d0..174d46d6e805 100644 --- a/bindings/c/cbindgen.toml +++ b/bindings/c/cbindgen.toml @@ -46,7 +46,7 @@ no_includes = true sys_includes = ["stdint.h", "stddef.h", "stdbool.h"] [parse] -include = ["opendal"] +include = ["opendal_c"] parse_deps = true [fn] diff --git a/bindings/c/examples/async_stat.c b/bindings/c/examples/async_stat.c new file mode 100644 index 000000000000..90f0b06a794e --- /dev/null +++ b/bindings/c/examples/async_stat.c @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include +#include +#include +#include + +#include "opendal.h" + +int main(int argc, char* argv[]) +{ + printf("Starting OpenDAL async C example...\n"); + + // Create a new async operator for the "memory" service + // No options needed for memory backend + printf("Creating async operator for 'memory' backend...\n"); + opendal_result_operator_new result_op = opendal_async_operator_new("memory", NULL); + if (result_op.error != NULL) { + // Use %.*s to print the message safely as it's not null-terminated + printf("Error creating operator: %.*s (Code: %d)\n", + (int)result_op.error->message.len, + (char*)result_op.error->message.data, + result_op.error->code); + opendal_error_free(result_op.error); + return 1; + } + + // IMPORTANT: Cast the operator pointer from the result struct + // The opendal_result_operator_new struct is reused, but for async, + // the `op` field points to an opendal_async_operator. + const opendal_async_operator* op = (const opendal_async_operator*)result_op.op; + assert(op != NULL); + printf("Async operator created successfully.\n"); + + // --- Async await-style API --- + const char* path = "non_existent_file.txt"; + printf("Calling async stat (future) for path: %s\n", path); + + opendal_result_future_stat future_result = opendal_async_operator_stat(op, path); + if (future_result.error != NULL) { + printf("Error creating future: %.*s (Code: %d)\n", + (int)future_result.error->message.len, + (char*)future_result.error->message.data, + future_result.error->code); + opendal_error_free(future_result.error); + printf("Cleaning up resources...\n"); + opendal_async_operator_free(op); + printf("OpenDAL async C example finished with errors.\n"); + return 1; + } + + opendal_result_stat stat_result = opendal_future_stat_await(future_result.future); + if (stat_result.error != NULL) { + printf("Await failed as expected for non-existent file (future API).\n"); + printf("Error: %.*s (Code: %d)\n", + (int)stat_result.error->message.len, + (char*)stat_result.error->message.data, + stat_result.error->code); + assert(stat_result.error->code == OPENDAL_NOT_FOUND); + opendal_error_free(stat_result.error); + } else if (stat_result.meta != NULL) { + // Should not happen in this example + opendal_metadata_free(stat_result.meta); + } + + // --- Async write/read/delete demo --- + const char* write_path = "greeting.txt"; + const char* message = "hi from async write"; + opendal_bytes data = { + .data = (uint8_t*)message, + .len = strlen(message), + .capacity = strlen(message), + }; + + printf("Writing '%s' to %s asynchronously...\n", message, write_path); + opendal_result_future_write write_future = opendal_async_operator_write(op, write_path, &data); + if (write_future.error != NULL) { + printf("Write future creation failed: %.*s\n", (int)write_future.error->message.len, (char*)write_future.error->message.data); + opendal_error_free(write_future.error); + } else { + opendal_error* write_err = opendal_future_write_await(write_future.future); + if (write_err != NULL) { + printf("Write failed: %.*s\n", (int)write_err->message.len, (char*)write_err->message.data); + opendal_error_free(write_err); + } else { + printf("Write completed. Reading it back asynchronously...\n"); + opendal_result_future_read read_future = opendal_async_operator_read(op, write_path); + if (read_future.error != NULL) { + printf("Read future creation failed: %.*s\n", (int)read_future.error->message.len, (char*)read_future.error->message.data); + opendal_error_free(read_future.error); + } else { + opendal_result_read read_result = opendal_future_read_await(read_future.future); + if (read_result.error != NULL) { + printf("Read failed: %.*s\n", (int)read_result.error->message.len, (char*)read_result.error->message.data); + opendal_error_free(read_result.error); + } else { + printf("Read back %zu bytes: %.*s\n", read_result.data.len, (int)read_result.data.len, read_result.data.data); + opendal_bytes_free(&read_result.data); + } + } + + printf("Deleting %s asynchronously...\n", write_path); + opendal_result_future_delete delete_future = opendal_async_operator_delete(op, write_path); + if (delete_future.error != NULL) { + printf("Delete future creation failed: %.*s\n", (int)delete_future.error->message.len, (char*)delete_future.error->message.data); + opendal_error_free(delete_future.error); + } else { + opendal_error* delete_err = opendal_future_delete_await(delete_future.future); + if (delete_err != NULL) { + printf("Delete failed: %.*s\n", (int)delete_err->message.len, (char*)delete_err->message.data); + opendal_error_free(delete_err); + } else { + printf("Delete completed.\n"); + } + } + } + } + + // --- Cleanup --- + printf("Cleaning up resources...\n"); + + // Free the operator + opendal_async_operator_free(op); + + printf("OpenDAL async C example finished.\n"); + return 0; +} diff --git a/bindings/c/examples/compare_sync_async.c b/bindings/c/examples/compare_sync_async.c new file mode 100644 index 000000000000..c3b0cc4e1ef7 --- /dev/null +++ b/bindings/c/examples/compare_sync_async.c @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "opendal.h" +#include +#include +#include + +/* -------- Sync (blocking) -------- */ +static void sync_example(void) +{ + opendal_result_operator_new r = opendal_operator_new("memory", NULL); + const opendal_operator* op = r.op; + + const char* msg = "hello sync"; + opendal_bytes data = { + .data = (uint8_t*)msg, + .len = strlen(msg), + .capacity = strlen(msg), + }; + + opendal_error* werr = opendal_operator_write(op, "sync.txt", &data); + if (werr) { + printf("sync write err: %d\n", werr->code); + opendal_error_free(werr); + } + + opendal_result_read rd = opendal_operator_read(op, "sync.txt"); + if (!rd.error) { + printf("[sync] got %zu bytes: %.*s\n", rd.data.len, (int)rd.data.len, rd.data.data); + opendal_bytes_free(&rd.data); + } else { + printf("sync read err: %d\n", rd.error->code); + opendal_error_free(rd.error); + } + + opendal_operator_delete(op, "sync.txt"); + opendal_operator_free(op); +} + +/* -------- Async (future/await) -------- */ +static void async_example(void) +{ + opendal_result_operator_new r = opendal_async_operator_new("memory", NULL); + const opendal_async_operator* op = (const opendal_async_operator*)r.op; + + const char* msg = "hello async"; + opendal_bytes data = { + .data = (uint8_t*)msg, + .len = strlen(msg), + .capacity = strlen(msg), + }; + + opendal_result_future_write wf = opendal_async_operator_write(op, "async.txt", &data); + opendal_error* werr = opendal_future_write_await(wf.future); + if (werr) { + printf("async write err: %d\n", werr->code); + opendal_error_free(werr); + } + + // Kick off two reads without awaiting immediately to illustrate overlap. + opendal_result_future_read rf1 = opendal_async_operator_read(op, "async.txt"); + opendal_result_future_read rf2 = opendal_async_operator_read(op, "async.txt"); + + // ... do other work here (placeholder) + printf("[async] doing other work before awaiting reads...\n"); + + opendal_result_read rd1 = opendal_future_read_await(rf1.future); + opendal_result_read rd2 = opendal_future_read_await(rf2.future); + + if (!rd1.error) { + printf("[async] read1 %zu bytes: %.*s\n", rd1.data.len, (int)rd1.data.len, rd1.data.data); + opendal_bytes_free(&rd1.data); + } else { + printf("async read1 err: %d\n", rd1.error->code); + opendal_error_free(rd1.error); + } + + if (!rd2.error) { + printf("[async] read2 %zu bytes: %.*s\n", rd2.data.len, (int)rd2.data.len, rd2.data.data); + opendal_bytes_free(&rd2.data); + } else { + printf("async read2 err: %d\n", rd2.error->code); + opendal_error_free(rd2.error); + } + + opendal_result_future_delete df = opendal_async_operator_delete(op, "async.txt"); + opendal_error* derr = opendal_future_delete_await(df.future); + if (derr) { + printf("async delete err: %d\n", derr->code); + opendal_error_free(derr); + } + + opendal_async_operator_free(op); +} + +int main(void) +{ + printf("--- sync example ---\n"); + sync_example(); + + printf("--- async example (blocking await) ---\n"); + async_example(); + + // Non-blocking polling style: start a read, poll until ready, then await once. + printf("--- async example (non-blocking poll) ---\n"); + opendal_result_operator_new r = opendal_async_operator_new("memory", NULL); + const opendal_async_operator* op = (const opendal_async_operator*)r.op; + + const char* msg = "hello poll"; + opendal_bytes data = { .data = (uint8_t*)msg, .len = strlen(msg), .capacity = strlen(msg) }; + opendal_result_future_write wf = opendal_async_operator_write(op, "poll.txt", &data); + opendal_error* werr = opendal_future_write_await(wf.future); + if (werr) { + opendal_error_free(werr); + } + + opendal_result_future_read rf = opendal_async_operator_read(op, "poll.txt"); + opendal_result_read rd = { 0 }; + while (1) { + opendal_future_status st = opendal_future_read_poll(rf.future, &rd); + if (st == OPENDAL_FUTURE_PENDING) { + // simulate doing other work + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 1 * 1000 * 1000; // 1ms + nanosleep(&ts, NULL); + continue; + } + break; + } + if (rd.error == NULL) { + printf("[async poll] got %zu bytes: %.*s\n", rd.data.len, (int)rd.data.len, rd.data.data); + opendal_bytes_free(&rd.data); + } else { + opendal_error_free(rd.error); + } + + opendal_result_future_delete df = opendal_async_operator_delete(op, "poll.txt"); + opendal_error* derr = opendal_future_delete_await(df.future); + if (derr) + opendal_error_free(derr); + + opendal_async_operator_free(op); + + return 0; +} diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 270de2e91acb..e12f6242ce19 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -87,6 +87,36 @@ typedef enum opendal_code { OPENDAL_RANGE_NOT_SATISFIED, } opendal_code; +/** + * Status returned by non-blocking future polling. + */ +typedef enum opendal_future_status { + /** + * Future is still pending. + */ + OPENDAL_FUTURE_PENDING = 0, + /** + * Future is ready and output has been written to the provided out param. + */ + OPENDAL_FUTURE_READY = 1, + /** + * Future completed with an error state (e.g., channel closed). + */ + OPENDAL_FUTURE_ERROR = 2, + /** + * Future was cancelled. + */ + OPENDAL_FUTURE_CANCELED = 3, +} opendal_future_status; + +typedef struct Option_JoinHandle Option_JoinHandle; + +typedef struct Option_Receiver_Result Option_Receiver_Result; + +typedef struct Option_Receiver_Result_Buffer Option_Receiver_Result_Buffer; + +typedef struct Option_Receiver_Result_Metadata Option_Receiver_Result_Metadata; + /** * \brief opendal_bytes carries raw-bytes with its length * @@ -224,6 +254,10 @@ typedef struct opendal_operator { * Only touch this on judging whether it is NULL. */ void *inner; + /** + * Shared async operator handle for reuse (Arc clone internally). + */ + void *async_inner; } opendal_operator; /** @@ -424,6 +458,113 @@ typedef struct opendal_result_list { struct opendal_error *error; } opendal_result_list; +/** + * \brief Represents an asynchronous OpenDAL Operator. + * + * This operator interacts with storage services using non-blocking APIs. + * Use `opendal_async_operator_new` to construct and `opendal_async_operator_free` to release. + */ +typedef struct opendal_async_operator { + /** + * Internal pointer to the Rust async Operator. + */ + void *inner; +} opendal_async_operator; + +/** + * Future handle for asynchronous stat operations. + */ +typedef struct opendal_future_stat { + /** + * Pointer to an owned JoinHandle wrapped in Option for safe extraction. + */ + struct Option_JoinHandle *handle; + /** + * Receiver for the stat result. + */ + struct Option_Receiver_Result_Metadata *rx; +} opendal_future_stat; + +/** + * Result type for creating an asynchronous stat future. + */ +typedef struct opendal_result_future_stat { + /** + * The future handle. Null when creation fails. + */ + struct opendal_future_stat *future; + /** + * The error information. Null on success. + */ + struct opendal_error *error; +} opendal_result_future_stat; + +/** + * Future handle for asynchronous read operations. + */ +typedef struct opendal_future_read { + struct Option_JoinHandle *handle; + struct Option_Receiver_Result_Buffer *rx; +} opendal_future_read; + +/** + * Result type for creating an asynchronous read future. + */ +typedef struct opendal_result_future_read { + /** + * The future handle. Null when creation fails. + */ + struct opendal_future_read *future; + /** + * The error information. Null on success. + */ + struct opendal_error *error; +} opendal_result_future_read; + +/** + * Future handle for asynchronous write operations. + */ +typedef struct opendal_future_write { + struct Option_JoinHandle *handle; + struct Option_Receiver_Result_Metadata *rx; +} opendal_future_write; + +/** + * Result type for creating an asynchronous write future. + */ +typedef struct opendal_result_future_write { + /** + * The future handle. Null when creation fails. + */ + struct opendal_future_write *future; + /** + * The error information. Null on success. + */ + struct opendal_error *error; +} opendal_result_future_write; + +/** + * Future handle for asynchronous delete operations. + */ +typedef struct opendal_future_delete { + struct Option_JoinHandle *handle; + struct Option_Receiver_Result *rx; +} opendal_future_delete; + +/** + * Result type for creating an asynchronous delete future. + */ +typedef struct opendal_result_future_delete { + /** + * The future handle. Null when creation fails. + */ + struct opendal_future_delete *future; + /** + * The error information. Null on success. + */ + struct opendal_error *error; +} opendal_result_future_delete; + /** * \brief Metadata for **operator**, users can use this metadata to get information * of operator. @@ -1319,6 +1460,126 @@ struct opendal_error *opendal_operator_copy(const struct opendal_operator *op, struct opendal_error *opendal_operator_check(const struct opendal_operator *op); +/** + * \brief Constructs a new asynchronous OpenDAL Operator. + * + * @param scheme The storage service scheme (e.g., "s3", "fs"). + * @param options Configuration options for the service. Can be NULL. + * @return Result containing the new operator or an error. + * + * \see opendal_operator_options + * \see opendal_result_operator_new (reused for simplicity, but contains async op) + * + * # Safety + * + * `scheme` must be a valid, null-terminated C string. + * `options` must be a valid pointer or NULL. + */ +struct opendal_result_operator_new opendal_async_operator_new(const char *scheme, + const struct opendal_operator_options *options); + +/** + * \brief Creates an asynchronous operator that shares the same backend as an existing blocking operator. + */ +struct opendal_result_operator_new opendal_async_operator_from_operator(const struct opendal_operator *op); + +/** + * \brief Frees an asynchronous OpenDAL Operator. + * + * # Safety + * + * `op` must be a valid pointer previously returned by `opendal_async_operator_new`. + * Calling with NULL does nothing. + */ +void opendal_async_operator_free(const struct opendal_async_operator *op); + +/** + * \brief Asynchronously gets metadata of a path using a callback. + * + * @param op A valid pointer to `opendal_async_operator`. + * @param path The path to the object or directory. + * + * # Safety + * `op` must be a valid `opendal_async_operator`. + * `path` must be a valid, null-terminated C string. + * \brief Asynchronously gets metadata of a path, returning a future handle. + * + * The returned future can be awaited via `opendal_future_stat_await` to obtain the + * resulting metadata or error, mirroring Rust's `async/await` ergonomics. + */ +struct opendal_result_future_stat opendal_async_operator_stat(const struct opendal_async_operator *op, + const char *path); + +struct opendal_result_stat opendal_future_stat_await(struct opendal_future_stat *future); + +enum opendal_future_status opendal_future_stat_poll(struct opendal_future_stat *future, + struct opendal_result_stat *out); + +bool opendal_future_stat_is_ready(const struct opendal_future_stat *future); + +/** + * \brief Cancel and free a stat future without awaiting it. + */ +void opendal_future_stat_free(struct opendal_future_stat *future); + +/** + * \brief Asynchronously reads data from a path. + * + * The returned future can be awaited via `opendal_future_read_await` to obtain + * the resulting bytes or error. + */ +struct opendal_result_future_read opendal_async_operator_read(const struct opendal_async_operator *op, + const char *path); + +struct opendal_result_read opendal_future_read_await(struct opendal_future_read *future); + +enum opendal_future_status opendal_future_read_poll(struct opendal_future_read *future, + struct opendal_result_read *out); + +bool opendal_future_read_is_ready(const struct opendal_future_read *future); + +/** + * \brief Cancel and free a read future without awaiting it. + */ +void opendal_future_read_free(struct opendal_future_read *future); + +/** + * \brief Asynchronously writes data to a path. + */ +struct opendal_result_future_write opendal_async_operator_write(const struct opendal_async_operator *op, + const char *path, + const struct opendal_bytes *bytes); + +struct opendal_error *opendal_future_write_await(struct opendal_future_write *future); + +enum opendal_future_status opendal_future_write_poll(struct opendal_future_write *future, + struct opendal_error **error_out); + +bool opendal_future_write_is_ready(const struct opendal_future_write *future); + +/** + * \brief Cancel and free a write future without awaiting it. + */ +void opendal_future_write_free(struct opendal_future_write *future); + +/** + * \brief Asynchronously deletes the specified path. + */ +struct opendal_result_future_delete opendal_async_operator_delete(const struct opendal_async_operator *op, + const char *path); + +struct opendal_error *opendal_future_delete_await(struct opendal_future_delete *future); + +enum opendal_future_status opendal_future_delete_poll(struct opendal_future_delete *future, + struct opendal_error **error_out); + +bool opendal_future_delete_is_ready(const struct opendal_future_delete *future); + +/** + * \brief Cancel and free a delete future without awaiting it. + */ +void opendal_future_delete_free(struct opendal_future_delete *future); + /** * \brief Get information of underlying accessor. * diff --git a/bindings/c/src/async_operator.rs b/bindings/c/src/async_operator.rs new file mode 100644 index 000000000000..284c875aba02 --- /dev/null +++ b/bindings/c/src/async_operator.rs @@ -0,0 +1,1003 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::collections::HashMap; +use std::ffi::c_void; +use std::os::raw::c_char; +use std::str::FromStr; +use tokio::sync::oneshot; +use tokio::sync::oneshot::error::TryRecvError; +use tokio::task; + +use ::opendal as core; + +use super::*; +use crate::error::opendal_error; +use crate::metadata::opendal_metadata; // Keep this +use crate::result::opendal_result_read; +use crate::result::opendal_result_stat; // Keep this +use crate::types::opendal_bytes; +use crate::types::opendal_operator_options; // Keep this + +/// Status returned by non-blocking future polling. +#[repr(C)] +#[derive(Copy, Clone)] +pub enum opendal_future_status { + /// Future is still pending. + OPENDAL_FUTURE_PENDING = 0, + /// Future is ready and output has been written to the provided out param. + OPENDAL_FUTURE_READY = 1, + /// Future completed with an error state (e.g., channel closed). + OPENDAL_FUTURE_ERROR = 2, + /// Future was cancelled. + OPENDAL_FUTURE_CANCELED = 3, +} + +macro_rules! impl_poll_result { + ($fn_impl:ident, $future_ty:ty, $out_ty:ty, $ok_ctor:expr, $err_ctor:expr) => { + unsafe fn $fn_impl(future: *mut $future_ty, out: *mut $out_ty) -> opendal_future_status { + if future.is_null() || out.is_null() { + return opendal_future_status::OPENDAL_FUTURE_ERROR; + } + + let future = &mut *future; + if future.rx.is_null() { + return opendal_future_status::OPENDAL_FUTURE_READY; + } + + let rx_opt = unsafe { &mut *future.rx }; + let Some(rx) = rx_opt.as_mut() else { + return opendal_future_status::OPENDAL_FUTURE_READY; + }; + + match rx.try_recv() { + Ok(Ok(value)) => { + rx_opt.take(); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + drop(handle); + } + } + + let out_ref = &mut *out; + *out_ref = $ok_ctor(value); + opendal_future_status::OPENDAL_FUTURE_READY + } + Ok(Err(err)) => { + rx_opt.take(); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + drop(handle); + } + } + let out_ref = &mut *out; + *out_ref = $err_ctor(err); + opendal_future_status::OPENDAL_FUTURE_READY + } + Err(TryRecvError::Empty) => opendal_future_status::OPENDAL_FUTURE_PENDING, + Err(TryRecvError::Closed) => { + rx_opt.take(); + opendal_future_status::OPENDAL_FUTURE_CANCELED + } + } + } + }; +} + +macro_rules! impl_poll_error_only { + ($fn_impl:ident, $future_ty:ty) => { + unsafe fn $fn_impl( + future: *mut $future_ty, + error_out: *mut *mut opendal_error, + ) -> opendal_future_status { + if future.is_null() || error_out.is_null() { + return opendal_future_status::OPENDAL_FUTURE_ERROR; + } + + let future = &mut *future; + if future.rx.is_null() { + return opendal_future_status::OPENDAL_FUTURE_READY; + } + + let rx_opt = unsafe { &mut *future.rx }; + let Some(rx) = rx_opt.as_mut() else { + return opendal_future_status::OPENDAL_FUTURE_READY; + }; + + match rx.try_recv() { + Ok(Ok(_)) => { + rx_opt.take(); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + drop(handle); + } + } + *error_out = std::ptr::null_mut(); + opendal_future_status::OPENDAL_FUTURE_READY + } + Ok(Err(err)) => { + rx_opt.take(); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + drop(handle); + } + } + *error_out = opendal_error::new(err); + opendal_future_status::OPENDAL_FUTURE_READY + } + Err(TryRecvError::Empty) => opendal_future_status::OPENDAL_FUTURE_PENDING, + Err(TryRecvError::Closed) => { + rx_opt.take(); + opendal_future_status::OPENDAL_FUTURE_CANCELED + } + } + } + }; +} + +macro_rules! impl_is_ready_fn { + ($fn_impl:ident, $future_ty:ty) => { + unsafe fn $fn_impl(future: *const $future_ty) -> bool { + if future.is_null() { + return false; + } + + let future = &*future; + if future.handle.is_null() { + return false; + } + + let handle_slot = &*future.handle; + match handle_slot.as_ref() { + Some(handle) => handle.is_finished(), + None => true, + } + } + }; +} + +macro_rules! impl_await_result { + ($fn_name:ident, $future_ty:ty, $out_ty:ty, $ok_ctor:expr, $err_ctor:expr) => { + #[no_mangle] + pub unsafe extern "C" fn $fn_name(future: *mut $future_ty) -> $out_ty { + if future.is_null() { + return $err_ctor(core::Error::new( + core::ErrorKind::Unexpected, + stringify!($fn_name).to_string() + " future is null", + )); + } + + let mut future = Box::from_raw(future); + if future.rx.is_null() { + return $err_ctor(core::Error::new( + core::ErrorKind::Unexpected, + stringify!($fn_name).to_string() + " receiver is null", + )); + } + + let mut rx_box = Box::from_raw(future.rx); + future.rx = std::ptr::null_mut(); + let rx = match rx_box.take() { + Some(rx) => rx, + None => { + return $err_ctor(core::Error::new( + core::ErrorKind::Unexpected, + stringify!($fn_name).to_string() + " already awaited", + )); + } + }; + + let recv_result = crate::operator::RUNTIME.block_on(rx); + + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + drop(handle); + } + } + + match recv_result { + Ok(Ok(v)) => $ok_ctor(v), + Ok(Err(e)) => $err_ctor(e), + Err(recv_err) => $err_ctor(core::Error::new( + core::ErrorKind::Unexpected, + format!("join error: {}", recv_err), + )), + } + } + }; +} + +macro_rules! impl_await_error_only { + ($fn_name:ident, $future_ty:ty) => { + #[no_mangle] + pub unsafe extern "C" fn $fn_name(future: *mut $future_ty) -> *mut opendal_error { + if future.is_null() { + return opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + stringify!($fn_name).to_string() + " future is null", + )); + } + + let mut future = Box::from_raw(future); + if future.rx.is_null() { + return opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + stringify!($fn_name).to_string() + " receiver is null", + )); + } + + let mut rx_box = Box::from_raw(future.rx); + future.rx = std::ptr::null_mut(); + let rx = match rx_box.take() { + Some(rx) => rx, + None => { + return opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + stringify!($fn_name).to_string() + " already awaited", + )); + } + }; + + let recv_result = crate::operator::RUNTIME.block_on(rx); + + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + drop(handle); + } + } + + match recv_result { + Ok(Ok(_)) => std::ptr::null_mut(), + Ok(Err(e)) => opendal_error::new(e), + Err(recv_err) => opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + format!("join error: {}", recv_err), + )), + } + } + }; +} + +/// Future handle for asynchronous stat operations. +#[repr(C)] +pub struct opendal_future_stat { + /// Pointer to an owned JoinHandle wrapped in Option for safe extraction. + handle: *mut Option>, + /// Receiver for the stat result. + rx: *mut Option>>, +} + +unsafe impl Send for opendal_future_stat {} + +/// Result type for creating an asynchronous stat future. +#[repr(C)] +pub struct opendal_result_future_stat { + /// The future handle. Null when creation fails. + pub future: *mut opendal_future_stat, + /// The error information. Null on success. + pub error: *mut opendal_error, +} + +unsafe impl Send for opendal_result_future_stat {} + +/// Future handle for asynchronous read operations. +#[repr(C)] +pub struct opendal_future_read { + handle: *mut Option>, + rx: *mut Option>>, +} + +unsafe impl Send for opendal_future_read {} + +/// Result type for creating an asynchronous read future. +#[repr(C)] +pub struct opendal_result_future_read { + /// The future handle. Null when creation fails. + pub future: *mut opendal_future_read, + /// The error information. Null on success. + pub error: *mut opendal_error, +} + +unsafe impl Send for opendal_result_future_read {} + +/// Future handle for asynchronous write operations. +#[repr(C)] +pub struct opendal_future_write { + handle: *mut Option>, + rx: *mut Option>>, +} + +unsafe impl Send for opendal_future_write {} + +/// Result type for creating an asynchronous write future. +#[repr(C)] +pub struct opendal_result_future_write { + /// The future handle. Null when creation fails. + pub future: *mut opendal_future_write, + /// The error information. Null on success. + pub error: *mut opendal_error, +} + +unsafe impl Send for opendal_result_future_write {} + +/// Future handle for asynchronous delete operations. +#[repr(C)] +pub struct opendal_future_delete { + handle: *mut Option>, + rx: *mut Option>>, +} + +unsafe impl Send for opendal_future_delete {} + +/// Result type for creating an asynchronous delete future. +#[repr(C)] +pub struct opendal_result_future_delete { + /// The future handle. Null when creation fails. + pub future: *mut opendal_future_delete, + /// The error information. Null on success. + pub error: *mut opendal_error, +} + +unsafe impl Send for opendal_result_future_delete {} + +/// \brief Represents an asynchronous OpenDAL Operator. +/// +/// This operator interacts with storage services using non-blocking APIs. +/// Use `opendal_async_operator_new` to construct and `opendal_async_operator_free` to release. +#[repr(C)] +pub struct opendal_async_operator { + /// Internal pointer to the Rust async Operator. + inner: *mut c_void, +} + +impl opendal_async_operator { + /// Returns a reference to the inner asynchronous `core::Operator`. + /// + /// # Safety + /// + /// The caller must ensure that the `opendal_async_operator` pointer is valid + /// and that the lifetime of the returned reference does not exceed the lifetime + /// of the `opendal_async_operator`. + pub(crate) unsafe fn as_ref(&self) -> &core::Operator { + &*(self.inner as *mut core::Operator) + } +} + +/// \brief Constructs a new asynchronous OpenDAL Operator. +/// +/// @param scheme The storage service scheme (e.g., "s3", "fs"). +/// @param options Configuration options for the service. Can be NULL. +/// @return Result containing the new operator or an error. +/// +/// \see opendal_operator_options +/// \see opendal_result_operator_new (reused for simplicity, but contains async op) +/// +/// # Safety +/// +/// `scheme` must be a valid, null-terminated C string. +/// `options` must be a valid pointer or NULL. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_new( + scheme: *const c_char, + options: *const opendal_operator_options, +) -> opendal_result_operator_new { + assert!(!scheme.is_null()); + let scheme_str = match std::ffi::CStr::from_ptr(scheme).to_str() { + Ok(s) => s, + Err(e) => { + let err = core::Error::new(core::ErrorKind::Unexpected, "invalid scheme string") + .set_source(e); + return opendal_result_operator_new { + op: std::ptr::null_mut(), // Represents async operator here + error: opendal_error::new(err), + }; + } + }; + + let scheme = match core::Scheme::from_str(scheme_str) { + Ok(s) => s, + Err(e) => { + return opendal_result_operator_new { + op: std::ptr::null_mut(), + error: opendal_error::new(e), + }; + } + }; + + let map: HashMap<_, _> = if options.is_null() { + HashMap::default() + } else { + (*options) + .deref() + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }; + + match core::Operator::via_iter(scheme, map) { + Ok(mut op) => { + // Apply common layers like retry + op = op.layer(core::layers::RetryLayer::new()); + + let async_op = Box::into_raw(Box::new(opendal_async_operator { + inner: Box::into_raw(Box::new(op)) as *mut c_void, + })); + + // We reuse opendal_result_operator_new, but the `op` field now points + // to an opendal_async_operator. The C code needs to cast appropriately. + opendal_result_operator_new { + op: async_op as *mut opendal_operator, // Cast needed for return type + error: std::ptr::null_mut(), + } + } + Err(e) => opendal_result_operator_new { + op: std::ptr::null_mut(), + error: opendal_error::new(e), + }, + } +} + +/// \brief Creates an asynchronous operator that shares the same backend as an existing blocking operator. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_from_operator( + op: *const opendal_operator, +) -> opendal_result_operator_new { + if op.is_null() { + return opendal_result_operator_new { + op: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "opendal_operator is null", + )), + }; + } + + let base = (*op).async_ref().clone(); + let async_op = Box::into_raw(Box::new(opendal_async_operator { + inner: Box::into_raw(Box::new(base)) as *mut c_void, + })); + + opendal_result_operator_new { + op: async_op as *mut opendal_operator, + error: std::ptr::null_mut(), + } +} + +/// \brief Frees an asynchronous OpenDAL Operator. +/// +/// # Safety +/// +/// `op` must be a valid pointer previously returned by `opendal_async_operator_new`. +/// Calling with NULL does nothing. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_free(op: *const opendal_async_operator) { + if !op.is_null() { + // Drop the inner Operator + drop(Box::from_raw((*op).inner as *mut core::Operator)); + // Drop the container struct itself + drop(Box::from_raw(op as *mut opendal_async_operator)); + } +} + +// --- Async Stat Operation --- + +/// \brief Asynchronously gets metadata of a path using a callback. +/// +/// @param op A valid pointer to `opendal_async_operator`. +/// @param path The path to the object or directory. +/// +/// # Safety +/// `op` must be a valid `opendal_async_operator`. +/// `path` must be a valid, null-terminated C string. +/// \brief Asynchronously gets metadata of a path, returning a future handle. +/// +/// The returned future can be awaited via `opendal_future_stat_await` to obtain the +/// resulting metadata or error, mirroring Rust's `async/await` ergonomics. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_stat( + op: *const opendal_async_operator, + path: *const c_char, +) -> opendal_result_future_stat { + if op.is_null() { + return opendal_result_future_stat { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "opendal_async_operator is null", + )), + }; + } + if path.is_null() { + return opendal_result_future_stat { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "path is null", + )), + }; + } + + let operator = (*op).as_ref(); + let path_str = match std::ffi::CStr::from_ptr(path).to_str() { + Ok(s) => s.to_string(), + Err(e) => { + return opendal_result_future_stat { + future: std::ptr::null_mut(), + error: opendal_error::new( + core::Error::new(core::ErrorKind::Unexpected, "invalid path string") + .set_source(e), + ), + }; + } + }; + + let operator_clone = operator.clone(); + let (tx, rx) = oneshot::channel(); + let handle = crate::operator::RUNTIME.spawn(async move { + let res = operator_clone.stat(&path_str).await; + let _ = tx.send(res); + }); + let future = Box::into_raw(Box::new(opendal_future_stat { + handle: Box::into_raw(Box::new(Some(handle))), + rx: Box::into_raw(Box::new(Some(rx))), + })); + + opendal_result_future_stat { + future, + error: std::ptr::null_mut(), + } +} + +impl_await_result!( + opendal_future_stat_await_impl, + opendal_future_stat, + opendal_result_stat, + |metadata| opendal_result_stat { + meta: Box::into_raw(Box::new(opendal_metadata::new(metadata))), + error: std::ptr::null_mut(), + }, + |e| opendal_result_stat { + meta: std::ptr::null_mut(), + error: opendal_error::new(e), + } +); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_stat_await( + future: *mut opendal_future_stat, +) -> opendal_result_stat { + opendal_future_stat_await_impl(future) +} + +impl_poll_result!( + opendal_future_stat_poll_impl, + opendal_future_stat, + opendal_result_stat, + |metadata| opendal_result_stat { + meta: Box::into_raw(Box::new(opendal_metadata::new(metadata))), + error: std::ptr::null_mut(), + }, + |e| opendal_result_stat { + meta: std::ptr::null_mut(), + error: opendal_error::new(e), + } +); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_stat_poll( + future: *mut opendal_future_stat, + out: *mut opendal_result_stat, +) -> opendal_future_status { + opendal_future_stat_poll_impl(future, out) +} + +impl_is_ready_fn!(opendal_future_stat_is_ready_impl, opendal_future_stat); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_stat_is_ready(future: *const opendal_future_stat) -> bool { + opendal_future_stat_is_ready_impl(future) +} + +/// \brief Cancel and free a stat future without awaiting it. +#[no_mangle] +pub unsafe extern "C" fn opendal_future_stat_free(future: *mut opendal_future_stat) { + if future.is_null() { + return; + } + + let mut future = Box::from_raw(future); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + handle.abort(); + } + } + if !future.rx.is_null() { + drop(Box::from_raw(future.rx)); + future.rx = std::ptr::null_mut(); + } +} + +// --- Async Read Operation --- + +/// \brief Asynchronously reads data from a path. +/// +/// The returned future can be awaited via `opendal_future_read_await` to obtain +/// the resulting bytes or error. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_read( + op: *const opendal_async_operator, + path: *const c_char, +) -> opendal_result_future_read { + if op.is_null() { + return opendal_result_future_read { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "opendal_async_operator is null", + )), + }; + } + if path.is_null() { + return opendal_result_future_read { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "path is null", + )), + }; + } + + let operator = (*op).as_ref(); + let path_str = match std::ffi::CStr::from_ptr(path).to_str() { + Ok(s) => s.to_string(), + Err(e) => { + return opendal_result_future_read { + future: std::ptr::null_mut(), + error: opendal_error::new( + core::Error::new(core::ErrorKind::Unexpected, "invalid path string") + .set_source(e), + ), + }; + } + }; + + let operator_clone = operator.clone(); + let (tx, rx) = oneshot::channel(); + let handle: task::JoinHandle<()> = crate::operator::RUNTIME.spawn(async move { + let res = operator_clone.read(&path_str).await; + let _ = tx.send(res); + }); + let future = Box::into_raw(Box::new(opendal_future_read { + handle: Box::into_raw(Box::new(Some(handle))), + rx: Box::into_raw(Box::new(Some(rx))), + })); + + opendal_result_future_read { + future, + error: std::ptr::null_mut(), + } +} + +impl_await_result!( + opendal_future_read_await_impl, + opendal_future_read, + opendal_result_read, + |buffer| opendal_result_read { + data: opendal_bytes::new(buffer), + error: std::ptr::null_mut(), + }, + |e| opendal_result_read { + data: opendal_bytes::empty(), + error: opendal_error::new(e), + } +); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_read_await( + future: *mut opendal_future_read, +) -> opendal_result_read { + opendal_future_read_await_impl(future) +} + +impl_poll_result!( + opendal_future_read_poll_impl, + opendal_future_read, + opendal_result_read, + |buffer| opendal_result_read { + data: opendal_bytes::new(buffer), + error: std::ptr::null_mut(), + }, + |e| opendal_result_read { + data: opendal_bytes::empty(), + error: opendal_error::new(e), + } +); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_read_poll( + future: *mut opendal_future_read, + out: *mut opendal_result_read, +) -> opendal_future_status { + opendal_future_read_poll_impl(future, out) +} + +impl_is_ready_fn!(opendal_future_read_is_ready_impl, opendal_future_read); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_read_is_ready(future: *const opendal_future_read) -> bool { + opendal_future_read_is_ready_impl(future) +} + +/// \brief Cancel and free a read future without awaiting it. +#[no_mangle] +pub unsafe extern "C" fn opendal_future_read_free(future: *mut opendal_future_read) { + if future.is_null() { + return; + } + + let mut future = Box::from_raw(future); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + handle.abort(); + } + } + if !future.rx.is_null() { + drop(Box::from_raw(future.rx)); + future.rx = std::ptr::null_mut(); + } +} + +// --- Async Write Operation --- + +/// \brief Asynchronously writes data to a path. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_write( + op: *const opendal_async_operator, + path: *const c_char, + bytes: *const opendal_bytes, +) -> opendal_result_future_write { + if op.is_null() { + return opendal_result_future_write { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "opendal_async_operator is null", + )), + }; + } + if path.is_null() { + return opendal_result_future_write { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "path is null", + )), + }; + } + if bytes.is_null() { + return opendal_result_future_write { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "bytes is null", + )), + }; + } + + let operator = (*op).as_ref(); + let path_str = match std::ffi::CStr::from_ptr(path).to_str() { + Ok(s) => s.to_string(), + Err(e) => { + return opendal_result_future_write { + future: std::ptr::null_mut(), + error: opendal_error::new( + core::Error::new(core::ErrorKind::Unexpected, "invalid path string") + .set_source(e), + ), + }; + } + }; + + let buffer: core::Buffer = core::Buffer::from(&*bytes); + let operator_clone = operator.clone(); + let (tx, rx) = oneshot::channel(); + let handle: task::JoinHandle<()> = crate::operator::RUNTIME.spawn(async move { + let res = operator_clone.write(&path_str, buffer).await; + let _ = tx.send(res); + }); + let future = Box::into_raw(Box::new(opendal_future_write { + handle: Box::into_raw(Box::new(Some(handle))), + rx: Box::into_raw(Box::new(Some(rx))), + })); + + opendal_result_future_write { + future, + error: std::ptr::null_mut(), + } +} + +impl_await_error_only!(opendal_future_write_await_impl, opendal_future_write); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_write_await( + future: *mut opendal_future_write, +) -> *mut opendal_error { + opendal_future_write_await_impl(future) +} + +impl_poll_error_only!(opendal_future_write_poll_impl, opendal_future_write); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_write_poll( + future: *mut opendal_future_write, + error_out: *mut *mut opendal_error, +) -> opendal_future_status { + opendal_future_write_poll_impl(future, error_out) +} + +impl_is_ready_fn!(opendal_future_write_is_ready_impl, opendal_future_write); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_write_is_ready( + future: *const opendal_future_write, +) -> bool { + opendal_future_write_is_ready_impl(future) +} + +/// \brief Cancel and free a write future without awaiting it. +#[no_mangle] +pub unsafe extern "C" fn opendal_future_write_free(future: *mut opendal_future_write) { + if future.is_null() { + return; + } + + let mut future = Box::from_raw(future); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + handle.abort(); + } + } + if !future.rx.is_null() { + drop(Box::from_raw(future.rx)); + future.rx = std::ptr::null_mut(); + } +} + +// --- Async Delete Operation --- + +/// \brief Asynchronously deletes the specified path. +#[no_mangle] +pub unsafe extern "C" fn opendal_async_operator_delete( + op: *const opendal_async_operator, + path: *const c_char, +) -> opendal_result_future_delete { + if op.is_null() { + return opendal_result_future_delete { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "opendal_async_operator is null", + )), + }; + } + if path.is_null() { + return opendal_result_future_delete { + future: std::ptr::null_mut(), + error: opendal_error::new(core::Error::new( + core::ErrorKind::Unexpected, + "path is null", + )), + }; + } + + let operator = (*op).as_ref(); + let path_str = match std::ffi::CStr::from_ptr(path).to_str() { + Ok(s) => s.to_string(), + Err(e) => { + return opendal_result_future_delete { + future: std::ptr::null_mut(), + error: opendal_error::new( + core::Error::new(core::ErrorKind::Unexpected, "invalid path string") + .set_source(e), + ), + }; + } + }; + + let operator_clone = operator.clone(); + let (tx, rx) = oneshot::channel(); + let handle: task::JoinHandle<()> = crate::operator::RUNTIME.spawn(async move { + let res = operator_clone.delete(&path_str).await; + let _ = tx.send(res); + }); + let future = Box::into_raw(Box::new(opendal_future_delete { + handle: Box::into_raw(Box::new(Some(handle))), + rx: Box::into_raw(Box::new(Some(rx))), + })); + + opendal_result_future_delete { + future, + error: std::ptr::null_mut(), + } +} + +impl_await_error_only!(opendal_future_delete_await_impl, opendal_future_delete); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_delete_await( + future: *mut opendal_future_delete, +) -> *mut opendal_error { + opendal_future_delete_await_impl(future) +} + +impl_poll_error_only!(opendal_future_delete_poll_impl, opendal_future_delete); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_delete_poll( + future: *mut opendal_future_delete, + error_out: *mut *mut opendal_error, +) -> opendal_future_status { + opendal_future_delete_poll_impl(future, error_out) +} + +impl_is_ready_fn!(opendal_future_delete_is_ready_impl, opendal_future_delete); + +#[no_mangle] +pub unsafe extern "C" fn opendal_future_delete_is_ready( + future: *const opendal_future_delete, +) -> bool { + opendal_future_delete_is_ready_impl(future) +} + +/// \brief Cancel and free a delete future without awaiting it. +#[no_mangle] +pub unsafe extern "C" fn opendal_future_delete_free(future: *mut opendal_future_delete) { + if future.is_null() { + return; + } + + let mut future = Box::from_raw(future); + if !future.handle.is_null() { + let mut handle_box = Box::from_raw(future.handle); + future.handle = std::ptr::null_mut(); + if let Some(handle) = (*handle_box).take() { + handle.abort(); + } + } + if !future.rx.is_null() { + drop(Box::from_raw(future.rx)); + future.rx = std::ptr::null_mut(); + } +} diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 1156428cb85c..e7241858e6c2 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -45,6 +45,19 @@ pub use metadata::opendal_metadata; mod operator; pub use operator::opendal_operator; +// Add new async modules and types +mod async_operator; +pub use async_operator::opendal_async_operator; +pub use async_operator::opendal_future_delete; +pub use async_operator::opendal_future_read; +pub use async_operator::opendal_future_stat; +pub use async_operator::opendal_future_status; +pub use async_operator::opendal_future_write; +pub use async_operator::opendal_result_future_delete; +pub use async_operator::opendal_result_future_read; +pub use async_operator::opendal_result_future_stat; +pub use async_operator::opendal_result_future_write; + mod operator_info; mod result; diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs index fb6c2571e7d1..96fa183ec019 100644 --- a/bindings/c/src/operator.rs +++ b/bindings/c/src/operator.rs @@ -25,7 +25,7 @@ use ::opendal as core; use super::*; -static RUNTIME: LazyLock = LazyLock::new(|| { +pub(crate) static RUNTIME: LazyLock = LazyLock::new(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() @@ -48,6 +48,8 @@ pub struct opendal_operator { /// The pointer to the opendal::blocking::Operator in the Rust code. /// Only touch this on judging whether it is NULL. inner: *mut c_void, + /// Shared async operator handle for reuse (Arc clone internally). + async_inner: *mut c_void, } impl opendal_operator { @@ -56,6 +58,10 @@ impl opendal_operator { // The use-after-free is undefined behavior unsafe { &*(self.inner as *mut core::blocking::Operator) } } + + pub(crate) fn async_ref(&self) -> &core::Operator { + unsafe { &*(self.async_inner as *mut core::Operator) } + } } impl opendal_operator { @@ -79,6 +85,7 @@ impl opendal_operator { unsafe { if !ptr.is_null() { drop(Box::from_raw((*ptr).inner as *mut core::blocking::Operator)); + drop(Box::from_raw((*ptr).async_inner as *mut core::Operator)); drop(Box::from_raw(ptr as *mut opendal_operator)); } } @@ -88,14 +95,14 @@ impl opendal_operator { fn build_operator( schema: core::Scheme, map: HashMap, -) -> core::Result { +) -> core::Result<(core::blocking::Operator, core::Operator)> { let op = core::Operator::via_iter(schema, map)?.layer(core::layers::RetryLayer::new()); let runtime = tokio::runtime::Handle::try_current().unwrap_or_else(|_| RUNTIME.handle().clone()); let _guard = runtime.enter(); - let op = core::blocking::Operator::new(op)?; - Ok(op) + let blocking = core::blocking::Operator::new(op.clone())?; + Ok((blocking, op)) } /// \brief Construct an operator based on `scheme` and `options` @@ -162,9 +169,10 @@ pub unsafe extern "C" fn opendal_operator_new( } match build_operator(scheme, map) { - Ok(op) => opendal_result_operator_new { + Ok((blocking, async_op)) => opendal_result_operator_new { op: Box::into_raw(Box::new(opendal_operator { - inner: Box::into_raw(Box::new(op)) as _, + inner: Box::into_raw(Box::new(blocking)) as _, + async_inner: Box::into_raw(Box::new(async_op)) as _, })), error: std::ptr::null_mut(), }, diff --git a/bindings/c/src/result.rs b/bindings/c/src/result.rs index 601a80138943..2486e31b8ce5 100644 --- a/bindings/c/src/result.rs +++ b/bindings/c/src/result.rs @@ -99,6 +99,8 @@ pub struct opendal_result_stat { pub error: *mut opendal_error, } +unsafe impl Send for opendal_result_stat {} + /// \brief The result type returned by opendal_operator_list(). /// /// The result type for opendal_operator_list(), the field `lister` contains the lister diff --git a/bindings/c/tests/Makefile b/bindings/c/tests/Makefile index 543b3e8a311e..052fd848fc0f 100644 --- a/bindings/c/tests/Makefile +++ b/bindings/c/tests/Makefile @@ -23,7 +23,7 @@ LIBS = -lopendal_c -luuid # Source files FRAMEWORK_SOURCES = test_framework.cpp -SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp +SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp test_suites_async.cpp RUNNER_SOURCES = test_runner.cpp ALL_SOURCES = $(FRAMEWORK_SOURCES) $(SUITE_SOURCES) $(RUNNER_SOURCES) @@ -102,4 +102,4 @@ test_framework.o: test_framework.h test_suites_basic.o: test_framework.h test_suites_list.o: test_framework.h test_suites_reader_writer.o: test_framework.h -test_runner.o: test_framework.h \ No newline at end of file +test_runner.o: test_framework.h diff --git a/bindings/c/tests/async_stat_test.cpp b/bindings/c/tests/async_stat_test.cpp new file mode 100644 index 000000000000..572e17c3abbf --- /dev/null +++ b/bindings/c/tests/async_stat_test.cpp @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "gtest/gtest.h" +#include +#include +#include +#include +// Include the generated OpenDAL C header +extern "C" { +#include "opendal.h" +} + +class OpendalAsyncStatTest : public ::testing::Test { +protected: + const opendal_async_operator* op; + + void SetUp() override + { + opendal_result_operator_new result_op = opendal_async_operator_new("memory", NULL); + EXPECT_TRUE(result_op.error == nullptr); + EXPECT_TRUE(result_op.op != nullptr); + + // Cast is necessary because opendal_async_operator_new reuses opendal_result_operator_new + this->op = reinterpret_cast(result_op.op); + EXPECT_TRUE(this->op); + } + + void TearDown() override + { + opendal_async_operator_free(this->op); // Use the async free function + } +}; + +TEST_F(OpendalAsyncStatTest, AsyncStatAwaitStyle) +{ + const char* path = "non_existent_file.txt"; + opendal_result_future_stat future_result = opendal_async_operator_stat(this->op, path); + ASSERT_TRUE(future_result.error == nullptr); + ASSERT_TRUE(future_result.future != nullptr); + + opendal_result_stat stat_result = opendal_future_stat_await(future_result.future); + EXPECT_TRUE(stat_result.meta == nullptr); + ASSERT_TRUE(stat_result.error != nullptr); + EXPECT_EQ(stat_result.error->code, OPENDAL_NOT_FOUND); + opendal_error_free(stat_result.error); +} + +TEST_F(OpendalAsyncStatTest, AsyncStatFreeFuture) +{ + const char* path = "non_existent_file.txt"; + opendal_result_future_stat future_result = opendal_async_operator_stat(this->op, path); + ASSERT_TRUE(future_result.error == nullptr); + ASSERT_TRUE(future_result.future != nullptr); + + opendal_future_stat_free(future_result.future); + // Nothing to assert beyond not crashing; the future is cancelled. +} + +TEST_F(OpendalAsyncStatTest, AsyncWriteThenRead) +{ + const char* path = "async_write_read.txt"; + const char* payload = "hello async"; + opendal_bytes data = { + .data = (uint8_t*)payload, + .len = strlen(payload), + .capacity = strlen(payload), + }; + + opendal_result_future_write write_result = opendal_async_operator_write(this->op, path, &data); + ASSERT_TRUE(write_result.error == nullptr); + ASSERT_TRUE(write_result.future != nullptr); + + opendal_error* write_err = opendal_future_write_await(write_result.future); + ASSERT_TRUE(write_err == nullptr); + + opendal_result_future_read read_result = opendal_async_operator_read(this->op, path); + ASSERT_TRUE(read_result.error == nullptr); + ASSERT_TRUE(read_result.future != nullptr); + + opendal_result_read read_out = opendal_future_read_await(read_result.future); + ASSERT_TRUE(read_out.error == nullptr); + ASSERT_EQ(read_out.data.len, strlen(payload)); + EXPECT_EQ(memcmp(read_out.data.data, payload, read_out.data.len), 0); + opendal_bytes_free(&read_out.data); + + opendal_result_future_delete delete_result = opendal_async_operator_delete(this->op, path); + ASSERT_TRUE(delete_result.error == nullptr); + opendal_error* delete_err = opendal_future_delete_await(delete_result.future); + ASSERT_TRUE(delete_err == nullptr); +} + +TEST_F(OpendalAsyncStatTest, AsyncDeleteMakesStatReturnNotFound) +{ + const char* path = "async_delete.txt"; + const char* payload = "cleanup"; + opendal_bytes data = { + .data = (uint8_t*)payload, + .len = strlen(payload), + .capacity = strlen(payload), + }; + + // Write first so the delete has work to do. + opendal_result_future_write write_result = opendal_async_operator_write(this->op, path, &data); + ASSERT_TRUE(write_result.error == nullptr); + ASSERT_TRUE(opendal_future_write_await(write_result.future) == nullptr); + + opendal_result_future_delete delete_result = opendal_async_operator_delete(this->op, path); + ASSERT_TRUE(delete_result.error == nullptr); + ASSERT_TRUE(opendal_future_delete_await(delete_result.future) == nullptr); + + // Stat should now report not found. + opendal_result_future_stat stat_future = opendal_async_operator_stat(this->op, path); + ASSERT_TRUE(stat_future.error == nullptr); + opendal_result_stat stat_result = opendal_future_stat_await(stat_future.future); + ASSERT_TRUE(stat_result.meta == nullptr); + ASSERT_TRUE(stat_result.error != nullptr); + EXPECT_EQ(stat_result.error->code, OPENDAL_NOT_FOUND); + opendal_error_free(stat_result.error); +} diff --git a/bindings/c/tests/example_test.cpp b/bindings/c/tests/example_test.cpp index b559d870cbc1..4c9599d2bef6 100644 --- a/bindings/c/tests/example_test.cpp +++ b/bindings/c/tests/example_test.cpp @@ -25,18 +25,19 @@ #include "test_framework.h" // Simple test function -void simple_test() { +void simple_test() +{ printf("Running simple test example...\n"); - + // Initialize test configuration opendal_test_config* config = opendal_test_config_new(); if (!config) { printf("Failed to create test config\n"); return; } - + printf("Testing with service: %s\n", config->scheme); - + // Test basic operator functionality opendal_error* error = opendal_operator_check(config->operator_instance); if (error) { @@ -48,78 +49,78 @@ void simple_test() { opendal_test_config_free(config); return; } - + printf("Operator check passed!\n"); - + // Test basic write/read if supported opendal_operator_info* info = opendal_operator_info_new(config->operator_instance); if (info) { opendal_capability cap = opendal_operator_info_get_full_capability(info); - + if (cap.write && cap.read) { printf("Testing write/read operations...\n"); - + const char* test_path = "simple_test.txt"; const char* test_content = "Hello, OpenDAL!"; - + // Write test data opendal_bytes data = { .data = (uint8_t*)test_content, .len = strlen(test_content), .capacity = strlen(test_content) }; - + error = opendal_operator_write(config->operator_instance, test_path, &data); if (error) { printf("Write failed: %d\n", error->code); opendal_error_free(error); } else { printf("Write successful!\n"); - + // Read test data back opendal_result_read result = opendal_operator_read(config->operator_instance, test_path); if (result.error) { printf("Read failed: %d\n", result.error->code); opendal_error_free(result.error); } else { - printf("Read successful! Content: %.*s\n", - (int)result.data.len, (char*)result.data.data); - + printf("Read successful! Content: %.*s\n", + (int)result.data.len, (char*)result.data.data); + // Verify content - if (result.data.len == strlen(test_content) && - memcmp(result.data.data, test_content, result.data.len) == 0) { + if (result.data.len == strlen(test_content) && memcmp(result.data.data, test_content, result.data.len) == 0) { printf("Content verification passed!\n"); } else { printf("Content verification failed!\n"); } - + opendal_bytes_free(&result.data); } - + // Cleanup opendal_operator_delete(config->operator_instance, test_path); } } else { printf("Write/read not supported by this service\n"); } - + opendal_operator_info_free(info); } - + printf("Simple test completed!\n"); opendal_test_config_free(config); } -int main() { +int main() +{ printf("OpenDAL C Binding Test Framework Example\n"); printf("========================================\n\n"); - + simple_test(); - + printf("\nTo run the full test suite, use:\n"); printf(" make test # Run all tests\n"); printf(" ./opendal_test_runner # Run test runner directly\n"); printf(" ./opendal_test_runner --help # See all options\n"); - + return 0; -} \ No newline at end of file +} \ No newline at end of file diff --git a/bindings/c/tests/test_framework.cpp b/bindings/c/tests/test_framework.cpp index 0b1a1d8c5e94..f05bc388b60c 100644 --- a/bindings/c/tests/test_framework.cpp +++ b/bindings/c/tests/test_framework.cpp @@ -25,32 +25,33 @@ int total_tests = 0; int passed_tests = 0; int failed_tests = 0; -opendal_test_config* opendal_test_config_new() { +opendal_test_config* opendal_test_config_new() +{ opendal_test_config* config = (opendal_test_config*)malloc(sizeof(opendal_test_config)); - if (!config) return NULL; - + if (!config) + return NULL; // Read environment variables for configuration const char* scheme = getenv("OPENDAL_TEST"); if (!scheme) { - scheme = "memory"; // Default to memory for testing + scheme = "memory"; // Default to memory for testing } - + config->scheme = strdup(scheme); config->options = opendal_operator_options_new(); - + // Read configuration from environment variables // Format: OPENDAL_{SCHEME}_{CONFIG_KEY} char env_prefix[256]; snprintf(env_prefix, sizeof(env_prefix), "OPENDAL_%s_", scheme); - + // Convert to uppercase for (char* p = env_prefix; *p; ++p) { *p = toupper(*p); } - + // Look for environment variables with this prefix - extern char **environ; - for (char **env = environ; *env; ++env) { + extern char** environ; + for (char** env = environ; *env; ++env) { if (strncmp(*env, env_prefix, strlen(env_prefix)) == 0) { char* key_value = strdup(*env + strlen(env_prefix)); char* equals = strchr(key_value, '='); @@ -58,24 +59,24 @@ opendal_test_config* opendal_test_config_new() { *equals = '\0'; char* key = key_value; char* value = equals + 1; - + // Convert key to lowercase for (char* p = key; *p; ++p) { *p = tolower(*p); } - + opendal_operator_options_set(config->options, key, value); } free(key_value); } } - + // Generate random root if not disabled const char* disable_random_root = getenv("OPENDAL_DISABLE_RANDOM_ROOT"); if (!disable_random_root || strcmp(disable_random_root, "true") != 0) { // Get existing root configuration if any const char* existing_root = NULL; - + // Check if root was already set from environment variables char root_env_var[256]; snprintf(root_env_var, sizeof(root_env_var), "OPENDAL_%s_ROOT", scheme); @@ -84,14 +85,14 @@ opendal_test_config* opendal_test_config_new() { *p = toupper(*p); } existing_root = getenv(root_env_var); - + // Generate random path based on existing root config->random_root = opendal_generate_random_path(existing_root); opendal_operator_options_set(config->options, "root", config->random_root); } else { config->random_root = NULL; } - + // Create operator opendal_result_operator_new result = opendal_operator_new(config->scheme, config->options); if (result.error) { @@ -103,14 +104,16 @@ opendal_test_config* opendal_test_config_new() { opendal_test_config_free(config); return NULL; } - + config->operator_instance = result.op; return config; } -void opendal_test_config_free(opendal_test_config* config) { - if (!config) return; - +void opendal_test_config_free(opendal_test_config* config) +{ + if (!config) + return; + if (config->operator_instance) { opendal_operator_free(config->operator_instance); } @@ -126,41 +129,58 @@ void opendal_test_config_free(opendal_test_config* config) { free(config); } -bool opendal_check_capability(const opendal_operator* op, opendal_required_capability required) { +bool opendal_check_capability(const opendal_operator* op, opendal_required_capability required) +{ opendal_operator_info* info = opendal_operator_info_new(op); - if (!info) return false; - + if (!info) + return false; + opendal_capability cap = opendal_operator_info_get_full_capability(info); - + bool result = true; - if (required.stat && !cap.stat) result = false; - if (required.read && !cap.read) result = false; - if (required.write && !cap.write) result = false; - if (required.delete_ && !cap.delete_) result = false; - if (required.list && !cap.list) result = false; - if (required.list_with_start_after && !cap.list_with_start_after) result = false; - if (required.list_with_recursive && !cap.list_with_recursive) result = false; - if (required.copy && !cap.copy) result = false; - if (required.rename && !cap.rename) result = false; - if (required.create_dir && !cap.create_dir) result = false; - if (required.presign && !cap.presign) result = false; - if (required.presign_read && !cap.presign_read) result = false; - if (required.presign_write && !cap.presign_write) result = false; - + if (required.stat && !cap.stat) + result = false; + if (required.read && !cap.read) + result = false; + if (required.write && !cap.write) + result = false; + if (required.delete_ && !cap.delete_) + result = false; + if (required.list && !cap.list) + result = false; + if (required.list_with_start_after && !cap.list_with_start_after) + result = false; + if (required.list_with_recursive && !cap.list_with_recursive) + result = false; + if (required.copy && !cap.copy) + result = false; + if (required.rename && !cap.rename) + result = false; + if (required.create_dir && !cap.create_dir) + result = false; + if (required.presign && !cap.presign) + result = false; + if (required.presign_read && !cap.presign_read) + result = false; + if (required.presign_write && !cap.presign_write) + result = false; + opendal_operator_info_free(info); return result; } -char* opendal_generate_random_path(const char* base_root) { +char* opendal_generate_random_path(const char* base_root) +{ uuid_t uuid; char uuid_str[37]; - + uuid_generate(uuid); uuid_unparse(uuid, uuid_str); - - char* path = (char*)malloc(512); // Increase size to accommodate longer paths - if (!path) return NULL; - + + char* path = (char*)malloc(512); // Increase size to accommodate longer paths + if (!path) + return NULL; + if (base_root && strlen(base_root) > 0) { // If base_root is provided, append the random UUID to it // Ensure proper path separator handling @@ -170,58 +190,62 @@ char* opendal_generate_random_path(const char* base_root) { // If no base_root, use /tmp as a safe default instead of filesystem root snprintf(path, 512, "/tmp/test_%s/", uuid_str); } - + return path; } -char* opendal_generate_random_content(size_t length) { +char* opendal_generate_random_content(size_t length) +{ char* content = (char*)malloc(length + 1); - if (!content) return NULL; - + if (!content) + return NULL; + srand(time(NULL)); for (size_t i = 0; i < length; i++) { content[i] = 'a' + (rand() % 26); } content[length] = '\0'; - + return content; } -void opendal_run_test_suite(opendal_test_suite* suite, opendal_test_config* config) { +void opendal_run_test_suite(opendal_test_suite* suite, opendal_test_config* config) +{ printf("\n=== Running Test Suite: %s ===\n", suite->name); - + for (size_t i = 0; i < suite->test_count; i++) { opendal_run_test_case(&suite->tests[i], config, suite->name); } - + printf("=== Test Suite %s Completed ===\n", suite->name); } -void opendal_run_test_case(opendal_test_case* test_case, opendal_test_config* config, const char* suite_name) { +void opendal_run_test_case(opendal_test_case* test_case, opendal_test_config* config, const char* suite_name) +{ total_tests++; - + // Check capabilities if (!opendal_check_capability(config->operator_instance, test_case->capability)) { printf("SKIPPED: %s::%s (missing required capabilities)\n", suite_name, test_case->name); return; } - + printf("RUNNING: %s::%s ... ", suite_name, test_case->name); fflush(stdout); - + // Create test context opendal_test_context ctx = { .config = config, .test_name = test_case->name, .suite_name = suite_name }; - + // Capture stdout to detect assertion failures int saved_failed_count = failed_tests; - + // Run the test test_case->func(&ctx); - + // Check if test passed or failed if (failed_tests > saved_failed_count) { printf("FAILED\n"); @@ -231,13 +255,14 @@ void opendal_run_test_case(opendal_test_case* test_case, opendal_test_config* co } } -void opendal_print_test_summary() { +void opendal_print_test_summary() +{ printf("\n=== Test Summary ===\n"); printf("Total tests: %d\n", total_tests); printf("Passed: %d\n", passed_tests); printf("Failed: %d\n", failed_tests); printf("Skipped: %d\n", total_tests - passed_tests - failed_tests); - + if (failed_tests > 0) { printf("\nTests FAILED!\n"); } else { @@ -245,24 +270,28 @@ void opendal_print_test_summary() { } } -opendal_test_data* opendal_test_data_new(const char* path, const char* content) { +opendal_test_data* opendal_test_data_new(const char* path, const char* content) +{ opendal_test_data* data = (opendal_test_data*)malloc(sizeof(opendal_test_data)); - if (!data) return NULL; - + if (!data) + return NULL; + data->path = strdup(path); - + size_t content_len = strlen(content); data->content.data = (uint8_t*)malloc(content_len); data->content.len = content_len; data->content.capacity = content_len; memcpy(data->content.data, content, content_len); - + return data; } -void opendal_test_data_free(opendal_test_data* data) { - if (!data) return; - +void opendal_test_data_free(opendal_test_data* data) +{ + if (!data) + return; + if (data->path) { free(data->path); } @@ -270,4 +299,4 @@ void opendal_test_data_free(opendal_test_data* data) { free(data->content.data); } free(data); -} \ No newline at end of file +} diff --git a/bindings/c/tests/test_framework.h b/bindings/c/tests/test_framework.h index fc525bdf53d6..09f33ecaab45 100644 --- a/bindings/c/tests/test_framework.h +++ b/bindings/c/tests/test_framework.h @@ -228,4 +228,4 @@ typedef struct opendal_test_data { opendal_test_data* opendal_test_data_new(const char* path, const char* content); void opendal_test_data_free(opendal_test_data* data); -#endif // _OPENDAL_TEST_FRAMEWORK_H \ No newline at end of file +#endif // _OPENDAL_TEST_FRAMEWORK_H diff --git a/bindings/c/tests/test_runner.cpp b/bindings/c/tests/test_runner.cpp index 24dc5848f92e..fe67d8f88632 100644 --- a/bindings/c/tests/test_runner.cpp +++ b/bindings/c/tests/test_runner.cpp @@ -18,24 +18,27 @@ */ #include "test_framework.h" -#include #include +#include // External test suite declarations extern opendal_test_suite basic_suite; extern opendal_test_suite list_suite; extern opendal_test_suite reader_writer_suite; +extern opendal_test_suite async_suite; // List of all test suites static opendal_test_suite* all_suites[] = { &basic_suite, &list_suite, &reader_writer_suite, + &async_suite, }; static const size_t num_suites = sizeof(all_suites) / sizeof(all_suites[0]); -void print_usage(const char* program_name) { +void print_usage(const char* program_name) +{ printf("Usage: %s [options]\n", program_name); printf("\nOptions:\n"); printf(" -h, --help Show this help message\n"); @@ -54,7 +57,8 @@ void print_usage(const char* program_name) { printf(" OPENDAL_TEST=fs OPENDAL_FS_ROOT=/tmp %s # Test with filesystem service\n", program_name); } -void list_suites() { +void list_suites() +{ printf("Available test suites:\n"); for (size_t i = 0; i < num_suites; i++) { printf(" %s (%zu tests)\n", all_suites[i]->name, all_suites[i]->test_count); @@ -64,7 +68,8 @@ void list_suites() { } } -bool run_specific_test(opendal_test_config* config, const char* suite_name, const char* test_name) { +bool run_specific_test(opendal_test_config* config, const char* suite_name, const char* test_name) +{ for (size_t i = 0; i < num_suites; i++) { opendal_test_suite* suite = all_suites[i]; if (strcmp(suite->name, suite_name) == 0) { @@ -83,7 +88,8 @@ bool run_specific_test(opendal_test_config* config, const char* suite_name, cons return false; } -bool run_specific_suite(opendal_test_config* config, const char* suite_name) { +bool run_specific_suite(opendal_test_config* config, const char* suite_name) +{ for (size_t i = 0; i < num_suites; i++) { if (strcmp(all_suites[i]->name, suite_name) == 0) { opendal_run_test_suite(all_suites[i], config); @@ -94,19 +100,21 @@ bool run_specific_suite(opendal_test_config* config, const char* suite_name) { return false; } -void run_all_suites(opendal_test_config* config) { +void run_all_suites(opendal_test_config* config) +{ printf("Running all test suites...\n"); - + for (size_t i = 0; i < num_suites; i++) { opendal_run_test_suite(all_suites[i], config); } } -int main(int argc, char* argv[]) { +int main(int argc, char* argv[]) +{ bool verbose = false; const char* suite_to_run = nullptr; const char* test_to_run = nullptr; - + // Parse command line arguments for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { @@ -139,7 +147,7 @@ int main(int argc, char* argv[]) { return 1; } } - + // Initialize test configuration printf("Initializing OpenDAL test framework...\n"); opendal_test_config* config = opendal_test_config_new(); @@ -147,12 +155,12 @@ int main(int argc, char* argv[]) { printf("Failed to initialize test configuration\n"); return 1; } - + printf("Service: %s\n", config->scheme); if (config->random_root) { printf("Random root: %s\n", config->random_root); } - + // Check operator capabilities first opendal_operator_info* info = opendal_operator_info_new(config->operator_instance); if (!info) { @@ -160,9 +168,9 @@ int main(int argc, char* argv[]) { opendal_test_config_free(config); return 1; } - + opendal_capability cap = opendal_operator_info_get_full_capability(info); - + // Check operator availability - only perform list-based check if list is supported if (cap.list) { opendal_error* check_error = opendal_operator_check(config->operator_instance); @@ -180,18 +188,18 @@ int main(int argc, char* argv[]) { // For KV adapters that don't support list, we'll do a basic capability check instead printf("Note: Operator doesn't support list operations (KV adapter), skipping standard check\n"); } - + printf("Operator is ready!\n"); printf("Capabilities: read=%s, write=%s, list=%s, stat=%s, delete=%s\n", - cap.read ? "yes" : "no", - cap.write ? "yes" : "no", - cap.list ? "yes" : "no", - cap.stat ? "yes" : "no", - cap.delete_ ? "yes" : "no"); - + cap.read ? "yes" : "no", + cap.write ? "yes" : "no", + cap.list ? "yes" : "no", + cap.stat ? "yes" : "no", + cap.delete_ ? "yes" : "no"); + opendal_operator_info_free(info); printf("\n"); - + // Run tests based on command line arguments if (test_to_run) { // Parse suite::test format @@ -203,14 +211,14 @@ int main(int argc, char* argv[]) { opendal_test_config_free(config); return 1; } - + *delimiter = '\0'; const char* suite_name = test_spec; const char* test_name = delimiter + 2; - + bool success = run_specific_test(config, suite_name, test_name); free(test_spec); - + if (!success) { opendal_test_config_free(config); return 1; @@ -224,13 +232,13 @@ int main(int argc, char* argv[]) { } else { run_all_suites(config); } - + // Print test summary opendal_print_test_summary(); - + // Cleanup opendal_test_config_free(config); - + // Return appropriate exit code return (failed_tests > 0) ? 1 : 0; } diff --git a/bindings/c/tests/test_suites_async.cpp b/bindings/c/tests/test_suites_async.cpp new file mode 100644 index 000000000000..cd8baec7b8a0 --- /dev/null +++ b/bindings/c/tests/test_suites_async.cpp @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "test_framework.h" + +#include +#include + +static void poll_until_ready(opendal_future_status (*poll_fn)(void*, void*), + void* future, + void* out, + opendal_future_status* final_status) +{ + const int max_attempts = 1000; + int attempts = 0; + opendal_future_status status; + do { + status = poll_fn(future, out); + if (status == OPENDAL_FUTURE_PENDING) { + usleep(1000); // yield briefly while waiting for runtime + } else { + break; + } + } while (++attempts < max_attempts); + *final_status = status; +} + +static void test_async_stat_await(opendal_test_context* ctx) +{ + opendal_result_operator_new async_res = + opendal_async_operator_from_operator(ctx->config->operator_instance); + OPENDAL_ASSERT_NO_ERROR(async_res.error, "Async operator creation should succeed"); + OPENDAL_ASSERT_NOT_NULL(async_res.op, "Async operator pointer must not be NULL"); + const opendal_async_operator* async_op = (const opendal_async_operator*)async_res.op; + const char* path = "async_stat_await.txt"; + const char* content = "async stat await"; + + opendal_bytes data; + data.data = (uint8_t*)content; + data.len = strlen(content); + data.capacity = strlen(content); + + opendal_result_future_write wf = opendal_async_operator_write(async_op, path, &data); + OPENDAL_ASSERT_NO_ERROR(wf.error, "Async write setup should succeed"); + opendal_error* write_err = opendal_future_write_await(wf.future); + OPENDAL_ASSERT_NO_ERROR(write_err, "Awaiting async write should succeed"); + + opendal_result_future_stat fut = opendal_async_operator_stat(async_op, path); + OPENDAL_ASSERT_NO_ERROR(fut.error, "opendal_async_operator_stat should succeed"); + OPENDAL_ASSERT_NOT_NULL(fut.future, "Stat future pointer should not be NULL"); + + opendal_result_stat stat = opendal_future_stat_await(fut.future); + OPENDAL_ASSERT_NO_ERROR(stat.error, "Awaiting stat future should not fail"); + OPENDAL_ASSERT_NOT_NULL(stat.meta, "Stat metadata should be available"); + OPENDAL_ASSERT(opendal_metadata_is_file(stat.meta), "Metadata should mark the entry as file"); + OPENDAL_ASSERT_EQ(strlen(content), opendal_metadata_content_length(stat.meta), + "Content length should match async stat await test payload"); + + opendal_metadata_free(stat.meta); + + opendal_result_future_delete df = opendal_async_operator_delete(async_op, path); + OPENDAL_ASSERT_NO_ERROR(df.error, "Async delete future should be created successfully"); + opendal_error* delete_err = opendal_future_delete_await(df.future); + OPENDAL_ASSERT_NO_ERROR(delete_err, "Async delete should succeed"); + + opendal_async_operator_free(async_op); +} + +static opendal_future_status stat_poll_bridge(void* fut, void* out) +{ + return opendal_future_stat_poll((struct opendal_future_stat*)fut, (struct opendal_result_stat*)out); +} + +static opendal_future_status read_poll_bridge(void* fut, void* out) +{ + return opendal_future_read_poll((struct opendal_future_read*)fut, (struct opendal_result_read*)out); +} + +static opendal_future_status write_poll_bridge(void* fut, void* err_out) +{ + return opendal_future_write_poll((struct opendal_future_write*)fut, (struct opendal_error**)err_out); +} + +static opendal_future_status poll_stat_ready(struct opendal_future_stat* future, opendal_result_stat* out) +{ + opendal_future_status status; + poll_until_ready(stat_poll_bridge, future, out, &status); + return status; +} + +static void test_async_stat_poll_not_found(opendal_test_context* ctx) +{ + opendal_result_operator_new async_res = + opendal_async_operator_from_operator(ctx->config->operator_instance); + OPENDAL_ASSERT_NO_ERROR(async_res.error, "Async operator creation should succeed"); + OPENDAL_ASSERT_NOT_NULL(async_res.op, "Async operator pointer must not be NULL"); + const opendal_async_operator* async_op = (const opendal_async_operator*)async_res.op; + const char* path = "async_stat_poll_missing.txt"; + + opendal_result_future_stat fut = opendal_async_operator_stat(async_op, path); + OPENDAL_ASSERT_NO_ERROR(fut.error, "Stat future creation should succeed"); + opendal_result_stat stat_out; + memset(&stat_out, 0, sizeof(stat_out)); + opendal_future_status status = poll_stat_ready(fut.future, &stat_out); + + OPENDAL_ASSERT(status == OPENDAL_FUTURE_READY, "Stat poll should eventually become ready"); + OPENDAL_ASSERT_NULL(stat_out.meta, "Metadata should be NULL for missing object"); + OPENDAL_ASSERT_NOT_NULL(stat_out.error, "Missing object should report an error"); + opendal_error_free(stat_out.error); + opendal_future_stat_free(fut.future); + opendal_async_operator_free(async_op); +} + +static opendal_future_status poll_read_ready(struct opendal_future_read* future, opendal_result_read* out) +{ + opendal_future_status status; + poll_until_ready(read_poll_bridge, future, out, &status); + return status; +} + +static opendal_future_status poll_write_ready(struct opendal_future_write* future, opendal_error** error_out) +{ + opendal_future_status status; + poll_until_ready(write_poll_bridge, future, error_out, &status); + return status; +} + +static void test_async_read_write_poll(opendal_test_context* ctx) +{ + opendal_result_operator_new async_res = + opendal_async_operator_from_operator(ctx->config->operator_instance); + OPENDAL_ASSERT_NO_ERROR(async_res.error, "Async operator creation should succeed"); + OPENDAL_ASSERT_NOT_NULL(async_res.op, "Async operator pointer must not be NULL"); + const opendal_async_operator* async_op = (const opendal_async_operator*)async_res.op; + const char* path = "async_read_write_poll.txt"; + const char* payload = "async read/write poll payload"; + + opendal_bytes data; + data.data = (uint8_t*)payload; + data.len = strlen(payload); + data.capacity = strlen(payload); + + opendal_result_future_write wf = opendal_async_operator_write(async_op, path, &data); + OPENDAL_ASSERT_NO_ERROR(wf.error, "Async write future creation should succeed"); + opendal_error* write_result = NULL; + opendal_future_status write_status = poll_write_ready(wf.future, &write_result); + OPENDAL_ASSERT(write_status == OPENDAL_FUTURE_READY, "Write poll should finish with READY"); + OPENDAL_ASSERT_NULL(write_result, "Write operation should succeed"); + opendal_future_write_free(wf.future); + + opendal_result_future_read rf = opendal_async_operator_read(async_op, path); + OPENDAL_ASSERT_NO_ERROR(rf.error, "Async read future creation should succeed"); + + opendal_result_read read_result; + memset(&read_result, 0, sizeof(read_result)); + opendal_future_status read_status = poll_read_ready(rf.future, &read_result); + OPENDAL_ASSERT(read_status == OPENDAL_FUTURE_READY, "Read poll should finish with READY"); + OPENDAL_ASSERT_NO_ERROR(read_result.error, "Async read should succeed"); + OPENDAL_ASSERT_EQ(data.len, read_result.data.len, "Read length should match written payload"); + OPENDAL_ASSERT(memcmp(read_result.data.data, data.data, data.len) == 0, "Read payload mismatch"); + + opendal_bytes_free(&read_result.data); + opendal_future_read_free(rf.future); + opendal_result_future_delete df = opendal_async_operator_delete(async_op, path); + OPENDAL_ASSERT_NO_ERROR(df.error, "Async delete future should be created successfully"); + opendal_error* del_err = opendal_future_delete_await(df.future); + OPENDAL_ASSERT_NO_ERROR(del_err, "Async delete should succeed"); + + opendal_async_operator_free(async_op); +} + +opendal_test_case async_tests[] = { + { "stat_await", test_async_stat_await, make_capability_write_stat() }, + { "stat_poll_not_found", test_async_stat_poll_not_found, make_capability_write_stat() }, + { "read_write_poll", test_async_read_write_poll, make_capability_read_write() }, +}; + +opendal_test_suite async_suite = { + "Async APIs", + async_tests, + sizeof(async_tests) / sizeof(async_tests[0]), +}; diff --git a/bindings/c/tests/test_suites_basic.cpp b/bindings/c/tests/test_suites_basic.cpp index af57ed9ab7f1..abc06946a984 100644 --- a/bindings/c/tests/test_suites_basic.cpp +++ b/bindings/c/tests/test_suites_basic.cpp @@ -20,13 +20,14 @@ #include "test_framework.h" // Test: Basic check operation -void test_check(opendal_test_context* ctx) { +void test_check(opendal_test_context* ctx) +{ // Get operator capabilities first opendal_operator_info* info = opendal_operator_info_new(ctx->config->operator_instance); OPENDAL_ASSERT_NOT_NULL(info, "Should be able to get operator info"); - + opendal_capability cap = opendal_operator_info_get_full_capability(info); - + if (cap.list) { // Only perform the standard check if list operations are supported opendal_error* error = opendal_operator_check(ctx->config->operator_instance); @@ -36,146 +37,151 @@ void test_check(opendal_test_context* ctx) { // If we got here, the operator is working properly printf("Note: Skipping list-based check for KV adapter\n"); } - + opendal_operator_info_free(info); } // Test: Basic write and read operation -void test_write_read(opendal_test_context* ctx) { +void test_write_read(opendal_test_context* ctx) +{ const char* path = "test_write_read.txt"; const char* content = "Hello, OpenDAL!"; - + // Write data opendal_bytes data; data.data = (uint8_t*)content; data.len = strlen(content); data.capacity = strlen(content); - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // Read data back opendal_result_read result = opendal_operator_read(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(result.error, "Read operation should succeed"); OPENDAL_ASSERT_EQ(strlen(content), result.data.len, "Read data length should match written data"); - + // Verify content OPENDAL_ASSERT(memcmp(content, result.data.data, result.data.len) == 0, "Read content should match written content"); - + // Cleanup opendal_bytes_free(&result.data); opendal_operator_delete(ctx->config->operator_instance, path); } // Test: Exists operation -void test_exists(opendal_test_context* ctx) { +void test_exists(opendal_test_context* ctx) +{ const char* path = "test_exists.txt"; const char* content = "test"; - + // File should not exist initially opendal_result_exists result = opendal_operator_exists(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(result.error, "Exists operation should succeed"); OPENDAL_ASSERT(!result.exists, "File should not exist initially"); - + // Write file opendal_bytes data; data.data = (uint8_t*)content; data.len = strlen(content); data.capacity = strlen(content); - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // File should exist now result = opendal_operator_exists(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(result.error, "Exists operation should succeed"); OPENDAL_ASSERT(result.exists, "File should exist after write"); - + // Cleanup opendal_operator_delete(ctx->config->operator_instance, path); } // Test: Stat operation -void test_stat(opendal_test_context* ctx) { +void test_stat(opendal_test_context* ctx) +{ const char* path = "test_stat.txt"; const char* content = "Hello, World!"; - + // Write file opendal_bytes data; data.data = (uint8_t*)content; data.len = strlen(content); data.capacity = strlen(content); - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // Stat file opendal_result_stat result = opendal_operator_stat(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(result.error, "Stat operation should succeed"); OPENDAL_ASSERT_NOT_NULL(result.meta, "Metadata should not be null"); - + // Check file properties OPENDAL_ASSERT(opendal_metadata_is_file(result.meta), "Should be identified as file"); OPENDAL_ASSERT(!opendal_metadata_is_dir(result.meta), "Should not be identified as directory"); OPENDAL_ASSERT_EQ(strlen(content), opendal_metadata_content_length(result.meta), "Content length should match"); - + // Cleanup opendal_metadata_free(result.meta); opendal_operator_delete(ctx->config->operator_instance, path); } // Test: Delete operation -void test_delete(opendal_test_context* ctx) { +void test_delete(opendal_test_context* ctx) +{ const char* path = "test_delete.txt"; const char* content = "to be deleted"; - + // Write file opendal_bytes data; data.data = (uint8_t*)content; data.len = strlen(content); data.capacity = strlen(content); - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // Verify file exists opendal_result_exists exists_result = opendal_operator_exists(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(exists_result.error, "Exists operation should succeed"); OPENDAL_ASSERT(exists_result.exists, "File should exist before deletion"); - + // Delete file error = opendal_operator_delete(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(error, "Delete operation should succeed"); - + // Verify file no longer exists exists_result = opendal_operator_exists(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(exists_result.error, "Exists operation should succeed"); OPENDAL_ASSERT(!exists_result.exists, "File should not exist after deletion"); - + // Delete should be idempotent error = opendal_operator_delete(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(error, "Delete operation should be idempotent"); } // Test: Create directory operation -void test_create_dir(opendal_test_context* ctx) { +void test_create_dir(opendal_test_context* ctx) +{ const char* dir_path = "test_dir/"; - + // Create directory opendal_error* error = opendal_operator_create_dir(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(error, "Create dir operation should succeed"); - + // Verify directory exists opendal_result_exists result = opendal_operator_exists(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(result.error, "Exists operation should succeed"); OPENDAL_ASSERT(result.exists, "Directory should exist after creation"); - + // Stat directory opendal_result_stat stat_result = opendal_operator_stat(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(stat_result.error, "Stat operation should succeed"); OPENDAL_ASSERT(opendal_metadata_is_dir(stat_result.meta), "Should be identified as directory"); OPENDAL_ASSERT(!opendal_metadata_is_file(stat_result.meta), "Should not be identified as file"); - + // Cleanup opendal_metadata_free(stat_result.meta); opendal_operator_delete(ctx->config->operator_instance, dir_path); @@ -183,16 +189,16 @@ void test_create_dir(opendal_test_context* ctx) { // Define the basic test suite opendal_test_case basic_tests[] = { - {"check", test_check, NO_CAPABILITY}, - {"write_read", test_write_read, make_capability_read_write()}, - {"exists", test_exists, make_capability_write()}, - {"stat", test_stat, make_capability_write_stat()}, - {"delete", test_delete, make_capability_write_delete()}, - {"create_dir", test_create_dir, make_capability_create_dir()}, + { "check", test_check, NO_CAPABILITY }, + { "write_read", test_write_read, make_capability_read_write() }, + { "exists", test_exists, make_capability_write() }, + { "stat", test_stat, make_capability_write_stat() }, + { "delete", test_delete, make_capability_write_delete() }, + { "create_dir", test_create_dir, make_capability_create_dir() }, }; opendal_test_suite basic_suite = { - "Basic Operations", // name - basic_tests, // tests - sizeof(basic_tests) / sizeof(basic_tests[0]) // test_count -}; \ No newline at end of file + "Basic Operations", // name + basic_tests, // tests + sizeof(basic_tests) / sizeof(basic_tests[0]) // test_count +}; \ No newline at end of file diff --git a/bindings/c/tests/test_suites_list.cpp b/bindings/c/tests/test_suites_list.cpp index b347525dcc1c..2d20d01aa9a9 100644 --- a/bindings/c/tests/test_suites_list.cpp +++ b/bindings/c/tests/test_suites_list.cpp @@ -22,17 +22,18 @@ #include // Test: Basic list operation -void test_list_basic(opendal_test_context* ctx) { +void test_list_basic(opendal_test_context* ctx) +{ const char* dir_path = "test_list_dir/"; - + // Create directory opendal_error* error = opendal_operator_create_dir(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(error, "Create dir operation should succeed"); - + // Create some test files - const char* test_files[] = {"test_list_dir/file1.txt", "test_list_dir/file2.txt", "test_list_dir/file3.txt"}; + const char* test_files[] = { "test_list_dir/file1.txt", "test_list_dir/file2.txt", "test_list_dir/file3.txt" }; const size_t num_files = sizeof(test_files) / sizeof(test_files[0]); - + for (size_t i = 0; i < num_files; i++) { opendal_bytes data; data.data = (uint8_t*)"test content"; @@ -41,12 +42,12 @@ void test_list_basic(opendal_test_context* ctx) { error = opendal_operator_write(ctx->config->operator_instance, test_files[i], &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); } - + // List directory opendal_result_list list_result = opendal_operator_list(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(list_result.error, "List operation should succeed"); OPENDAL_ASSERT_NOT_NULL(list_result.lister, "Lister should not be null"); - + // Collect all entries std::set found_paths; while (true) { @@ -55,32 +56,32 @@ void test_list_basic(opendal_test_context* ctx) { OPENDAL_ASSERT_NO_ERROR(next_result.error, "Lister next should not fail"); break; } - + if (!next_result.entry) { // End of list break; } - + char* path = opendal_entry_path(next_result.entry); OPENDAL_ASSERT_NOT_NULL(path, "Entry path should not be null"); found_paths.insert(std::string(path)); - + free(path); opendal_entry_free(next_result.entry); } - + // Check if the directory itself is included in the listing bool dir_included = found_paths.count(dir_path) > 0; - + // Verify we found all files, and optionally the directory itself size_t expected_count = num_files + (dir_included ? 1 : 0); OPENDAL_ASSERT_EQ(expected_count, found_paths.size(), "Should find all files and optionally the directory"); - + // All files should be present for (size_t i = 0; i < num_files; i++) { OPENDAL_ASSERT(found_paths.count(test_files[i]) > 0, "Should find all test files"); } - + // Cleanup opendal_lister_free(list_result.lister); for (size_t i = 0; i < num_files; i++) { @@ -90,18 +91,19 @@ void test_list_basic(opendal_test_context* ctx) { } // Test: List empty directory -void test_list_empty_dir(opendal_test_context* ctx) { +void test_list_empty_dir(opendal_test_context* ctx) +{ const char* dir_path = "test_empty_dir/"; - + // Create directory opendal_error* error = opendal_operator_create_dir(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(error, "Create dir operation should succeed"); - + // List directory opendal_result_list list_result = opendal_operator_list(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(list_result.error, "List operation should succeed"); OPENDAL_ASSERT_NOT_NULL(list_result.lister, "Lister should not be null"); - + // Collect entries std::set found_paths; while (true) { @@ -110,61 +112,62 @@ void test_list_empty_dir(opendal_test_context* ctx) { OPENDAL_ASSERT_NO_ERROR(next_result.error, "Lister next should not fail"); break; } - + if (!next_result.entry) { break; } - + char* path = opendal_entry_path(next_result.entry); found_paths.insert(std::string(path)); free(path); opendal_entry_free(next_result.entry); } - + // Some services include the directory itself, others don't bool dir_included = found_paths.count(dir_path) > 0; size_t expected_count = dir_included ? 1 : 0; OPENDAL_ASSERT_EQ(expected_count, found_paths.size(), "Should find empty listing or just the directory"); - + if (dir_included) { OPENDAL_ASSERT(found_paths.count(dir_path) > 0, "Should find the directory itself if it's included"); } - + // Cleanup opendal_lister_free(list_result.lister); opendal_operator_delete(ctx->config->operator_instance, dir_path); } // Test: List nested directories -void test_list_nested(opendal_test_context* ctx) { +void test_list_nested(opendal_test_context* ctx) +{ const char* base_dir = "test_nested/"; const char* sub_dir = "test_nested/subdir/"; const char* file_in_base = "test_nested/base_file.txt"; const char* file_in_sub = "test_nested/subdir/sub_file.txt"; - + // Create directories opendal_error* error = opendal_operator_create_dir(ctx->config->operator_instance, base_dir); OPENDAL_ASSERT_NO_ERROR(error, "Create base dir should succeed"); - + error = opendal_operator_create_dir(ctx->config->operator_instance, sub_dir); OPENDAL_ASSERT_NO_ERROR(error, "Create sub dir should succeed"); - + // Create files opendal_bytes data; data.data = (uint8_t*)"test content"; data.len = 12; data.capacity = 12; - + error = opendal_operator_write(ctx->config->operator_instance, file_in_base, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write to base dir should succeed"); - + error = opendal_operator_write(ctx->config->operator_instance, file_in_sub, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write to sub dir should succeed"); - + // List base directory opendal_result_list list_result = opendal_operator_list(ctx->config->operator_instance, base_dir); OPENDAL_ASSERT_NO_ERROR(list_result.error, "List operation should succeed"); - + std::set found_paths; while (true) { opendal_result_lister_next next_result = opendal_lister_next(list_result.lister); @@ -172,34 +175,34 @@ void test_list_nested(opendal_test_context* ctx) { OPENDAL_ASSERT_NO_ERROR(next_result.error, "Lister next should not fail"); break; } - + if (!next_result.entry) { break; } - + char* path = opendal_entry_path(next_result.entry); found_paths.insert(std::string(path)); free(path); opendal_entry_free(next_result.entry); } - + // Should find base dir, sub dir, and file in base bool base_dir_included = found_paths.count(base_dir) > 0; - size_t expected_count = 2 + (base_dir_included ? 1 : 0); // sub_dir + file_in_base + optionally base_dir + size_t expected_count = 2 + (base_dir_included ? 1 : 0); // sub_dir + file_in_base + optionally base_dir OPENDAL_ASSERT_EQ(expected_count, found_paths.size(), "Should find correct number of items in base directory"); - + // These should always be present OPENDAL_ASSERT(found_paths.count(sub_dir) > 0, "Should find sub directory"); OPENDAL_ASSERT(found_paths.count(file_in_base) > 0, "Should find file in base directory"); - + // Base directory may or may not be included depending on the service if (base_dir_included) { OPENDAL_ASSERT(found_paths.count(base_dir) > 0, "Should find base directory if it's included"); } - + // Should NOT find file in subdirectory when listing base directory non-recursively OPENDAL_ASSERT(found_paths.count(file_in_sub) == 0, "Should not find file in subdirectory"); - + // Cleanup opendal_lister_free(list_result.lister); opendal_operator_delete(ctx->config->operator_instance, file_in_sub); @@ -209,25 +212,26 @@ void test_list_nested(opendal_test_context* ctx) { } // Test: Entry name vs path -void test_entry_name_path(opendal_test_context* ctx) { +void test_entry_name_path(opendal_test_context* ctx) +{ const char* dir_path = "test_entry_names/"; const char* file_path = "test_entry_names/test_file.txt"; - + // Create directory and file opendal_error* error = opendal_operator_create_dir(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(error, "Create dir should succeed"); - + opendal_bytes data; data.data = (uint8_t*)"test"; data.len = 4; data.capacity = 4; error = opendal_operator_write(ctx->config->operator_instance, file_path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write should succeed"); - + // List directory opendal_result_list list_result = opendal_operator_list(ctx->config->operator_instance, dir_path); OPENDAL_ASSERT_NO_ERROR(list_result.error, "List operation should succeed"); - + bool found_file = false; while (true) { opendal_result_lister_next next_result = opendal_lister_next(list_result.lister); @@ -235,27 +239,27 @@ void test_entry_name_path(opendal_test_context* ctx) { OPENDAL_ASSERT_NO_ERROR(next_result.error, "Lister next should not fail"); break; } - + if (!next_result.entry) { break; } - + char* path = opendal_entry_path(next_result.entry); char* name = opendal_entry_name(next_result.entry); - + if (strcmp(path, file_path) == 0) { found_file = true; OPENDAL_ASSERT_STR_EQ("test_file.txt", name, "Entry name should be just the filename"); OPENDAL_ASSERT_STR_EQ(file_path, path, "Entry path should be the full path"); } - + free(path); free(name); opendal_entry_free(next_result.entry); } - + OPENDAL_ASSERT(found_file, "Should have found the test file"); - + // Cleanup opendal_lister_free(list_result.lister); opendal_operator_delete(ctx->config->operator_instance, file_path); @@ -264,14 +268,14 @@ void test_entry_name_path(opendal_test_context* ctx) { // Define the list test suite opendal_test_case list_tests[] = { - {"list_basic", test_list_basic, make_capability_write_create_dir_list()}, - {"list_empty_dir", test_list_empty_dir, make_capability_create_dir_list()}, - {"list_nested", test_list_nested, make_capability_write_create_dir_list()}, - {"entry_name_path", test_entry_name_path, make_capability_write_create_dir_list()}, + { "list_basic", test_list_basic, make_capability_write_create_dir_list() }, + { "list_empty_dir", test_list_empty_dir, make_capability_create_dir_list() }, + { "list_nested", test_list_nested, make_capability_write_create_dir_list() }, + { "entry_name_path", test_entry_name_path, make_capability_write_create_dir_list() }, }; opendal_test_suite list_suite = { - "List Operations", // name - list_tests, // tests - sizeof(list_tests) / sizeof(list_tests[0]) // test_count -}; \ No newline at end of file + "List Operations", // name + list_tests, // tests + sizeof(list_tests) / sizeof(list_tests[0]) // test_count +}; \ No newline at end of file diff --git a/bindings/c/tests/test_suites_reader_writer.cpp b/bindings/c/tests/test_suites_reader_writer.cpp index 1987c0035a3f..52cde1af93d9 100644 --- a/bindings/c/tests/test_suites_reader_writer.cpp +++ b/bindings/c/tests/test_suites_reader_writer.cpp @@ -20,170 +20,171 @@ #include "test_framework.h" // Test: Basic reader operations -void test_reader_basic(opendal_test_context* ctx) { +void test_reader_basic(opendal_test_context* ctx) +{ const char* path = "test_reader.txt"; const char* content = "Hello, OpenDAL Reader!"; size_t content_len = strlen(content); - + // Write test data first opendal_bytes data = { .data = (uint8_t*)content, .len = content_len, .capacity = content_len }; - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // Create reader opendal_result_operator_reader reader_result = opendal_operator_reader(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(reader_result.error, "Reader creation should succeed"); OPENDAL_ASSERT_NOT_NULL(reader_result.reader, "Reader should not be null"); - + // Read entire content uint8_t buffer[100]; opendal_result_reader_read read_result = opendal_reader_read(reader_result.reader, buffer, sizeof(buffer)); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read operation should succeed"); OPENDAL_ASSERT_EQ(content_len, read_result.size, "Read size should match content length"); - + // Verify content OPENDAL_ASSERT(memcmp(content, buffer, content_len) == 0, "Read content should match written content"); - + // Cleanup opendal_reader_free(reader_result.reader); opendal_operator_delete(ctx->config->operator_instance, path); } // Test: Reader seek operations -void test_reader_seek(opendal_test_context* ctx) { +void test_reader_seek(opendal_test_context* ctx) +{ const char* path = "test_reader_seek.txt"; const char* content = "0123456789ABCDEFGHIJ"; size_t content_len = strlen(content); - + // Write test data opendal_bytes data = { .data = (uint8_t*)content, .len = content_len, .capacity = content_len }; - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // Create reader opendal_result_operator_reader reader_result = opendal_operator_reader(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(reader_result.error, "Reader creation should succeed"); - + // Test seek from current position opendal_result_reader_seek seek_result = opendal_reader_seek(reader_result.reader, 5, OPENDAL_SEEK_CUR); OPENDAL_ASSERT_NO_ERROR(seek_result.error, "Seek from current should succeed"); OPENDAL_ASSERT_EQ(5, seek_result.pos, "Position should be 5"); - + // Read after seek uint8_t buffer[5]; opendal_result_reader_read read_result = opendal_reader_read(reader_result.reader, buffer, 5); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read after seek should succeed"); OPENDAL_ASSERT_EQ(5, read_result.size, "Should read 5 bytes"); OPENDAL_ASSERT(memcmp("56789", buffer, 5) == 0, "Should read correct content after seek"); - + // Test seek from beginning seek_result = opendal_reader_seek(reader_result.reader, 0, OPENDAL_SEEK_SET); OPENDAL_ASSERT_NO_ERROR(seek_result.error, "Seek from beginning should succeed"); OPENDAL_ASSERT_EQ(0, seek_result.pos, "Position should be 0"); - + // Read from beginning read_result = opendal_reader_read(reader_result.reader, buffer, 5); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read from beginning should succeed"); OPENDAL_ASSERT(memcmp("01234", buffer, 5) == 0, "Should read correct content from beginning"); - + // Test seek from end seek_result = opendal_reader_seek(reader_result.reader, -5, OPENDAL_SEEK_END); OPENDAL_ASSERT_NO_ERROR(seek_result.error, "Seek from end should succeed"); OPENDAL_ASSERT_EQ(content_len - 5, seek_result.pos, "Position should be content_len - 5"); - + // Read from near end read_result = opendal_reader_read(reader_result.reader, buffer, 5); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read from near end should succeed"); OPENDAL_ASSERT(memcmp("FGHIJ", buffer, 5) == 0, "Should read correct content from near end"); - + // Cleanup opendal_reader_free(reader_result.reader); opendal_operator_delete(ctx->config->operator_instance, path); } // Test: Basic writer operations -void test_writer_basic(opendal_test_context* ctx) { +void test_writer_basic(opendal_test_context* ctx) +{ const char* path = "test_writer.txt"; const char* content1 = "Hello, "; const char* content2 = "OpenDAL Writer!"; - + // Create writer opendal_result_operator_writer writer_result = opendal_operator_writer(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(writer_result.error, "Writer creation should succeed"); OPENDAL_ASSERT_NOT_NULL(writer_result.writer, "Writer should not be null"); - + // Write first part opendal_bytes data1; data1.data = (uint8_t*)content1; data1.len = strlen(content1); data1.capacity = strlen(content1); - + opendal_result_writer_write write_result = opendal_writer_write(writer_result.writer, &data1); OPENDAL_ASSERT_NO_ERROR(write_result.error, "First write should succeed"); OPENDAL_ASSERT_EQ(strlen(content1), write_result.size, "Write size should match content length"); - + // Write second part - handle OneShotWriter limitation opendal_bytes data2; data2.data = (uint8_t*)content2; data2.len = strlen(content2); data2.capacity = strlen(content2); - + write_result = opendal_writer_write(writer_result.writer, &data2); - + // Check if this is a OneShotWriter limitation - if (write_result.error != NULL && - write_result.error->message.data != NULL && - strstr((char*)write_result.error->message.data, "OneShotWriter doesn't support multiple write") != NULL) { + if (write_result.error != NULL && write_result.error->message.data != NULL && strstr((char*)write_result.error->message.data, "OneShotWriter doesn't support multiple write") != NULL) { printf("Note: Service uses OneShotWriter, skipping multiple write test\n"); - + // Close current writer and verify single write worked opendal_error* error = opendal_writer_close(writer_result.writer); OPENDAL_ASSERT_NO_ERROR(error, "Writer close should succeed"); - + // Verify first write content opendal_result_read read_result = opendal_operator_read(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read should succeed"); OPENDAL_ASSERT_EQ(strlen(content1), read_result.data.len, "Content length should match first write"); - OPENDAL_ASSERT(memcmp(content1, read_result.data.data, read_result.data.len) == 0, - "Content should match first write"); - + OPENDAL_ASSERT(memcmp(content1, read_result.data.data, read_result.data.len) == 0, + "Content should match first write"); + // Cleanup opendal_bytes_free(&read_result.data); opendal_writer_free(writer_result.writer); opendal_operator_delete(ctx->config->operator_instance, path); return; } - + OPENDAL_ASSERT_NO_ERROR(write_result.error, "Second write should succeed"); OPENDAL_ASSERT_EQ(strlen(content2), write_result.size, "Write size should match content length"); - + // Close writer opendal_error* error = opendal_writer_close(writer_result.writer); OPENDAL_ASSERT_NO_ERROR(error, "Writer close should succeed"); - + // Verify written content opendal_result_read read_result = opendal_operator_read(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read should succeed"); - + size_t expected_len = strlen(content1) + strlen(content2); OPENDAL_ASSERT_EQ(expected_len, read_result.data.len, "Total content length should match"); - + // Verify combined content char expected_content[100]; snprintf(expected_content, sizeof(expected_content), "%s%s", content1, content2); - OPENDAL_ASSERT(memcmp(expected_content, read_result.data.data, read_result.data.len) == 0, - "Combined content should match"); - + OPENDAL_ASSERT(memcmp(expected_content, read_result.data.data, read_result.data.len) == 0, + "Combined content should match"); + // Cleanup opendal_bytes_free(&read_result.data); opendal_writer_free(writer_result.writer); @@ -191,62 +192,61 @@ void test_writer_basic(opendal_test_context* ctx) { } // Test: Writer with large data -void test_writer_large_data(opendal_test_context* ctx) { +void test_writer_large_data(opendal_test_context* ctx) +{ const char* path = "test_writer_large.txt"; const size_t chunk_size = 1024; const size_t num_chunks = 10; - + // Create writer opendal_result_operator_writer writer_result = opendal_operator_writer(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(writer_result.error, "Writer creation should succeed"); - + // Generate and write chunks char* chunk_data = opendal_generate_random_content(chunk_size); OPENDAL_ASSERT_NOT_NULL(chunk_data, "Chunk data generation should succeed"); - + opendal_bytes chunk; chunk.data = (uint8_t*)chunk_data; chunk.len = chunk_size; chunk.capacity = chunk_size; - + size_t total_written = 0; bool is_one_shot_writer = false; - + for (size_t i = 0; i < num_chunks; i++) { opendal_result_writer_write write_result = opendal_writer_write(writer_result.writer, &chunk); - + // Check for OneShotWriter limitation on subsequent writes - if (i > 0 && write_result.error != NULL && - write_result.error->message.data != NULL && - strstr((char*)write_result.error->message.data, "OneShotWriter doesn't support multiple write") != NULL) { + if (i > 0 && write_result.error != NULL && write_result.error->message.data != NULL && strstr((char*)write_result.error->message.data, "OneShotWriter doesn't support multiple write") != NULL) { printf("Note: Service uses OneShotWriter, completed %zu write(s)\n", i); is_one_shot_writer = true; break; } - + OPENDAL_ASSERT_NO_ERROR(write_result.error, "Write should succeed"); OPENDAL_ASSERT_EQ(chunk_size, write_result.size, "Write size should match chunk size"); total_written += write_result.size; } - + // Close writer opendal_error* error = opendal_writer_close(writer_result.writer); OPENDAL_ASSERT_NO_ERROR(error, "Writer close should succeed"); - + // Verify total size - adjust expectations for OneShotWriter opendal_result_stat stat_result = opendal_operator_stat(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(stat_result.error, "Stat should succeed"); - + if (is_one_shot_writer) { // For OneShotWriter, we expect only one chunk to be written - OPENDAL_ASSERT_EQ(chunk_size, opendal_metadata_content_length(stat_result.meta), - "OneShotWriter should have written one chunk"); + OPENDAL_ASSERT_EQ(chunk_size, opendal_metadata_content_length(stat_result.meta), + "OneShotWriter should have written one chunk"); } else { // For normal writers, we expect all chunks - OPENDAL_ASSERT_EQ(chunk_size * num_chunks, opendal_metadata_content_length(stat_result.meta), - "Total file size should match all chunks"); + OPENDAL_ASSERT_EQ(chunk_size * num_chunks, opendal_metadata_content_length(stat_result.meta), + "Total file size should match all chunks"); } - + // Cleanup free(chunk_data); opendal_metadata_free(stat_result.meta); @@ -255,46 +255,47 @@ void test_writer_large_data(opendal_test_context* ctx) { } // Test: Reader partial read -void test_reader_partial_read(opendal_test_context* ctx) { +void test_reader_partial_read(opendal_test_context* ctx) +{ const char* path = "test_reader_partial.txt"; const char* content = "0123456789ABCDEFGHIJ0123456789"; size_t content_len = strlen(content); - + // Write test data opendal_bytes data; data.data = (uint8_t*)content; data.len = content_len; data.capacity = content_len; - + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); - + // Create reader opendal_result_operator_reader reader_result = opendal_operator_reader(ctx->config->operator_instance, path); OPENDAL_ASSERT_NO_ERROR(reader_result.error, "Reader creation should succeed"); - + // Read in small chunks const size_t chunk_size = 5; uint8_t buffer[chunk_size]; size_t total_read = 0; - + while (total_read < content_len) { opendal_result_reader_read read_result = opendal_reader_read(reader_result.reader, buffer, chunk_size); OPENDAL_ASSERT_NO_ERROR(read_result.error, "Read should succeed"); - + if (read_result.size == 0) { - break; // EOF + break; // EOF } - + // Verify chunk content - OPENDAL_ASSERT(memcmp(content + total_read, buffer, read_result.size) == 0, - "Chunk content should match"); - + OPENDAL_ASSERT(memcmp(content + total_read, buffer, read_result.size) == 0, + "Chunk content should match"); + total_read += read_result.size; } - + OPENDAL_ASSERT_EQ(content_len, total_read, "Total read should match content length"); - + // Cleanup opendal_reader_free(reader_result.reader); opendal_operator_delete(ctx->config->operator_instance, path); @@ -302,15 +303,15 @@ void test_reader_partial_read(opendal_test_context* ctx) { // Define the reader/writer test suite opendal_test_case reader_writer_tests[] = { - {"reader_basic", test_reader_basic, make_capability_read_write()}, - {"reader_seek", test_reader_seek, make_capability_read_write()}, - {"writer_basic", test_writer_basic, make_capability_read_write()}, - {"writer_large_data", test_writer_large_data, make_capability_write_stat()}, - {"reader_partial_read", test_reader_partial_read, make_capability_read_write()}, + { "reader_basic", test_reader_basic, make_capability_read_write() }, + { "reader_seek", test_reader_seek, make_capability_read_write() }, + { "writer_basic", test_writer_basic, make_capability_read_write() }, + { "writer_large_data", test_writer_large_data, make_capability_write_stat() }, + { "reader_partial_read", test_reader_partial_read, make_capability_read_write() }, }; opendal_test_suite reader_writer_suite = { - "Reader and Writer Operations", // name - reader_writer_tests, // tests - sizeof(reader_writer_tests) / sizeof(reader_writer_tests[0]) // test_count -}; \ No newline at end of file + "Reader and Writer Operations", // name + reader_writer_tests, // tests + sizeof(reader_writer_tests) / sizeof(reader_writer_tests[0]) // test_count +}; \ No newline at end of file