add unit tests

This commit is contained in:
Moritz Hölting 2024-08-15 22:37:39 +02:00
parent a2d20dab8e
commit 0c4c957d67
12 changed files with 403 additions and 23 deletions

15
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Cargo build & test
on:
push:
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Rust project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo test --verbose

View File

@ -23,3 +23,6 @@ serde = { version = "1.0.197", optional = true, features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
tracing = "0.1.40" tracing = "0.1.40"
zip = { version = "2.1.3", default-features = false, features = ["deflate", "time"], optional = true } zip = { version = "2.1.3", default-features = false, features = ["deflate", "time"], optional = true }
[dev-dependencies]
tempfile = "3.10.1"

View File

@ -421,7 +421,7 @@ impl Condition {
/// Will fail if the condition contains an `Or` variant. Use `compile` instead. /// Will fail if the condition contains an `Or` variant. Use `compile` instead.
fn str_cond(&self) -> Option<String> { fn str_cond(&self) -> Option<String> {
match self { match self {
Self::Atom(s) => Some("if ".to_string() + &s), Self::Atom(s) => Some("if ".to_string() + s),
Self::Not(n) => match *(*n).clone() { Self::Not(n) => match *(*n).clone() {
Self::Atom(s) => Some("unless ".to_string() + &s), Self::Atom(s) => Some("unless ".to_string() + &s),
_ => None, _ => None,
@ -491,7 +491,7 @@ mod tests {
#[allow(clippy::redundant_clone)] #[allow(clippy::redundant_clone)]
#[test] #[test]
fn test_condition() { fn test_condition() {
let c1 = Condition::Atom("foo".to_string()); let c1 = Condition::from("foo");
let c2 = Condition::Atom("bar".to_string()); let c2 = Condition::Atom("bar".to_string());
let c3 = Condition::Atom("baz".to_string()); let c3 = Condition::Atom("baz".to_string());
@ -574,4 +574,56 @@ mod tests {
] ]
); );
} }
#[test]
fn test_combine_conditions_commands() {
let conditions = vec!["a", "b", "c"]
.into_iter()
.map(str::to_string)
.collect();
let commands = &[(true, "1".to_string()), (false, "2".to_string())];
let combined = combine_conditions_commands(conditions, commands);
assert_eq!(
combined,
vec![
(true, "a 1".to_string()),
(false, "2".to_string()),
(true, "b 1".to_string()),
(false, "2".to_string()),
(true, "c 1".to_string()),
(false, "2".to_string())
]
);
}
#[test]
fn test_compile() {
let compiled = Execute::As(
"@ְa".to_string(),
Box::new(Execute::If(
"block ~ ~-1 ~ minecraft:stone".into(),
Box::new(Execute::Run(Box::new("say hi".into()))),
None,
)),
)
.compile(
&CompileOptions::default(),
&MutCompilerState::default(),
&FunctionCompilerState::default(),
);
assert_eq!(
compiled,
vec!["execute as @ְa if block ~ ~-1 ~ minecraft:stone run say hi".to_string()]
);
let direct = Execute::Run(Box::new("say direct".into())).compile(
&CompileOptions::default(),
&MutCompilerState::default(),
&FunctionCompilerState::default(),
);
assert_eq!(direct, vec!["say direct".to_string()]);
}
} }

View File

@ -56,6 +56,7 @@ impl Command {
#[must_use] #[must_use]
fn get_count(&self, options: &CompileOptions) -> usize { fn get_count(&self, options: &CompileOptions) -> usize {
match self { match self {
// TODO: change comment to compile to `1`, make sure nothing breaks
Self::Comment(_) => 0, Self::Comment(_) => 0,
Self::Debug(_) => usize::from(options.debug), Self::Debug(_) => usize::from(options.debug),
Self::Raw(cmd) => cmd.split('\n').count(), Self::Raw(cmd) => cmd.split('\n').count(),
@ -266,3 +267,60 @@ fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive<u8>) -> bool {
}) })
}) })
} }
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use crate::util::compile::CompilerState;
use super::*;
#[test]
fn test_raw() {
let command_a = Command::Raw("say Hello, world!".to_string());
let command_b = Command::raw("say foo bar");
let options = &CompileOptions::default();
let global_state = &Mutex::new(CompilerState::default());
let function_state = &FunctionCompilerState::default();
assert_eq!(
command_a.compile(options, global_state, function_state),
vec!["say Hello, world!".to_string()]
);
assert_eq!(command_a.get_count(options), 1);
assert_eq!(
command_b.compile(options, global_state, function_state),
vec!["say foo bar".to_string()]
);
assert_eq!(command_b.get_count(options), 1);
}
#[test]
fn test_comment() {
let comment = Command::Comment("this is a comment".to_string());
let options = &CompileOptions::default();
let global_state = &Mutex::new(CompilerState::default());
let function_state = &FunctionCompilerState::default();
assert_eq!(
comment.compile(options, global_state, function_state),
vec!["#this is a comment".to_string()]
);
assert_eq!(comment.get_count(options), 0);
}
#[test]
fn test_validate() {
let tag = Command::raw("tag @s add foo");
assert!(tag.validate(&(6..=9)));
assert!(!tag.validate(&(2..=5)));
let kill = Command::raw("kill @p");
assert!(kill.validate(&(2..=40)));
}
}

