diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a211ad5e..65509eeb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,5 +16,5 @@ jobs: - uses: actions/checkout@v4 - name: Build run: cargo build --verbose - - name: Run validity tests - run: cargo test validity --verbose + - name: Run all tests + run: cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index 334ed92e..2ea816ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,14 +2,25 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "amber" version = "0.3.2-alpha" dependencies = [ + "assert_cmd", "clap", "colored", "heraclitus-compiler", "itertools", + "predicates", "similar-string", "tempfile", ] @@ -63,12 +74,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "assert_cmd" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "capitalize" version = "0.1.0" @@ -137,6 +180,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "either" version = "1.12.0" @@ -159,6 +214,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" @@ -209,6 +273,27 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "pad" version = "0.1.6" @@ -218,6 +303,36 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.84" @@ -236,6 +351,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustix" version = "0.38.34" @@ -249,6 +393,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "similar-string" version = "1.4.3" @@ -284,6 +448,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -302,6 +472,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index c6ddb201..63458944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ itertools = "0.11.0" clap = { version = "4.4.18", features = ["derive"] } tempfile = "3.10.1" +[dev-dependencies] +assert_cmd = "2.0.14" +predicates = "3.1.0" + [profile.release] strip = true lto = true @@ -33,7 +37,11 @@ ci = "github" # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", +] # Publish jobs to run in CI pr-run-mode = "skip" # Whether cargo-dist should create a Github Release or use an existing draft diff --git a/src/compiler.rs b/src/compiler.rs index 22806c17..5ee7c1c7 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -1,13 +1,13 @@ -use heraclitus_compiler::prelude::*; use crate::modules::block::Block; +use crate::rules; use crate::translate::check_all_blocks; -use crate::utils::{ParserMetadata, TranslateMetadata}; use crate::translate::module::TranslateModule; -use crate::rules; -use std::process::Command; +use crate::utils::{ParserMetadata, TranslateMetadata}; +use colored::Colorize; +use heraclitus_compiler::prelude::*; use std::env; +use std::process::{Command, ExitStatus}; use std::time::Instant; -use colored::Colorize; const NO_CODE_PROVIDED: &str = "No code has been provided to the compiler"; const AMBER_DEBUG_PARSER: &str = "AMBER_DEBUG_PARSER"; @@ -15,15 +15,16 @@ const AMBER_DEBUG_TIME: &str = "AMBER_DEBUG_TIME"; pub struct AmberCompiler { pub cc: Compiler, - pub path: Option + pub path: Option, } impl AmberCompiler { pub fn new(code: String, path: Option) -> AmberCompiler { AmberCompiler { cc: Compiler::new("Amber", rules::get_rules()), - path - }.load_code(code) + path, + } + .load_code(code) } fn env_flag_set(flag: &str) -> bool { @@ -45,15 +46,19 @@ impl AmberCompiler { Ok(tokens) => { if Self::env_flag_set(AMBER_DEBUG_TIME) { let pathname = self.path.clone().unwrap_or(String::from("unknown")); - println!("[{}]\tin\t{}ms\t{pathname}", "Tokenize".cyan(), time.elapsed().as_millis()); + println!( + "[{}]\tin\t{}ms\t{pathname}", + "Tokenize".cyan(), + time.elapsed().as_millis() + ); } Ok(tokens) - }, + } Err((err_type, pos)) => { let error_message = match err_type { LexerErrorType::Singleline => { format!("Singleline {} not closed", pos.data.as_ref().unwrap()) - }, + } LexerErrorType::Unclosed => { format!("Unclosed {}", pos.data.as_ref().unwrap()) } @@ -81,18 +86,25 @@ impl AmberCompiler { }; if Self::env_flag_set(AMBER_DEBUG_TIME) { let pathname = self.path.clone().unwrap_or(String::from("unknown")); - println!("[{}]\tin\t{}ms\t{pathname}", "Parsed".blue(), time.elapsed().as_millis()); + println!( + "[{}]\tin\t{}ms\t{pathname}", + "Parsed".blue(), + time.elapsed().as_millis() + ); } // Return result match result { Ok(()) => Ok((block, meta)), - Err(failure) => Err(failure.unwrap_loud()) + Err(failure) => Err(failure.unwrap_loud()), } } pub fn translate(&self, block: Block, meta: ParserMetadata) -> String { let imports_sorted = meta.import_cache.topological_sort(); - let imports_blocks = meta.import_cache.files.iter() + let imports_blocks = meta + .import_cache + .files + .iter() .map(|file| file.metadata.as_ref().map(|meta| meta.block.clone())) .collect::>>(); let mut meta = TranslateMetadata::new(meta); @@ -105,15 +117,15 @@ impl AmberCompiler { } if Self::env_flag_set(AMBER_DEBUG_TIME) { let pathname = self.path.clone().unwrap_or(String::from("unknown")); - println!("[{}]\tin\t{}ms\t{pathname}", "Translate".magenta(), time.elapsed().as_millis()); + println!( + "[{}]\tin\t{}ms\t{pathname}", + "Translate".magenta(), + time.elapsed().as_millis() + ); } result.push(block.translate(&mut meta)); let res = result.join("\n"); - format!( - "{}\n{}", - include_str!("header.sh"), - res - ) + format!("{}\n{}", include_str!("header.sh"), res) } pub fn compile(&self) -> Result<(Vec, String), Message> { @@ -122,24 +134,30 @@ impl AmberCompiler { .map(|(block, meta)| (meta.messages.clone(), self.translate(block, meta))) } - pub fn execute(code: String, flags: &[String]) { + pub fn execute(code: String, flags: &[String]) -> Result { let code = format!("set -- {};\n\n{}", flags.join(" "), code); - Command::new("/usr/bin/env").arg("bash").arg("-c").arg(code).spawn().unwrap().wait().unwrap(); + Ok(Command::new("/usr/bin/env") + .arg("bash") + .arg("-c") + .arg(code) + .spawn()? + .wait()?) } #[allow(dead_code)] pub fn test_eval(&mut self) -> Result { self.compile().map_or_else(Err, |(_, code)| { - let child = Command::new("/usr/bin/env").arg("bash") - .arg("-c").arg::<&str>(code.as_ref()) - .output().unwrap(); + let child = Command::new("/usr/bin/env") + .arg("bash") + .arg("-c") + .arg::<&str>(code.as_ref()) + .output() + .unwrap(); Ok(String::from_utf8_lossy(&child.stdout).to_string()) }) } pub fn import_std() -> String { - [ - include_str!("std/main.ab") - ].join("\n") + [include_str!("std/main.ab")].join("\n") } } diff --git a/src/main.rs b/src/main.rs index 7207ccb0..88f27314 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use crate::compiler::AmberCompiler; use clap::Parser; use colored::Colorize; use heraclitus_compiler::prelude::*; +use std::error::Error; use std::fs; use std::io::prelude::*; use std::path::PathBuf; @@ -27,7 +28,7 @@ struct Cli { eval: Option, } -fn main() { +fn main() -> Result<(), Box> { let cli = Cli::parse(); if let Some(code) = cli.eval { @@ -36,7 +37,8 @@ fn main() { Ok((messages, code)) => { messages.iter().for_each(|m| m.show()); (!messages.is_empty()).then(|| render_dash()); - AmberCompiler::execute(code, &vec![]) + let exit_status = AmberCompiler::execute(code, &vec![])?; + std::process::exit(exit_status.code().unwrap_or(1)); } Err(err) => { err.show(); @@ -70,7 +72,8 @@ fn main() { // Execute the code else { (!messages.is_empty()).then(|| render_dash()); - AmberCompiler::execute(code, &vec![]); + let exit_status = AmberCompiler::execute(code, &vec![])?; + std::process::exit(exit_status.code().unwrap_or(1)); } } Err(err) => { @@ -86,6 +89,7 @@ fn main() { } } else { } + Ok(()) } #[cfg(target_os = "windows")] diff --git a/src/tests/cli.rs b/src/tests/cli.rs new file mode 100644 index 00000000..6f2373d5 --- /dev/null +++ b/src/tests/cli.rs @@ -0,0 +1,35 @@ +// Tests for the amber CLI binary itself. +// Make sure to run `cargo build` before running these tests. + +use assert_cmd::prelude::*; +use predicates::prelude::*; +use std::io::Write; +use std::process::Command; +use tempfile::NamedTempFile; + +// Test that the bash error code is forwarded to the exit code of amber. +#[test] +fn bash_error_exit_code() -> Result<(), Box> { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; + + let mut file = NamedTempFile::new()?; + + writeln!( + file, + r#" + main {{ + $ notexistingcommand $? + }} + "# + )?; + + cmd.arg(file.path()); + cmd.assert() + .failure() + .stderr(predicate::str::contains( + "notexistingcommand: command not found", + )) + .code(127); + + Ok(()) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 92c1b190..70694626 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,19 +1,17 @@ use crate::compiler::AmberCompiler; -pub mod validity; +pub mod cli; pub mod stdlib; +pub mod validity; #[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()) - } - + ($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 {