use crate::Result;
use aws_sdk_s3::{primitives::ByteStream, Client};
use bytes::Bytes;
use futures::StreamExt;
use rand::prelude::*;
use sha2::Digest;
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
path::Path,
};
use tokio::io::AsyncReadExt;
use tracing::instrument;
use url::Url;
const MAX_CONCURRENCY: usize = 10;
#[derive(Clone, Debug)]
pub struct TestObject {
pub key: String,
pub size: usize,
}
impl TestObject {
pub fn new(key: impl Into<String>, size: impl AsRef<str>) -> Self {
let key = key.into();
let size = byte_unit::Byte::from_str(size).unwrap();
Self {
key,
size: size.get_bytes() as usize,
}
}
}
#[derive(Clone, Debug)]
pub struct TestObjectWithData {
pub key: String,
pub url: Url,
pub data: Vec<u8>,
pub hash: [u8; 32],
}
pub fn prepend_unique_prefix(
objects: impl IntoIterator<Item = TestObject>,
) -> (String, impl IntoIterator<Item = TestObject>) {
let prefix = format!("{:08x}/", rand::thread_rng().next_u32());
let objects = {
let prefix = prefix.clone();
objects.into_iter().map(move |mut object| {
object.key = format!("{}{}", prefix, object.key);
object
})
};
(prefix, objects)
}
pub async fn make_test_data(
client: &Client,
bucket: &str,
objects: impl IntoIterator<Item = TestObject>,
) -> Result<HashMap<String, TestObjectWithData>> {
let create_futs = objects.into_iter().map(|test_object| async move {
let data =
make_test_data_object(client, bucket, &test_object.key.clone(), test_object.size)
.await?;
Result::<_>::Ok((test_object.key, data))
});
let mut test_data_stream = futures::stream::iter(create_futs).buffer_unordered(MAX_CONCURRENCY);
let mut test_objects = HashMap::new();
while let Some(result) = test_data_stream.next().await {
let (key, data) = result?;
let mut hasher = sha2::Sha256::new();
hasher.update(&data);
let mut hash = [0u8; 32];
hash.copy_from_slice(&hasher.finalize());
let object = TestObjectWithData {
url: format!("s3://{}/{}", bucket, key).parse().unwrap(),
key: key.clone(),
data,
hash,
};
assert!(
test_objects.insert(object.key.clone(), object).is_none(),
"BUG: test data contains the same key '{}' more than once",
key
);
}
Ok(test_objects)
}
pub async fn make_test_data_object(
client: &Client,
bucket: &str,
key: &str,
size: usize,
) -> Result<Vec<u8>> {
let mut rand = rand::thread_rng();
let mut data = vec![0u8; size];
rand.fill(&mut data[..]);
client
.put_object()
.bucket(bucket)
.key(key.to_string())
.body(ByteStream::from(Bytes::from(data.clone())))
.send()
.await?;
Result::<_>::Ok(data)
}
pub async fn validate_test_data_in_dir<Keys, Item>(
test_data: &HashMap<String, TestObjectWithData>,
path: &Path,
expected_keys: Keys,
) -> Result<()>
where
Keys: IntoIterator<Item = Item>,
Item: Into<Cow<'static, str>>,
{
println!(
"Test data dir {} contains the following files:",
path.display()
);
let files = walkdir::WalkDir::new(path)
.into_iter()
.filter(|result| {
if let Ok(entry) = &result {
!entry.file_type().is_dir()
} else {
true
}
})
.map(|result| {
let entry = result?;
let relative_path = entry.path().strip_prefix(path)?.to_owned();
println!(
" {} ({} bytes)",
relative_path.display(),
entry.path().metadata()?.len()
);
Result::<_>::Ok(relative_path)
})
.collect::<Result<Vec<_>>>()?;
let mut expected_test_data: HashMap<String, &TestObjectWithData> = expected_keys.into_iter()
.map(|item| {
let key = item.into();
let data = test_data.get(key.as_ref())
.unwrap_or_else(|| panic!("BUG: test specifies expected key '{key}' but the `test_data` collection doesn't have such an entry"));
#[cfg(windows)]
let key = key.replace('/', "\\");
#[cfg(not(windows))]
let key = key.to_string();
(key, data)
})
.collect();
let mut expected_keys = expected_test_data
.keys()
.map(|key| key.to_string())
.collect::<HashSet<_>>();
for relative_path in files {
let key = relative_path.to_string_lossy();
let test_data = expected_test_data.remove(key.as_ref()).unwrap_or_else(|| {
panic!(
"Tar archive contains file `{}` which is not among the expected test data",
relative_path.display()
);
});
expected_keys.remove(key.as_ref());
let mut file = tokio::fs::File::open(path.join(&relative_path)).await?;
let metadata = file.metadata().await?;
let mut data = Vec::with_capacity(metadata.len() as usize);
file.read_to_end(&mut data).await?;
let mut hasher = sha2::Sha256::new();
hasher.update(&data);
let mut hash = [0u8; 32];
hash.copy_from_slice(&hasher.finalize());
assert_eq!(
hash,
test_data.hash,
"File '{}' (key '{}') hash doesn't match expected value",
relative_path.display(),
key
);
}
if !expected_keys.is_empty() {
panic!(
"One or more test data objects were not found in the archive: {}",
expected_keys.into_iter().collect::<Vec<_>>().join(",")
)
}
Ok(())
}
#[instrument(err, skip_all, fields(bucket, prefix))]
pub async fn validate_test_data_in_s3<Keys, Item>(
client: &aws_sdk_s3::Client,
test_data: &HashMap<String, TestObjectWithData>,
bucket: &str,
prefix: &str,
expected_keys: Keys,
) -> Result<()>
where
Keys: IntoIterator<Item = Item>,
Item: Into<Cow<'static, str>>,
{
let mut objects = HashMap::new();
let mut pages = client
.list_objects_v2()
.bucket(bucket)
.prefix(prefix)
.into_paginator()
.send();
let mut results = vec![];
while let Some(result) = pages.next().await {
let page = result?;
let result: Result<Vec<aws_sdk_s3::types::Object>> = Ok(page.contents.unwrap_or_default());
results.push(result?);
}
for object in results.into_iter().flatten() {
objects.insert(object.key().unwrap().to_owned(), object);
}
let mut expected_test_data: HashMap<Cow<'static, str>, &TestObjectWithData> = expected_keys.into_iter()
.map(|item| {
let key = item.into();
let data = test_data.get(key.as_ref())
.unwrap_or_else(|| panic!("BUG: test specifies expected key '{key}' but the `test_data` collection doesn't have such an entry"));
(key, data)
})
.collect();
let mut expected_keys = expected_test_data
.keys()
.map(|key| key.to_string())
.collect::<HashSet<_>>();
for (key, _object) in objects {
let relative_key = key.strip_prefix(prefix).unwrap();
let test_data = expected_test_data.remove(relative_key).unwrap_or_else(|| {
panic!(
"Bucket contains object `{}` which is not among the expected test data",
relative_key
);
});
expected_keys.remove(relative_key);
let hash = {
let response = client.get_object().bucket(bucket).key(&key).send().await?;
let mut body = response.body;
let mut hasher = sha2::Sha256::new();
while let Some(bytes) = body.try_next().await? {
hasher.update(bytes);
}
let mut hash = [0u8; 32];
hash.copy_from_slice(&hasher.finalize());
hash
};
assert_eq!(
hash, test_data.hash,
"S3 object '{}' (key '{}') hash doesn't match expected value",
relative_key, key
);
}
if !expected_keys.is_empty() {
panic!(
"One or more test data objects were not found in the archive: {}",
expected_keys.into_iter().collect::<Vec<_>>().join(",")
)
}
Ok(())
}