Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions liboxia-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ edition = "2021"

[lib]
name = "liboxia_ffi"
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]


[dependencies]
liboxia = { workspace = true }
tokio = { workspace = true }
libc = { workspace = true }
libc = { workspace = true }

[dev-dependencies]
testcontainers = "0.23"
tokio = { workspace = true }
238 changes: 238 additions & 0 deletions liboxia-ffi/tests/c/test_ffi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../liboxia_ffi.h"

#define ASSERT(cond, msg) do { \
if (!(cond)) { \
fprintf(stderr, "FAIL: %s (line %d): %s\n", __func__, __LINE__, msg); \
return 1; \
} \
} while(0)

#define ASSERT_EQ_INT(a, b, msg) do { \
if ((a) != (b)) { \
fprintf(stderr, "FAIL: %s (line %d): %s (expected %d, got %d)\n", \
__func__, __LINE__, msg, (int)(b), (int)(a)); \
return 1; \
} \
} while(0)

static OxiaClient* client = NULL;

static int setup(const char* address) {
COxiaClientOptions options;
options.service_address = address;
options.namespace_ = "default";

COxiaError err = oxia_client_new(options, &client);
ASSERT_EQ_INT(err, Ok, "oxia_client_new should succeed");
ASSERT(client != NULL, "client should not be NULL");
return 0;
}

/* Test: put and get a key */
static int test_put_get(void) {
const char* key = "c-test/key1";
const char* value = "hello-from-c";

/* Put */
COxiaPutResult* put_result = NULL;
COxiaError err = oxia_client_put(client, key,
(const uint8_t*)value, strlen(value), &put_result);
ASSERT_EQ_INT(err, Ok, "put should succeed");
ASSERT(put_result != NULL, "put_result should not be NULL");
ASSERT(put_result->version_id >= 0, "version_id should be non-negative");
ASSERT(strcmp(put_result->key, key) == 0, "put result key should match");
oxia_put_result_free(put_result);

/* Get */
COxiaGetResult* get_result = NULL;
err = oxia_client_get(client, key, &get_result);
ASSERT_EQ_INT(err, Ok, "get should succeed");
ASSERT(get_result != NULL, "get_result should not be NULL");
ASSERT(strcmp(get_result->key, key) == 0, "get result key should match");
ASSERT(get_result->value_len == strlen(value), "value length should match");
ASSERT(memcmp(get_result->value, value, get_result->value_len) == 0,
"value content should match");
ASSERT(get_result->version_id >= 0, "version_id should be non-negative");
oxia_get_result_free(get_result);

printf(" PASS: test_put_get\n");
return 0;
}

/* Test: get non-existent key returns KeyNotFound */
static int test_get_not_found(void) {
COxiaGetResult* get_result = NULL;
COxiaError err = oxia_client_get(client, "c-test/nonexistent", &get_result);
ASSERT_EQ_INT(err, KeyNotFound, "get non-existent key should return KeyNotFound");
ASSERT(get_result == NULL, "get_result should be NULL on error");

printf(" PASS: test_get_not_found\n");
return 0;
}

/* Test: put, delete, then get should return KeyNotFound */
static int test_delete(void) {
const char* key = "c-test/to-delete";
const char* value = "deleteme";

COxiaPutResult* put_result = NULL;
COxiaError err = oxia_client_put(client, key,
(const uint8_t*)value, strlen(value), &put_result);
ASSERT_EQ_INT(err, Ok, "put should succeed");
oxia_put_result_free(put_result);

err = oxia_client_delete(client, key);
ASSERT_EQ_INT(err, Ok, "delete should succeed");

COxiaGetResult* get_result = NULL;
err = oxia_client_get(client, key, &get_result);
ASSERT_EQ_INT(err, KeyNotFound, "get after delete should return KeyNotFound");

printf(" PASS: test_delete\n");
return 0;
}