View File

@ -74,3 +74,33 @@ impl Function {
self.commands.iter().all(|c| c.validate(pack_formats)) self.commands.iter().all(|c| c.validate(pack_formats))
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compile::CompilerState;
use std::sync::Mutex;
#[test]
fn test_function() {
let mut function = Function::new("namespace", "name");
assert_eq!(function.get_commands().len(), 0);
function.add_command(Command::raw("say Hello, world!"));
assert_eq!(function.get_commands().len(), 1);
let options = &CompileOptions::default();
let global_state = &Mutex::new(CompilerState::default());
let function_state = &FunctionCompilerState::default();
let compiled = function.compile(options, global_state, function_state);
assert!(matches!(
compiled,
VFile::Text(content) if content == "say Hello, world!"
));
}
}

View File

@ -158,3 +158,56 @@ fn generate_mcmeta(dp: &Datapack, _options: &CompileOptions, _state: &MutCompile
VFile::Text(content.to_string()) VFile::Text(content.to_string())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_datapack() {
let template_dir = tempfile::tempdir().expect("error creating tempdir");
let mut dp = Datapack::new(Datapack::LATEST_FORMAT)
.with_description("My datapack")
.with_template_folder(template_dir.path())
.expect("error reading template folder");
assert_eq!(dp.namespaces.len(), 0);
let _ = dp.namespace_mut("foo");
assert_eq!(dp.namespaces.len(), 1);
}
#[test]
fn test_generate_mcmeta() {
let dp = &Datapack::new(Datapack::LATEST_FORMAT).with_description("foo");
let state = Mutex::new(CompilerState::default());
let mcmeta = generate_mcmeta(dp, &CompileOptions::default(), &state);
let json = if let VFile::Text(text) = mcmeta {
serde_json::from_str::<serde_json::Value>(&text).unwrap()
} else {
panic!("mcmeta should be text not binary")
};
let pack = json
.as_object()
.expect("mcmeta is not object")
.get("pack")
.expect("no pack value")
.as_object()
.expect("mcmeta pack is not object");
assert_eq!(
pack.get("description")
.expect("no key pack.description")
.as_str(),
Some("foo")
);
assert_eq!(
pack.get("pack_format")
.expect("no key pack.pack_format")
.as_u64(),
Some(u64::from(Datapack::LATEST_FORMAT))
);
}
}

View File

