Skip to content

Commit aeafec9

Browse files
committed
test: add 4 more integration tests for real-world patterns
- test_list_partial_range: verify partial range queries return correct subset - test_read_your_writes: 50 sequential put+get pairs verify consistency - test_multiple_clients: two independent clients reading each other's writes - test_cas_loop: CAS (compare-and-swap) counter pattern with version checks - Total: 42 integration tests + 12 unit tests + 1 doctest = 55 tests Signed-off-by: mattisonchao <mattisonchao@gmail.com>
1 parent ac3fc23 commit aeafec9

File tree

1 file changed

+293
-0
lines changed

1 file changed

+293
-0
lines changed

liboxia-native/tests/integration_tests.rs

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,3 +1294,296 @@ async fn test_range_scan_with_partition_key() {
12941294
.unwrap();
12951295
client.shutdown().await.unwrap();
12961296
}
1297+
1298+
// ============================================================
1299+
// Stress / throughput test
1300+
// ============================================================
1301+
1302+
#[tokio::test]
1303+
async fn test_high_throughput_writes() {
1304+
let (_container, address) = start_oxia().await;
1305+
let client = new_client(&address).await;
1306+
1307+
let num_ops = 100;
1308+
let mut handles = Vec::new();
1309+
1310+
// Flood with concurrent put operations
1311+
for i in 0..num_ops {
1312+
let c = client.clone();
1313+
handles.push(tokio::spawn(async move {
1314+
c.put(
1315+
format!("stress/{:04}", i),
1316+
format!("val-{}", i).into_bytes(),
1317+
)
1318+
.await
1319+
}));
1320+
}
1321+
1322+
let mut success_count = 0;
1323+
for handle in handles {
1324+
if handle.await.unwrap().is_ok() {
1325+
success_count += 1;
1326+
}
1327+
}
1328+
assert_eq!(success_count, num_ops, "All puts should succeed");
1329+
1330+
// Verify all keys exist
1331+
let list = client
1332+
.list("stress/".to_string(), "stress/~".to_string())
1333+
.await
1334+
.unwrap();
1335+
assert_eq!(list.keys.len(), num_ops);
1336+
1337+
// Range scan all
1338+
let scan = client
1339+
.range_scan("stress/".to_string(), "stress/~".to_string())
1340+
.await
1341+
.unwrap();
1342+
assert_eq!(scan.records.len(), num_ops);
1343+
1344+
// Flood with concurrent delete operations
1345+
let mut del_handles = Vec::new();
1346+
for i in 0..num_ops {
1347+
let c = client.clone();
1348+
del_handles.push(tokio::spawn(async move {
1349+
c.delete(format!("stress/{:04}", i)).await
1350+
}));
1351+
}
1352+
for handle in del_handles {
1353+
handle.await.unwrap().unwrap();
1354+
}
1355+
1356+
// Verify all keys deleted
1357+
let list = client
1358+
.list("stress/".to_string(), "stress/~".to_string())
1359+
.await
1360+
.unwrap();
1361+
assert_eq!(list.keys.len(), 0);
1362+
1363+
client.shutdown().await.unwrap();
1364+
}
1365+
1366+
// ============================================================
1367+
// Notification Display test
1368+
// ============================================================
1369+
1370+
#[tokio::test]
1371+
async fn test_notification_display() {
1372+
let created = Notification::KeyCreated(KeyCreated {
1373+
key: "test/key".to_string(),
1374+
version_id: Some(42),
1375+
});
1376+
assert!(format!("{}", created).contains("test/key"));
1377+
assert!(format!("{}", created).contains("42"));
1378+
1379+
let deleted = Notification::KeyDeleted(KeyDeleted {
1380+
key: "test/key".to_string(),
1381+
});
1382+
assert!(format!("{}", deleted).contains("test/key"));
1383+
1384+
let modified = Notification::KeyModified(KeyModified {
1385+
key: "test/key".to_string(),
1386+
version_id: Some(43),
1387+
});
1388+
assert!(format!("{}", modified).contains("test/key"));
1389+
1390+
let unknown = Notification::Unknown();
1391+
assert_eq!(format!("{}", unknown), "Unknown");
1392+
}
1393+
1394+
// ============================================================
1395+
// Multiple secondary indexes per record
1396+
// ============================================================
1397+
1398+
#[tokio::test]
1399+
async fn test_multiple_secondary_indexes() {
1400+
let (_container, address) = start_oxia().await;
1401+
let client = new_client(&address).await;
1402+
1403+
// Put a record with multiple secondary indexes
1404+
client
1405+
.put_with_options(
1406+
"multi-idx/record1".to_string(),
1407+
b"some-data".to_vec(),
1408+
vec![PutOption::SecondaryIndexes(vec![
1409+
SecondaryIndex {
1410+
index_name: "by-type".to_string(),
1411+
secondary_key: "document".to_string(),
1412+
},
1413+
SecondaryIndex {
1414+
index_name: "by-status".to_string(),
1415+
secondary_key: "active".to_string(),
1416+
},
1417+
])],
1418+
)
1419+
.await
1420+
.unwrap();
1421+
1422+
// Query via first secondary index
1423+
let list1 = client
1424+
.list_with_options(
1425+
"d".to_string(),
1426+
"e".to_string(),
1427+
vec![ListOption::UseIndex("by-type".to_string())],
1428+
)
1429+
.await
1430+
.unwrap();
1431+
assert!(
1432+
!list1.keys.is_empty(),
1433+
"Should find record via by-type index"
1434+
);
1435+
1436+
// Query via second secondary index
1437+
let list2 = client
1438+
.list_with_options(
1439+
"a".to_string(),
1440+
"b".to_string(),
1441+
vec![ListOption::UseIndex("by-status".to_string())],
1442+
)
1443+
.await
1444+
.unwrap();
1445+
assert!(
1446+
!list2.keys.is_empty(),
1447+
"Should find record via by-status index"
1448+
);
1449+
1450+
client
1451+
.delete_range("multi-idx/".to_string(), "multi-idx/~".to_string())
1452+
.await
1453+
.unwrap();
1454+
client.shutdown().await.unwrap();
1455+
}
1456+
1457+
// ============================================================
1458+
// List and scan partial ranges
1459+
// ============================================================
1460+
1461+
#[tokio::test]
1462+
async fn test_list_partial_range() {
1463+
let (_container, address) = start_oxia().await;
1464+
let client = new_client(&address).await;
1465+
1466+
for key in &["pr/a", "pr/b", "pr/c", "pr/d", "pr/e"] {
1467+
client.put(key.to_string(), b"v".to_vec()).await.unwrap();
1468+
}
1469+
1470+
// List only a subset of the range
1471+
let result = client
1472+
.list("pr/b".to_string(), "pr/d".to_string())
1473+
.await
1474+
.unwrap();
1475+
assert_eq!(result.keys.len(), 2);
1476+
assert!(result.keys.contains(&"pr/b".to_string()));
1477+
assert!(result.keys.contains(&"pr/c".to_string()));
1478+
1479+
client
1480+
.delete_range("pr/".to_string(), "pr/~".to_string())
1481+
.await
1482+
.unwrap();
1483+
client.shutdown().await.unwrap();
1484+
}
1485+
1486+
// ============================================================
1487+
// Put and immediately get - verify consistency
1488+
// ============================================================
1489+
1490+
#[tokio::test]
1491+
async fn test_read_your_writes() {
1492+
let (_container, address) = start_oxia().await;
1493+
let client = new_client(&address).await;
1494+
1495+
for i in 0..50 {
1496+
let key = format!("ryw/{}", i);
1497+
let value = format!("value-{}", i);
1498+
client
1499+
.put(key.clone(), value.clone().into_bytes())
1500+
.await
1501+
.unwrap();
1502+
let result = client.get(key).await.unwrap();
1503+
assert_eq!(result.value, Some(value.into_bytes()));
1504+
}
1505+
1506+
client
1507+
.delete_range("ryw/".to_string(), "ryw/~".to_string())
1508+
.await
1509+
.unwrap();
1510+
client.shutdown().await.unwrap();
1511+
}
1512+
1513+
// ============================================================
1514+
// Multiple clients operating concurrently
1515+
// ============================================================
1516+
1517+
#[tokio::test]
1518+
async fn test_multiple_clients() {
1519+
let (_container, address) = start_oxia().await;
1520+
let client1 = new_client(&address).await;
1521+
let client2 = new_client(&address).await;
1522+
1523+
// Client 1 writes
1524+
client1
1525+
.put("mc/key1".to_string(), b"from-client-1".to_vec())
1526+
.await
1527+
.unwrap();
1528+
1529+
// Client 2 reads
1530+
let result = client2.get("mc/key1".to_string()).await.unwrap();
1531+
assert_eq!(result.value, Some(b"from-client-1".to_vec()));
1532+
1533+
// Client 2 writes
1534+
client2
1535+
.put("mc/key2".to_string(), b"from-client-2".to_vec())
1536+
.await
1537+
.unwrap();
1538+
1539+
// Client 1 reads
1540+
let result = client1.get("mc/key2".to_string()).await.unwrap();
1541+
assert_eq!(result.value, Some(b"from-client-2".to_vec()));
1542+
1543+
client1
1544+
.delete_range("mc/".to_string(), "mc/~".to_string())
1545+
.await
1546+
.unwrap();
1547+
client1.shutdown().await.unwrap();
1548+
client2.shutdown().await.unwrap();
1549+
}
1550+
1551+
// ============================================================
1552+
// CAS (Compare-And-Swap) loop pattern
1553+
// ============================================================
1554+
1555+
#[tokio::test]
1556+
async fn test_cas_loop() {
1557+
let (_container, address) = start_oxia().await;
1558+
let client = new_client(&address).await;
1559+
1560+
// Create initial record
1561+
let initial = client
1562+
.put("cas/counter".to_string(), b"0".to_vec())
1563+
.await
1564+
.unwrap();
1565+
1566+
// Simulate a CAS loop: read-modify-write with version check
1567+
let mut current_version = initial.version.version_id;
1568+
for i in 1..=5 {
1569+
let result = client
1570+
.put_with_options(
1571+
"cas/counter".to_string(),
1572+
format!("{}", i).into_bytes(),
1573+
vec![PutOption::ExpectVersionId(current_version)],
1574+
)
1575+
.await
1576+
.unwrap();
1577+
current_version = result.version.version_id;
1578+
}
1579+
1580+
// Verify final value
1581+
let final_result = client.get("cas/counter".to_string()).await.unwrap();
1582+
assert_eq!(final_result.value, Some(b"5".to_vec()));
1583+
1584+
client
1585+
.delete_range("cas/".to_string(), "cas/~".to_string())
1586+
.await
1587+
.unwrap();
1588+
client.shutdown().await.unwrap();
1589+
}

0 commit comments

Comments
 (0)