diff --git a/Cargo.lock b/Cargo.lock index d05314df13..536d57fffa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,7 @@ name = "cli_utils" version = "0.1.0" dependencies = [ "bumpalo", + "const_format", "criterion 0.3.5 (git+https://github.com/Anton-4/criterion.rs)", "rlimit", "roc_cli", @@ -1759,6 +1760,12 @@ dependencies = [ "ahash 0.7.6", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -3528,6 +3535,8 @@ dependencies = [ "roc_target", "roc_test_utils", "serial_test", + "strum", + "strum_macros", "target-lexicon", "tempfile", "wasmer", @@ -4603,6 +4612,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96acfc1b70604b8b2f1ffa4c57e59176c7dbb05d556c71ecd2f5498a1dee7f8" + +[[package]] +name = "strum_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.81" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4e019df2c6..976a9d19bc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -85,6 +85,8 @@ indoc = "1.0.3" serial_test = "0.5.1" criterion = { git = "https://github.com/Anton-4/criterion.rs"} cli_utils = { path = "../cli_utils" } +strum = "0.24.0" +strum_macros = "0.24" # Wasmer singlepass compiler only works on x86_64. [target.'cfg(target_arch = "x86_64")'.dev-dependencies] diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 18d4e4de53..abc782292a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -3,16 +3,15 @@ extern crate const_format; use build::BuiltFile; use bumpalo::Bump; -use clap::Command; -use clap::{Arg, ArgMatches}; +use clap::{Arg, ArgMatches, Command}; use roc_build::link::LinkType; use roc_error_macros::user_error; use roc_load::{LoadingProblem, Threading}; use roc_mono::ir::OptLevel; use std::env; +use std::ffi::OsStr; use std::io; -use std::path::Path; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use target_lexicon::BinaryFormat; use target_lexicon::{ @@ -96,6 +95,16 @@ pub fn build_app<'a>() -> Command<'a> { .possible_values(["true", "false"]) .required(false); + let roc_file_to_run = Arg::new(ROC_FILE) + .help("The .roc file of an app to run") + .allow_invalid_utf8(true); + + let args_for_app = Arg::new(ARGS_FOR_APP) + .help("Arguments to pass into the app being run") + .requires(ROC_FILE) + .allow_invalid_utf8(true) + .multiple_values(true); + let app = Command::new("roc") .version(concatcp!(VERSION, "\n")) .about("Runs the given .roc file, if there are no compilation errors.\nUse one of the SUBCOMMANDS below to do something else!") @@ -132,6 +141,7 @@ pub fn build_app<'a>() -> Command<'a> { .arg( Arg::new(ROC_FILE) .help("The .roc file to build") + .allow_invalid_utf8(true) .required(true), ) ) @@ -148,11 +158,8 @@ pub fn build_app<'a>() -> Command<'a> { .arg(flag_linker.clone()) .arg(flag_precompiled.clone()) .arg(flag_valgrind.clone()) - .arg( - Arg::new(ROC_FILE) - .help("The .roc file of an app to run") - .required(true), - ) + .arg(roc_file_to_run.clone().required(true)) + .arg(args_for_app.clone()) ) .subcommand(Command::new(CMD_FORMAT) .about("Format a .roc file using standard Roc formatting") @@ -177,6 +184,7 @@ pub fn build_app<'a>() -> Command<'a> { .arg( Arg::new(ROC_FILE) .help("The .roc file of an app to check") + .allow_invalid_utf8(true) .required(true), ) ) @@ -200,17 +208,8 @@ pub fn build_app<'a>() -> Command<'a> { .arg(flag_linker) .arg(flag_precompiled) .arg(flag_valgrind) - .arg( - Arg::new(ROC_FILE) - .help("The .roc file of an app to build and run") - .required(false), - ) - .arg( - Arg::new(ARGS_FOR_APP) - .help("Arguments to pass into the app being run") - .requires(ROC_FILE) - .multiple_values(true), - ); + .arg(roc_file_to_run.required(false)) + .arg(args_for_app); if cfg!(feature = "editor") { app.subcommand( @@ -236,8 +235,8 @@ pub fn docs(files: Vec) { #[derive(Debug, PartialEq, Eq)] pub enum BuildConfig { BuildOnly, - BuildAndRun { roc_file_arg_index: usize }, - BuildAndRunIfNoErrors { roc_file_arg_index: usize }, + BuildAndRun, + BuildAndRunIfNoErrors, } pub enum FormatMode { @@ -255,7 +254,7 @@ pub fn build( use BuildConfig::*; let arena = Bump::new(); - let filename = matches.value_of(ROC_FILE).unwrap(); + let filename = matches.value_of_os(ROC_FILE).unwrap(); let original_cwd = std::env::current_dir()?; let opt_level = match ( @@ -372,7 +371,7 @@ pub fn build( // Return a nonzero exit code if there were problems Ok(problems.exit_code()) } - BuildAndRun { roc_file_arg_index } => { + BuildAndRun => { if problems.errors > 0 || problems.warnings > 0 { println!( "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m", @@ -403,15 +402,11 @@ pub fn build( ); } - roc_run( - arena, - &original_cwd, - triple, - roc_file_arg_index, - &binary_path, - ) + let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default(); + + roc_run(arena, &original_cwd, triple, args, &binary_path) } - BuildAndRunIfNoErrors { roc_file_arg_index } => { + BuildAndRunIfNoErrors => { if problems.errors == 0 { if problems.warnings > 0 { println!( @@ -427,13 +422,9 @@ pub fn build( ); } - roc_run( - arena, - &original_cwd, - triple, - roc_file_arg_index, - &binary_path, - ) + let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default(); + + roc_run(arena, &original_cwd, triple, args, &binary_path) } else { println!( "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nYou can run the program anyway with: \x1B[32mroc run {}\x1B[39m", @@ -460,7 +451,7 @@ pub fn build( "warnings" }, total_time.as_millis(), - filename + filename.to_string_lossy() ); Ok(problems.exit_code()) @@ -479,17 +470,14 @@ pub fn build( } } -#[cfg(target_family = "unix")] -fn roc_run( +fn roc_run<'a, I: IntoIterator>( arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! cwd: &Path, triple: Triple, - roc_file_arg_index: usize, + args: I, binary_path: &Path, ) -> io::Result { - use std::os::unix::process::CommandExt; - - let mut cmd = match triple.architecture { + match triple.architecture { Architecture::Wasm32 => { // If possible, report the generated executable name relative to the current dir. let generated_filename = binary_path @@ -500,19 +488,44 @@ fn roc_run( // since the process is about to exit anyway. std::mem::forget(arena); - let args = std::env::args() - .skip(roc_file_arg_index) - .collect::>(); + if cfg!(target_family = "unix") { + use std::os::unix::ffi::OsStrExt; - run_with_wasmer(generated_filename, &args); - return Ok(0); + run_with_wasmer( + generated_filename, + args.into_iter().map(|os_str| os_str.as_bytes()), + ); + } else { + run_with_wasmer( + generated_filename, + args.into_iter().map(|os_str| { + os_str.to_str().expect( + "Roc does not currently support passing non-UTF8 arguments to Wasmer.", + ) + }), + ); + } + + Ok(0) + } + _ => { + if cfg!(target_family = "unix") { + roc_run_unix(cwd, args, binary_path) + } else { + roc_run_non_unix(arena, cwd, args, binary_path) + } } - _ => std::process::Command::new(&binary_path), - }; - - if let Architecture::Wasm32 = triple.architecture { - cmd.arg(binary_path); } +} + +fn roc_run_unix, S: AsRef>( + cwd: &Path, + args: I, + binary_path: &Path, +) -> io::Result { + use std::os::unix::process::CommandExt; + + let mut cmd = std::process::Command::new(&binary_path); // Forward all the arguments after the .roc file argument // to the new process. This way, you can do things like: @@ -521,10 +534,8 @@ fn roc_run( // // ...and have it so that app.roc will receive only `foo`, // `bar`, and `baz` as its arguments. - for (index, arg) in std::env::args().enumerate() { - if index > roc_file_arg_index { - cmd.arg(arg); - } + for arg in args { + cmd.arg(arg); } // This is much faster than spawning a subprocess if we're on a UNIX system! @@ -536,29 +547,36 @@ fn roc_run( Err(err) } -#[cfg(not(target_family = "unix"))] -fn roc_run(cmd: &mut Command) -> io::Result { - // Run the compiled app - let exit_status = cmd - .spawn() - .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) - .wait() - .expect("TODO gracefully handle block_on failing when `roc` spawns a subprocess for the compiled app"); +fn roc_run_non_unix, S: AsRef>( + _arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! + _cwd: &Path, + _args: I, + _binary_path: &Path, +) -> io::Result { + todo!("TODO support running roc programs on non-UNIX targets"); + // let mut cmd = std::process::Command::new(&binary_path); - // `roc [FILE]` exits with the same status code as the app it ran. - // - // If you want to know whether there were compilation problems - // via status code, use either `roc build` or `roc check` instead! - match exit_status.code() { - Some(code) => Ok(code), - None => { - todo!("TODO gracefully handle the `roc [FILE]` subprocess terminating with a signal."); - } - } + // // Run the compiled app + // let exit_status = cmd + // .spawn() + // .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) + // .wait() + // .expect("TODO gracefully handle block_on failing when `roc` spawns a subprocess for the compiled app"); + + // // `roc [FILE]` exits with the same status code as the app it ran. + // // + // // If you want to know whether there were compilation problems + // // via status code, use either `roc build` or `roc check` instead! + // match exit_status.code() { + // Some(code) => Ok(code), + // None => { + // todo!("TODO gracefully handle the `roc [FILE]` subprocess terminating with a signal."); + // } + // } } #[cfg(feature = "run-wasm32")] -fn run_with_wasmer(wasm_path: &std::path::Path, args: &[String]) { +fn run_with_wasmer, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) { use wasmer::{Instance, Module, Store}; let store = Store::default(); @@ -589,8 +607,8 @@ fn run_with_wasmer(wasm_path: &std::path::Path, args: &[String]) { } #[cfg(not(feature = "run-wasm32"))] -fn run_with_wasmer(_wasm_path: &std::path::Path, _args: &[String]) { - println!("Running wasm files not support"); +fn run_with_wasmer, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) { + println!("Running wasm files is not supported on this target."); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/cli/src/main.rs b/cli/src/main.rs index 0a21c66ed5..97d1031894 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -27,43 +27,31 @@ fn main() -> io::Result<()> { let exit_code = match matches.subcommand() { None => { - match matches.index_of(ROC_FILE) { - Some(arg_index) => { - let roc_file_arg_index = arg_index + 1; // Not sure why this +1 is necessary, but it is! + if matches.is_present(ROC_FILE) { + build( + &matches, + BuildConfig::BuildAndRunIfNoErrors, + Triple::host(), + LinkType::Executable, + ) + } else { + launch_editor(None)?; - build( - &matches, - BuildConfig::BuildAndRunIfNoErrors { roc_file_arg_index }, - Triple::host(), - LinkType::Executable, - ) - } - - None => { - launch_editor(None)?; - - Ok(0) - } + Ok(0) } } Some((CMD_RUN, matches)) => { - match matches.index_of(ROC_FILE) { - Some(arg_index) => { - let roc_file_arg_index = arg_index + 1; // Not sure why this +1 is necessary, but it is! + if matches.is_present(ROC_FILE) { + build( + matches, + BuildConfig::BuildAndRun, + Triple::host(), + LinkType::Executable, + ) + } else { + eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command."); - build( - matches, - BuildConfig::BuildAndRun { roc_file_arg_index }, - Triple::host(), - LinkType::Executable, - ) - } - - None => { - eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command."); - - Ok(1) - } + Ok(1) } } Some((CMD_BUILD, matches)) => { @@ -90,7 +78,7 @@ fn main() -> io::Result<()> { let arena = bumpalo::Bump::new(); let emit_timings = matches.is_present(FLAG_TIME); - let filename = matches.value_of(ROC_FILE).unwrap(); + let filename = matches.value_of_os(ROC_FILE).unwrap(); let roc_file_path = PathBuf::from(filename); let src_dir = roc_file_path.parent().unwrap().to_owned(); diff --git a/cli/tests/cli_run.rs b/cli/tests/cli_run.rs index f184626b0e..dba199f2fd 100644 --- a/cli/tests/cli_run.rs +++ b/cli/tests/cli_run.rs @@ -14,10 +14,29 @@ mod cli_run { known_bad_file, run_cmd, run_roc, run_with_valgrind, Out, ValgrindError, ValgrindErrorXWhat, }; + use const_format::concatcp; use indoc::indoc; + use roc_cli::{CMD_BUILD, CMD_CHECK, CMD_FORMAT, CMD_RUN}; use roc_test_utils::assert_multiline_str_eq; use serial_test::serial; + use std::iter; use std::path::{Path, PathBuf}; + use strum::IntoEnumIterator; + use strum_macros::EnumIter; + + const OPTIMIZE_FLAG: &str = concatcp!("--", roc_cli::FLAG_OPTIMIZE); + const VALGRIND_FLAG: &str = concatcp!("--", roc_cli::FLAG_VALGRIND); + const LINKER_FLAG: &str = concatcp!("--", roc_cli::FLAG_LINKER); + const CHECK_FLAG: &str = concatcp!("--", roc_cli::FLAG_CHECK); + #[allow(dead_code)] + const TARGET_FLAG: &str = concatcp!("--", roc_cli::FLAG_TARGET); + + #[derive(Debug, EnumIter)] + enum CliMode { + RocBuild, + RocRun, + Roc, + } #[cfg(not(debug_assertions))] use roc_collections::all::MutMap; @@ -65,7 +84,7 @@ mod cli_run { } fn check_compile_error(file: &Path, flags: &[&str], expected: &str) { - let compile_out = run_roc(&[&["check", file.to_str().unwrap()], flags].concat()); + let compile_out = run_roc([CMD_CHECK, file.to_str().unwrap()].iter().chain(flags), &[]); let err = compile_out.stdout.trim(); let err = strip_colors(err); @@ -77,19 +96,41 @@ mod cli_run { } fn check_format_check_as_expected(file: &Path, expects_success_exit_code: bool) { - let flags = &["--check"]; - let out = run_roc(&[&["format", file.to_str().unwrap()], &flags[..]].concat()); - if expects_success_exit_code { - assert!(out.status.success()); - } else { - assert!(!out.status.success()); - } + let out = run_roc([CMD_FORMAT, file.to_str().unwrap(), CHECK_FLAG], &[]); + + assert_eq!(out.status.success(), expects_success_exit_code); } - fn build_example(file: &Path, flags: &[&str]) -> Out { - let compile_out = run_roc(&[&["build", file.to_str().unwrap()], flags].concat()); - if !compile_out.stderr.is_empty() { - panic!("roc build had stderr: {}", compile_out.stderr); + fn run_roc_on<'a, I: IntoIterator>( + file: &'a Path, + args: I, + stdin: &[&str], + input_file: Option, + ) -> Out { + let compile_out = match input_file { + Some(input_file) => run_roc( + // converting these all to String avoids lifetime issues + args.into_iter().map(|arg| arg.to_string()).chain([ + file.to_str().unwrap().to_string(), + input_file.to_str().unwrap().to_string(), + ]), + stdin, + ), + None => run_roc( + args.into_iter().chain(iter::once(file.to_str().unwrap())), + stdin, + ), + }; + + if !compile_out.stderr.is_empty() && + // If there is any stderr, it should be reporting the runtime and that's it! + !(compile_out.stderr.starts_with("runtime: ") + && compile_out.stderr.ends_with("ms\n")) + { + panic!( + "`roc` command had unexpected stderr: {}", + compile_out.stderr + ); } assert!(compile_out.status.success(), "bad status {:?}", compile_out); @@ -106,89 +147,104 @@ mod cli_run { expected_ending: &str, use_valgrind: bool, ) { - let mut all_flags = vec![]; - all_flags.extend_from_slice(flags); + for cli_mode in CliMode::iter() { + let flags = { + let mut vec = flags.to_vec(); - if use_valgrind { - all_flags.extend_from_slice(&["--valgrind"]); - } + if use_valgrind { + vec.push(VALGRIND_FLAG); + } - build_example(file, &all_flags[..]); - - let out = if use_valgrind && ALLOW_VALGRIND { - let (valgrind_out, raw_xml) = if let Some(ref input_file) = input_file { - run_with_valgrind( - stdin, - &[ - file.with_file_name(executable_filename).to_str().unwrap(), - input_file.to_str().unwrap(), - ], - ) - } else { - run_with_valgrind( - stdin, - &[file.with_file_name(executable_filename).to_str().unwrap()], - ) + vec.into_iter() }; - if valgrind_out.status.success() { - let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| { - panic!("failed to parse the `valgrind` xml output. Error was:\n\n{:?}\n\nvalgrind xml was: \"{}\"\n\nvalgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", err, raw_xml, valgrind_out.stdout, valgrind_out.stderr); - }); + let out = match cli_mode { + CliMode::RocBuild => { + run_roc_on(file, iter::once(CMD_BUILD).chain(flags.clone()), &[], None); - if !memory_errors.is_empty() { - for error in memory_errors { - let ValgrindError { - kind, - what: _, - xwhat, - } = error; - println!("Valgrind Error: {}\n", kind); + if use_valgrind && ALLOW_VALGRIND { + let (valgrind_out, raw_xml) = if let Some(ref input_file) = input_file { + run_with_valgrind( + stdin.clone().iter().copied(), + &[ + file.with_file_name(executable_filename).to_str().unwrap(), + input_file.clone().to_str().unwrap(), + ], + ) + } else { + run_with_valgrind( + stdin.clone().iter().copied(), + &[file.with_file_name(executable_filename).to_str().unwrap()], + ) + }; - if let Some(ValgrindErrorXWhat { - text, - leakedbytes: _, - leakedblocks: _, - }) = xwhat - { - println!(" {}", text); + if valgrind_out.status.success() { + let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| { + panic!("failed to parse the `valgrind` xml output. Error was:\n\n{:?}\n\nvalgrind xml was: \"{}\"\n\nvalgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", err, raw_xml, valgrind_out.stdout, valgrind_out.stderr); + }); + + if !memory_errors.is_empty() { + for error in memory_errors { + let ValgrindError { + kind, + what: _, + xwhat, + } = error; + println!("Valgrind Error: {}\n", kind); + + if let Some(ValgrindErrorXWhat { + text, + leakedbytes: _, + leakedblocks: _, + }) = xwhat + { + println!(" {}", text); + } + } + panic!("Valgrind reported memory errors"); + } + } else { + let exit_code = match valgrind_out.status.code() { + Some(code) => format!("exit code {}", code), + None => "no exit code".to_string(), + }; + + panic!("`valgrind` exited with {}. valgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", exit_code, valgrind_out.stdout, valgrind_out.stderr); } - } - panic!( - "Valgrind reported memory errors in {:?} with flags {:?}", - file, flags - ); - } - } else { - let exit_code = match valgrind_out.status.code() { - Some(code) => format!("exit code {}", code), - None => "no exit code".to_string(), - }; - panic!("`valgrind` exited with {}. valgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", exit_code, valgrind_out.stdout, valgrind_out.stderr); + valgrind_out + } else if let Some(ref input_file) = input_file { + run_cmd( + file.with_file_name(executable_filename).to_str().unwrap(), + stdin.iter().copied(), + &[input_file.to_str().unwrap()], + ) + } else { + run_cmd( + file.with_file_name(executable_filename).to_str().unwrap(), + stdin.iter().copied(), + &[], + ) + } + } + CliMode::Roc => run_roc_on(file, flags.clone(), stdin, input_file.clone()), + CliMode::RocRun => run_roc_on( + file, + iter::once(CMD_RUN).chain(flags.clone()), + stdin, + input_file.clone(), + ), + }; + + if !&out.stdout.ends_with(expected_ending) { + panic!( + "expected output to end with {:?} but instead got {:#?} - stderr was: {:#?}", + expected_ending, out.stdout, out.stderr + ); } - valgrind_out - } else if let Some(input_file) = input_file { - run_cmd( - file.with_file_name(executable_filename).to_str().unwrap(), - stdin, - &[input_file.to_str().unwrap()], - ) - } else { - run_cmd( - file.with_file_name(executable_filename).to_str().unwrap(), - stdin, - &[], - ) - }; - if !&out.stdout.ends_with(expected_ending) { - panic!( - "expected output to end with {:?} but instead got {:#?} - stderr was: {:#?}", - expected_ending, out.stdout, out.stderr - ); + assert!(out.status.success()); } - assert!(out.status.success()); } #[cfg(feature = "wasm32-cli-run")] @@ -202,9 +258,13 @@ mod cli_run { ) { assert_eq!(input_file, None, "Wasm does not support input files"); let mut flags = flags.to_vec(); - flags.push("--target=wasm32"); + flags.push(concatcp!(TARGET_FLAG, "=wasm32")); - let compile_out = run_roc(&[&["build", file.to_str().unwrap()], flags.as_slice()].concat()); + let compile_out = run_roc( + [CMD_BUILD, file.to_str().unwrap()] + .iter() + .chain(flags.as_slice()), + ); if !compile_out.stderr.is_empty() { panic!("{}", compile_out.stderr); } @@ -261,7 +321,7 @@ mod cli_run { } "hello-gui" | "breakout" => { // Since these require opening a window, we do `roc build` on them but don't run them. - build_example(&file_name, &["--optimize"]); + run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], None); return; } @@ -286,7 +346,7 @@ mod cli_run { &file_name, example.stdin, example.executable_filename, - &["--optimize"], + &[OPTIMIZE_FLAG], example.input_file.and_then(|file| Some(example_file(dir_name, file))), example.expected_ending, example.use_valgrind, @@ -299,7 +359,7 @@ mod cli_run { &file_name, example.stdin, example.executable_filename, - &["--linker", "legacy"], + &[LINKER_FLAG, "legacy"], example.input_file.and_then(|file| Some(example_file(dir_name, file))), example.expected_ending, example.use_valgrind, @@ -516,7 +576,7 @@ mod cli_run { &file_name, benchmark.stdin, benchmark.executable_filename, - &["--optimize"], + &[OPTIMIZE_FLAG], benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), benchmark.expected_ending, benchmark.use_valgrind, @@ -558,7 +618,7 @@ mod cli_run { &file_name, benchmark.stdin, benchmark.executable_filename, - &["--optimize"], + &[OPTIMIZE_FLAG], benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), benchmark.expected_ending, ); @@ -590,7 +650,7 @@ mod cli_run { &file_name, benchmark.stdin, benchmark.executable_filename, - &["--target=x86_32"], + [concatcp!(TARGET_FLAG, "=x86_32")], benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), benchmark.expected_ending, benchmark.use_valgrind, @@ -600,7 +660,7 @@ mod cli_run { &file_name, benchmark.stdin, benchmark.executable_filename, - &["--target=x86_32", "--optimize"], + [concatcp!(TARGET_FLAG, "=x86_32"), OPTIMIZE_FLAG], benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))), benchmark.expected_ending, benchmark.use_valgrind, @@ -819,7 +879,7 @@ mod cli_run { &fixture_file("multi-dep-str", "Main.roc"), &[], "multi-dep-str", - &["--optimize"], + &[OPTIMIZE_FLAG], None, "I am Dep2.str2\n", true, @@ -847,7 +907,7 @@ mod cli_run { &fixture_file("multi-dep-thunk", "Main.roc"), &[], "multi-dep-thunk", - &["--optimize"], + &[OPTIMIZE_FLAG], None, "I am Dep2.value2\n", true, diff --git a/cli_utils/Cargo.toml b/cli_utils/Cargo.toml index 6f90db6737..a309bde670 100644 --- a/cli_utils/Cargo.toml +++ b/cli_utils/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0.130", features = ["derive"] } serde-xml-rs = "0.5.1" strip-ansi-escapes = "0.1.1" tempfile = "3.2.0" +const_format = "0.2.22" [target.'cfg(unix)'.dependencies] rlimit = "0.6.2" diff --git a/cli_utils/src/bench_utils.rs b/cli_utils/src/bench_utils.rs index d14de367bd..8ba4727efd 100644 --- a/cli_utils/src/bench_utils.rs +++ b/cli_utils/src/bench_utils.rs @@ -1,9 +1,13 @@ use crate::helpers::{example_file, run_cmd, run_roc}; +use const_format::concatcp; use criterion::{black_box, measurement::Measurement, BenchmarkGroup}; +use roc_cli::CMD_BUILD; use std::{path::Path, thread}; const CFOLD_STACK_SIZE: usize = 8192 * 100000; +const OPTIMIZE_FLAG: &str = concatcp!("--", roc_cli::FLAG_OPTIMIZE); + fn exec_bench_w_input( file: &Path, stdin_str: &'static str, @@ -11,9 +15,10 @@ fn exec_bench_w_input( expected_ending: &str, bench_group_opt: Option<&mut BenchmarkGroup>, ) { - let flags: &[&str] = &["--optimize"]; - - let compile_out = run_roc(&[&["build", file.to_str().unwrap()], flags].concat()); + let compile_out = run_roc( + [CMD_BUILD, OPTIMIZE_FLAG, file.to_str().unwrap()], + &[stdin_str], + ); if !compile_out.stderr.is_empty() { panic!("{}", compile_out.stderr); @@ -45,12 +50,12 @@ fn check_cmd_output( let out = if cmd_str.contains("cfold") { let child = thread::Builder::new() .stack_size(CFOLD_STACK_SIZE) - .spawn(move || run_cmd(&cmd_str, &[stdin_str], &[])) + .spawn(move || run_cmd(&cmd_str, [stdin_str], &[])) .unwrap(); child.join().unwrap() } else { - run_cmd(&cmd_str, &[stdin_str], &[]) + run_cmd(&cmd_str, [stdin_str], &[]) }; if !&out.stdout.ends_with(expected_ending) { @@ -93,12 +98,12 @@ fn bench_cmd( if let Some(bench_group) = bench_group_opt { bench_group.bench_function(&format!("Benchmarking {:?}", executable_filename), |b| { - b.iter(|| run_cmd(black_box(&cmd_str), black_box(&[stdin_str]), &[])) + b.iter(|| run_cmd(black_box(&cmd_str), black_box([stdin_str]), &[])) }); } else { run_cmd( black_box(file.with_file_name(executable_filename).to_str().unwrap()), - black_box(&[stdin_str]), + black_box([stdin_str]), &[], ); } diff --git a/cli_utils/src/helpers.rs b/cli_utils/src/helpers.rs index e83556176e..96d330a8b6 100644 --- a/cli_utils/src/helpers.rs +++ b/cli_utils/src/helpers.rs @@ -7,6 +7,7 @@ extern crate tempfile; use serde::Deserialize; use serde_xml_rs::from_str; use std::env; +use std::ffi::OsStr; use std::io::Read; use std::io::Write; use std::path::PathBuf; @@ -44,18 +45,34 @@ pub fn path_to_roc_binary() -> PathBuf { path } -#[allow(dead_code)] -pub fn run_roc(args: &[&str]) -> Out { +pub fn run_roc, S: AsRef>(args: I, stdin_vals: &[&str]) -> Out { let mut cmd = Command::new(path_to_roc_binary()); for arg in args { cmd.arg(arg); } - let output = cmd - .output() + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() .expect("failed to execute compiled `roc` binary in CLI test"); + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + + for stdin_str in stdin_vals.iter() { + stdin + .write_all(stdin_str.as_bytes()) + .expect("Failed to write to stdin"); + } + } + + let output = child + .wait_with_output() + .expect("failed to get output for compiled `roc` binary in CLI test"); + Out { stdout: String::from_utf8(output.stdout).unwrap(), stderr: String::from_utf8(output.stderr).unwrap(), @@ -63,8 +80,11 @@ pub fn run_roc(args: &[&str]) -> Out { } } -#[allow(dead_code)] -pub fn run_cmd(cmd_name: &str, stdin_vals: &[&str], args: &[&str]) -> Out { +pub fn run_cmd<'a, I: IntoIterator>( + cmd_name: &str, + stdin_vals: I, + args: &[&str], +) -> Out { let mut cmd = Command::new(cmd_name); for arg in args { @@ -99,8 +119,10 @@ pub fn run_cmd(cmd_name: &str, stdin_vals: &[&str], args: &[&str]) -> Out { } } -#[allow(dead_code)] -pub fn run_with_valgrind(stdin_vals: &[&str], args: &[&str]) -> (Out, String) { +pub fn run_with_valgrind<'a, I: IntoIterator>( + stdin_vals: I, + args: &[&str], +) -> (Out, String) { //TODO: figure out if there is a better way to get the valgrind executable. let mut cmd = Command::new("valgrind"); let named_tempfile = diff --git a/examples/false-interpreter/platform/src/lib.rs b/examples/false-interpreter/platform/src/lib.rs index 0b60869fa7..7efee7307c 100644 --- a/examples/false-interpreter/platform/src/lib.rs +++ b/examples/false-interpreter/platform/src/lib.rs @@ -74,7 +74,9 @@ pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut #[no_mangle] pub extern "C" fn rust_main() -> i32 { - let arg = env::args().nth(1).unwrap(); + let arg = env::args() + .nth(1) + .expect("Please pass a .false file as a command-line argument to the false interpreter!"); let arg = RocStr::from(arg.as_str()); let size = unsafe { roc_main_size() } as usize;