mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-12 23:36:52 +00:00
tests: Add garage integration tests (base)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub use process::*;
|
||||
|
||||
mod process;
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
mod simple;
|
||||
@@ -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!");
|
||||
}
|
||||
Reference in New Issue
Block a user