use std::path::PathBuf;
use std::{
ffi::OsStr,
fs::{self, File, Metadata},
io::Read,
path::Path,
};
use anyhow::{anyhow, Context};
use kythera_lib::{pascal_case_split, Abi, WasmActor};
use walkdir::WalkDir;
#[derive(Clone, Debug)]
pub struct Test {
pub actor: WasmActor,
pub tests: Vec<WasmActor>,
}
fn read_file_data<P: AsRef<Path>>(path: P) -> anyhow::Result<(String, Vec<u8>)> {
let path = path.as_ref();
let mut file =
File::open(path).with_context(|| format!("Could not open file {}", path.display()))?;
let file_size = file.metadata().as_ref().map(Metadata::len).unwrap_or(0);
let mut content = Vec::with_capacity(file_size as usize);
file.read_to_end(&mut content)
.with_context(|| format!("Could not read file {}", path.display()))?;
let file_name = path
.file_name()
.expect("Actor file name should be valid")
.to_string_lossy()
.into_owned();
Ok((file_name, content))
}
fn set_abi_extension<P: AsRef<Path>>(path: P) -> anyhow::Result<String> {
let mut path_buf = PathBuf::from(path.as_ref());
path_buf.set_extension("cbor");
path_buf
.into_os_string()
.into_string()
.map_err(|_| anyhow!("Failed to convert abi path to string"))
}
fn read_actor<P: AsRef<Path>>(binary_path: P) -> anyhow::Result<WasmActor> {
let abi_path = set_abi_extension(&binary_path)?;
let (file_name, bytecode) = read_file_data(binary_path)?;
let abi: Abi = kythera_lib::from_slice(&read_file_data(abi_path)?.1)?;
Ok(WasmActor::new(file_name, bytecode, abi))
}
pub fn search_files<P: AsRef<Path>>(path: P) -> anyhow::Result<Vec<Test>> {
let (target_actor_paths, mut test_artifacts_paths): (Vec<String>, Vec<String>) =
fs::read_dir(path)
.context("Could not read the input path")?
.filter_map(Result::ok)
.filter_map(|e| e.path().into_os_string().into_string().ok())
.filter(|path| path.ends_with(".wasm") || path.ends_with(".t"))
.inspect(|path| {
let filename = Path::new(path)
.file_name()
.and_then(OsStr::to_str)
.filter(|f| !pascal_case_split(f).is_empty());
if filename.is_none() {
log::warn!("file {path} is not in PascalCase");
}
})
.partition(|path| path.ends_with(".wasm") && !path.ends_with(".t.wasm"));
let mut tests = vec![];
for target_actor_path in target_actor_paths {
let target_actor = match read_actor(&target_actor_path) {
Ok(target_actor) => target_actor,
Err(err) => {
log::error!("Could not get target Actor for binary {target_actor_path}: {err}");
continue;
}
};
let mut actor_tests = vec![];
test_artifacts_paths.retain(|test_path| {
let test_path = Path::new(test_path);
let test_path_stem = test_path
.file_stem()
.and_then(OsStr::to_str)
.expect("Test path file stem should be valid UTF-8");
let main_actor_stem = Path::new(&target_actor_path)
.file_stem()
.and_then(OsStr::to_str)
.expect("Target Actor file stem should be valid UTF-8");
if !test_path_stem.starts_with(main_actor_stem) {
return true;
}
if test_path.is_file() {
let test = match read_actor(test_path) {
Ok(test) => test,
Err(err) => {
log::error!("Could not read test file {}: {err}", test_path.display());
return false;
}
};
actor_tests.push(test);
} else {
let subdir_tests = WalkDir::new(test_path)
.into_iter()
.filter_map(Result::ok)
.filter_map(|tp| tp.into_path().into_os_string().into_string().ok())
.filter(|tp| tp.ends_with(".wasm"))
.filter_map(|tp| match read_actor(&tp) {
Ok(actor_test) => Some(actor_test),
Err(err) => {
log::error!("Could not read test file {}: {err}", tp);
None
}
});
actor_tests.extend(subdir_tests);
actor_tests.sort();
}
false
});
tests.push(Test {
actor: target_actor,
tests: actor_tests,
});
}
for left in test_artifacts_paths {
log::warn!("Test {left} not read, it is missing its Actor");
}
Ok(tests)
}
#[cfg(test)]
mod tests {
use super::search_files;
use kythera_lib::{to_vec, Abi, Method};
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use tempfile::tempdir;
fn create_actors_in_dir(dir_path: &Path, actors: Vec<(&str, &Abi)>) {
for (name, abi) in actors {
File::create(dir_path.join(name.to_owned() + ".wasm"))
.unwrap()
.sync_data()
.unwrap();
let mut actor_abi_file =
File::create(dir_path.join(name.to_owned() + ".cbor")).unwrap();
actor_abi_file.write_all(&to_vec(abi).unwrap()).unwrap();
actor_abi_file.sync_data().unwrap();
}
}
#[test]
fn actor_without_abi() {
let dir = tempdir().unwrap();
let dir_path = dir.path();
File::create(dir_path.join("token.wasm"))
.unwrap()
.sync_data()
.unwrap();
let tests = search_files(dir_path).unwrap();
assert_eq!(0, tests.len());
}
#[test]
fn actor_with_test_file() {
let dir = tempdir().unwrap();
let dir_path = dir.path();
let target_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("Transfer").unwrap()],
};
let test_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("TestTransfer").unwrap()],
};
create_actors_in_dir(
dir_path,
vec![("token", &target_actor_abi), ("token.t", &test_actor_abi)],
);
let tests = search_files(dir_path).unwrap();
assert_eq!(1, tests.len());
let test = &tests[0];
assert_eq!("token.wasm", test.actor.name());
assert_eq!(&target_actor_abi, test.actor.abi());
assert_eq!(1, test.tests.len());
assert_eq!("token.t.wasm", test.tests[0].name());
assert_eq!(&test_actor_abi, test.tests[0].abi());
}
#[test]
fn actor_with_test_dir() {
let dir = tempdir().unwrap();
let dir_path = dir.path();
let target_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("Transfer").unwrap()],
};
let test_1_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("TestTransferOne").unwrap()],
};
let test_2_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("TestTransferTwo").unwrap()],
};
create_actors_in_dir(dir_path, vec![("token", &target_actor_abi)]);
let subdir_path = dir_path.join("token.t");
fs::create_dir(&subdir_path).unwrap();
create_actors_in_dir(
subdir_path.as_path(),
vec![("test1", &test_1_actor_abi), ("test2", &test_2_actor_abi)],
);
let tests = search_files(dir_path).unwrap();
assert_eq!(1, tests.len());
let test = &tests[0];
assert_eq!("token.wasm", test.actor.name());
assert_eq!(&target_actor_abi, test.actor.abi());
assert_eq!(2, test.tests.len());
assert_eq!("test1.wasm", test.tests[0].name());
assert_eq!(&test_1_actor_abi, test.tests[0].abi());
assert_eq!("test2.wasm", test.tests[1].name());
assert_eq!(&test_2_actor_abi, test.tests[1].abi());
}
#[test]
fn actor_with_sub_test_dirs() {
let dir = tempdir().unwrap();
let dir_path = dir.path();
let target_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("Transfer").unwrap()],
};
let test_1_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("TestTransferOne").unwrap()],
};
let test_2_1_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("TestTransferTwoOne").unwrap()],
};
let test_2_2_actor_abi = Abi {
constructor: Method::new_from_name("Constructor").ok(),
set_up: None,
methods: vec![Method::new_from_name("TestTransferTwoTwo").unwrap()],
};
create_actors_in_dir(dir_path, vec![("token", &target_actor_abi)]);
let subdir_path = dir_path.join("token.t");
fs::create_dir(&subdir_path).unwrap();
create_actors_in_dir(subdir_path.as_path(), vec![("test1", &test_1_actor_abi)]);
let subsubdir_path = subdir_path.join("test2");
fs::create_dir(&subsubdir_path).unwrap();
create_actors_in_dir(
subsubdir_path.as_path(),
vec![
("test2.1", &test_2_1_actor_abi),
("test2.2", &test_2_2_actor_abi),
],
);
let tests = search_files(dir_path).unwrap();
assert_eq!(1, tests.len());
let test = &tests[0];
assert_eq!("token.wasm", test.actor.name());
assert_eq!(&target_actor_abi, test.actor.abi());
assert_eq!(3, test.tests.len());
assert_eq!("test1.wasm", test.tests[0].name());
assert_eq!(&test_1_actor_abi, test.tests[0].abi());
assert_eq!("test2.1.wasm", test.tests[1].name());
assert_eq!(&test_2_1_actor_abi, test.tests[1].abi());
assert_eq!("test2.2.wasm", test.tests[2].name());
assert_eq!(&test_2_2_actor_abi, test.tests[2].abi());
}
}