/* Test: list keys in range */
static int test_list(void) {
const char* keys[] = {"c-test/list/a", "c-test/list/b", "c-test/list/c"};
const char* values[] = {"val-a", "val-b", "val-c"};

for (int i = 0; i < 3; i++) {
COxiaPutResult* put_result = NULL;
COxiaError err = oxia_client_put(client, keys[i],
(const uint8_t*)values[i], strlen(values[i]), &put_result);
ASSERT_EQ_INT(err, Ok, "put should succeed");
oxia_put_result_free(put_result);
}

COxiaListResult* list_result = NULL;
COxiaError err = oxia_client_list(client, "c-test/list/", "c-test/list/~", &list_result);
ASSERT_EQ_INT(err, Ok, "list should succeed");
ASSERT(list_result != NULL, "list_result should not be NULL");
ASSERT(list_result->keys_len == 3, "should have 3 keys");

/* Keys should be sorted */
for (int i = 0; i < 3; i++) {
ASSERT(strcmp(list_result->keys[i], keys[i]) == 0, "key should match");
}
oxia_list_result_free(list_result);

printf(" PASS: test_list\n");
return 0;
}

/* Test: delete_range removes all keys in range */
static int test_delete_range(void) {
const char* keys[] = {"c-test/range/x", "c-test/range/y", "c-test/range/z"};
const char* values[] = {"vx", "vy", "vz"};

for (int i = 0; i < 3; i++) {
COxiaPutResult* put_result = NULL;
COxiaError err = oxia_client_put(client, keys[i],
(const uint8_t*)values[i], strlen(values[i]), &put_result);
ASSERT_EQ_INT(err, Ok, "put should succeed");
oxia_put_result_free(put_result);
}

COxiaError err = oxia_client_delete_range(client, "c-test/range/", "c-test/range/~");
ASSERT_EQ_INT(err, Ok, "delete_range should succeed");

/* Verify all keys are gone */
COxiaListResult* list_result = NULL;
err = oxia_client_list(client, "c-test/range/", "c-test/range/~", &list_result);
ASSERT_EQ_INT(err, Ok, "list should succeed");
ASSERT(list_result != NULL, "list_result should not be NULL");
ASSERT(list_result->keys_len == 0, "should have 0 keys after delete_range");
oxia_list_result_free(list_result);

printf(" PASS: test_delete_range\n");
return 0;
}

/* Test: overwrite a key and verify version changes */
static int test_put_overwrite(void) {
const char* key = "c-test/overwrite";

COxiaPutResult* r1 = NULL;
COxiaError err = oxia_client_put(client, key,
(const uint8_t*)"v1", 2, &r1);
ASSERT_EQ_INT(err, Ok, "first put should succeed");
int64_t v1 = r1->version_id;
oxia_put_result_free(r1);

COxiaPutResult* r2 = NULL;
err = oxia_client_put(client, key,
(const uint8_t*)"v2", 2, &r2);
ASSERT_EQ_INT(err, Ok, "second put should succeed");
ASSERT(r2->version_id > v1, "version should increase on overwrite");
oxia_put_result_free(r2);

/* Verify latest value */
COxiaGetResult* get_result = NULL;
err = oxia_client_get(client, key, &get_result);
ASSERT_EQ_INT(err, Ok, "get should succeed");
ASSERT(get_result->value_len == 2, "value length should be 2");
ASSERT(memcmp(get_result->value, "v2", 2) == 0, "value should be v2");
oxia_get_result_free(get_result);

printf(" PASS: test_put_overwrite\n");
return 0;
}

/* Test: binary data (not just strings) */
static int test_binary_value(void) {
const char* key = "c-test/binary";
uint8_t binary_data[] = {0x00, 0x01, 0xFF, 0xFE, 0x80, 0x7F};

COxiaPutResult* put_result = NULL;
COxiaError err = oxia_client_put(client, key,
binary_data, sizeof(binary_data), &put_result);
ASSERT_EQ_INT(err, Ok, "put binary should succeed");
oxia_put_result_free(put_result);

COxiaGetResult* get_result = NULL;
err = oxia_client_get(client, key, &get_result);
ASSERT_EQ_INT(err, Ok, "get binary should succeed");
ASSERT(get_result->value_len == sizeof(binary_data), "binary value length should match");
ASSERT(memcmp(get_result->value, binary_data, sizeof(binary_data)) == 0,
"binary value content should match");
oxia_get_result_free(get_result);

printf(" PASS: test_binary_value\n");
return 0;
}

