aboutsummaryrefslogtreecommitdiff
path: root/src/garage
diff options
context:
space:
mode:
authorJill <kokakiwi@deuxfleurs.fr>2022-02-02 15:35:52 +0100
committerJill <kokakiwi@deuxfleurs.fr>2022-02-02 15:35:52 +0100
commit539b25652502693fed311699829f3bd28e604aa5 (patch)
tree031fb891d26630c87beca6f7ac67d21c65f6d440 /src/garage
parentf67029ce2af2f8870836d5bb908254d7fdbbe71b (diff)
downloadgarage-539b25652502693fed311699829f3bd28e604aa5.tar.gz
garage-539b25652502693fed311699829f3bd28e604aa5.zip
tests: Add garage integration tests (base)
Diffstat (limited to 'src/garage')
-rw-r--r--src/garage/Cargo.toml10
-rw-r--r--src/garage/tests/common/client.rs22
-rw-r--r--src/garage/tests/common/ext/mod.rs3
-rw-r--r--src/garage/tests/common/ext/process.rs55
-rw-r--r--src/garage/tests/common/garage.rs217
-rw-r--r--src/garage/tests/common/macros.rs11
-rw-r--r--src/garage/tests/common/mod.rs45
-rw-r--r--src/garage/tests/lib.rs4
-rw-r--r--src/garage/tests/simple.rs61
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!");
+}