@ -109,13 +109,7 @@ impl Namespace {
// compile tags // compile tags
for ((path, tag_type), tag) in &self.tags { for ((path, tag_type), tag) in &self.tags {
let vfile = tag.compile(options, state); let vfile = tag.compile(options, state);
root_folder.add_file( root_folder.add_file(&format!("tags/{tag_type}/{path}.json"), vfile);
&format!(
"tags/{tag_type}/{path}.json",
tag_type = tag_type.to_string()
),
vfile,
);
} }
root_folder root_folder
@ -129,3 +123,23 @@ impl Namespace {
.all(|function| function.validate(pack_formats)) .all(|function| function.validate(pack_formats))
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_namespace() {
let mut namespace = Namespace::new("foo");
assert_eq!(namespace.get_name(), "foo");
assert_eq!(namespace.get_functions().len(), 0);
assert_eq!(namespace.get_tags().len(), 0);
let _ = namespace.function_mut("bar");
assert_eq!(namespace.get_functions().len(), 1);
assert!(namespace.function("bar").is_some());
assert!(namespace.function("baz").is_none());
}
}

View File

@ -1,5 +1,7 @@
//! A tag for various types. //! A tag for various types.
use std::fmt::Display;
use crate::{ use crate::{
util::compile::{CompileOptions, MutCompilerState}, util::compile::{CompileOptions, MutCompilerState},
virtual_fs::VFile, virtual_fs::VFile,
@ -81,9 +83,9 @@ pub enum TagType {
/// `Others(<registry path>)` => `data/<namespace>/tags/<registry path>` /// `Others(<registry path>)` => `data/<namespace>/tags/<registry path>`
Others(String), Others(String),
} }
impl ToString for TagType { impl Display for TagType {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { let str = match self {
Self::Blocks => "block".to_string(), Self::Blocks => "block".to_string(),
Self::Fluids => "fluid".to_string(), Self::Fluids => "fluid".to_string(),
Self::Items => "item".to_string(), Self::Items => "item".to_string(),
@ -91,7 +93,8 @@ impl ToString for TagType {
Self::GameEvents => "game_event".to_string(), Self::GameEvents => "game_event".to_string(),
Self::Functions => "function".to_string(), Self::Functions => "function".to_string(),
Self::Others(path) => path.to_string(), Self::Others(path) => path.to_string(),
} };
f.write_str(&str)
} }
} }
@ -122,11 +125,53 @@ impl TagValue {
match self { match self {
Self::Simple(value) => serde_json::Value::String(value.clone()), Self::Simple(value) => serde_json::Value::String(value.clone()),
Self::Advanced { id, required } => { Self::Advanced { id, required } => {
let mut map = serde_json::Map::new(); serde_json::json!({
map.insert("id".to_string(), serde_json::Value::String(id.clone())); "id": id.clone(),
map.insert("required".to_string(), serde_json::Value::Bool(*required)); "required": *required
serde_json::Value::Object(map) })
} }
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tag() {
let mut tag = Tag::new(false);
assert!(!tag.get_replace());
tag.set_replace(true);
assert!(tag.get_replace());
tag.add_value(TagValue::from("foo:bar"));
tag.add_value(TagValue::Advanced {
id: "bar:baz".to_string(),
required: true,
});
assert_eq!(tag.get_values().len(), 2);
let compiled = tag.compile(&CompileOptions::default(), &MutCompilerState::default());
if let VFile::Text(text) = compiled {
let deserialized = serde_json::from_str::<serde_json::Value>(&text)
.expect("Failed to deserialize tag");
assert_eq!(
deserialized,
serde_json::json!({
"replace": true,
"values": [
"foo:bar",
{
"id": "bar:baz",
"required": true
}
]
})
);
}
}
}

View File

@ -8,7 +8,7 @@
rustdoc::broken_intra_doc_links, rustdoc::broken_intra_doc_links,
clippy::missing_errors_doc clippy::missing_errors_doc
)] )]
#![warn(clippy::all, clippy::pedantic)] #![warn(clippy::all, clippy::pedantic, clippy::perf)]
#![allow(clippy::missing_panics_doc, clippy::missing_const_for_fn)] #![allow(clippy::missing_panics_doc, clippy::missing_const_for_fn)]
pub mod datapack; pub mod datapack;

View File

