Skip to content

Commit f9e236b

Browse files
authored
test: add integration tests for real-world patterns and edge cases (#19)
* 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> * test: add binary data, size overwrite, and CAS conflict tests - test_binary_values: verify non-UTF8 binary data round-trip - test_overwrite_different_sizes: small->large->small value overwrites - test_concurrent_cas_conflict: two concurrent CAS updates to same key, verify exactly one succeeds and one gets UnexpectedVersionId - Total: 45 integration tests + 12 unit tests + 1 doctest = 58 tests Signed-off-by: mattisonchao <mattisonchao@gmail.com> --------- Signed-off-by: mattisonchao <mattisonchao@gmail.com>
1 parent 6069121 commit f9e236b

File tree

1 file changed

+256
-0
lines changed

1 file changed

+256
-0
lines changed

liboxia-native/tests/integration_tests.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,3 +1462,259 @@ async fn test_multiple_secondary_indexes() {
14621462
.unwrap();
14631463
client.shutdown().await.unwrap();
14641464
}
1465+
1466+
// ============================================================
1467+
// List and scan partial ranges
1468+
// ============================================================
1469+
1470+
#[tokio::test]
1471+
async fn test_list_partial_range() {
1472+
let (_container, address) = start_oxia().await;
1473+
let client = new_client(&address).await;
1474+
1475+
for key in &["pr/a", "pr/b", "pr/c", "pr/d", "pr/e"] {
1476+
client.put(key.to_string(), b"v".to_vec()).await.unwrap();
1477+
}
1478+
1479+
// List only a subset of the range
1480+
let result = client
1481+
.list("pr/b".to_string(), "pr/d".to_string())
1482+
.await
1483+
.unwrap();
1484+
assert_eq!(result.keys.len(), 2);
1485+
assert!(result.keys.contains(&"pr/b".to_string()));
1486+
assert!(result.keys.contains(&"pr/c".to_string()));
1487+
1488+
client
1489+
.delete_range("pr/".to_string(), "pr/~".to_string())
1490+
.await
1491+
.unwrap();
1492+
client.shutdown().await.unwrap();
1493+
}
1494+
1495+
// ============================================================
1496+
// Put and immediately get - verify consistency
1497+
// ============================================================
1498+
1499+
#[tokio::test]
1500+
async fn test_read_your_writes() {
1501+
let (_container, address) = start_oxia().await;
1502+
let client = new_client(&address).await;
1503+
1504+
for i in 0..50 {
1505+
let key = format!("ryw/{}", i);
1506+
let value = format!("value-{}", i);
1507+
client
1508+
.put(key.clone(), value.clone().into_bytes())
1509+
.await
1510+
.unwrap();
1511+
let result = client.get(key).await.unwrap();
1512+
assert_eq!(result.value, Some(value.into_bytes()));
1513+
}
1514+
1515+
client
1516+
.delete_range("ryw/".to_string(), "ryw/~".to_string())
1517+
.await
1518+
.unwrap();
1519+
client.shutdown().await.unwrap();
1520+
}
1521+
1522+
// ============================================================
1523+
// Multiple clients operating concurrently
1524+
// ============================================================
1525+
1526+
#[tokio::test]
1527+
async fn test_multiple_clients() {
1528+
let (_container, address) = start_oxia().await;
1529+
let client1 = new_client(&address).await;
1530+
let client2 = new_client(&address).await;
1531+
1532+
// Client 1 writes
1533+
client1
1534+
.put("mc/key1".to_string(), b"from-client-1".to_vec())
1535+
.await
1536+
.unwrap();
1537+
1538+
// Client 2 reads
1539+
let result = client2.get("mc/key1".to_string()).await.unwrap();
1540+
assert_eq!(result.value, Some(b"from-client-1".to_vec()));
1541+
1542+
// Client 2 writes
1543+
client2
1544+
.put("mc/key2".to_string(), b"from-client-2".to_vec())
1545+
.await
1546+
.unwrap();
1547+
1548+
// Client 1 reads
1549+
let result = client1.get("mc/key2".to_string()).await.unwrap();
1550+
assert_eq!(result.value, Some(b"from-client-2".to_vec()));
1551+
1552+
client1
1553+
.delete_range("mc/".to_string(), "mc/~".to_string())
1554+
.await
1555+
.unwrap();
1556+
client1.shutdown().await.unwrap();
1557+
client2.shutdown().await.unwrap();
1558+
}
1559+
1560+
// ============================================================
1561+
// CAS (Compare-And-Swap) loop pattern
1562+
// ============================================================
1563+
1564+
#[tokio::test]
1565+
async fn test_cas_loop() {
1566+
let (_container, address) = start_oxia().await;
1567+
let client = new_client(&address).await;
1568+
1569+
// Create initial record
1570+
let initial = client
1571+
.put("cas/counter".to_string(), b"0".to_vec())
1572+
.await
1573+
.unwrap();
1574+
1575+
// Simulate a CAS loop: read-modify-write with version check
1576+
let mut current_version = initial.version.version_id;
1577+
for i in 1..=5 {
1578+
let result = client
1579+
.put_with_options(
1580+
"cas/counter".to_string(),
1581+
format!("{}", i).into_bytes(),
1582+
vec![PutOption::ExpectVersionId(current_version)],
1583+
)
1584+
.await
1585+
.unwrap();
1586+
current_version = result.version.version_id;
1587+
}
1588+
1589+
// Verify final value
1590+
let final_result = client.get("cas/counter".to_string()).await.unwrap();
1591+
assert_eq!(final_result.value, Some(b"5".to_vec()));
1592+
1593+
client
1594+
.delete_range("cas/".to_string(), "cas/~".to_string())
1595+
.await
1596+
.unwrap();
1597+
client.shutdown().await.unwrap();
1598+
}
1599+
1600+
// ============================================================
1601+
// Binary (non-UTF8) data
1602+
// ============================================================
1603+
1604+
#[tokio::test]
1605+
async fn test_binary_values() {
1606+
let (_container, address) = start_oxia().await;
1607+
let client = new_client(&address).await;
1608+
1609+
// Store binary data that isn't valid UTF-8
1610+
let binary_data: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE, 0x80, 0x90, 0xAB, 0xCD];
1611+
client
1612+
.put("bin/data".to_string(), binary_data.clone())
1613+
.await
1614+
.unwrap();
1615+
1616+
let result = client.get("bin/data".to_string()).await.unwrap();
1617+
assert_eq!(result.value, Some(binary_data));
1618+
1619+
client
1620+
.delete_range("bin/".to_string(), "bin/~".to_string())
1621+
.await
1622+
.unwrap();
1623+
client.shutdown().await.unwrap();
1624+
}
1625+
1626+
// ============================================================
1627+
// Overwrite with different value sizes
1628+
// ============================================================
1629+
1630+
#[tokio::test]
1631+
async fn test_overwrite_different_sizes() {
1632+
let (_container, address) = start_oxia().await;
1633+
let client = new_client(&address).await;
1634+
1635+
// Start with a small value
1636+
client
1637+
.put("sizes/key".to_string(), b"small".to_vec())
1638+
.await
1639+
.unwrap();
1640+
1641+
// Overwrite with a larger value
1642+
let large_value = vec![b'x'; 10000];
1643+
client
1644+
.put("sizes/key".to_string(), large_value.clone())
1645+
.await
1646+
.unwrap();
1647+
1648+
let result = client.get("sizes/key".to_string()).await.unwrap();
1649+
assert_eq!(result.value, Some(large_value));
1650+
1651+
// Overwrite back to a small value
1652+
client
1653+
.put("sizes/key".to_string(), b"tiny".to_vec())
1654+
.await
1655+
.unwrap();
1656+
1657+
let result = client.get("sizes/key".to_string()).await.unwrap();
1658+
assert_eq!(result.value, Some(b"tiny".to_vec()));
1659+
1660+
client
1661+
.delete_range("sizes/".to_string(), "sizes/~".to_string())
1662+
.await
1663+
.unwrap();
1664+
client.shutdown().await.unwrap();
1665+
}
1666+
1667+
// ============================================================
1668+
// Concurrent CAS conflict detection
1669+
// ============================================================
1670+
1671+
#[tokio::test]
1672+
async fn test_concurrent_cas_conflict() {
1673+
let (_container, address) = start_oxia().await;
1674+
let client = new_client(&address).await;
1675+
1676+
// Create a key
1677+
let initial = client
1678+
.put("conflict/key".to_string(), b"v0".to_vec())
1679+
.await
1680+
.unwrap();
1681+
let version = initial.version.version_id;
1682+
1683+
// Two concurrent CAS updates with the same version - one should fail
1684+
let c1 = client.clone();
1685+
let c2 = client.clone();
1686+
1687+
let h1 = tokio::spawn(async move {
1688+
c1.put_with_options(
1689+
"conflict/key".to_string(),
1690+
b"v1-from-c1".to_vec(),
1691+
vec![PutOption::ExpectVersionId(version)],
1692+
)
1693+
.await
1694+
});
1695+
1696+
let h2 = tokio::spawn(async move {
1697+
c2.put_with_options(
1698+
"conflict/key".to_string(),
1699+
b"v1-from-c2".to_vec(),
1700+
vec![PutOption::ExpectVersionId(version)],
1701+
)
1702+
.await
1703+
});
1704+
1705+
let r1 = h1.await.unwrap();
1706+
let r2 = h2.await.unwrap();
1707+
1708+
// Exactly one should succeed and one should fail with UnexpectedVersionId
1709+
let (successes, failures): (Vec<_>, Vec<_>) = vec![r1, r2].into_iter().partition(|r| r.is_ok());
1710+
1711+
assert_eq!(successes.len(), 1, "Exactly one CAS should succeed");
1712+
assert_eq!(failures.len(), 1, "Exactly one CAS should fail");
1713+
assert!(matches!(failures[0], Err(OxiaError::UnexpectedVersionId())));
1714+
1715+
client
1716+
.delete_range("conflict/".to_string(), "conflict/~".to_string())
1717+
.await
1718+
.unwrap();
1719+
client.shutdown().await.unwrap();
1720+
}

0 commit comments

Comments
 (0)