#![recursion_limit = "256"]
#![cfg(feature = "embedded-core")]
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use falkordb::{
EmbeddedConfig, EmbeddedServer, EntityType, FalkorClientBuilder, FalkorConnectionInfo,
IndexType,
};
#[cfg(feature = "tokio")]
use futures::StreamExt;
fn common_module_paths() -> Vec<PathBuf> {
vec![
PathBuf::from("/usr/lib/redis/modules/falkordb.so"),
PathBuf::from("/usr/local/lib/redis/modules/falkordb.so"),
PathBuf::from("/opt/homebrew/lib/redis/modules/falkordb.so"),
PathBuf::from("./falkordb.so"),
]
}
fn resolve_module_path() -> Option<PathBuf> {
if std::env::var("SKIP_INTEGRATION_TESTS").is_ok() {
return None;
}
if let Ok(path) = std::env::var("FALKORDB_MODULE_PATH") {
let path = PathBuf::from(path);
return path.exists().then_some(path);
}
common_module_paths().into_iter().find(|p| p.exists())
}
fn embedded_config() -> Option<EmbeddedConfig> {
static SOCKET_NONCE: AtomicU32 = AtomicU32::new(0);
let module = resolve_module_path()?;
Some(EmbeddedConfig {
redis_server_path: std::env::var("REDIS_SERVER_PATH").ok().map(PathBuf::from),
falkordb_module_path: Some(module),
socket_path: Some(PathBuf::from(format!(
"/tmp/fdb-emb-{}-{}.sock",
std::process::id(),
SOCKET_NONCE.fetch_add(1, Ordering::Relaxed)
))),
..Default::default()
})
}
fn retry_until<T>(
mut op: impl FnMut() -> T,
done: impl Fn(&T) -> bool,
) -> T {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let value = op();
if done(&value) || std::time::Instant::now() >= deadline {
return value;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[test]
fn test_embedded_sync_full_surface() {
let Some(config) = embedded_config() else {
return;
};
let client = FalkorClientBuilder::new()
.with_connection_info(FalkorConnectionInfo::Embedded(config))
.build()
.expect("embedded sync client should build");
assert!(client.connection_pool_size() >= 1);
let mut graph = client.select_graph("embedded_sync");
let _ = graph.delete();
let mut graph = client.select_graph("embedded_sync");
graph
.query("CREATE (:Person {name: 'Alice', age: 30}), (:Person {name: 'Bob', age: 40})")
.execute()
.expect("write query should succeed");
let mut res = graph
.query("MATCH (p:Person) WHERE p.age >= $min_age RETURN p.age")
.with_param("min_age", 35)
.execute()
.expect("parameterized query should succeed");
let ages: Vec<i64> = res
.data
.by_ref()
.map(|row| {
row.expect("row should parse")
.try_get_at::<i64>(0)
.expect("column 0 should be an i64")
})
.collect();
assert_eq!(ages, vec![40]);
let mut ro = graph
.ro_query("MATCH (p:Person) RETURN count(p)")
.execute()
.expect("read-only query should succeed");
let count = ro
.data
.next()
.expect("expected a row")
.expect("row should parse")
.try_get_at::<i64>(0)
.expect("column 0 should be an i64");
assert_eq!(count, 2);
graph
.create_index(IndexType::Range, EntityType::Node, "Person", &["age"], None)
.expect("index creation should succeed");
let indices = retry_until(
|| graph.list_indices().expect("list indices"),
|res| !res.data.is_empty(),
);
assert!(!indices.data.is_empty(), "index should be listed");
graph.delete().expect("cleanup");
}
#[cfg(feature = "tokio")]
mod async_flavours {
use super::*;
use falkordb::{ConnectionStrategy, FalkorAsyncClient};
use std::num::NonZeroU8;
use std::sync::Arc;
fn strategies() -> [ConnectionStrategy; 3] {
[
ConnectionStrategy::Pooled {
size: NonZeroU8::new(4).unwrap(),
},
ConnectionStrategy::Multiplexed {
connections: NonZeroU8::new(8).unwrap(),
},
ConnectionStrategy::Multiplexed {
connections: NonZeroU8::new(1).unwrap(),
},
]
}
async fn build_async(strategy: ConnectionStrategy) -> Option<FalkorAsyncClient> {
let config = embedded_config()?;
Some(
FalkorClientBuilder::new_async()
.with_connection_info(FalkorConnectionInfo::Embedded(config))
.with_connection_strategy(strategy)
.build()
.await
.expect("embedded async client should build for an available module"),
)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_embedded_async_full_surface_all_strategies() {
if embedded_config().is_none() {
return;
}
for strategy in strategies() {
let client = build_async(strategy)
.await
.expect("embedded async client should build for an available module");
assert_eq!(client.connection_strategy(), strategy);
let graph_name = format!("embedded_async_{}", strategy.connection_count().get());
let _ = client.select_graph(&graph_name).delete().await;
let mut graph = client.select_graph(&graph_name);
graph
.query("UNWIND range(1, 10) AS i CREATE (:N {v: i})")
.execute()
.await
.expect("write query should succeed");
let mut res = graph
.query("MATCH (n:N) WHERE n.v > $threshold RETURN count(n)")
.with_param("threshold", 5)
.execute()
.await
.expect("parameterized query should succeed");
let count = res
.data
.next()
.await
.expect("expected a row")
.expect("row should parse")
.try_get_at::<i64>(0)
.expect("column 0 should be an i64");
assert_eq!(count, 5, "strategy {strategy:?} parameterized count");
let mut ro = graph
.ro_query("MATCH (n:N) RETURN count(n)")
.execute()
.await
.expect("read-only query should succeed");
let count = ro
.data
.next()
.await
.expect("expected a row")
.expect("row should parse")
.try_get_at::<i64>(0)
.expect("column 0 should be an i64");
assert_eq!(count, 10, "strategy {strategy:?} read-only count");
graph.delete().await.expect("cleanup");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_embedded_async_concurrent_single_socket() {
const CONCURRENCY: i64 = 200;
if embedded_config().is_none() {
return;
}
let client = build_async(ConnectionStrategy::Multiplexed {
connections: NonZeroU8::new(1).unwrap(),
})
.await
.expect("embedded async client should build for an available module");
let client = Arc::new(client);
let graph_name = "embedded_async_concurrent";
let _ = client.select_graph(graph_name).delete().await;
let handles: Vec<_> = (0..CONCURRENCY)
.map(|i| {
let client = client.clone();
tokio::spawn(async move {
let mut graph = client.select_graph(graph_name);
let mut res = graph
.query(format!("RETURN {i}"))
.execute()
.await
.expect("concurrent query should succeed");
res.data
.next()
.await
.expect("expected a row")
.expect("row should parse")
.try_get_at::<i64>(0)
.expect("column 0 should be an i64")
})
})
.collect();
for (expected, handle) in handles.into_iter().enumerate() {
let actual = handle.await.expect("task should not panic");
assert_eq!(
actual, expected as i64,
"embedded multiplexed: response routed to the wrong caller"
);
}
client.select_graph(graph_name).delete().await.ok();
}
}
#[test]
fn test_embedded_server_lifecycle() {
let Some(config) = embedded_config() else {
return;
};
let socket_path;
{
let server = EmbeddedServer::start(config).expect("embedded server should start");
socket_path = server.socket_path().to_path_buf();
assert!(
socket_path.exists(),
"socket should exist while server runs"
);
assert_eq!(
server.connection_string(),
format!("unix://{}", socket_path.display())
);
}
assert!(
!socket_path.exists(),
"socket should be removed after the server is dropped"
);
}