@ -40,7 +40,7 @@ pub struct CompilerState {}
pub type MutCompilerState = Mutex<CompilerState>; pub type MutCompilerState = Mutex<CompilerState>;
/// State of the compiler for each function that can change during compilation. /// State of the compiler for each function that can change during compilation.
#[derive(Debug, Getters)] #[derive(Debug, Getters, Default)]
pub struct FunctionCompilerState { pub struct FunctionCompilerState {
/// Next unique identifier. /// Next unique identifier.
uid_counter: Mutex<usize>, uid_counter: Mutex<usize>,

View File

@ -38,7 +38,7 @@ impl<T> ExtendableQueue<T> {
/// Get the queue. /// Get the queue.
#[must_use] #[must_use]
pub fn get(&self) -> &Arc<RwLock<VecDeque<T>>> { pub fn get_arc(&self) -> &Arc<RwLock<VecDeque<T>>> {
&self.queue &self.queue
} }
@ -85,3 +85,39 @@ impl<T> Iterator for ExtendableQueue<T> {
self.queue.write().unwrap().pop_front() self.queue.write().unwrap().pop_front()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_queue() {
let mut queue = ExtendableQueue::default();
queue.push(1);
queue.push(2);
queue.push(3);
assert_eq!(queue.len(), 3);
let mut count = 0;
while let Some(el) = queue.next() {
count += el;
if el == 1 {
queue.extend(vec![4, 5, 6]);
}
}
assert_eq!(count, 21);
assert!(queue.is_empty());
}
#[test]
fn test_from() {
let base = vec![1, 2, 3, 4];
let queue = ExtendableQueue::from(base.clone());
assert!(queue.into_iter().zip(base).all(|(a, b)| a == b));
}
}

View File

@ -284,11 +284,9 @@ impl TryFrom<&Path> for VFolder {
if let Some(name) = name { if let Some(name) = name {
if path.is_dir() { if path.is_dir() {
root_vfolder.add_existing_folder(&name, Self::try_from(path.as_path())?); root_vfolder.add_existing_folder(&name, Self::try_from(path.as_path())?);
} else if path.is_file() { } else {
let file = VFile::try_from(path.as_path())?; let file = VFile::try_from(path.as_path())?;
root_vfolder.add_file(&name, file); root_vfolder.add_file(&name, file);
} else {
unreachable!("Path is neither file nor directory");
} }
} else { } else {
return Err(io::Error::new( return Err(io::Error::new(
@ -353,6 +351,8 @@ mod tests {
let v_file_2 = VFile::from("baz"); let v_file_2 = VFile::from("baz");
v_folder.add_file("bar/baz.txt", v_file_2); v_folder.add_file("bar/baz.txt", v_file_2);
v_folder.add_file("bar/foo.bin", VFile::Binary(vec![1, 2, 3, 4]));
assert_eq!(v_folder.get_files().len(), 1); assert_eq!(v_folder.get_files().len(), 1);
assert_eq!(v_folder.get_folders().len(), 1); assert_eq!(v_folder.get_folders().len(), 1);
assert!(v_folder.get_file("bar/baz.txt").is_some()); assert!(v_folder.get_file("bar/baz.txt").is_some());
@ -361,5 +361,79 @@ mod tests {
.expect("folder not found") .expect("folder not found")
.get_file("baz.txt") .get_file("baz.txt")
.is_some()); .is_some());
let temp = tempfile::tempdir().expect("failed to create temp dir");
v_folder.place(temp.path()).expect("failed to place folder");
assert_eq!(
fs::read_to_string(temp.path().join("foo.txt")).expect("failed to read file"),
"foo"
);
assert_eq!(
fs::read_to_string(temp.path().join("bar/baz.txt")).expect("failed to read file"),
"baz"
);
assert_eq!(
fs::read(temp.path().join("bar/foo.bin")).expect("failed to read file"),
vec![1, 2, 3, 4]
);
}
#[test]
fn test_flatten() {
let mut v_folder = VFolder::new();
v_folder.add_file("a.txt", VFile::from("a"));
v_folder.add_file("a/b.txt", VFile::from("b"));
v_folder.add_file("a/b/c.txt", VFile::from("c"));
let flattened = v_folder.flatten();
assert_eq!(flattened.len(), 3);
assert!(flattened.iter().any(|(path, _)| path == "a.txt"));
assert!(flattened.iter().any(|(path, _)| path == "a/b.txt"));
assert!(flattened.iter().any(|(path, _)| path == "a/b/c.txt"));
}
#[test]
fn test_merge() {
let mut first = VFolder::new();
first.add_file("a.txt", VFile::from("a"));
first.add_file("a/b.txt", VFile::from("b"));
let mut second = VFolder::new();
second.add_file("a.txt", VFile::from("a2"));
second.add_file("c.txt", VFile::from("c"));
second.add_file("c/d.txt", VFile::from("d"));
second.add_file("a/e.txt", VFile::from("e"));
let replaced = first.merge(second);
assert_eq!(replaced.len(), 1);
assert!(first.get_file("a.txt").is_some());
assert!(first.get_file("a/b.txt").is_some());
assert!(first.get_file("c.txt").is_some());
assert!(first.get_file("c/d.txt").is_some());
assert!(first.get_file("a/e.txt").is_some());
}
#[test]
fn test_try_from() {
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
fs::create_dir_all(temp_dir.path().join("bar")).expect("failed to create dir");
fs::write(temp_dir.path().join("foo.txt"), "foo").expect("failed to write file");
fs::write(temp_dir.path().join("bar/baz.txt"), "baz").expect("failed to write file");
let v_folder = VFolder::try_from(temp_dir.path()).expect("failed to convert");
assert_eq!(v_folder.get_files().len(), 1);
assert_eq!(v_folder.get_folders().len(), 1);
if let VFile::Binary(data) = v_folder.get_file("foo.txt").expect("file not found") {
assert_eq!(data, b"foo");
} else {
panic!("File is not binary");
}
if let VFile::Binary(data) = v_folder.get_file("bar/baz.txt").expect("file not found") {
assert_eq!(data, b"baz");
} else {
panic!("File is not binary");
}
} }
} }