use faucet_source_spanner::{SpannerConnection, SpannerCredentials};
use gcloud_googleapis::spanner::admin::database::v1::CreateDatabaseRequest;
use gcloud_googleapis::spanner::admin::instance::v1::{CreateInstanceRequest, Instance};
use gcloud_spanner::client::Client;
use gcloud_spanner::statement::Statement;
use testcontainers::{ContainerAsync, GenericImage, core::IntoContainerPort, runners::AsyncRunner};
pub const PROJECT: &str = "test-project";
pub const INSTANCE: &str = "test-instance";
pub struct Emulator {
pub _container: ContainerAsync<GenericImage>,
pub host: String,
}
pub async fn start_emulator() -> Option<Emulator> {
let image = GenericImage::new("gcr.io/cloud-spanner-emulator/emulator", "latest")
.with_exposed_port(9010.tcp());
let container = match image.start().await {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping: Docker not available ({e})");
return None;
}
};
let port = container.get_host_port_ipv4(9010).await.ok()?;
Some(Emulator {
_container: container,
host: format!("127.0.0.1:{port}"),
})
}
pub fn connection(host: &str, database: &str) -> SpannerConnection {
SpannerConnection {
project_id: PROJECT.into(),
instance: INSTANCE.into(),
database: database.into(),
auth: SpannerCredentials::ApplicationDefault,
max_sessions: 20,
emulator_host: Some(host.to_string()),
}
}
pub async fn create_database(host: &str, database: &str, ddl: &[&str]) {
let conn = connection(host, database);
let mut admin = None;
for _ in 0..120 {
match conn.connect_admin().await {
Ok(a) => {
admin = Some(a);
break;
}
Err(_) => tokio::time::sleep(std::time::Duration::from_millis(500)).await,
}
}
let admin = admin.expect("spanner emulator admin endpoint never became reachable");
let mut created = false;
for _ in 0..120 {
let req = CreateInstanceRequest {
parent: format!("projects/{PROJECT}"),
instance_id: INSTANCE.into(),
instance: Some(Instance {
name: format!("projects/{PROJECT}/instances/{INSTANCE}"),
config: format!("projects/{PROJECT}/instanceConfigs/emulator-config"),
display_name: "faucet-test".into(),
node_count: 1,
..Default::default()
}),
};
match admin.instance().create_instance(req, None).await {
Ok(mut op) => {
op.wait(None).await.expect("instance LRO");
created = true;
break;
}
Err(status) if status.code() == gcloud_gax::grpc::Code::AlreadyExists => {
created = true;
break;
}
Err(_) => tokio::time::sleep(std::time::Duration::from_millis(500)).await,
}
}
assert!(created, "spanner emulator never became ready");
let req = CreateDatabaseRequest {
parent: format!("projects/{PROJECT}/instances/{INSTANCE}"),
create_statement: format!("CREATE DATABASE `{database}`"),
extra_statements: ddl.iter().map(|s| s.to_string()).collect(),
..Default::default()
};
admin
.database()
.create_database(req, None)
.await
.expect("create database")
.wait(None)
.await
.expect("database LRO");
}
pub async fn raw_client(host: &str, database: &str) -> Client {
connection(host, database).connect().await.expect("client")
}
pub async fn execute_dml(client: &Client, sql: &str) {
let sql = sql.to_string();
client
.read_write_transaction(|tx| {
let sql = sql.clone();
Box::pin(async move {
tx.update(Statement::new(sql)).await?;
Ok::<_, gcloud_spanner::client::Error>(())
})
})
.await
.expect("dml");
}