diff options
Diffstat (limited to 'src/garage')
-rw-r--r-- | src/garage/Cargo.toml | 10 | ||||
-rw-r--r-- | src/garage/tests/common/client.rs | 22 | ||||
-rw-r--r-- | src/garage/tests/common/ext/mod.rs | 3 | ||||
-rw-r--r-- | src/garage/tests/common/ext/process.rs | 55 | ||||
-rw-r--r-- | src/garage/tests/common/garage.rs | 217 | ||||
-rw-r--r-- | src/garage/tests/common/macros.rs | 11 | ||||
-rw-r--r-- | src/garage/tests/common/mod.rs | 45 | ||||
-rw-r--r-- | src/garage/tests/lib.rs | 4 | ||||
-rw-r--r-- | src/garage/tests/simple.rs | 61 |
9 files changed, 428 insertions, 0 deletions
diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index cd6564ce..82ae1896 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -12,6 +12,10 @@ readme = "../../README.md" name = "garage" path = "main.rs" +[[test]] +name = "integration" +path = "tests/lib.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -45,3 +49,9 @@ tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi #netapp = { version = "0.3.0", git = "https://git.deuxfleurs.fr/lx/netapp" } netapp = "0.3.0" + +[dev-dependencies] +aws-sdk-s3 = "0.6" +http = "0.2" + +static_init = "1.0" diff --git a/src/garage/tests/common/client.rs b/src/garage/tests/common/client.rs new file mode 100644 index 00000000..c5ddc6e5 --- /dev/null +++ b/src/garage/tests/common/client.rs @@ -0,0 +1,22 @@ +use aws_sdk_s3::{Client, Config, Credentials, Endpoint}; + +use super::garage::Instance; + +pub fn build_client(instance: &Instance) -> Client { + let credentials = Credentials::new( + &instance.key.id, + &instance.key.secret, + None, + None, + "garage-integ-test", + ); + let endpoint = Endpoint::immutable(instance.uri()); + + let config = Config::builder() + .region(super::REGION) + .credentials_provider(credentials) + .endpoint_resolver(endpoint) + .build(); + + Client::from_conf(config) +} diff --git a/src/garage/tests/common/ext/mod.rs b/src/garage/tests/common/ext/mod.rs new file mode 100644 index 00000000..6090dd96 --- /dev/null +++ b/src/garage/tests/common/ext/mod.rs @@ -0,0 +1,3 @@ +pub use process::*; + +mod process; diff --git a/src/garage/tests/common/ext/process.rs b/src/garage/tests/common/ext/process.rs new file mode 100644 index 00000000..ba533b6c --- /dev/null +++ b/src/garage/tests/common/ext/process.rs @@ -0,0 +1,55 @@ +use std::process; + +pub trait CommandExt { + fn quiet(&mut self) -> &mut Self; + + fn expect_success_status(&mut self, msg: &str) -> process::ExitStatus; + fn expect_success_output(&mut self, msg: &str) -> process::Output; +} + +impl CommandExt for process::Command { + fn quiet(&mut self) -> &mut Self { + self.stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + } + + fn expect_success_status(&mut self, msg: &str) -> process::ExitStatus { + let status = self.status().expect(msg); + status.expect_success(msg); + status + } + fn expect_success_output(&mut self, msg: &str) -> process::Output { + let output = self.output().expect(msg); + output.expect_success(msg); + output + } +} + +pub trait OutputExt { + fn expect_success(&self, msg: &str); +} + +impl OutputExt for process::Output { + fn expect_success(&self, msg: &str) { + self.status.expect_success(msg) + } +} + +pub trait ExitStatusExt { + fn expect_success(&self, msg: &str); +} + +impl ExitStatusExt for process::ExitStatus { + fn expect_success(&self, msg: &str) { + if !self.success() { + match self.code() { + Some(code) => panic!( + "Command exited with code {code}: {msg}", + code = code, + msg = msg + ), + None => panic!("Command exited with signal: {msg}", msg = msg), + } + } + } +} diff --git a/src/garage/tests/common/garage.rs b/src/garage/tests/common/garage.rs new file mode 100644 index 00000000..e579f03c --- /dev/null +++ b/src/garage/tests/common/garage.rs @@ -0,0 +1,217 @@ +use std::mem::MaybeUninit; +use std::path::{Path, PathBuf}; +use std::process; +use std::sync::Once; + +use super::ext::*; + +// https://xkcd.com/221/ +const DEFAULT_PORT: u16 = 49995; + +static GARAGE_TEST_SECRET: &str = + "c3ea8cb80333d04e208d136698b1a01ae370d463f0d435ab2177510b3478bf44"; + +#[derive(Debug, Default)] +pub struct Key { + pub name: String, + pub id: String, + pub secret: String, +} + +pub struct Instance { + process: process::Child, + pub path: PathBuf, + pub key: Key, + pub api_port: u16, +} + +impl Instance { + fn new() -> Instance { + use std::{env, fs}; + + let port = env::var("GARAGE_TEST_INTEGRATION_PORT") + .map(|value| value.parse().expect("Invalid port provided")) + .ok() + .unwrap_or(DEFAULT_PORT); + + let path = env::var("GARAGE_TEST_INTEGRATION_PATH") + .map(PathBuf::from) + .ok() + .unwrap_or_else(|| env::temp_dir().join(format!("garage-integ-test-{}", port))); + + // Clean test runtime directory + if path.exists() { + fs::remove_dir_all(&path).expect("Could not clean test runtime directory"); + } + fs::create_dir(&path).expect("Could not create test runtime directory"); + + let config = format!( + r#" +metadata_dir = "{path}/meta" +data_dir = "{path}/data" + +replication_mode = "1" + +rpc_bind_addr = "127.0.0.1:{rpc_port}" +rpc_public_addr = "127.0.0.1:{rpc_port}" +rpc_secret = "{secret}" + +[s3_api] +s3_region = "{region}" +api_bind_addr = "127.0.0.1:{api_port}" +root_domain = ".s3.garage" + +[s3_web] +bind_addr = "127.0.0.1:{web_port}" +root_domain = ".web.garage" +index = "index.html" +"#, + path = path.display(), + secret = GARAGE_TEST_SECRET, + region = super::REGION, + api_port = port, + rpc_port = port + 1, + web_port = port + 2, + ); + fs::write(path.join("config.toml"), config).expect("Could not write garage config file"); + + let stdout = + fs::File::create(path.join("stdout.log")).expect("Could not create stdout logfile"); + let stderr = + fs::File::create(path.join("stderr.log")).expect("Could not create stderr logfile"); + + let child = command(&path.join("config.toml")) + .arg("server") + .stdout(stdout) + .stderr(stderr) + .env("RUST_LOG", "garage=info,garage_api=debug") + .spawn() + .expect("Could not start garage"); + + Instance { + process: child, + path, + key: Key::default(), + api_port: port, + } + } + + fn setup(&mut self) { + use std::{thread, time::Duration}; + + // Wait for node to be ready + thread::sleep(Duration::from_secs(2)); + + self.setup_layout(); + + self.key = self.new_key("garage_test"); + } + + fn setup_layout(&self) { + let node_id = self.node_id(); + let node_short_id = &node_id[..64]; + + self.command() + .args(["layout", "assign"]) + .arg(node_short_id) + .args(["-c", "1", "-z", "unzonned"]) + .quiet() + .expect_success_status("Could not assign garage node layout"); + self.command() + .args(["layout", "apply"]) + .args(["--version", "1"]) + .quiet() + .expect_success_status("Could not apply garage node layout"); + } + + fn terminate(&mut self) { + // TODO: Terminate "gracefully" the process with SIGTERM instead of directly SIGKILL it. + self.process + .kill() + .expect("Could not terminate garage process"); + } + + pub fn command(&self) -> process::Command { + command(&self.path.join("config.toml")) + } + + pub fn node_id(&self) -> String { + let output = self + .command() + .args(["node", "id"]) + .expect_success_output("Could not get node ID"); + String::from_utf8(output.stdout).unwrap() + } + + pub fn uri(&self) -> http::Uri { + format!("http://127.0.0.1:{api_port}", api_port = self.api_port) + .parse() + .expect("Could not build garage endpoint URI") + } + + pub fn new_key(&self, name: &str) -> Key { + let mut key = Key::default(); + + let output = self + .command() + .args(["key", "new"]) + .args(["--name", name]) + .expect_success_output("Could not create key"); + let stdout = String::from_utf8(output.stdout).unwrap(); + + for line in stdout.lines() { + if let Some(key_id) = line.strip_prefix("Key ID: ") { + key.id = key_id.to_owned(); + continue; + } + if let Some(key_secret) = line.strip_prefix("Secret key: ") { + key.secret = key_secret.to_owned(); + continue; + } + } + assert!(!key.id.is_empty(), "Invalid key: Key ID is empty"); + assert!(!key.secret.is_empty(), "Invalid key: Key secret is empty"); + + Key { + name: name.to_owned(), + ..key + } + } +} + +impl Drop for Instance { + fn drop(&mut self) { + self.terminate() + } +} + +static mut INSTANCE: MaybeUninit<Instance> = MaybeUninit::uninit(); +static INSTANCE_INIT: Once = Once::new(); + +#[static_init::destructor] +extern "C" fn terminate_instance() { + if INSTANCE_INIT.is_completed() { + unsafe { + INSTANCE.assume_init_drop(); + } + } +} + +pub fn instance() -> &'static Instance { + INSTANCE_INIT.call_once(|| unsafe { + let mut instance = Instance::new(); + instance.setup(); + + INSTANCE.write(instance); + }); + + unsafe { INSTANCE.assume_init_ref() } +} + +pub fn command(config_path: &Path) -> process::Command { + let mut command = process::Command::new(env!("CARGO_BIN_EXE_garage")); + + command.arg("-c").arg(config_path); + + command +} diff --git a/src/garage/tests/common/macros.rs b/src/garage/tests/common/macros.rs new file mode 100644 index 00000000..18d0ffeb --- /dev/null +++ b/src/garage/tests/common/macros.rs @@ -0,0 +1,11 @@ +macro_rules! assert_bytes_eq { + ($stream:expr, $bytes:expr) => { + let data = $stream + .collect() + .await + .expect("Error reading data") + .into_bytes(); + + assert_eq!(data.as_ref(), $bytes); + }; +} diff --git a/src/garage/tests/common/mod.rs b/src/garage/tests/common/mod.rs new file mode 100644 index 00000000..b5f7f0b2 --- /dev/null +++ b/src/garage/tests/common/mod.rs @@ -0,0 +1,45 @@ +use aws_sdk_s3::{Client, Region}; +use ext::*; + +#[macro_use] +pub mod macros; + +pub mod client; +pub mod ext; +pub mod garage; + +const REGION: Region = Region::from_static("garage-integ-test"); + +pub struct Context { + pub garage: &'static garage::Instance, + pub client: Client, +} + +impl Context { + fn new() -> Self { + let garage = garage::instance(); + let client = client::build_client(garage); + + Context { garage, client } + } + + pub fn create_bucket(&self, name: &str) { + self.garage + .command() + .args(["bucket", "create", name]) + .quiet() + .expect_success_status("Could not create bucket"); + self.garage + .command() + .args(["bucket", "allow"]) + .args(["--owner", "--read", "--write"]) + .arg(name) + .args(["--key", &self.garage.key.name]) + .quiet() + .expect_success_status("Could not allow key for bucket"); + } +} + +pub fn context() -> Context { + Context::new() +} diff --git a/src/garage/tests/lib.rs b/src/garage/tests/lib.rs new file mode 100644 index 00000000..627d4468 --- /dev/null +++ b/src/garage/tests/lib.rs @@ -0,0 +1,4 @@ +#[macro_use] +mod common; + +mod simple; diff --git a/src/garage/tests/simple.rs b/src/garage/tests/simple.rs new file mode 100644 index 00000000..a627c770 --- /dev/null +++ b/src/garage/tests/simple.rs @@ -0,0 +1,61 @@ +use crate::common; + +#[tokio::test] +async fn test_simple() { + use aws_sdk_s3::ByteStream; + + let ctx = common::context(); + ctx.create_bucket("test-simple"); + + let data = ByteStream::from_static(b"Hello world!"); + + ctx.client + .put_object() + .bucket("test-simple") + .key("test") + .body(data) + .send() + .await + .unwrap(); + + let res = ctx + .client + .get_object() + .bucket("test-simple") + .key("test") + .send() + .await + .unwrap(); + + assert_bytes_eq!(res.body, b"Hello world!"); +} + +#[tokio::test] +async fn test_simple_2() { + use aws_sdk_s3::ByteStream; + + let ctx = common::context(); + ctx.create_bucket("test-simple-2"); + + let data = ByteStream::from_static(b"Hello world!"); + + ctx.client + .put_object() + .bucket("test-simple-2") + .key("test") + .body(data) + .send() + .await + .unwrap(); + + let res = ctx + .client + .get_object() + .bucket("test-simple-2") + .key("test") + .send() + .await + .unwrap(); + + assert_bytes_eq!(res.body, b"Hello world!"); +} |