use std::{sync::Arc, time::Duration};
use arrow_array::{Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use graphar::{FileType, storage::ChunkStore};
use testcontainers::{
GenericImage, ImageExt,
core::{ContainerPort, ExecCommand, WaitFor},
runners::AsyncRunner,
};
fn ensure_docker_host() {
if std::env::var("DOCKER_HOST").is_err() {
let uid = std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(1000);
let sock = format!("/run/user/{uid}/podman/podman.sock");
if std::path::Path::new(&sock).exists() {
unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{sock}")) };
}
}
}
fn sample_batch() -> RecordBatch {
RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("name", DataType::Utf8, false),
])),
vec![
Arc::new(Int64Array::from(vec![1, 2, 3])),
Arc::new(StringArray::from(vec!["a", "b", "c"])),
],
)
.unwrap()
}
async fn round_trip_all_formats(store: &ChunkStore) {
for ft in [
FileType::Parquet,
FileType::Csv,
FileType::Json,
FileType::Orc,
FileType::ArrowIpc,
] {
let rel = format!("vertex/person/id/chunk0.{}", ft.extension());
store
.write_chunk(&rel, &[sample_batch()], &ft)
.await
.unwrap_or_else(|e| panic!("write {ft:?}: {e}"));
let batches = store
.read_chunk(&rel, &ft)
.await
.unwrap_or_else(|e| panic!("read {ft:?}: {e}"));
let rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(rows, 3, "{ft:?}");
}
}
#[tokio::test]
#[ignore = "needs podman; run with --ignored"]
async fn chunk_store_against_seaweedfs() {
ensure_docker_host();
let container = GenericImage::new("docker.io/chrislusf/seaweedfs", "latest")
.with_exposed_port(ContainerPort::Tcp(8333))
.with_wait_for(WaitFor::message_on_stderr("Starting S3 API Server"))
.with_cmd(["server", "-s3", "-s3.port=8333"])
.with_startup_timeout(Duration::from_secs(120))
.start()
.await
.expect("start seaweedfs");
let port = container.get_host_port_ipv4(8333).await.unwrap();
let mut exec = container
.exec(ExecCommand::new([
"sh",
"-c",
"echo 's3.bucket.create -name knut' | weed shell",
]))
.await
.expect("weed shell bucket create");
let _ = exec.stdout_to_vec().await;
tokio::time::sleep(Duration::from_secs(2)).await;
let store = ChunkStore::s3_compatible(
&format!("http://127.0.0.1:{port}"),
"knut",
"any",
"any",
"graphs/itest",
)
.unwrap();
round_trip_all_formats(&store).await;
}
#[tokio::test]
#[ignore = "needs podman; run with --ignored"]
async fn chunk_store_against_rustfs() {
ensure_docker_host();
let container = GenericImage::new("docker.io/rustfs/rustfs", "latest")
.with_exposed_port(ContainerPort::Tcp(9000))
.with_startup_timeout(Duration::from_secs(120))
.start()
.await
.expect("start rustfs");
let port = container.get_host_port_ipv4(9000).await.unwrap();
tokio::time::sleep(Duration::from_secs(3)).await;
let mut exec = container
.exec(ExecCommand::new([
"curl",
"-s",
"-X",
"PUT",
"--user",
"rustfsadmin:rustfsadmin",
"--aws-sigv4",
"aws:amz:us-east-1:s3",
"http://127.0.0.1:9000/knut",
]))
.await
.expect("create bucket via signed PUT");
let _ = exec.stdout_to_vec().await;
tokio::time::sleep(Duration::from_secs(1)).await;
let store = ChunkStore::s3_compatible(
&format!("http://127.0.0.1:{port}"),
"knut",
"rustfsadmin",
"rustfsadmin",
"graphs/itest",
)
.unwrap();
round_trip_all_formats(&store).await;
}