From 0c4c957d672f8a7fd072372ddc714224b4a103f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Thu, 15 Aug 2024 22:37:39 +0200 Subject: [PATCH] add unit tests --- .github/workflows/test.yml | 15 +++++++ Cargo.toml | 3 ++ src/datapack/command/execute.rs | 56 ++++++++++++++++++++++- src/datapack/command/mod.rs | 58 ++++++++++++++++++++++++ src/datapack/function.rs | 30 +++++++++++++ src/datapack/mod.rs | 53 ++++++++++++++++++++++ src/datapack/namespace.rs | 28 +++++++++--- src/datapack/tag.rs | 61 +++++++++++++++++++++---- src/lib.rs | 2 +- src/util/compile.rs | 2 +- src/util/extendable_queue.rs | 38 +++++++++++++++- src/virtual_fs.rs | 80 +++++++++++++++++++++++++++++++-- 12 files changed, 403 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..74d11be --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index e09077f..471d432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,6 @@ serde = { version = "1.0.197", optional = true, features = ["derive"] } serde_json = "1.0.114" tracing = "0.1.40" zip = { version = "2.1.3", default-features = false, features = ["deflate", "time"], optional = true } + +[dev-dependencies] +tempfile = "3.10.1" diff --git a/src/datapack/command/execute.rs b/src/datapack/command/execute.rs index a97004b..2ed0baa 100644 --- a/src/datapack/command/execute.rs +++ b/src/datapack/command/execute.rs @@ -421,7 +421,7 @@ impl Condition { /// Will fail if the condition contains an `Or` variant. Use `compile` instead. fn str_cond(&self) -> Option { 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::Atom(s) => Some("unless ".to_string() + &s), _ => None, @@ -491,7 +491,7 @@ mod tests { #[allow(clippy::redundant_clone)] #[test] fn test_condition() { - let c1 = Condition::Atom("foo".to_string()); + let c1 = Condition::from("foo"); let c2 = Condition::Atom("bar".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()]); + } } diff --git a/src/datapack/command/mod.rs b/src/datapack/command/mod.rs index 248c9af..c3170fb 100644 --- a/src/datapack/command/mod.rs +++ b/src/datapack/command/mod.rs @@ -56,6 +56,7 @@ impl Command { #[must_use] fn get_count(&self, options: &CompileOptions) -> usize { match self { + // TODO: change comment to compile to `1`, make sure nothing breaks Self::Comment(_) => 0, Self::Debug(_) => usize::from(options.debug), Self::Raw(cmd) => cmd.split('\n').count(), @@ -266,3 +267,60 @@ fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive) -> 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))); + } +} diff --git a/src/datapack/function.rs b/src/datapack/function.rs index 3fe8392..34bf309 100644 --- a/src/datapack/function.rs +++ b/src/datapack/function.rs @@ -74,3 +74,33 @@ impl Function { 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!" + )); + } +} diff --git a/src/datapack/mod.rs b/src/datapack/mod.rs index e424139..4da602a 100644 --- a/src/datapack/mod.rs +++ b/src/datapack/mod.rs @@ -158,3 +158,56 @@ fn generate_mcmeta(dp: &Datapack, _options: &CompileOptions, _state: &MutCompile 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::(&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)) + ); + } +} diff --git a/src/datapack/namespace.rs b/src/datapack/namespace.rs index 540184a..f1f2ab9 100644 --- a/src/datapack/namespace.rs +++ b/src/datapack/namespace.rs @@ -109,13 +109,7 @@ impl Namespace { // compile tags for ((path, tag_type), tag) in &self.tags { let vfile = tag.compile(options, state); - root_folder.add_file( - &format!( - "tags/{tag_type}/{path}.json", - tag_type = tag_type.to_string() - ), - vfile, - ); + root_folder.add_file(&format!("tags/{tag_type}/{path}.json"), vfile); } root_folder @@ -129,3 +123,23 @@ impl Namespace { .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()); + } +} diff --git a/src/datapack/tag.rs b/src/datapack/tag.rs index e16cd27..7aebde5 100644 --- a/src/datapack/tag.rs +++ b/src/datapack/tag.rs @@ -1,5 +1,7 @@ //! A tag for various types. +use std::fmt::Display; + use crate::{ util::compile::{CompileOptions, MutCompilerState}, virtual_fs::VFile, @@ -81,9 +83,9 @@ pub enum TagType { /// `Others()` => `data//tags/` Others(String), } -impl ToString for TagType { - fn to_string(&self) -> String { - match self { +impl Display for TagType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { Self::Blocks => "block".to_string(), Self::Fluids => "fluid".to_string(), Self::Items => "item".to_string(), @@ -91,7 +93,8 @@ impl ToString for TagType { Self::GameEvents => "game_event".to_string(), Self::Functions => "function".to_string(), Self::Others(path) => path.to_string(), - } + }; + f.write_str(&str) } } @@ -122,11 +125,53 @@ impl TagValue { match self { Self::Simple(value) => serde_json::Value::String(value.clone()), Self::Advanced { id, required } => { - let mut map = serde_json::Map::new(); - map.insert("id".to_string(), serde_json::Value::String(id.clone())); - map.insert("required".to_string(), serde_json::Value::Bool(*required)); - serde_json::Value::Object(map) + serde_json::json!({ + "id": id.clone(), + "required": *required + }) } } } } + +#[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::(&text) + .expect("Failed to deserialize tag"); + assert_eq!( + deserialized, + serde_json::json!({ + "replace": true, + "values": [ + "foo:bar", + { + "id": "bar:baz", + "required": true + } + ] + }) + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0e69c51..0330fec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ rustdoc::broken_intra_doc_links, 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)] pub mod datapack; diff --git a/src/util/compile.rs b/src/util/compile.rs index ac1227d..4c52632 100644 --- a/src/util/compile.rs +++ b/src/util/compile.rs @@ -40,7 +40,7 @@ pub struct CompilerState {} pub type MutCompilerState = Mutex; /// State of the compiler for each function that can change during compilation. -#[derive(Debug, Getters)] +#[derive(Debug, Getters, Default)] pub struct FunctionCompilerState { /// Next unique identifier. uid_counter: Mutex, diff --git a/src/util/extendable_queue.rs b/src/util/extendable_queue.rs index 030c788..db61318 100644 --- a/src/util/extendable_queue.rs +++ b/src/util/extendable_queue.rs @@ -38,7 +38,7 @@ impl ExtendableQueue { /// Get the queue. #[must_use] - pub fn get(&self) -> &Arc>> { + pub fn get_arc(&self) -> &Arc>> { &self.queue } @@ -85,3 +85,39 @@ impl Iterator for ExtendableQueue { 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)); + } +} diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index f047c73..f5d09c6 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -284,11 +284,9 @@ impl TryFrom<&Path> for VFolder { if let Some(name) = name { if path.is_dir() { 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())?; root_vfolder.add_file(&name, file); - } else { - unreachable!("Path is neither file nor directory"); } } else { return Err(io::Error::new( @@ -353,6 +351,8 @@ mod tests { let v_file_2 = VFile::from("baz"); 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_folders().len(), 1); assert!(v_folder.get_file("bar/baz.txt").is_some()); @@ -361,5 +361,79 @@ mod tests { .expect("folder not found") .get_file("baz.txt") .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"); + } } }