|
|
@@ -0,0 +1,648 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""
|
|
|
+Test suite for CDK FFI Key-Value Store operations
|
|
|
+
|
|
|
+Tests the KVStore trait functionality exposed through the FFI bindings,
|
|
|
+including read, write, list, and remove operations with transaction support.
|
|
|
+"""
|
|
|
+
|
|
|
+import asyncio
|
|
|
+import os
|
|
|
+import sys
|
|
|
+import tempfile
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+# Setup paths before importing cdk_ffi
|
|
|
+repo_root = Path(__file__).parent.parent.parent.parent
|
|
|
+bindings_path = repo_root / "target" / "bindings" / "python"
|
|
|
+lib_path = repo_root / "target" / "release"
|
|
|
+
|
|
|
+# Copy the library to the bindings directory so Python can find it
|
|
|
+import shutil
|
|
|
+
|
|
|
+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)
|
|
|
+
|
|
|
+# Add target/bindings/python to path to load cdk_ffi module
|
|
|
+sys.path.insert(0, str(bindings_path))
|
|
|
+
|
|
|
+import cdk_ffi
|
|
|
+
|
|
|
+# Helper functions
|
|
|
+
|
|
|
+def create_test_db():
|
|
|
+ """Create a temporary SQLite database for testing"""
|
|
|
+ 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 cleanup_db(db_path):
|
|
|
+ """Clean up the temporary database file"""
|
|
|
+ if os.path.exists(db_path):
|
|
|
+ os.unlink(db_path)
|
|
|
+
|
|
|
+
|
|
|
+# Basic KV Store Tests
|
|
|
+
|
|
|
+async def test_kv_write_and_read():
|
|
|
+ """Test basic write and read operations"""
|
|
|
+ print("\n=== Test: KV Write and Read ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write a value using KV transaction
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ test_data = b"Hello, KVStore!"
|
|
|
+ await kv_tx.kv_write("app", "config", "greeting", test_data)
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written value to KV store")
|
|
|
+
|
|
|
+ # Read it back using a new transaction
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx2.kv_read("app", "config", "greeting")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ 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():
|
|
|
+ """Test reading a key that doesn't exist"""
|
|
|
+ print("\n=== Test: KV Read Nonexistent Key ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx.kv_read("nonexistent", "namespace", "key")
|
|
|
+ await kv_tx.rollback()
|
|
|
+
|
|
|
+ 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():
|
|
|
+ """Test overwriting an existing value"""
|
|
|
+ print("\n=== Test: KV Overwrite ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write initial value
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("app", "data", "counter", b"1")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written initial value")
|
|
|
+
|
|
|
+ # Overwrite with new value
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ await kv_tx2.kv_write("app", "data", "counter", b"42")
|
|
|
+ await kv_tx2.commit()
|
|
|
+ print(" Overwrote with new value")
|
|
|
+
|
|
|
+ # Read back
|
|
|
+ kv_tx3 = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx3.kv_read("app", "data", "counter")
|
|
|
+ await kv_tx3.rollback()
|
|
|
+
|
|
|
+ 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():
|
|
|
+ """Test removing a key"""
|
|
|
+ print("\n=== Test: KV Remove ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write a value
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("app", "temp", "to_delete", b"delete me")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written value to delete")
|
|
|
+
|
|
|
+ # Verify it exists
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx2.kv_read("app", "temp", "to_delete")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+ assert result is not None, "Value should exist before removal"
|
|
|
+ print(" Verified value exists")
|
|
|
+
|
|
|
+ # Remove it
|
|
|
+ kv_tx3 = await db.begin_db_transaction()
|
|
|
+ await kv_tx3.kv_remove("app", "temp", "to_delete")
|
|
|
+ await kv_tx3.commit()
|
|
|
+ print(" Removed value")
|
|
|
+
|
|
|
+ # Verify it's gone
|
|
|
+ kv_tx4 = await db.begin_db_transaction()
|
|
|
+ result_after = await kv_tx4.kv_read("app", "temp", "to_delete")
|
|
|
+ await kv_tx4.rollback()
|
|
|
+
|
|
|
+ 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():
|
|
|
+ """Test listing keys in a namespace"""
|
|
|
+ print("\n=== Test: KV List Keys ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write multiple keys
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("myapp", "settings", "theme", b"dark")
|
|
|
+ await kv_tx.kv_write("myapp", "settings", "language", b"en")
|
|
|
+ await kv_tx.kv_write("myapp", "settings", "timezone", b"UTC")
|
|
|
+ await kv_tx.kv_write("myapp", "other", "unrelated", b"data")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written multiple keys")
|
|
|
+
|
|
|
+ # List keys in the settings namespace
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ keys = await kv_tx2.kv_list("myapp", "settings")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ 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():
|
|
|
+ """Test listing keys in an empty or nonexistent namespace"""
|
|
|
+ print("\n=== Test: KV List Empty Namespace ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ keys = await kv_tx.kv_list("nonexistent", "namespace")
|
|
|
+ await kv_tx.rollback()
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+
|
|
|
+# Transaction Tests
|
|
|
+
|
|
|
+async def test_kv_transaction_commit():
|
|
|
+ """Test that KV changes persist after commit"""
|
|
|
+ print("\n=== Test: KV Transaction Commit ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write and commit
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("test", "commit", "key1", b"committed")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written and committed")
|
|
|
+
|
|
|
+ # Verify in new transaction
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx2.kv_read("test", "commit", "key1")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ assert result is not None, "Value should persist after commit"
|
|
|
+ assert bytes(result) == b"committed", f"Expected b'committed', got {bytes(result)}"
|
|
|
+ print(" Value persists after commit")
|
|
|
+
|
|
|
+ print(" Test passed: KV transaction commit works")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ cleanup_db(db_path)
|
|
|
+
|
|
|
+
|
|
|
+async def test_kv_transaction_rollback():
|
|
|
+ """Test that KV changes are reverted after rollback"""
|
|
|
+ print("\n=== Test: KV Transaction Rollback ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write and rollback
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("test", "rollback", "key1", b"should_not_persist")
|
|
|
+ await kv_tx.rollback()
|
|
|
+ print(" Written and rolled back")
|
|
|
+
|
|
|
+ # Verify not persisted
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx2.kv_read("test", "rollback", "key1")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ assert result is None, f"Value should not persist after rollback, got {bytes(result) if result else None}"
|
|
|
+ print(" Value not persisted after rollback")
|
|
|
+
|
|
|
+ print(" Test passed: KV transaction rollback works")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ cleanup_db(db_path)
|
|
|
+
|
|
|
+
|
|
|
+async def test_kv_transaction_atomicity():
|
|
|
+ """Test that multiple operations in a transaction are atomic"""
|
|
|
+ print("\n=== Test: KV Transaction Atomicity ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Perform multiple writes in one transaction
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("atomic", "test", "key1", b"value1")
|
|
|
+ await kv_tx.kv_write("atomic", "test", "key2", b"value2")
|
|
|
+ await kv_tx.kv_write("atomic", "test", "key3", b"value3")
|
|
|
+
|
|
|
+ # Read within same transaction (should see uncommitted values)
|
|
|
+ keys_before = await kv_tx.kv_list("atomic", "test")
|
|
|
+ print(f" Keys within transaction: {keys_before}")
|
|
|
+
|
|
|
+ # Rollback all
|
|
|
+ await kv_tx.rollback()
|
|
|
+ print(" Rolled back transaction")
|
|
|
+
|
|
|
+ # Verify none persisted
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ keys_after = await kv_tx2.kv_list("atomic", "test")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ assert len(keys_after) == 0, f"Expected no keys after rollback, got {keys_after}"
|
|
|
+ print(" No keys persisted after rollback")
|
|
|
+
|
|
|
+ # Now do the same but commit
|
|
|
+ kv_tx3 = await db.begin_db_transaction()
|
|
|
+ await kv_tx3.kv_write("atomic", "test", "key1", b"value1")
|
|
|
+ await kv_tx3.kv_write("atomic", "test", "key2", b"value2")
|
|
|
+ await kv_tx3.kv_write("atomic", "test", "key3", b"value3")
|
|
|
+ await kv_tx3.commit()
|
|
|
+ print(" Committed transaction with 3 keys")
|
|
|
+
|
|
|
+ # Verify all persisted
|
|
|
+ kv_tx4 = await db.begin_db_transaction()
|
|
|
+ keys_final = await kv_tx4.kv_list("atomic", "test")
|
|
|
+ await kv_tx4.rollback()
|
|
|
+
|
|
|
+ assert len(keys_final) == 3, f"Expected 3 keys after commit, got {len(keys_final)}"
|
|
|
+ print(f" All 3 keys persisted: {keys_final}")
|
|
|
+
|
|
|
+ print(" Test passed: KV transaction atomicity works")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ cleanup_db(db_path)
|
|
|
+
|
|
|
+
|
|
|
+async def test_kv_read_within_transaction():
|
|
|
+ """Test reading values within the same transaction they were written"""
|
|
|
+ print("\n=== Test: KV Read Within Transaction ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+
|
|
|
+ # Write a value
|
|
|
+ await kv_tx.kv_write("intra", "tx", "mykey", b"myvalue")
|
|
|
+
|
|
|
+ # Read it back in same transaction
|
|
|
+ result = await kv_tx.kv_read("intra", "tx", "mykey")
|
|
|
+
|
|
|
+ assert result is not None, "Should be able to read uncommitted value in same tx"
|
|
|
+ assert bytes(result) == b"myvalue", f"Expected b'myvalue', got {bytes(result)}"
|
|
|
+ print(" Can read uncommitted value within transaction")
|
|
|
+
|
|
|
+ await kv_tx.rollback()
|
|
|
+
|
|
|
+ print(" Test passed: KV read within transaction works")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ cleanup_db(db_path)
|
|
|
+
|
|
|
+
|
|
|
+# Namespace Isolation Tests
|
|
|
+
|
|
|
+async def test_kv_namespace_isolation():
|
|
|
+ """Test that different namespaces are isolated"""
|
|
|
+ print("\n=== Test: KV Namespace Isolation ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write same key in different namespaces
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("app1", "config", "key", b"app1_value")
|
|
|
+ await kv_tx.kv_write("app2", "config", "key", b"app2_value")
|
|
|
+ await kv_tx.kv_write("app1", "other", "key", b"app1_other_value")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written same key in different namespaces")
|
|
|
+
|
|
|
+ # Read from each namespace
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+
|
|
|
+ result1 = await kv_tx2.kv_read("app1", "config", "key")
|
|
|
+ result2 = await kv_tx2.kv_read("app2", "config", "key")
|
|
|
+ result3 = await kv_tx2.kv_read("app1", "other", "key")
|
|
|
+
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+
|
|
|
+# Binary Data Tests
|
|
|
+
|
|
|
+async def test_kv_binary_data():
|
|
|
+ """Test storing and retrieving binary data"""
|
|
|
+ print("\n=== Test: KV Binary Data ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Various binary data types
|
|
|
+ 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])),
|
|
|
+ ]
|
|
|
+
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ for name, data in test_cases:
|
|
|
+ await kv_tx.kv_write("binary", "test", name, data)
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(f" Written {len(test_cases)} binary test cases")
|
|
|
+
|
|
|
+ # Read back and verify
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ for name, expected_data in test_cases:
|
|
|
+ result = await kv_tx2.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)")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ print(" Test passed: KV binary data works")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ cleanup_db(db_path)
|
|
|
+
|
|
|
+
|
|
|
+async def test_kv_large_value():
|
|
|
+ """Test storing a large value"""
|
|
|
+ print("\n=== Test: KV Large Value ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Create a 1MB value
|
|
|
+ large_data = bytes([i % 256 for i in range(1024 * 1024)])
|
|
|
+
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("large", "data", "megabyte", large_data)
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(f" Written {len(large_data)} bytes")
|
|
|
+
|
|
|
+ # Read back
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ result = await kv_tx2.kv_read("large", "data", "megabyte")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+
|
|
|
+# Key Name Tests
|
|
|
+
|
|
|
+async def test_kv_special_key_names():
|
|
|
+ """Test keys with special characters"""
|
|
|
+ print("\n=== Test: KV Special Key Names ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ special_keys = [
|
|
|
+ "simple",
|
|
|
+ "with-dashes",
|
|
|
+ "with_underscores",
|
|
|
+ "MixedCase",
|
|
|
+ "numbers123",
|
|
|
+ "unicode_", # Note: Using underscore instead of actual unicode for simplicity
|
|
|
+ "empty_value",
|
|
|
+ ]
|
|
|
+
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ for i, key in enumerate(special_keys):
|
|
|
+ print(key)
|
|
|
+ await kv_tx.kv_write("special", "keys", key, f"value_{i}".encode())
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(f" Written {len(special_keys)} special keys")
|
|
|
+
|
|
|
+ # List and verify
|
|
|
+ kv_tx2 = await db.begin_db_transaction()
|
|
|
+ keys = await kv_tx2.kv_list("special", "keys")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+
|
|
|
+# Database Read Method Tests
|
|
|
+
|
|
|
+async def test_kv_database_read_methods():
|
|
|
+ """Test kv_read and kv_list methods on the database object (not transaction)"""
|
|
|
+ print("\n=== Test: KV Database Read Methods ===")
|
|
|
+
|
|
|
+ db, db_path = create_test_db()
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Write some data first
|
|
|
+ kv_tx = await db.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("dbread", "test", "key1", b"value1")
|
|
|
+ await kv_tx.kv_write("dbread", "test", "key2", b"value2")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written test data")
|
|
|
+
|
|
|
+ # Read back using database-level kv_read (not transaction)
|
|
|
+ result = await db.kv_read("dbread", "test", "key1")
|
|
|
+ assert result is not None, "Expected to read key1"
|
|
|
+ assert bytes(result) == b"value1", f"Expected b'value1', got {bytes(result)}"
|
|
|
+ print(" db.kv_read() works")
|
|
|
+
|
|
|
+ keys = await db.kv_list("dbread", "test")
|
|
|
+ assert len(keys) == 2, f"Expected 2 keys, got {len(keys)}"
|
|
|
+ print(f" db.kv_list() works: {keys}")
|
|
|
+
|
|
|
+ print(" Test passed: KV database read methods work")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ cleanup_db(db_path)
|
|
|
+
|
|
|
+
|
|
|
+# Persistence Test
|
|
|
+
|
|
|
+async def test_kv_persistence_across_instances():
|
|
|
+ """Test that KV data persists when reopening the database"""
|
|
|
+ print("\n=== Test: KV Persistence Across Instances ===")
|
|
|
+
|
|
|
+ db_path = None
|
|
|
+ try:
|
|
|
+ # Create and write
|
|
|
+ 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)
|
|
|
+
|
|
|
+ kv_tx = await db1.begin_db_transaction()
|
|
|
+ await kv_tx.kv_write("persist", "test", "mykey", b"persistent_value")
|
|
|
+ await kv_tx.commit()
|
|
|
+ print(" Written and committed with first db instance")
|
|
|
+
|
|
|
+ # Delete reference to first db (simulating closing)
|
|
|
+ del db1
|
|
|
+ await asyncio.sleep(0.1)
|
|
|
+ print(" First db instance closed")
|
|
|
+
|
|
|
+ # Reopen and read
|
|
|
+ backend2 = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
|
|
|
+ db2 = cdk_ffi.create_wallet_db(backend2)
|
|
|
+
|
|
|
+ kv_tx2 = await db2.begin_db_transaction()
|
|
|
+ result = await kv_tx2.kv_read("persist", "test", "mykey")
|
|
|
+ await kv_tx2.rollback()
|
|
|
+
|
|
|
+ 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 main():
|
|
|
+ """Run all KV store tests"""
|
|
|
+ print("Starting CDK FFI Key-Value Store Tests")
|
|
|
+ print("=" * 60)
|
|
|
+
|
|
|
+ tests = [
|
|
|
+ # Basic operations
|
|
|
+ ("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),
|
|
|
+ # Transaction tests
|
|
|
+ ("KV Transaction Commit", test_kv_transaction_commit),
|
|
|
+ ("KV Transaction Rollback", test_kv_transaction_rollback),
|
|
|
+ ("KV Transaction Atomicity", test_kv_transaction_atomicity),
|
|
|
+ ("KV Read Within Transaction", test_kv_read_within_transaction),
|
|
|
+ # Namespace tests
|
|
|
+ ("KV Namespace Isolation", test_kv_namespace_isolation),
|
|
|
+ # Data tests
|
|
|
+ ("KV Binary Data", test_kv_binary_data),
|
|
|
+ ("KV Large Value", test_kv_large_value),
|
|
|
+ ("KV Special Key Names", test_kv_special_key_names),
|
|
|
+ # Database methods
|
|
|
+ ("KV Database Read Methods", test_kv_database_read_methods),
|
|
|
+ # Persistence
|
|
|
+ ("KV Persistence Across Instances", test_kv_persistence_across_instances),
|
|
|
+ ]
|
|
|
+
|
|
|
+ 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)
|