From 40741c5b0b1beee02cbd916a66ed7ecb4e0d63e1 Mon Sep 17 00:00:00 2001 From: Abdou Seck Date: Wed, 3 Jun 2020 17:18:48 -0400 Subject: [PATCH 1/4] Use .to_string rather than format macro --- src/verify.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index c9a7b6b8..6e0e45ec 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -124,16 +124,16 @@ fn prompt_for_completion(exercise: &Exercise, prompt_output: Option) -> Mode::Clippy => "The code is compiling, and 📎 Clippy 📎 is happy!", }; - println!(""); + println!(); println!("🎉 🎉 {} 🎉 🎉", success_msg); - println!(""); + println!(); if let Some(output) = prompt_output { println!("Output:"); println!("{}", separator()); println!("{}", output); println!("{}", separator()); - println!(""); + println!(); } println!("You can keep working on this exercise,"); @@ -141,12 +141,12 @@ fn prompt_for_completion(exercise: &Exercise, prompt_output: Option) -> "or jump into the next one by removing the {} comment:", style("`I AM NOT DONE`").bold() ); - println!(""); + println!(); for context_line in context { let formatted_line = if context_line.important { format!("{}", style(context_line.line).bold()) } else { - format!("{}", context_line.line) + context_line.line.to_string() }; println!( From 02a2fe48714a4546b28d38fb611e6bfce9f43cf6 Mon Sep 17 00:00:00 2001 From: Abdou Seck Date: Wed, 3 Jun 2020 17:19:28 -0400 Subject: [PATCH 2/4] Collapse nested if statements --- src/main.rs | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4fd60831..f3f7f071 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,8 +27,22 @@ fn main() { .version(crate_version!()) .author("Olivia Hugger, Carol Nichols") .about("Rustlings is a collection of small exercises to get you used to writing and reading Rust code") - .subcommand(SubCommand::with_name("verify").alias("v").about("Verifies all exercises according to the recommended order")) - .subcommand(SubCommand::with_name("watch").alias("w").about("Reruns `verify` when files were edited")) + .arg( + Arg::with_name("verbose") + .short("V") + .long("verbose") + .help("Show tests' standard output") + ) + .subcommand( + SubCommand::with_name("verify") + .alias("v") + .about("Verifies all exercises according to the recommended order") + ) + .subcommand( + SubCommand::with_name("watch") + .alias("w") + .about("Reruns `verify` when files were edited") + ) .subcommand( SubCommand::with_name("run") .alias("r") @@ -43,7 +57,7 @@ fn main() { ) .get_matches(); - if None == matches.subcommand_name() { + if matches.subcommand_name().is_none() { println!(); println!(r#" welcome to... "#); println!(r#" _ _ _ "#); @@ -105,22 +119,20 @@ fn main() { verify(&exercises).unwrap_or_else(|_| std::process::exit(1)); } - if matches.subcommand_matches("watch").is_some() { - if watch(&exercises).is_ok() { - println!( - "{emoji} All exercises completed! {emoji}", - emoji = Emoji("🎉", "★") - ); - println!(""); - println!("We hope you enjoyed learning about the various aspects of Rust!"); - println!( - "If you noticed any issues, please don't hesitate to report them to our repo." - ); - println!("You can also contribute your own exercises to help the greater community!"); - println!(""); - println!("Before reporting an issue or contributing, please read our guidelines:"); - println!("https://github.com/rust-lang/rustlings/blob/master/CONTRIBUTING.md"); - } + if matches.subcommand_matches("watch").is_some() && watch(&exercises).is_ok() { + println!( + "{emoji} All exercises completed! {emoji}", + emoji = Emoji("🎉", "★") + ); + println!(); + println!("We hope you enjoyed learning about the various aspects of Rust!"); + println!( + "If you noticed any issues, please don't hesitate to report them to our repo." + ); + println!("You can also contribute your own exercises to help the greater community!"); + println!(); + println!("Before reporting an issue or contributing, please read our guidelines:"); + println!("https://github.com/rust-lang/rustlings/blob/master/CONTRIBUTING.md"); } if matches.subcommand_name().is_none() { From 8ad5f9bf531a4848b1104b7b389a20171624c82f Mon Sep 17 00:00:00 2001 From: Abdou Seck Date: Thu, 4 Jun 2020 10:31:17 -0400 Subject: [PATCH 3/4] feat: Add a --nocapture option to display test harnesses' outputs This new feature can be accessed by invoking rustlings with --nocapture. Both unit and integration tests added. closes #262 BREAKING CHANGES: The following function take a new boolean argument: * `run` * `verify` * `test` * `compile_and_test` --- info.toml | 4 +-- src/exercise.rs | 44 +++++++++++++++++++++++++++- src/main.rs | 20 ++++++------- src/run.rs | 11 +++++-- src/verify.rs | 32 ++++++++++++++++---- tests/fixture/success/testSuccess.rs | 1 + tests/integration_tests.rs | 22 ++++++++++++++ 7 files changed, 113 insertions(+), 21 deletions(-) diff --git a/info.toml b/info.toml index 2c871ad2..2f0884c6 100644 --- a/info.toml +++ b/info.toml @@ -802,7 +802,7 @@ name = "try_from_into" path = "exercises/conversions/try_from_into.rs" mode = "test" hint = """ -Follow the steps provided right before the `From` implementation. +Follow the steps provided right before the `TryFrom` implementation. You can also use the example at https://doc.rust-lang.org/std/convert/trait.TryFrom.html""" [[exercises]] @@ -819,4 +819,4 @@ mode = "test" hint = """ The implementation of FromStr should return an Ok with a Person object, or an Err with a string if the string is not valid. -This is a some like an `try_from_into` exercise.""" +This is almost like the `try_from_into` exercise.""" diff --git a/src/exercise.rs b/src/exercise.rs index d1eaa1a6..177b7f38 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -11,15 +11,21 @@ const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE"; const CONTEXT: usize = 2; const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/clippy/Cargo.toml"; +// Get a temporary file name that is hopefully unique to this process +#[inline] fn temp_file() -> String { format!("./temp_{}", process::id()) } +// The mode of the exercise. #[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum Mode { + // Indicates that the exercise should be compiled as a binary Compile, + // Indicates that the exercise should be compiled as a test harness Test, + // Indicates that the exercise should be linted with clippy Clippy, } @@ -28,41 +34,60 @@ pub struct ExerciseList { pub exercises: Vec, } +// A representation of a rustlings exercise. +// This is deserialized from the accompanying info.toml file #[derive(Deserialize)] pub struct Exercise { + // Name of the exercise pub name: String, + // The path to the file containing the exercise's source code pub path: PathBuf, + // The mode of the exercise (Test, Compile, or Clippy) pub mode: Mode, + // The hint text associated with the exercise pub hint: String, } +// An enum to track of the state of an Exercise. +// An Exercise can be either Done or Pending #[derive(PartialEq, Debug)] pub enum State { + // The state of the exercise once it's been completed Done, + // The state of the exercise while it's not completed yet Pending(Vec), } +// The context information of a pending exercise #[derive(PartialEq, Debug)] pub struct ContextLine { + // The source code that is still pending completion pub line: String, + // The line number of the source code still pending completion pub number: usize, + // Whether or not this is important pub important: bool, } +// The result of compiling an exercise pub struct CompiledExercise<'a> { exercise: &'a Exercise, _handle: FileHandle, } impl<'a> CompiledExercise<'a> { + // Run the compiled exercise pub fn run(&self) -> Result { self.exercise.run() } } +// A representation of an already executed binary #[derive(Debug)] pub struct ExerciseOutput { + // The textual contents of the standard output of the binary pub stdout: String, + // The textual contents of the standard error of the binary pub stderr: String, } @@ -140,7 +165,11 @@ path = "{}.rs""#, } fn run(&self) -> Result { - let cmd = Command::new(&temp_file()) + let arg = match self.mode { + Mode::Test => "--show-output", + _ => "" + }; + let cmd = Command::new(&temp_file()).arg(arg) .output() .expect("Failed to run 'run' command"); @@ -205,6 +234,7 @@ impl Display for Exercise { } } +#[inline] fn clean() { let _ignored = remove_file(&temp_file()); } @@ -280,4 +310,16 @@ mod test { assert_eq!(exercise.state(), State::Done); } + + #[test] + fn test_exercise_with_output() { + let exercise = Exercise { + name: "finished_exercise".into(), + path: PathBuf::from("tests/fixture/success/testSuccess.rs"), + mode: Mode::Test, + hint: String::new(), + }; + let out = exercise.compile().unwrap().run().unwrap(); + assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); + } } diff --git a/src/main.rs b/src/main.rs index f3f7f071..9c64de2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,10 +28,9 @@ fn main() { .author("Olivia Hugger, Carol Nichols") .about("Rustlings is a collection of small exercises to get you used to writing and reading Rust code") .arg( - Arg::with_name("verbose") - .short("V") - .long("verbose") - .help("Show tests' standard output") + Arg::with_name("nocapture") + .long("nocapture") + .help("Show outputs from the test exercises") ) .subcommand( SubCommand::with_name("verify") @@ -87,6 +86,7 @@ fn main() { let toml_str = &fs::read_to_string("info.toml").unwrap(); let exercises = toml::from_str::(toml_str).unwrap().exercises; + let verbose = matches.is_present("nocapture"); if let Some(ref matches) = matches.subcommand_matches("run") { let name = matches.value_of("name").unwrap(); @@ -98,7 +98,7 @@ fn main() { std::process::exit(1) }); - run(&exercise).unwrap_or_else(|_| std::process::exit(1)); + run(&exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); } if let Some(ref matches) = matches.subcommand_matches("hint") { @@ -116,10 +116,10 @@ fn main() { } if matches.subcommand_matches("verify").is_some() { - verify(&exercises).unwrap_or_else(|_| std::process::exit(1)); + verify(&exercises, verbose).unwrap_or_else(|_| std::process::exit(1)); } - if matches.subcommand_matches("watch").is_some() && watch(&exercises).is_ok() { + if matches.subcommand_matches("watch").is_some() && watch(&exercises, verbose).is_ok() { println!( "{emoji} All exercises completed! {emoji}", emoji = Emoji("🎉", "★") @@ -161,7 +161,7 @@ fn spawn_watch_shell(failed_exercise_hint: &Arc>>) { }); } -fn watch(exercises: &[Exercise]) -> notify::Result<()> { +fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> { /* Clears the terminal with an ANSI escape code. Works in UNIX and newer Windows terminals. */ fn clear_screen() { @@ -176,7 +176,7 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> { clear_screen(); let to_owned_hint = |t: &Exercise| t.hint.to_owned(); - let failed_exercise_hint = match verify(exercises.iter()) { + let failed_exercise_hint = match verify(exercises.iter(), verbose) { Ok(_) => return Ok(()), Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))), }; @@ -191,7 +191,7 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> { .iter() .skip_while(|e| !filepath.ends_with(&e.path)); clear_screen(); - match verify(pending_exercises) { + match verify(pending_exercises, verbose) { Ok(_) => return Ok(()), Err(exercise) => { let mut failed_exercise_hint = failed_exercise_hint.lock().unwrap(); diff --git a/src/run.rs b/src/run.rs index ebb0ae64..fdabb3e0 100644 --- a/src/run.rs +++ b/src/run.rs @@ -2,15 +2,22 @@ use crate::exercise::{Exercise, Mode}; use crate::verify::test; use indicatif::ProgressBar; -pub fn run(exercise: &Exercise) -> Result<(), ()> { +// Invoke the rust compiler on the path of the given exercise, +// and run the ensuing binary. +// The verbose argument helps determine whether or not to show +// the output from the test harnesses (if the mode of the exercise is test) +pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { match exercise.mode { - Mode::Test => test(exercise)?, + Mode::Test => test(exercise, verbose)?, Mode::Compile => compile_and_run(exercise)?, Mode::Clippy => compile_and_run(exercise)?, } Ok(()) } +// Invoke the rust compiler on the path of the given exercise +// and run the ensuing binary. +// This is strictly for non-test binaries, so output is displayed fn compile_and_run(exercise: &Exercise) -> Result<(), ()> { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); diff --git a/src/verify.rs b/src/verify.rs index 6e0e45ec..fac04919 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -2,10 +2,18 @@ use crate::exercise::{CompiledExercise, Exercise, Mode, State}; use console::style; use indicatif::ProgressBar; -pub fn verify<'a>(start_at: impl IntoIterator) -> Result<(), &'a Exercise> { +// Verify that the provided container of Exercise objects +// can be compiled and run without any failures. +// Any such failures will be reported to the end user. +// If the Exercise being verified is a test, the verbose boolean +// determines whether or not the test harness outputs are displayed. +pub fn verify<'a>( + start_at: impl IntoIterator, + verbose: bool +) -> Result<(), &'a Exercise> { for exercise in start_at { let compile_result = match exercise.mode { - Mode::Test => compile_and_test(&exercise, RunMode::Interactive), + Mode::Test => compile_and_test(&exercise, RunMode::Interactive, verbose), Mode::Compile => compile_and_run_interactively(&exercise), Mode::Clippy => compile_only(&exercise), }; @@ -21,11 +29,13 @@ enum RunMode { NonInteractive, } -pub fn test(exercise: &Exercise) -> Result<(), ()> { - compile_and_test(exercise, RunMode::NonInteractive)?; +// Compile and run the resulting test harness of the given Exercise +pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> { + compile_and_test(exercise, RunMode::NonInteractive, verbose)?; Ok(()) } +// Invoke the rust compiler without running the resulting binary fn compile_only(exercise: &Exercise) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); @@ -38,6 +48,7 @@ fn compile_only(exercise: &Exercise) -> Result { Ok(prompt_for_completion(&exercise, None)) } +// Compile the given Exercise and run the resulting binary in an interactive mode fn compile_and_run_interactively(exercise: &Exercise) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); @@ -63,7 +74,11 @@ fn compile_and_run_interactively(exercise: &Exercise) -> Result { Ok(prompt_for_completion(&exercise, Some(output.stdout))) } -fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result { +// Compile the given Exercise as a test harness and display +// the output if verbose is set to true +fn compile_and_test( + exercise: &Exercise, run_mode: RunMode, verbose: bool +) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Testing {}...", exercise).as_str()); progress_bar.enable_steady_tick(100); @@ -73,7 +88,10 @@ fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result progress_bar.finish_and_clear(); match result { - Ok(_) => { + Ok(output) => { + if verbose { + println!("{}", output.stdout); + } success!("Successfully tested {}", &exercise); if let RunMode::Interactive = run_mode { Ok(prompt_for_completion(&exercise, None)) @@ -92,6 +110,8 @@ fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result } } +// Compile the given Exercise and return an object with information +// about the state of the compilation fn compile<'a, 'b>( exercise: &'a Exercise, progress_bar: &'b ProgressBar, diff --git a/tests/fixture/success/testSuccess.rs b/tests/fixture/success/testSuccess.rs index 589057cc..7139b50b 100644 --- a/tests/fixture/success/testSuccess.rs +++ b/tests/fixture/success/testSuccess.rs @@ -1,4 +1,5 @@ #[test] fn passing() { + println!("THIS TEST TOO SHALL PASS"); assert!(true); } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 683e5640..0f49b5a8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -159,3 +159,25 @@ fn run_test_exercise_does_not_prompt() { .code(0) .stdout(predicates::str::contains("I AM NOT DONE").not()); } + +#[test] +fn run_single_test_success_with_output() { + Command::cargo_bin("rustlings") + .unwrap() + .args(&["--nocapture", "r", "testSuccess"]) + .current_dir("tests/fixture/success/") + .assert() + .code(0) + .stdout(predicates::str::contains("THIS TEST TOO SHALL PAS")); +} + +#[test] +fn run_single_test_success_without_output() { + Command::cargo_bin("rustlings") + .unwrap() + .args(&["r", "testSuccess"]) + .current_dir("tests/fixture/success/") + .assert() + .code(0) + .stdout(predicates::str::contains("THIS TEST TOO SHALL PAS").not()); +} \ No newline at end of file From 9e4fb1009f1c9e3433915c03e22c2af422e5c5fe Mon Sep 17 00:00:00 2001 From: Abdou Seck Date: Fri, 5 Jun 2020 16:33:14 -0400 Subject: [PATCH 4/4] fix(installation): Provide a backup git reference when tag can't be curl closes #423 If the parsed JSON data curled during a bash installation is not valid, use the repository's tag files as a backup. If those files don't exist somehow, then checkout the master branch and install it. --- install.sh | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 532728e3..c32f5126 100755 --- a/install.sh +++ b/install.sh @@ -102,12 +102,30 @@ Path=${1:-rustlings/} echo "Cloning Rustlings at $Path..." git clone -q https://github.com/rust-lang/rustlings $Path +cd $Path + Version=$(curl -s https://api.github.com/repos/rust-lang/rustlings/releases/latest | ${PY} -c "import json,sys;obj=json.load(sys.stdin);print(obj['tag_name']);") CargoBin="${CARGO_HOME:-$HOME/.cargo}/bin" +if [[ -z ${Version} ]] +then + echo "The latest tag version could not be fetched remotely." + echo "Using the local git repository..." + Version=$(ls -tr .git/refs/tags/ | tail -1) + if [[ -z ${Version} ]] + then + echo "No valid tag version found" + echo "Rustlings will be installed using the master branch" + Version="master" + else + Version="tags/${Version}" + fi +else + Version="tags/${Version}" +fi + echo "Checking out version $Version..." -cd $Path -git checkout -q tags/$Version +git checkout -q ${Version} echo "Installing the 'rustlings' executable..." cargo install --force --path .