test_kvstore.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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.
  6. """
  7. import asyncio
  8. import os
  9. import sys
  10. import tempfile
  11. import shutil
  12. from pathlib import Path
  13. # Setup paths before importing cdk_ffi
  14. repo_root = Path(__file__).parent.parent.parent.parent
  15. bindings_path = repo_root / "target" / "bindings" / "python"
  16. lib_path = repo_root / "target" / "release"
  17. lib_file = "libcdk_ffi.dylib" if sys.platform == "darwin" else "libcdk_ffi.so"
  18. src_lib = lib_path / lib_file
  19. dst_lib = bindings_path / lib_file
  20. if src_lib.exists() and not dst_lib.exists():
  21. shutil.copy2(src_lib, dst_lib)
  22. # Add target/bindings/python to path to load cdk_ffi module
  23. sys.path.insert(0, str(bindings_path))
  24. import cdk_ffi
  25. # Helper functions
  26. def create_test_db():
  27. """Create a temporary SQLite database for testing"""
  28. tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
  29. db_path = tmp.name
  30. tmp.close()
  31. backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
  32. db = cdk_ffi.create_wallet_db(backend)
  33. return db, db_path
  34. def cleanup_db(db_path):
  35. """Clean up the temporary database file"""
  36. if os.path.exists(db_path):
  37. os.unlink(db_path)
  38. # Basic KV Store Tests
  39. async def test_kv_write_and_read():
  40. """Test basic write and read operations"""
  41. print("\n=== Test: KV Write and Read ===")
  42. db, db_path = create_test_db()
  43. try:
  44. # Write a value
  45. test_data = b"Hello, KVStore!"
  46. await db.kv_write("app", "config", "greeting", test_data)
  47. print(" Written value to KV store")
  48. # Read it back
  49. result = await db.kv_read("app", "config", "greeting")
  50. assert result is not None, "Expected to read back the value"
  51. assert bytes(result) == test_data, f"Expected {test_data}, got {bytes(result)}"
  52. print(" Read back correct value")
  53. print(" Test passed: KV write and read work")
  54. finally:
  55. cleanup_db(db_path)
  56. async def test_kv_read_nonexistent():
  57. """Test reading a key that doesn't exist"""
  58. print("\n=== Test: KV Read Nonexistent Key ===")
  59. db, db_path = create_test_db()
  60. try:
  61. result = await db.kv_read("nonexistent", "namespace", "key")
  62. assert result is None, f"Expected None for nonexistent key, got {result}"
  63. print(" Correctly returns None for nonexistent key")
  64. print(" Test passed: Reading nonexistent key returns None")
  65. finally:
  66. cleanup_db(db_path)
  67. async def test_kv_overwrite():
  68. """Test overwriting an existing value"""
  69. print("\n=== Test: KV Overwrite ===")
  70. db, db_path = create_test_db()
  71. try:
  72. # Write initial value
  73. await db.kv_write("app", "data", "counter", b"1")
  74. print(" Written initial value")
  75. # Overwrite with new value
  76. await db.kv_write("app", "data", "counter", b"42")
  77. print(" Overwrote with new value")
  78. # Read back
  79. result = await db.kv_read("app", "data", "counter")
  80. assert result is not None, "Expected to read back the value"
  81. assert bytes(result) == b"42", f"Expected b'42', got {bytes(result)}"
  82. print(" Read back overwritten value")
  83. print(" Test passed: KV overwrite works")
  84. finally:
  85. cleanup_db(db_path)
  86. async def test_kv_remove():
  87. """Test removing a key"""
  88. print("\n=== Test: KV Remove ===")
  89. db, db_path = create_test_db()
  90. try:
  91. # Write a value
  92. await db.kv_write("app", "temp", "to_delete", b"delete me")
  93. print(" Written value to delete")
  94. # Verify it exists
  95. result = await db.kv_read("app", "temp", "to_delete")
  96. assert result is not None, "Value should exist before removal"
  97. print(" Verified value exists")
  98. # Remove it
  99. await db.kv_remove("app", "temp", "to_delete")
  100. print(" Removed value")
  101. # Verify it's gone
  102. result_after = await db.kv_read("app", "temp", "to_delete")
  103. assert result_after is None, f"Expected None after removal, got {result_after}"
  104. print(" Verified value is removed")
  105. print(" Test passed: KV remove works")
  106. finally:
  107. cleanup_db(db_path)
  108. async def test_kv_list_keys():
  109. """Test listing keys in a namespace"""
  110. print("\n=== Test: KV List Keys ===")
  111. db, db_path = create_test_db()
  112. try:
  113. # Write multiple keys
  114. await db.kv_write("myapp", "settings", "theme", b"dark")
  115. await db.kv_write("myapp", "settings", "language", b"en")
  116. await db.kv_write("myapp", "settings", "timezone", b"UTC")
  117. await db.kv_write("myapp", "other", "unrelated", b"data")
  118. print(" Written multiple keys")
  119. # List keys in the settings namespace
  120. keys = await db.kv_list("myapp", "settings")
  121. assert len(keys) == 3, f"Expected 3 keys, got {len(keys)}"
  122. assert "theme" in keys, "Expected 'theme' in keys"
  123. assert "language" in keys, "Expected 'language' in keys"
  124. assert "timezone" in keys, "Expected 'timezone' in keys"
  125. assert "unrelated" not in keys, "'unrelated' should not be in settings namespace"
  126. print(f" Listed keys: {keys}")
  127. print(" Test passed: KV list works")
  128. finally:
  129. cleanup_db(db_path)
  130. async def test_kv_list_empty_namespace():
  131. """Test listing keys in an empty or nonexistent namespace"""
  132. print("\n=== Test: KV List Empty Namespace ===")
  133. db, db_path = create_test_db()
  134. try:
  135. keys = await db.kv_list("nonexistent", "namespace")
  136. assert isinstance(keys, list), "Expected a list"
  137. assert len(keys) == 0, f"Expected empty list, got {keys}"
  138. print(" Empty namespace returns empty list")
  139. print(" Test passed: KV list on empty namespace works")
  140. finally:
  141. cleanup_db(db_path)
  142. # Namespace Isolation Tests
  143. async def test_kv_namespace_isolation():
  144. """Test that different namespaces are isolated"""
  145. print("\n=== Test: KV Namespace Isolation ===")
  146. db, db_path = create_test_db()
  147. try:
  148. # Write same key in different namespaces
  149. await db.kv_write("app1", "config", "key", b"app1_value")
  150. await db.kv_write("app2", "config", "key", b"app2_value")
  151. await db.kv_write("app1", "other", "key", b"app1_other_value")
  152. print(" Written same key in different namespaces")
  153. # Read from each namespace
  154. result1 = await db.kv_read("app1", "config", "key")
  155. result2 = await db.kv_read("app2", "config", "key")
  156. result3 = await db.kv_read("app1", "other", "key")
  157. assert bytes(result1) == b"app1_value", f"Expected b'app1_value', got {bytes(result1)}"
  158. assert bytes(result2) == b"app2_value", f"Expected b'app2_value', got {bytes(result2)}"
  159. assert bytes(result3) == b"app1_other_value", f"Expected b'app1_other_value', got {bytes(result3)}"
  160. print(" Each namespace has correct value")
  161. print(" Test passed: KV namespace isolation works")
  162. finally:
  163. cleanup_db(db_path)
  164. # Binary Data Tests
  165. async def test_kv_binary_data():
  166. """Test storing and retrieving binary data"""
  167. print("\n=== Test: KV Binary Data ===")
  168. db, db_path = create_test_db()
  169. try:
  170. # Various binary data types
  171. test_cases = [
  172. ("empty", b""),
  173. ("null_byte", b"\x00"),
  174. ("all_bytes", bytes(range(256))),
  175. ("utf8_special", "Hello World".encode("utf-8")),
  176. ("random_binary", bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE])),
  177. ]
  178. for name, data in test_cases:
  179. await db.kv_write("binary", "test", name, data)
  180. print(f" Written {len(test_cases)} binary test cases")
  181. # Read back and verify
  182. for name, expected_data in test_cases:
  183. result = await db.kv_read("binary", "test", name)
  184. assert result is not None, f"Expected data for {name}"
  185. actual_data = bytes(result)
  186. assert actual_data == expected_data, f"Mismatch for {name}: expected {expected_data!r}, got {actual_data!r}"
  187. print(f" '{name}': OK ({len(actual_data)} bytes)")
  188. print(" Test passed: KV binary data works")
  189. finally:
  190. cleanup_db(db_path)
  191. async def test_kv_large_value():
  192. """Test storing a large value"""
  193. print("\n=== Test: KV Large Value ===")
  194. db, db_path = create_test_db()
  195. try:
  196. # Create a 1MB value
  197. large_data = bytes([i % 256 for i in range(1024 * 1024)])
  198. await db.kv_write("large", "data", "megabyte", large_data)
  199. print(f" Written {len(large_data)} bytes")
  200. # Read back
  201. result = await db.kv_read("large", "data", "megabyte")
  202. assert result is not None, "Expected to read large value"
  203. result_bytes = bytes(result)
  204. assert len(result_bytes) == len(large_data), f"Size mismatch: {len(result_bytes)} vs {len(large_data)}"
  205. assert result_bytes == large_data, "Data mismatch"
  206. print(f" Read back {len(result_bytes)} bytes correctly")
  207. print(" Test passed: KV large value works")
  208. finally:
  209. cleanup_db(db_path)
  210. # Key Name Tests
  211. async def test_kv_special_key_names():
  212. """Test keys with special characters"""
  213. print("\n=== Test: KV Special Key Names ===")
  214. db, db_path = create_test_db()
  215. try:
  216. special_keys = [
  217. "simple",
  218. "with-dashes",
  219. "with_underscores",
  220. "MixedCase",
  221. "numbers123",
  222. "unicode_", # Note: Using underscore instead of actual unicode for simplicity
  223. "empty_value",
  224. ]
  225. for i, key in enumerate(special_keys):
  226. await db.kv_write("special", "keys", key, f"value_{i}".encode())
  227. print(f" Written {len(special_keys)} special keys")
  228. # List and verify
  229. keys = await db.kv_list("special", "keys")
  230. assert len(keys) == len(special_keys), f"Expected {len(special_keys)} keys, got {len(keys)}"
  231. for key in special_keys:
  232. assert key in keys, f"Key '{key}' not found in list"
  233. print(f" All special keys stored and listed correctly")
  234. print(" Test passed: KV special key names work")
  235. finally:
  236. cleanup_db(db_path)
  237. # Persistence Test
  238. async def test_kv_persistence_across_instances():
  239. """Test that KV data persists when reopening the database"""
  240. print("\n=== Test: KV Persistence Across Instances ===")
  241. db_path = None
  242. try:
  243. # Create and write
  244. with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
  245. db_path = tmp.name
  246. backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
  247. db1 = cdk_ffi.create_wallet_db(backend)
  248. await db1.kv_write("persist", "test", "mykey", b"persistent_value")
  249. print(" Written and committed with first db instance")
  250. # Delete reference to first db (simulating closing)
  251. del db1
  252. await asyncio.sleep(0.1)
  253. print(" First db instance closed")
  254. # Reopen and read
  255. backend2 = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
  256. db2 = cdk_ffi.create_wallet_db(backend2)
  257. result = await db2.kv_read("persist", "test", "mykey")
  258. assert result is not None, "Data should persist across db instances"
  259. assert bytes(result) == b"persistent_value", f"Expected b'persistent_value', got {bytes(result)}"
  260. print(" Data persisted across db instances")
  261. print(" Test passed: KV persistence across instances works")
  262. finally:
  263. if db_path and os.path.exists(db_path):
  264. os.unlink(db_path)
  265. async def main():
  266. """Run all KV store tests"""
  267. print("Starting CDK FFI Key-Value Store Tests")
  268. print("=" * 60)
  269. tests = [
  270. # Basic operations
  271. ("KV Write and Read", test_kv_write_and_read),
  272. ("KV Read Nonexistent", test_kv_read_nonexistent),
  273. ("KV Overwrite", test_kv_overwrite),
  274. ("KV Remove", test_kv_remove),
  275. ("KV List Keys", test_kv_list_keys),
  276. ("KV List Empty Namespace", test_kv_list_empty_namespace),
  277. # Namespace tests
  278. ("KV Namespace Isolation", test_kv_namespace_isolation),
  279. # Data tests
  280. ("KV Binary Data", test_kv_binary_data),
  281. ("KV Large Value", test_kv_large_value),
  282. ("KV Special Key Names", test_kv_special_key_names),
  283. # Persistence
  284. ("KV Persistence Across Instances", test_kv_persistence_across_instances),
  285. ]
  286. passed = 0
  287. failed = 0
  288. for test_name, test_func in tests:
  289. try:
  290. await test_func()
  291. passed += 1
  292. except Exception as e:
  293. failed += 1
  294. print(f"\n Test failed: {test_name}")
  295. print(f" Error: {e}")
  296. import traceback
  297. traceback.print_exc()
  298. print("\n" + "=" * 60)
  299. print(f"Test Results: {passed} passed, {failed} failed")
  300. print("=" * 60)
  301. return 0 if failed == 0 else 1
  302. if __name__ == "__main__":
  303. exit_code = asyncio.run(main())
  304. sys.exit(exit_code)