use std::{
fs::{read, read_to_string, File},
io::{self, Read},
iter::once,
path::{Path, PathBuf},
};
use log::trace;
use oicana_input::{
input::{
blob::{Blob, BlobInput},
json::JsonInput,
},
CompilationConfig, CompilationMode, TemplateInputs,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use typst::foundations::Dict;
pub mod collect;
pub mod execution;
#[derive(Debug)]
pub struct Test {
pub inputs: TemplateInputs,
pub name: String,
pub snapshot: Snapshot,
pub collection: PathBuf,
pub descriptor: String,
}
#[derive(Debug)]
pub enum Snapshot {
None,
Missing(PathBuf),
Some(PathBuf),
}
impl Test {
pub fn new(
template_test: TemplateTest,
collection_name: Option<String>,
path_components: &[String],
collection_path: &Path,
root: &Path,
) -> Result<Self, PrepareTestError> {
let maybe_snapshot = root.join(template_test.snapshot.unwrap_or(format!(
"{}{}.png",
collection_name
.as_ref()
.map(|name| format!("{name}."))
.unwrap_or("".to_owned()),
template_test.name
)));
let snapshot = if maybe_snapshot.is_file() {
Snapshot::Some(maybe_snapshot)
} else {
Snapshot::Missing(maybe_snapshot)
};
let inputs = Self::build_inputs(template_test.mode, template_test.inputs, root)?;
trace!("Collecting test {}", &template_test.name);
Ok(Test {
inputs,
collection: collection_path.to_path_buf(),
descriptor: path_components
.iter()
.cloned()
.chain(collection_name)
.chain(once(template_test.name.clone()))
.collect::<Vec<String>>()
.join(" > "),
name: template_test.name,
snapshot,
})
}
fn build_inputs(
mode: CompilationMode,
input_values: Vec<InputValue>,
root: &Path,
) -> Result<TemplateInputs, PrepareTestError> {
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::new(mode));
for input in input_values {
match input {
InputValue::Json(json) => {
let file_path = root.join(json.file);
let value = read_to_string(&file_path)
.map_err(|source| PrepareTestError::Io { file_path, source })?;
inputs.with_input(JsonInput::new(json.key, value));
}
InputValue::Blob(blob) => {
let file_path = root.join(blob.file);
let value = read(&file_path)
.map_err(|source| PrepareTestError::Io { file_path, source })?;
let mut blob_value: Blob = value.into();
blob_value.metadata = match blob.meta {
Some(meta) => Deserialize::deserialize(meta)?,
None => Dict::new(),
};
inputs.with_input(BlobInput::new(blob.key, blob_value));
}
}
}
Ok(inputs)
}
}
#[derive(Debug, Error)]
pub enum PrepareTestError {
#[error("Failed to read file '{file_path}': {source}")]
Io {
file_path: PathBuf,
#[source]
source: io::Error,
},
#[error("Failed to find snapshot file at '{0}'")]
NoSnapshot(PathBuf),
#[error("Failed to convert metadata to Typst dictionary '{0}'")]
FailedToConvertMetadata(#[from] toml::de::Error),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct TemplateTestCollection {
pub tests_version: u8,
pub name: Option<String>,
#[serde(default = "Vec::new", rename = "test")]
pub tests: Vec<TemplateTest>,
}
impl TemplateTestCollection {
fn read_from(path: &Path) -> Result<Self, TestCollectionError> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let mut collection = toml::de::from_str::<TemplateTestCollection>(&content)?;
if collection.name.is_none() {
let file_name = path
.file_name()
.map(|file_name| {
file_name
.to_string_lossy()
.strip_suffix(".tests.toml")
.unwrap_or("")
.trim()
.to_owned()
})
.unwrap_or_default();
if !file_name.is_empty() {
collection.name = Some(file_name);
}
}
Ok(collection)
}
}
#[derive(Debug, Error)]
pub enum TestCollectionError {
#[error("Failed to read file")]
IoError(#[from] io::Error),
#[error("Failed to parse test collection")]
ParsingError(#[from] toml::de::Error),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct TemplateTest {
pub name: String,
pub snapshot: Option<String>,
#[serde(default = "Vec::new")]
pub inputs: Vec<InputValue>,
#[serde(default = "production")]
pub mode: CompilationMode,
}
fn production() -> CompilationMode {
CompilationMode::Production
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum InputValue {
#[serde(rename = "json")]
Json(JsonInputValue),
#[serde(rename = "blob")]
Blob(BlobInputValue),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct JsonInputValue {
pub key: String,
pub file: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct BlobInputValue {
pub key: String,
pub file: String,
pub meta: Option<toml::Value>,
}
#[cfg(test)]
mod tests {
use crate::TemplateTestCollection;
use oicana_input::CompilationMode;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn takes_name_from_file_content() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join("bar.tests.toml");
let mut file = File::create(&path).unwrap();
write!(
&mut file,
r#"
name = "foo"
tests_version = 1
"#
)
.unwrap();
let test_collection = TemplateTestCollection::read_from(&path)
.expect("Failed to read test collection from file");
assert_eq!(test_collection.name, Some(String::from("foo")));
}
#[test]
fn takes_name_from_file_name() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join("bar.tests.toml");
let mut file = File::create(&path).unwrap();
write!(
&mut file,
r#"
tests_version = 1
"#
)
.unwrap();
let test_collection = TemplateTestCollection::read_from(&path)
.expect("Failed to read test collection from file");
assert_eq!(test_collection.name, Some(String::from("bar")));
}
#[test]
fn no_name() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join("tests.toml");
let mut file = File::create(&path).unwrap();
write!(
&mut file,
r#"
tests_version = 1
"#
)
.unwrap();
let test_collection = TemplateTestCollection::read_from(&path)
.expect("Failed to read test collection from file");
assert_eq!(test_collection.name, None);
}
#[test]
fn mode() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join("tests.toml");
let mut file = File::create(&path).unwrap();
write!(
&mut file,
r#"
tests_version = 1
[[test]]
name = "test"
mode = "development"
"#
)
.unwrap();
let test_collection = TemplateTestCollection::read_from(&path)
.expect("Failed to read test collection from file");
assert_eq!(test_collection.tests[0].mode, CompilationMode::Development);
}
#[test]
fn default_mode() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join("tests.toml");
let mut file = File::create(&path).unwrap();
write!(
&mut file,
r#"
tests_version = 1
[[test]]
name = "test"
"#
)
.unwrap();
let test_collection = TemplateTestCollection::read_from(&path)
.expect("Failed to read test collection from file");
assert_eq!(test_collection.tests[0].mode, CompilationMode::Production);
}
#[test]
fn short_mode() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().join("tests.toml");
let mut file = File::create(&path).unwrap();
write!(
&mut file,
r#"
tests_version = 1
[[test]]
name = "test"
mode = "dev"
"#
)
.unwrap();
let test_collection = TemplateTestCollection::read_from(&path)
.expect("Failed to read test collection from file");
assert_eq!(test_collection.tests[0].mode, CompilationMode::Development);
}
}