import asyncio
import os
import sys
import tempfile
import shutil
from pathlib import Path
repo_root = Path(__file__).parent.parent.parent.parent
bindings_path = repo_root / "target" / "bindings" / "python"
lib_path = repo_root / "target" / "release"
lib_file = "libcdk_ffi.dylib" if sys.platform == "darwin" else "libcdk_ffi.so"
src_lib = lib_path / lib_file
dst_lib = bindings_path / lib_file
if src_lib.exists() and not dst_lib.exists():
shutil.copy2(src_lib, dst_lib)
sys.path.insert(0, str(bindings_path))
import cdk_ffi
def create_test_db():
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
db_path = tmp.name
tmp.close()
backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
db = cdk_ffi.create_wallet_db(backend)
return db, db_path
def create_test_memory_db():
backend = cdk_ffi.WalletDbBackend.SQLITE(path=":memory:")
return cdk_ffi.create_wallet_db(backend)
def cleanup_db(db_path):
if os.path.exists(db_path):
os.unlink(db_path)
async def test_kv_write_and_read():
print("\n=== Test: KV Write and Read ===")
db, db_path = create_test_db()
try:
test_data = b"Hello, KVStore!"
await db.kv_write("app", "config", "greeting", test_data)
print(" Written value to KV store")
result = await db.kv_read("app", "config", "greeting")
assert result is not None, "Expected to read back the value"
assert bytes(result) == test_data, f"Expected {test_data}, got {bytes(result)}"
print(" Read back correct value")
print(" Test passed: KV write and read work")
finally:
cleanup_db(db_path)
async def test_kv_read_nonexistent():
print("\n=== Test: KV Read Nonexistent Key ===")
db, db_path = create_test_db()
try:
result = await db.kv_read("nonexistent", "namespace", "key")
assert result is None, f"Expected None for nonexistent key, got {result}"
print(" Correctly returns None for nonexistent key")
print(" Test passed: Reading nonexistent key returns None")
finally:
cleanup_db(db_path)
async def test_kv_overwrite():
print("\n=== Test: KV Overwrite ===")
db, db_path = create_test_db()
try:
await db.kv_write("app", "data", "counter", b"1")
print(" Written initial value")
await db.kv_write("app", "data", "counter", b"42")
print(" Overwrote with new value")
result = await db.kv_read("app", "data", "counter")
assert result is not None, "Expected to read back the value"
assert bytes(result) == b"42", f"Expected b'42', got {bytes(result)}"
print(" Read back overwritten value")
print(" Test passed: KV overwrite works")
finally:
cleanup_db(db_path)
async def test_kv_remove():
print("\n=== Test: KV Remove ===")
db, db_path = create_test_db()
try:
await db.kv_write("app", "temp", "to_delete", b"delete me")
print(" Written value to delete")
result = await db.kv_read("app", "temp", "to_delete")
assert result is not None, "Value should exist before removal"
print(" Verified value exists")
await db.kv_remove("app", "temp", "to_delete")
print(" Removed value")
result_after = await db.kv_read("app", "temp", "to_delete")
assert result_after is None, f"Expected None after removal, got {result_after}"
print(" Verified value is removed")
print(" Test passed: KV remove works")
finally:
cleanup_db(db_path)
async def test_kv_list_keys():
print("\n=== Test: KV List Keys ===")
db, db_path = create_test_db()
try:
await db.kv_write("myapp", "settings", "theme", b"dark")
await db.kv_write("myapp", "settings", "language", b"en")
await db.kv_write("myapp", "settings", "timezone", b"UTC")
await db.kv_write("myapp", "other", "unrelated", b"data")
print(" Written multiple keys")
keys = await db.kv_list("myapp", "settings")
assert len(keys) == 3, f"Expected 3 keys, got {len(keys)}"
assert "theme" in keys, "Expected 'theme' in keys"
assert "language" in keys, "Expected 'language' in keys"
assert "timezone" in keys, "Expected 'timezone' in keys"
assert "unrelated" not in keys, "'unrelated' should not be in settings namespace"
print(f" Listed keys: {keys}")
print(" Test passed: KV list works")
finally:
cleanup_db(db_path)
async def test_kv_list_empty_namespace():
print("\n=== Test: KV List Empty Namespace ===")
db, db_path = create_test_db()
try:
keys = await db.kv_list("nonexistent", "namespace")
assert isinstance(keys, list), "Expected a list"
assert len(keys) == 0, f"Expected empty list, got {keys}"
print(" Empty namespace returns empty list")
print(" Test passed: KV list on empty namespace works")
finally:
cleanup_db(db_path)
async def test_kv_namespace_isolation():
print("\n=== Test: KV Namespace Isolation ===")
db, db_path = create_test_db()
try:
await db.kv_write("app1", "config", "key", b"app1_value")
await db.kv_write("app2", "config", "key", b"app2_value")
await db.kv_write("app1", "other", "key", b"app1_other_value")
print(" Written same key in different namespaces")
result1 = await db.kv_read("app1", "config", "key")
result2 = await db.kv_read("app2", "config", "key")
result3 = await db.kv_read("app1", "other", "key")
assert bytes(result1) == b"app1_value", f"Expected b'app1_value', got {bytes(result1)}"
assert bytes(result2) == b"app2_value", f"Expected b'app2_value', got {bytes(result2)}"
assert bytes(result3) == b"app1_other_value", f"Expected b'app1_other_value', got {bytes(result3)}"
print(" Each namespace has correct value")
print(" Test passed: KV namespace isolation works")
finally:
cleanup_db(db_path)
async def test_kv_binary_data():
print("\n=== Test: KV Binary Data ===")
db, db_path = create_test_db()
try:
test_cases = [
("empty", b""),
("null_byte", b"\x00"),
("all_bytes", bytes(range(256))),
("utf8_special", "Hello World".encode("utf-8")),
("random_binary", bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE])),
]
for name, data in test_cases:
await db.kv_write("binary", "test", name, data)
print(f" Written {len(test_cases)} binary test cases")
for name, expected_data in test_cases:
result = await db.kv_read("binary", "test", name)
assert result is not None, f"Expected data for {name}"
actual_data = bytes(result)
assert actual_data == expected_data, f"Mismatch for {name}: expected {expected_data!r}, got {actual_data!r}"
print(f" '{name}': OK ({len(actual_data)} bytes)")
print(" Test passed: KV binary data works")
finally:
cleanup_db(db_path)
async def test_kv_large_value():
print("\n=== Test: KV Large Value ===")
db, db_path = create_test_db()
try:
large_data = bytes([i % 256 for i in range(1024 * 1024)])
await db.kv_write("large", "data", "megabyte", large_data)
print(f" Written {len(large_data)} bytes")
result = await db.kv_read("large", "data", "megabyte")
assert result is not None, "Expected to read large value"
result_bytes = bytes(result)
assert len(result_bytes) == len(large_data), f"Size mismatch: {len(result_bytes)} vs {len(large_data)}"
assert result_bytes == large_data, "Data mismatch"
print(f" Read back {len(result_bytes)} bytes correctly")
print(" Test passed: KV large value works")
finally:
cleanup_db(db_path)
async def test_kv_special_key_names():
print("\n=== Test: KV Special Key Names ===")
db, db_path = create_test_db()
try:
special_keys = [
"simple",
"with-dashes",
"with_underscores",
"MixedCase",
"numbers123",
"unicode_", "empty_value",
]
for i, key in enumerate(special_keys):
await db.kv_write("special", "keys", key, f"value_{i}".encode())
print(f" Written {len(special_keys)} special keys")
keys = await db.kv_list("special", "keys")
assert len(keys) == len(special_keys), f"Expected {len(special_keys)} keys, got {len(keys)}"
for key in special_keys:
assert key in keys, f"Key '{key}' not found in list"
print(f" All special keys stored and listed correctly")
print(" Test passed: KV special key names work")
finally:
cleanup_db(db_path)
async def test_kv_persistence_across_instances():
print("\n=== Test: KV Persistence Across Instances ===")
db_path = None
try:
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
db_path = tmp.name
backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
db1 = cdk_ffi.create_wallet_db(backend)
await db1.kv_write("persist", "test", "mykey", b"persistent_value")
print(" Written and committed with first db instance")
del db1
await asyncio.sleep(0.1)
print(" First db instance closed")
backend2 = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
db2 = cdk_ffi.create_wallet_db(backend2)
result = await db2.kv_read("persist", "test", "mykey")
assert result is not None, "Data should persist across db instances"
assert bytes(result) == b"persistent_value", f"Expected b'persistent_value', got {bytes(result)}"
print(" Data persisted across db instances")
print(" Test passed: KV persistence across instances works")
finally:
if db_path and os.path.exists(db_path):
os.unlink(db_path)
async def test_kv_in_memory_concurrent_access():
print("\n=== Test: KV In-Memory Concurrent Access ===")
db = create_test_memory_db()
async def write_and_read(index):
key = f"key_{index}"
value = f"value_{index}".encode()
await db.kv_write("memory", "concurrent", key, value)
result = await db.kv_read("memory", "concurrent", key)
assert result is not None, f"Expected value for {key}"
assert bytes(result) == value, f"Expected {value}, got {bytes(result)}"
await asyncio.wait_for(
asyncio.gather(*(write_and_read(index) for index in range(64))),
timeout=10.0,
)
keys = await db.kv_list("memory", "concurrent")
assert len(keys) == 64, f"Expected 64 keys, got {len(keys)}"
print(" Test passed: concurrent in-memory KV access works")
async def main():
print("Starting CDK FFI Key-Value Store Tests")
print("=" * 60)
tests = [
("KV Write and Read", test_kv_write_and_read),
("KV Read Nonexistent", test_kv_read_nonexistent),
("KV Overwrite", test_kv_overwrite),
("KV Remove", test_kv_remove),
("KV List Keys", test_kv_list_keys),
("KV List Empty Namespace", test_kv_list_empty_namespace),
("KV Namespace Isolation", test_kv_namespace_isolation),
("KV Binary Data", test_kv_binary_data),
("KV Large Value", test_kv_large_value),
("KV Special Key Names", test_kv_special_key_names),
("KV Persistence Across Instances", test_kv_persistence_across_instances),
("KV In-Memory Concurrent Access", test_kv_in_memory_concurrent_access),
]
passed = 0
failed = 0
for test_name, test_func in tests:
try:
await test_func()
passed += 1
except Exception as e:
failed += 1
print(f"\n Test failed: {test_name}")
print(f" Error: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)
print(f"Test Results: {passed} passed, {failed} failed")
print("=" * 60)
return 0 if failed == 0 else 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)