int main(int argc, char* argv[]) {
const char* address = getenv("OXIA_ADDRESS");
if (!address || strlen(address) == 0) {
fprintf(stderr, "OXIA_ADDRESS environment variable is required\n");
return 1;
}

printf("Running C FFI tests against %s\n", address);

if (setup(address) != 0) return 1;

int failures = 0;
failures += test_put_get();
failures += test_get_not_found();
failures += test_delete();
failures += test_list();
failures += test_delete_range();
failures += test_put_overwrite();
failures += test_binary_value();

/* Cleanup: delete all test keys */
oxia_client_delete_range(client, "c-test/", "c-test/~");

COxiaError err = oxia_client_shutdown(client);
if (err != Ok) {
fprintf(stderr, "WARNING: shutdown returned error %d\n", err);
}

printf("\n%s: %d test(s) failed\n", failures == 0 ? "ALL PASSED" : "FAILED", failures);
return failures;
}
92 changes: 92 additions & 0 deletions liboxia-ffi/tests/c_ffi_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::process::Command;
use testcontainers::core::ports::ContainerPort;
use testcontainers::core::wait::WaitFor;
use testcontainers::runners::AsyncRunner;
use testcontainers::{GenericImage, ImageExt};

const OXIA_PORT: u16 = 6648;
const DEFAULT_OXIA_IMAGE: &str = "oxia/oxia";
const DEFAULT_OXIA_TAG: &str = "main";

#[tokio::test]
async fn test_c_ffi_integration() {
let image = std::env::var("OXIA_IMAGE").unwrap_or_else(|_| DEFAULT_OXIA_IMAGE.to_string());
let tag = std::env::var("OXIA_TAG").unwrap_or_else(|_| DEFAULT_OXIA_TAG.to_string());

// Start Oxia container
let container = GenericImage::new(image, tag)
.with_exposed_port(ContainerPort::Tcp(OXIA_PORT))
.with_wait_for(WaitFor::message_on_stdout("Started Grpc server"))
.with_cmd(vec!["oxia", "standalone"])
.start()
.await
.expect("Failed to start Oxia container");

let host_port = container
.get_host_port_ipv4(OXIA_PORT)
.await
.expect("Failed to get host port");

let address = format!("http://127.0.0.1:{}", host_port);

// Find the library output directory
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_dir = std::path::Path::new(manifest_dir)
.parent()
.expect("Failed to find workspace dir");

// Build the FFI library first (should already be built by cargo test)
let target_dir = workspace_dir.join("target").join("debug");

let c_test_src = std::path::Path::new(manifest_dir)
.join("tests")
.join("c")
.join("test_ffi.c");
let c_test_bin = target_dir.join("test_ffi_c");

// Compile the C test
let compile = Command::new("cc")
.args([
c_test_src.to_str().unwrap(),
"-o",
c_test_bin.to_str().unwrap(),
&format!("-L{}", target_dir.display()),
"-lliboxia_ffi",
&format!("-Wl,-rpath,{}", target_dir.display()),
"-Wall",
"-Wextra",
])
.output()
.expect("Failed to run cc compiler");

assert!(
compile.status.success(),
"C test compilation failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&compile.stdout),
String::from_utf8_lossy(&compile.stderr)
);

// Run the C test
let run = Command::new(c_test_bin.to_str().unwrap())
.env("OXIA_ADDRESS", &address)
.env("LD_LIBRARY_PATH", target_dir.to_str().unwrap())
.env("DYLD_LIBRARY_PATH", target_dir.to_str().unwrap())
.output()
.expect("Failed to run C test binary");

let stdout = String::from_utf8_lossy(&run.stdout);
let stderr = String::from_utf8_lossy(&run.stderr);

println!("C test stdout:\n{}", stdout);
if !stderr.is_empty() {
eprintln!("C test stderr:\n{}", stderr);
}

assert!(
run.status.success(),
"C FFI test failed with exit code {:?}\nstdout: {}\nstderr: {}",
run.status.code(),
stdout,
stderr
);
}
Loading