test_kvstore.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. #!/usr/bin/env python3
  2. """
  3. Test suite for CDK FFI Key-Value Store operations
  4. Tests the KVStore trait functionality exposed through the FFI bindings,
  5. including read, write, list, and remove operations with transaction support.
  6. """
  7. import asyncio
  8. import os
  9. import sys
  10. import tempfile
  11. from pathlib import Path
  12. # Setup paths before importing cdk_ffi
  13. repo_root = Path(__file__).parent.parent.parent.parent
  14. bindings_path = repo_root / "target" / "bindings" / "python"
  15. lib_path = repo_root / "target" / "release"
  16. # Copy the library to the bindings directory so Python can find it
  17. import shutil
  18. lib_file = "libcdk_ffi.dylib" if sys.platform == "darwin" else "libcdk_ffi.so"
  19. src_lib = lib_path / lib_file
  20. dst_lib = bindings_path / lib_file
  21. if src_lib.exists() and not dst_lib.exists():
  22. shutil.copy2(src_lib, dst_lib)
  23. # Add target/bindings/python to path to load cdk_ffi module
  24. sys.path.insert(0, str(bindings_path))
  25. import cdk_ffi
  26. # Helper functions
  27. def create_test_db():
  28. """Create a temporary SQLite database for testing"""
  29. tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
  30. db_path = tmp.name
  31. tmp.close()
  32. backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
  33. db = cdk_ffi.create_wallet_db(backend)
  34. return db, db_path
  35. def cleanup_db(db_path):
  36. """Clean up the temporary database file"""
  37. if os.path.exists(db_path):
  38. os.unlink(db_path)
  39. # Basic KV Store Tests
  40. async def test_kv_write_and_read():
  41. """Test basic write and read operations"""
  42. print("\n=== Test: KV Write and Read ===")
  43. db, db_path = create_test_db()
  44. try:
  45. # Write a value using KV transaction
  46. kv_tx = await db.begin_db_transaction()
  47. test_data = b"Hello, KVStore!"
  48. await kv_tx.kv_write("app", "config", "greeting", test_data)
  49. await kv_tx.commit()
  50. print(" Written value to KV store")
  51. # Read it back using a new transaction
  52. kv_tx2 = await db.begin_db_transaction()
  53. result = await kv_tx2.kv_read("app", "config", "greeting")
  54. await kv_tx2.rollback()
  55. assert result is not None, "Expected to read back the value"
  56. assert bytes(result) == test_data, f"Expected {test_data}, got {bytes(result)}"
  57. print(" Read back correct value")
  58. print(" Test passed: KV write and read work")
  59. finally:
  60. cleanup_db(db_path)
  61. async def test_kv_read_nonexistent():
  62. """Test reading a key that doesn't exist"""
  63. print("\n=== Test: KV Read Nonexistent Key ===")
  64. db, db_path = create_test_db()
  65. try:
  66. kv_tx = await db.begin_db_transaction()
  67. result = await kv_tx.kv_read("nonexistent", "namespace", "key")
  68. await kv_tx.rollback()
  69. assert result is None, f"Expected None for nonexistent key, got {result}"
  70. print(" Correctly returns None for nonexistent key")
  71. print(" Test passed: Reading nonexistent key returns None")
  72. finally:
  73. cleanup_db(db_path)
  74. async def test_kv_overwrite():
  75. """Test overwriting an existing value"""
  76. print("\n=== Test: KV Overwrite ===")
  77. db, db_path = create_test_db()
  78. try:
  79. # Write initial value
  80. kv_tx = await db.begin_db_transaction()
  81. await kv_tx.kv_write("app", "data", "counter", b"1")
  82. await kv_tx.commit()
  83. print(" Written initial value")
  84. # Overwrite with new value
  85. kv_tx2 = await db.begin_db_transaction()
  86. await kv_tx2.kv_write("app", "data", "counter", b"42")
  87. await kv_tx2.commit()
  88. print(" Overwrote with new value")
  89. # Read back
  90. kv_tx3 = await db.begin_db_transaction()
  91. result = await kv_tx3.kv_read("app", "data", "counter")
  92. await kv_tx3.rollback()
  93. assert result is not None, "Expected to read back the value"
  94. assert bytes(result) == b"42", f"Expected b'42', got {bytes(result)}"
  95. print(" Read back overwritten value")
  96. print(" Test passed: KV overwrite works")
  97. finally:
  98. cleanup_db(db_path)
  99. async def test_kv_remove():
  100. """Test removing a key"""
  101. print("\n=== Test: KV Remove ===")
  102. db, db_path = create_test_db()
  103. try:
  104. # Write a value
  105. kv_tx = await db.begin_db_transaction()
  106. await kv_tx.kv_write("app", "temp", "to_delete", b"delete me")
  107. await kv_tx.commit()
  108. print(" Written value to delete")
  109. # Verify it exists
  110. kv_tx2 = await db.begin_db_transaction()
  111. result = await kv_tx2.kv_read("app", "temp", "to_delete")
  112. await kv_tx2.rollback()
  113. assert result is not None, "Value should exist before removal"
  114. print(" Verified value exists")
  115. # Remove it
  116. kv_tx3 = await db.begin_db_transaction()
  117. await kv_tx3.kv_remove("app", "temp", "to_delete")
  118. await kv_tx3.commit()
  119. print(" Removed value")
  120. # Verify it's gone
  121. kv_tx4 = await db.begin_db_transaction()
  122. result_after = await kv_tx4.kv_read("app", "temp", "to_delete")
  123. await kv_tx4.rollback()
  124. assert result_after is None, f"Expected None after removal, got {result_after}"
  125. print(" Verified value is removed")
  126. print(" Test passed: KV remove works")
  127. finally:
  128. cleanup_db(db_path)
  129. async def test_kv_list_keys():
  130. """Test listing keys in a namespace"""
  131. print("\n=== Test: KV List Keys ===")
  132. db, db_path = create_test_db()
  133. try:
  134. # Write multiple keys
  135. kv_tx = await db.begin_db_transaction()
  136. await kv_tx.kv_write("myapp", "settings", "theme", b"dark")
  137. await kv_tx.kv_write("myapp", "settings", "language", b"en")
  138. await kv_tx.kv_write("myapp", "settings", "timezone", b"UTC")
  139. await kv_tx.kv_write("myapp", "other", "unrelated", b"data")
  140. await kv_tx.commit()
  141. print(" Written multiple keys")
  142. # List keys in the settings namespace
  143. kv_tx2 = await db.begin_db_transaction()
  144. keys = await kv_tx2.kv_list("myapp", "settings")
  145. await kv_tx2.rollback()
  146. assert len(keys) == 3, f"Expected 3 keys, got {len(keys)}"
  147. assert "theme" in keys, "Expected 'theme' in keys"
  148. assert "language" in keys, "Expected 'language' in keys"
  149. assert "timezone" in keys, "Expected 'timezone' in keys"
  150. assert "unrelated" not in keys, "'unrelated' should not be in settings namespace"
  151. print(f" Listed keys: {keys}")
  152. print(" Test passed: KV list works")
  153. finally:
  154. cleanup_db(db_path)
  155. async def test_kv_list_empty_namespace():
  156. """Test listing keys in an empty or nonexistent namespace"""
  157. print("\n=== Test: KV List Empty Namespace ===")
  158. db, db_path = create_test_db()
  159. try:
  160. kv_tx = await db.begin_db_transaction()
  161. keys = await kv_tx.kv_list("nonexistent", "namespace")
  162. await kv_tx.rollback()
  163. assert isinstance(keys, list), "Expected a list"
  164. assert len(keys) == 0, f"Expected empty list, got {keys}"
  165. print(" Empty namespace returns empty list")
  166. print(" Test passed: KV list on empty namespace works")
  167. finally:
  168. cleanup_db(db_path)
  169. # Transaction Tests
  170. async def test_kv_transaction_commit():
  171. """Test that KV changes persist after commit"""
  172. print("\n=== Test: KV Transaction Commit ===")
  173. db, db_path = create_test_db()
  174. try:
  175. # Write and commit
  176. kv_tx = await db.begin_db_transaction()
  177. await kv_tx.kv_write("test", "commit", "key1", b"committed")
  178. await kv_tx.commit()
  179. print(" Written and committed")
  180. # Verify in new transaction
  181. kv_tx2 = await db.begin_db_transaction()
  182. result = await kv_tx2.kv_read("test", "commit", "key1")
  183. await kv_tx2.rollback()
  184. assert result is not None, "Value should persist after commit"
  185. assert bytes(result) == b"committed", f"Expected b'committed', got {bytes(result)}"
  186. print(" Value persists after commit")
  187. print(" Test passed: KV transaction commit works")
  188. finally:
  189. cleanup_db(db_path)
  190. async def test_kv_transaction_rollback():
  191. """Test that KV changes are reverted after rollback"""
  192. print("\n=== Test: KV Transaction Rollback ===")
  193. db, db_path = create_test_db()
  194. try:
  195. # Write and rollback
  196. kv_tx = await db.begin_db_transaction()
  197. await kv_tx.kv_write("test", "rollback", "key1", b"should_not_persist")
  198. await kv_tx.rollback()
  199. print(" Written and rolled back")
  200. # Verify not persisted
  201. kv_tx2 = await db.begin_db_transaction()
  202. result = await kv_tx2.kv_read("test", "rollback", "key1")
  203. await kv_tx2.rollback()
  204. assert result is None, f"Value should not persist after rollback, got {bytes(result) if result else None}"
  205. print(" Value not persisted after rollback")
  206. print(" Test passed: KV transaction rollback works")
  207. finally:
  208. cleanup_db(db_path)
  209. async def test_kv_transaction_atomicity():
  210. """Test that multiple operations in a transaction are atomic"""
  211. print("\n=== Test: KV Transaction Atomicity ===")
  212. db, db_path = create_test_db()
  213. try:
  214. # Perform multiple writes in one transaction
  215. kv_tx = await db.begin_db_transaction()
  216. await kv_tx.kv_write("atomic", "test", "key1", b"value1")
  217. await kv_tx.kv_write("atomic", "test", "key2", b"value2")
  218. await kv_tx.kv_write("atomic", "test", "key3", b"value3")
  219. # Read within same transaction (should see uncommitted values)
  220. keys_before = await kv_tx.kv_list("atomic", "test")
  221. print(f" Keys within transaction: {keys_before}")
  222. # Rollback all
  223. await kv_tx.rollback()
  224. print(" Rolled back transaction")
  225. # Verify none persisted
  226. kv_tx2 = await db.begin_db_transaction()
  227. keys_after = await kv_tx2.kv_list("atomic", "test")
  228. await kv_tx2.rollback()
  229. assert len(keys_after) == 0, f"Expected no keys after rollback, got {keys_after}"
  230. print(" No keys persisted after rollback")
  231. # Now do the same but commit
  232. kv_tx3 = await db.begin_db_transaction()
  233. await kv_tx3.kv_write("atomic", "test", "key1", b"value1")
  234. await kv_tx3.kv_write("atomic", "test", "key2", b"value2")
  235. await kv_tx3.kv_write("atomic", "test", "key3", b"value3")
  236. await kv_tx3.commit()
  237. print(" Committed transaction with 3 keys")
  238. # Verify all persisted
  239. kv_tx4 = await db.begin_db_transaction()
  240. keys_final = await kv_tx4.kv_list("atomic", "test")
  241. await kv_tx4.rollback()
  242. assert len(keys_final) == 3, f"Expected 3 keys after commit, got {len(keys_final)}"
  243. print(f" All 3 keys persisted: {keys_final}")
  244. print(" Test passed: KV transaction atomicity works")
  245. finally:
  246. cleanup_db(db_path)
  247. async def test_kv_read_within_transaction():
  248. """Test reading values within the same transaction they were written"""
  249. print("\n=== Test: KV Read Within Transaction ===")
  250. db, db_path = create_test_db()
  251. try:
  252. kv_tx = await db.begin_db_transaction()
  253. # Write a value
  254. await kv_tx.kv_write("intra", "tx", "mykey", b"myvalue")
  255. # Read it back in same transaction
  256. result = await kv_tx.kv_read("intra", "tx", "mykey")
  257. assert result is not None, "Should be able to read uncommitted value in same tx"
  258. assert bytes(result) == b"myvalue", f"Expected b'myvalue', got {bytes(result)}"
  259. print(" Can read uncommitted value within transaction")
  260. await kv_tx.rollback()
  261. print(" Test passed: KV read within transaction works")
  262. finally:
  263. cleanup_db(db_path)
  264. # Namespace Isolation Tests
  265. async def test_kv_namespace_isolation():
  266. """Test that different namespaces are isolated"""
  267. print("\n=== Test: KV Namespace Isolation ===")
  268. db, db_path = create_test_db()
  269. try:
  270. # Write same key in different namespaces
  271. kv_tx = await db.begin_db_transaction()
  272. await kv_tx.kv_write("app1", "config", "key", b"app1_value")
  273. await kv_tx.kv_write("app2", "config", "key", b"app2_value")
  274. await kv_tx.kv_write("app1", "other", "key", b"app1_other_value")
  275. await kv_tx.commit()
  276. print(" Written same key in different namespaces")
  277. # Read from each namespace
  278. kv_tx2 = await db.begin_db_transaction()
  279. result1 = await kv_tx2.kv_read("app1", "config", "key")
  280. result2 = await kv_tx2.kv_read("app2", "config", "key")
  281. result3 = await kv_tx2.kv_read("app1", "other", "key")
  282. await kv_tx2.rollback()
  283. assert bytes(result1) == b"app1_value", f"Expected b'app1_value', got {bytes(result1)}"
  284. assert bytes(result2) == b"app2_value", f"Expected b'app2_value', got {bytes(result2)}"
  285. assert bytes(result3) == b"app1_other_value", f"Expected b'app1_other_value', got {bytes(result3)}"
  286. print(" Each namespace has correct value")
  287. print(" Test passed: KV namespace isolation works")
  288. finally:
  289. cleanup_db(db_path)
  290. # Binary Data Tests
  291. async def test_kv_binary_data():
  292. """Test storing and retrieving binary data"""
  293. print("\n=== Test: KV Binary Data ===")
  294. db, db_path = create_test_db()
  295. try:
  296. # Various binary data types
  297. test_cases = [
  298. ("empty", b""),
  299. ("null_byte", b"\x00"),
  300. ("all_bytes", bytes(range(256))),
  301. ("utf8_special", "Hello World".encode("utf-8")),
  302. ("random_binary", bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE])),
  303. ]
  304. kv_tx = await db.begin_db_transaction()
  305. for name, data in test_cases:
  306. await kv_tx.kv_write("binary", "test", name, data)
  307. await kv_tx.commit()
  308. print(f" Written {len(test_cases)} binary test cases")
  309. # Read back and verify
  310. kv_tx2 = await db.begin_db_transaction()
  311. for name, expected_data in test_cases:
  312. result = await kv_tx2.kv_read("binary", "test", name)
  313. assert result is not None, f"Expected data for {name}"
  314. actual_data = bytes(result)
  315. assert actual_data == expected_data, f"Mismatch for {name}: expected {expected_data!r}, got {actual_data!r}"
  316. print(f" '{name}': OK ({len(actual_data)} bytes)")
  317. await kv_tx2.rollback()
  318. print(" Test passed: KV binary data works")
  319. finally:
  320. cleanup_db(db_path)
  321. async def test_kv_large_value():
  322. """Test storing a large value"""
  323. print("\n=== Test: KV Large Value ===")
  324. db, db_path = create_test_db()
  325. try:
  326. # Create a 1MB value
  327. large_data = bytes([i % 256 for i in range(1024 * 1024)])
  328. kv_tx = await db.begin_db_transaction()
  329. await kv_tx.kv_write("large", "data", "megabyte", large_data)
  330. await kv_tx.commit()
  331. print(f" Written {len(large_data)} bytes")
  332. # Read back
  333. kv_tx2 = await db.begin_db_transaction()
  334. result = await kv_tx2.kv_read("large", "data", "megabyte")
  335. await kv_tx2.rollback()
  336. assert result is not None, "Expected to read large value"
  337. result_bytes = bytes(result)
  338. assert len(result_bytes) == len(large_data), f"Size mismatch: {len(result_bytes)} vs {len(large_data)}"
  339. assert result_bytes == large_data, "Data mismatch"
  340. print(f" Read back {len(result_bytes)} bytes correctly")
  341. print(" Test passed: KV large value works")
  342. finally:
  343. cleanup_db(db_path)
  344. # Key Name Tests
  345. async def test_kv_special_key_names():
  346. """Test keys with special characters"""
  347. print("\n=== Test: KV Special Key Names ===")
  348. db, db_path = create_test_db()
  349. try:
  350. special_keys = [
  351. "simple",
  352. "with-dashes",
  353. "with_underscores",
  354. "MixedCase",
  355. "numbers123",
  356. "unicode_", # Note: Using underscore instead of actual unicode for simplicity
  357. "empty_value",
  358. ]
  359. kv_tx = await db.begin_db_transaction()
  360. for i, key in enumerate(special_keys):
  361. print(key)
  362. await kv_tx.kv_write("special", "keys", key, f"value_{i}".encode())
  363. await kv_tx.commit()
  364. print(f" Written {len(special_keys)} special keys")
  365. # List and verify
  366. kv_tx2 = await db.begin_db_transaction()
  367. keys = await kv_tx2.kv_list("special", "keys")
  368. await kv_tx2.rollback()
  369. assert len(keys) == len(special_keys), f"Expected {len(special_keys)} keys, got {len(keys)}"
  370. for key in special_keys:
  371. assert key in keys, f"Key '{key}' not found in list"
  372. print(f" All special keys stored and listed correctly")
  373. print(" Test passed: KV special key names work")
  374. finally:
  375. cleanup_db(db_path)
  376. # Database Read Method Tests
  377. async def test_kv_database_read_methods():
  378. """Test kv_read and kv_list methods on the database object (not transaction)"""
  379. print("\n=== Test: KV Database Read Methods ===")
  380. db, db_path = create_test_db()
  381. try:
  382. # Write some data first
  383. kv_tx = await db.begin_db_transaction()
  384. await kv_tx.kv_write("dbread", "test", "key1", b"value1")
  385. await kv_tx.kv_write("dbread", "test", "key2", b"value2")
  386. await kv_tx.commit()
  387. print(" Written test data")
  388. # Read back using database-level kv_read (not transaction)
  389. result = await db.kv_read("dbread", "test", "key1")
  390. assert result is not None, "Expected to read key1"
  391. assert bytes(result) == b"value1", f"Expected b'value1', got {bytes(result)}"
  392. print(" db.kv_read() works")
  393. keys = await db.kv_list("dbread", "test")
  394. assert len(keys) == 2, f"Expected 2 keys, got {len(keys)}"
  395. print(f" db.kv_list() works: {keys}")
  396. print(" Test passed: KV database read methods work")
  397. finally:
  398. cleanup_db(db_path)
  399. # Persistence Test
  400. async def test_kv_persistence_across_instances():
  401. """Test that KV data persists when reopening the database"""
  402. print("\n=== Test: KV Persistence Across Instances ===")
  403. db_path = None
  404. try:
  405. # Create and write
  406. with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
  407. db_path = tmp.name
  408. backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
  409. db1 = cdk_ffi.create_wallet_db(backend)
  410. kv_tx = await db1.begin_db_transaction()
  411. await kv_tx.kv_write("persist", "test", "mykey", b"persistent_value")
  412. await kv_tx.commit()
  413. print(" Written and committed with first db instance")
  414. # Delete reference to first db (simulating closing)
  415. del db1
  416. await asyncio.sleep(0.1)
  417. print(" First db instance closed")
  418. # Reopen and read
  419. backend2 = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
  420. db2 = cdk_ffi.create_wallet_db(backend2)
  421. kv_tx2 = await db2.begin_db_transaction()
  422. result = await kv_tx2.kv_read("persist", "test", "mykey")
  423. await kv_tx2.rollback()
  424. assert result is not None, "Data should persist across db instances"
  425. assert bytes(result) == b"persistent_value", f"Expected b'persistent_value', got {bytes(result)}"
  426. print(" Data persisted across db instances")
  427. print(" Test passed: KV persistence across instances works")
  428. finally:
  429. if db_path and os.path.exists(db_path):
  430. os.unlink(db_path)
  431. async def main():
  432. """Run all KV store tests"""
  433. print("Starting CDK FFI Key-Value Store Tests")
  434. print("=" * 60)
  435. tests = [
  436. # Basic operations
  437. ("KV Write and Read", test_kv_write_and_read),
  438. ("KV Read Nonexistent", test_kv_read_nonexistent),
  439. ("KV Overwrite", test_kv_overwrite),
  440. ("KV Remove", test_kv_remove),
  441. ("KV List Keys", test_kv_list_keys),
  442. ("KV List Empty Namespace", test_kv_list_empty_namespace),
  443. # Transaction tests
  444. ("KV Transaction Commit", test_kv_transaction_commit),
  445. ("KV Transaction Rollback", test_kv_transaction_rollback),
  446. ("KV Transaction Atomicity", test_kv_transaction_atomicity),
  447. ("KV Read Within Transaction", test_kv_read_within_transaction),
  448. # Namespace tests
  449. ("KV Namespace Isolation", test_kv_namespace_isolation),
  450. # Data tests
  451. ("KV Binary Data", test_kv_binary_data),
  452. ("KV Large Value", test_kv_large_value),
  453. ("KV Special Key Names", test_kv_special_key_names),
  454. # Database methods
  455. ("KV Database Read Methods", test_kv_database_read_methods),
  456. # Persistence
  457. ("KV Persistence Across Instances", test_kv_persistence_across_instances),
  458. ]
  459. passed = 0
  460. failed = 0
  461. for test_name, test_func in tests:
  462. try:
  463. await test_func()
  464. passed += 1
  465. except Exception as e:
  466. failed += 1
  467. print(f"\n Test failed: {test_name}")
  468. print(f" Error: {e}")
  469. import traceback
  470. traceback.print_exc()
  471. print("\n" + "=" * 60)
  472. print(f"Test Results: {passed} passed, {failed} failed")
  473. print("=" * 60)
  474. return 0 if failed == 0 else 1
  475. if __name__ == "__main__":
  476. exit_code = asyncio.run(main())
  477. sys.exit(exit_code)