diff --git a/Cargo.lock b/Cargo.lock index b5a13836..bb5b3544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "heraclitus-compiler", "itertools", "similar-string", + "tempfile", ] [[package]] @@ -62,12 +63,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "capitalize" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702cfe2f052c6542d55a9924efef63dd315c168cc4f315a824fb0f47da4e11c8" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.4" @@ -130,6 +143,22 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "heck" version = "0.5.0" @@ -168,6 +197,18 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "pad" version = "0.1.6" @@ -195,6 +236,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "similar-string" version = "1.4.3" @@ -218,6 +272,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 043409bf..783eb2d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ similar-string = "1.4.2" colored = "2.0.0" itertools = "0.11.0" clap = { version = "4.4.18", features = ["derive"] } +tempfile = "3.10.1" [profile.release] debug = true diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a0363a6d..92c1b190 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1 +1,21 @@ -pub mod validity; \ No newline at end of file +use crate::compiler::AmberCompiler; + +pub mod validity; +pub mod stdlib; + +#[macro_export] +macro_rules! test_amber { + ($code:expr, $result:expr) => { + { + match AmberCompiler::new($code.to_string(), None).test_eval() { + Ok(result) => assert_eq!(result.trim_end_matches('\n'), $result), + Err(err) => panic!("ERROR: {}", err.message.unwrap()) + } + + } + }; +} + +pub fn compile_code>(code: T) -> String { + AmberCompiler::new(code.into(), None).compile().unwrap().1 +} diff --git a/src/tests/stdlib.rs b/src/tests/stdlib.rs new file mode 100644 index 00000000..00c9ae34 --- /dev/null +++ b/src/tests/stdlib.rs @@ -0,0 +1,405 @@ +use crate::compiler::AmberCompiler; +use crate::test_amber; +use crate::tests::compile_code; +use std::fs; +use std::io::Read; +use std::io::Write; +use std::path::PathBuf; +use tempfile::tempdir; +use tempfile::TempDir; +use std::process::{Command, Stdio}; + +fn mkfile() -> (PathBuf, TempDir) { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + assert!(temp_dir.path().is_dir(), "Temp directory is not a directory!"); + + let file_path = temp_dir.path().join("test_file.txt"); + + let mut file = fs::File::create(&file_path).expect("Failed to create temporary file"); + file.write_all(b"This is a sample file.\n").expect("Failed to write to temporary file"); + file.flush().expect("Failed to flush file"); + + (file_path, temp_dir) +} + +#[test] +fn input() { + let prompt_message = "Please enter your name:"; + let code = format!(r#" + import * from "std" + main {{ + let name = input("{}") + echo "Hello, " + name + }} + "#, prompt_message); + + let input = "Amber"; + let expected_output = format!("{}Hello, {}", prompt_message, input); + + let compiled_code = compile_code(code); + + let mut child = Command::new("bash") + .arg("-c") + .arg(compiled_code) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to execute process"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + stdin.write_all(input.as_bytes()).expect("Failed to write to stdin"); + } + + let output = child.wait_with_output().expect("Failed to read stdout"); + let output_str = String::from_utf8(output.stdout).expect("Failed to convert stdout to String"); + + assert_eq!(output_str.trim_end_matches('\n'), expected_output); +} + +#[test] +fn replace_once() { + let code = " + import * from \"std\" + main { + echo replace_once(\"hello world!\", \"world\", \"Amber\") + } + "; + + test_amber!(code, "hello Amber!") +} + +#[test] +fn replace() { + let code = " + import * from \"std\" + main { + echo replace(\"banana banana\", \"banana\", \"apple\") + } + "; + test_amber!(code, "apple apple") +} + +#[test] +fn replace_regex() { + let code = " + import * from \"std\" + main { + echo replace_regex(\"abc123def\", \"[0-9][0-9]*\", \"456\") + } + "; + test_amber!(code, "abc456def") +} + +#[test] +fn file_read() { + let (file_path, temp_dir) = mkfile(); + + let code = format!( + " + import * from \"std\" + main {{ + let f = file_read(\"{file_path}\") failed {{ echo \"Failed\" }} + echo f + }} + ", + file_path = file_path.to_str().unwrap() + ); + + test_amber!(code, "This is a sample file."); + + temp_dir.close().expect("Couldn't close temp dir"); +} + +#[test] +fn file_write() { + let (file_path, temp_dir) = mkfile(); + + let code = format!( + " + import * from \"std\" + main {{ + unsafe file_write(\"{file_path}\", \"Hello, Amber!\") + }} + ", + file_path = file_path.to_str().unwrap() + ); + + test_amber!(code, ""); + + let mut file_content = String::new(); + fs::File::open(&file_path) + .expect("Failed to open temporary file") + .read_to_string(&mut file_content) + .expect("Failed to read from temporary file"); + + assert_eq!(file_content.trim(), "Hello, Amber!"); + + temp_dir.close().expect("Couldn't close temp dir"); +} + +#[test] +fn file_append() { + let (file_path, temp_dir) = mkfile(); + + let code = format!( + " + import * from \"std\" + main {{ + unsafe file_append(\"{file_path}\", \"Appending this line.\") + }} + ", + file_path = file_path.to_str().unwrap() + ); + + test_amber!(code, ""); + + let mut file_content = String::new(); + fs::File::open(&file_path) + .expect("Failed to open temporary file") + .read_to_string(&mut file_content) + .expect("Failed to read from temporary file"); + + assert_eq!(file_content.trim(), "This is a sample file.\nAppending this line."); + + temp_dir.close().expect("Couldn't close temp dir"); +} + +#[test] +fn split() { + let code = " + import * from \"std\" + main { + let array = split(\"apple,banana,cherry\", \",\") + echo array[1] + } + "; + test_amber!(code, "banana") +} + +#[test] +fn join() { + let code = " + import * from \"std\" + main { + echo join([\"apple\", \"banana\", \"cherry\"], \", \") + } + "; + test_amber!(code, "apple,banana,cherry") +} + +#[test] +fn trim() { + let code = " + import * from \"std\" + main { + echo trim(\" hello world \") + } + "; + test_amber!(code, "hello world") +} + +#[test] +fn trim_left() { + let code = " + import * from \"std\" + main { + echo trim_left(\" hello world \") + } + "; + test_amber!(code, "hello world ") +} + +#[test] +fn trim_right() { + let code = " + import * from \"std\" + main { + echo trim_right(\" hello world \") + } + "; + test_amber!(code, " hello world") +} + +#[test] +fn lower() { + let code = " + import * from \"std\" + main { + echo lower(\"HELLO WORLD\") + } + "; + test_amber!(code, "hello world") +} + +#[test] +fn upper() { + let code = " + import * from \"std\" + main { + echo upper(\"hello world\") + } + "; + test_amber!(code, "HELLO WORLD") +} + +#[test] +fn len_string() { + let code = " + import * from \"std\" + main { + echo len(\"hello\") + } + "; + test_amber!(code, "5") +} + +#[test] +fn len_list() { + let code = " + import * from \"std\" + main { + echo len([1, 2, 3, 4]) + } + "; + test_amber!(code, "4") +} + +#[test] +fn parse() { + let code = " + import * from \"std\" + main { + echo parse(\"123\")? + } + "; + test_amber!(code, "123") +} + +#[test] +fn chars() { + let code = " + import * from \"std\" + main { + echo chars(\"hello\") + } + "; + test_amber!(code, "h e l l o") +} + +#[test] +fn sum() { + let code = " + import * from \"std\" + main { + echo sum([1, 2, 3, 4]) + } + "; + test_amber!(code, "10") +} + +#[test] +fn has_failed() { + let code = " + import * from \"std\" + main { + if has_failed(\"ls /nonexistent\") { + echo \"Command failed\" + } else { + echo \"Command succeeded\" + } + } + "; + test_amber!(code, "Command failed") +} + +#[test] +fn exit() { + let code = " + import * from \"std\" + main { + exit(37) + } + "; + let code = compile_code(code); + let mut cmd = Command::new("bash") + .arg("-c").arg(code) + .stdout(Stdio::null()).stderr(Stdio::null()) + .spawn().expect("Couldn't spawn bash"); + + assert_eq!(cmd.wait().expect("Couldn't wait for bash to execute").code(), Some(37)); +} + +#[test] +fn includes() { + let code = " + import * from \"std\" + main { + if includes([\"apple\", \"banana\", \"cherry\"], \"banana\") { + echo \"Found\" + } else { + echo \"Not Found\" + } + } + "; + test_amber!(code, "Found") +} + +#[test] +fn dir_exist() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + + let code = format!( + " + import * from \"std\" + main {{ + if dir_exist(\"{tmpdir}\") {{ + echo \"Found\" + }} else {{ + echo \"Not Found\" + }} + }} + ", + tmpdir = temp_dir.path().to_str().unwrap() + ); + test_amber!(code, "Found") +} + +#[test] +fn file_exist() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let file_path = temp_dir.path().join("test_file.txt"); + + let _file = fs::File::create(&file_path).expect("Failed to create temporary file"); + + let code = format!( + " + import * from \"std\" + main {{ + if file_exist(\"{file_path}\") {{ + echo \"Found\" + }} else {{ + echo \"Not Found\" + }} + }} + ", + file_path = file_path.to_str().unwrap() + ); + test_amber!(code, "Found"); + + fs::remove_file(&file_path).expect("Failed to delete temporary file"); +} + +#[test] +fn lines() { + let code = " + import { lines } from \"std\" + main { + loop line in lines(\"hello\\nworld\") { + echo \"line: \" + line + } + } + "; + test_amber!(code, "line: hello\nline: world") +} + diff --git a/src/tests/validity.rs b/src/tests/validity.rs index ea663a7f..58bf972b 100644 --- a/src/tests/validity.rs +++ b/src/tests/validity.rs @@ -1,16 +1,5 @@ use crate::compiler::AmberCompiler; - -macro_rules! test_amber { - ($code:expr, $result:expr) => { - { - match AmberCompiler::new($code.to_string(), None).test_eval() { - Ok(result) => assert_eq!(result.trim(), $result), - Err(err) => panic!("ERROR: {}", err.message.unwrap()) - } - - } - }; -} +use crate::test_amber; #[test] fn hello_world() { @@ -374,7 +363,7 @@ fn infinite_loop() { } } "; - test_amber!(code, "1 2 3 4 6 7 8 9 10"); + test_amber!(code, "1 2 3 4 6 7 8 9 10 "); } #[test] @@ -742,84 +731,6 @@ fn status() { test_amber!(code, "127\n127"); } -#[test] -fn test_std_library() { - let code = " - import * from \"std\" - - main { - // Split a string into a list of strings (characters) - echo chars(\"hello world\") - // Split a string into a list of strings by a delimiter - echo split(\"hello world\", \"l\") - // Split a multiline string to lines of string - loop line in lines(\"hello\nworld\") { - echo line - } - // Split a multiline string into a list of string by one or more spaces - loop word in words(\"hello world ciao mondo\") { - echo word - } - // Split a joined string into a list of string - loop word in words(join([\"hello\", \"world\"], \" \")) { - echo word - } - // Join a list of strings into a string - echo join(split(\"hello world\", \"l\"), \"l\") - // Transform string into a lowercase string - echo lower(\"HELLO WORLD\") - // Transform string into an uppercase string - echo upper(\"hello world\") - // Trim whitespace from the beginning and end of a string - echo \"|{trim(\" hello world \")}|\" - // Trim whitespace from the beginning of a string - echo \"|{trim_left(\" hello world \")}|\" - // Trim whitespace from the end of a string - echo \"|{trim_right(\" hello world \")}|\" - // Get the length of a string or list - echo len(\"hello world\") - echo len([1,2,3]) - // Replace all occurrences of a substring with another substring - echo replace(\"hello world\", \"world\", \"universe\") - // Parse text into a number - echo parse(\"123\")? - // Parse text into a number - do some code if failed - parse(\"XDDDDabc123\") failed { - echo \"Parsing Failed\" - } - // Check if array includes certain word - echo includes([\"hello\", \"world\"], \"hello\") - // Check if array does not include certain word - echo includes([\"hello\", \"world\"], \"other\") - } - "; - test_amber!(code, vec![ - "h e l l o w o r l d", - "he o wor d", - "hello", - "world", - "hello", - "world", - "ciao", - "mondo", - "hello", - "world", - "hello world", - "hello world", - "HELLO WORLD", - "|hello world|", - "|hello world |", - "| hello world|", - "11", - "3", - "hello universe", - "123", - "Parsing Failed", - "1", - "0", - ].join("\n")); -} - #[test] fn chained_modifiers() { let code = " diff --git a/test_files/str/trim.ab b/test_files/str/trim.ab index 6739a068..256efe64 100644 --- a/test_files/str/trim.ab +++ b/test_files/str/trim.ab @@ -1,7 +1,7 @@ pub fun trim(text) { - return "$\{{nameof text}##*( )}" + return unsafe $echo "{text}" | xargs$ } main { trim("long text") -} \ No newline at end of file +}