use std::path::PathBuf;
use std::process::{Child, Command};
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
use heroindex_client::HeroIndexClient;
use serde_json::json;
static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
fn unique_paths() -> (PathBuf, PathBuf) {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let socket = PathBuf::from(format!("/tmp/heroindex_test_{}_{}.sock", pid, id));
let data = PathBuf::from(format!("/tmp/heroindex_test_data_{}_{}", pid, id));
(socket, data)
}
struct TestServer {
child: Child,
socket_path: PathBuf,
data_dir: PathBuf,
}
impl TestServer {
fn start() -> Self {
let (socket_path, data_dir) = unique_paths();
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_dir_all(&data_dir);
let binary_path = if std::path::Path::new("./target/debug/heroindex").exists() {
"./target/debug/heroindex".to_string()
} else if std::path::Path::new("../target/debug/heroindex").exists() {
"../target/debug/heroindex".to_string()
} else {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
format!("{}/../target/debug/heroindex", manifest_dir)
};
let child = Command::new(&binary_path)
.arg("--dir")
.arg(&data_dir)
.arg("--socket")
.arg(&socket_path)
.spawn()
.expect("Failed to start server");
for _ in 0..50 {
if socket_path.exists() {
break;
}
thread::sleep(Duration::from_millis(100));
}
thread::sleep(Duration::from_millis(200));
Self {
child,
socket_path,
data_dir,
}
}
async fn connect(&self) -> HeroIndexClient {
HeroIndexClient::connect(&self.socket_path)
.await
.expect("Failed to connect")
}
}
impl Drop for TestServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_file(&self.socket_path);
let _ = std::fs::remove_dir_all(&self.data_dir);
}
}
#[tokio::test]
async fn test_server_ping() {
let server = TestServer::start();
let mut client = server.connect().await;
let result = client.ping().await.unwrap();
assert_eq!(result.status, "ok");
assert!(!result.version.is_empty());
}
#[tokio::test]
async fn test_rpc_discover() {
let server = TestServer::start();
let mut client = server.connect().await;
let result = client.discover().await.unwrap();
assert_eq!(result.get("openrpc").unwrap(), "1.2.6");
assert!(result.get("info").is_some());
assert!(result.get("methods").is_some());
let methods = result.get("methods").unwrap().as_array().unwrap();
assert!(!methods.is_empty());
let method_names: Vec<&str> = methods
.iter()
.map(|m| m.get("name").unwrap().as_str().unwrap())
.collect();
assert!(method_names.contains(&"rpc.discover"));
assert!(method_names.contains(&"server.ping"));
assert!(method_names.contains(&"db.create"));
assert!(method_names.contains(&"search.query"));
}
#[tokio::test]
async fn test_create_and_list_databases() {
let server = TestServer::start();
let mut client = server.connect().await;
let result = client.db_list().await.unwrap();
assert!(result.databases.is_empty());
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true},
{"name": "body", "type": "text", "stored": true, "indexed": true},
{"name": "count", "type": "u64", "stored": true, "indexed": true, "fast": true}
]
});
let result = client.db_create("test_db", schema.clone()).await.unwrap();
assert!(result.success);
assert_eq!(result.name, "test_db");
let result = client.db_list().await.unwrap();
assert_eq!(result.databases.len(), 1);
assert_eq!(result.databases[0].name, "test_db");
let err = client.db_create("test_db", schema).await.unwrap_err();
assert!(matches!(err, heroindex_client::Error::Rpc { .. }));
}
#[tokio::test]
async fn test_multiple_databases() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
for i in 0..5 {
let name = format!("db_{}", i);
let result = client.db_create(&name, schema.clone()).await.unwrap();
assert!(result.success);
}
let result = client.db_list().await.unwrap();
assert_eq!(result.databases.len(), 5);
let result = client.db_delete("db_2").await.unwrap();
assert!(result.success);
let result = client.db_list().await.unwrap();
assert_eq!(result.databases.len(), 4);
}
#[tokio::test]
async fn test_select_and_info() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
client.db_create("mydb", schema).await.unwrap();
let err = client.db_info().await.unwrap_err();
assert!(matches!(err, heroindex_client::Error::Rpc { .. }));
let result = client.db_select("mydb").await.unwrap();
assert!(result.success);
assert_eq!(result.name, "mydb");
let result = client.db_info().await.unwrap();
assert_eq!(result.name, "mydb");
assert_eq!(result.doc_count, 0);
}
#[tokio::test]
async fn test_add_and_search_documents() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true},
{"name": "body", "type": "text", "stored": true, "indexed": true}
]
});
client.db_create("articles", schema).await.unwrap();
client.db_select("articles").await.unwrap();
client
.doc_add(json!({"title": "hello world", "body": "this is my first article"}))
.await
.unwrap();
client
.doc_add(
json!({"title": "rust programming", "body": "rust is a systems programming language"}),
)
.await
.unwrap();
client
.doc_add(
json!({"title": "search engines", "body": "building a search engine with tantivy"}),
)
.await
.unwrap();
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client
.search(
json!({"type": "match", "field": "body", "value": "rust"}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 1);
assert_eq!(result.hits.len(), 1);
assert!(
result.hits[0]
.doc
.get("title")
.unwrap()
.as_str()
.unwrap()
.contains("rust")
);
}
#[tokio::test]
async fn test_batch_add() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
client.db_create("batch_test", schema).await.unwrap();
client.db_select("batch_test").await.unwrap();
let docs: Vec<serde_json::Value> = (0..100)
.map(|i| json!({"title": format!("document number {}", i)}))
.collect();
let result = client.doc_add_batch(docs).await.unwrap();
assert!(result.success);
assert_eq!(result.count, 100);
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 100);
}
#[tokio::test]
async fn test_fuzzy_search() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
client.db_create("fuzzy_test", schema).await.unwrap();
client.db_select("fuzzy_test").await.unwrap();
client
.doc_add(json!({"title": "hello world"}))
.await
.unwrap();
client
.doc_add(json!({"title": "programming language"}))
.await
.unwrap();
client
.doc_add(json!({"title": "search engine"}))
.await
.unwrap();
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client
.search(
json!({"type": "fuzzy", "field": "title", "value": "helo", "distance": 1}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 1);
assert!(
result.hits[0]
.doc
.get("title")
.unwrap()
.as_str()
.unwrap()
.contains("hello")
);
let result = client
.search(
json!({"type": "fuzzy", "field": "title", "value": "serch", "distance": 1}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 1);
}
#[tokio::test]
async fn test_boolean_queries() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true},
{"name": "category", "type": "str", "stored": true, "indexed": true}
]
});
client.db_create("bool_test", schema).await.unwrap();
client.db_select("bool_test").await.unwrap();
client
.doc_add(json!({"title": "rust programming", "category": "tech"}))
.await
.unwrap();
client
.doc_add(json!({"title": "python programming", "category": "tech"}))
.await
.unwrap();
client
.doc_add(json!({"title": "cooking recipes", "category": "food"}))
.await
.unwrap();
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client
.search(
json!({
"type": "boolean",
"must": [{"type": "match", "field": "title", "value": "programming"}],
"should": [{"type": "match", "field": "title", "value": "rust"}]
}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 2);
assert!(
result.hits[0]
.doc
.get("title")
.unwrap()
.as_str()
.unwrap()
.contains("rust")
);
let result = client
.search(
json!({
"type": "boolean",
"must": [{"type": "match", "field": "title", "value": "programming"}],
"must_not": [{"type": "match", "field": "title", "value": "python"}]
}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 1);
}
#[tokio::test]
async fn test_range_queries() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true},
{"name": "price", "type": "u64", "stored": true, "indexed": true, "fast": true}
]
});
client.db_create("range_test", schema).await.unwrap();
client.db_select("range_test").await.unwrap();
for price in [10, 25, 50, 75, 100, 150, 200] {
client
.doc_add(json!({"title": format!("product at ${}", price), "price": price}))
.await
.unwrap();
}
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client
.search(
json!({"type": "range", "field": "price", "gte": 50, "lt": 150}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 3); }
#[tokio::test]
async fn test_concurrent_connections() {
let server = TestServer::start();
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true},
{"name": "conn_id", "type": "u64", "stored": true, "indexed": true}
]
});
let mut client1 = server.connect().await;
client1.db_create("concurrent_test", schema).await.unwrap();
let mut client2 = server.connect().await;
client1.db_select("concurrent_test").await.unwrap();
client2.db_select("concurrent_test").await.unwrap();
for i in 0..10 {
client1
.doc_add(json!({"title": format!("doc from conn1 #{}", i), "conn_id": 1}))
.await
.unwrap();
client2
.doc_add(json!({"title": format!("doc from conn2 #{}", i), "conn_id": 2}))
.await
.unwrap();
}
client1.commit().await.unwrap();
client2.reload().await.unwrap();
let result = client1.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 20);
let result = client2.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 20);
let result = client1
.search(
json!({"type": "term", "field": "conn_id", "value": 1}),
20,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 10);
let result = client2
.search(
json!({"type": "term", "field": "conn_id", "value": 2}),
20,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 10);
}
#[tokio::test]
async fn test_separate_databases_per_connection() {
let server = TestServer::start();
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
let mut client_setup = server.connect().await;
client_setup
.db_create("db_a", schema.clone())
.await
.unwrap();
client_setup.db_create("db_b", schema).await.unwrap();
let mut client1 = server.connect().await;
client1.db_select("db_a").await.unwrap();
let mut client2 = server.connect().await;
client2.db_select("db_b").await.unwrap();
client1
.doc_add(json!({"title": "document in a"}))
.await
.unwrap();
client2
.doc_add(json!({"title": "document in b"}))
.await
.unwrap();
client1.commit().await.unwrap();
client2.commit().await.unwrap();
client1.reload().await.unwrap();
client2.reload().await.unwrap();
let result = client1.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 1);
let result = client2.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 1);
let result = client1.search(json!({"type": "all"}), 10, 0).await.unwrap();
assert!(
result.hits[0]
.doc
.get("title")
.unwrap()
.as_str()
.unwrap()
.contains("document in a")
);
let result = client2.search(json!({"type": "all"}), 10, 0).await.unwrap();
assert!(
result.hits[0]
.doc
.get("title")
.unwrap()
.as_str()
.unwrap()
.contains("document in b")
);
}
#[tokio::test]
async fn test_delete_documents() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "id", "type": "str", "stored": true, "indexed": true},
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
client.db_create("delete_test", schema).await.unwrap();
client.db_select("delete_test").await.unwrap();
client
.doc_add(json!({"id": "doc1", "title": "first document"}))
.await
.unwrap();
client
.doc_add(json!({"id": "doc2", "title": "second document"}))
.await
.unwrap();
client
.doc_add(json!({"id": "doc3", "title": "third document"}))
.await
.unwrap();
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 3);
client.doc_delete("id", json!("doc2")).await.unwrap();
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client.count(json!({"type": "all"})).await.unwrap();
assert_eq!(result.count, 2);
let result = client
.search(
json!({"type": "term", "field": "id", "value": "doc2"}),
10,
0,
)
.await
.unwrap();
assert_eq!(result.total_hits, 0);
}
#[tokio::test]
async fn test_server_stats() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true}
]
});
client.db_create("stats_db1", schema.clone()).await.unwrap();
client.db_select("stats_db1").await.unwrap();
for i in 0..50 {
client
.doc_add(json!({"title": format!("document {}", i)}))
.await
.unwrap();
}
client.commit().await.unwrap();
client.reload().await.unwrap();
client.db_create("stats_db2", schema).await.unwrap();
client.db_select("stats_db2").await.unwrap();
for i in 0..30 {
client
.doc_add(json!({"title": format!("document {}", i)}))
.await
.unwrap();
}
client.commit().await.unwrap();
client.reload().await.unwrap();
let result = client.stats().await.unwrap();
assert_eq!(result.databases, 2);
assert_eq!(result.total_docs, 80);
}
#[tokio::test]
async fn test_schema_get() {
let server = TestServer::start();
let mut client = server.connect().await;
let schema = json!({
"fields": [
{"name": "title", "type": "text", "stored": true, "indexed": true},
{"name": "count", "type": "u64", "stored": true, "indexed": true}
]
});
client.db_create("schema_test", schema).await.unwrap();
client.db_select("schema_test").await.unwrap();
let result = client.schema().await.unwrap();
assert_eq!(result.fields.len(), 2);
let field_names: Vec<&str> = result.fields.iter().map(|f| f.name.as_str()).collect();
assert!(field_names.contains(&"title"));
assert!(field_names.contains(&"count"));
}