Merge pull request #422 from AbdouSeck/show-tests-prints

This commit is contained in:
fmoko 2020-06-12 23:04:51 +02:00 committed by GitHub
commit e1e453075f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 164 additions and 42 deletions

View File

@ -804,7 +804,7 @@ name = "try_from_into"
path = "exercises/conversions/try_from_into.rs" path = "exercises/conversions/try_from_into.rs"
mode = "test" mode = "test"
hint = """ 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""" You can also use the example at https://doc.rust-lang.org/std/convert/trait.TryFrom.html"""
[[exercises]] [[exercises]]
@ -821,4 +821,4 @@ mode = "test"
hint = """ hint = """
The implementation of FromStr should return an Ok with a Person object, The implementation of FromStr should return an Ok with a Person object,
or an Err with a string if the string is not valid. 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."""

View File

@ -102,12 +102,30 @@ Path=${1:-rustlings/}
echo "Cloning Rustlings at $Path..." echo "Cloning Rustlings at $Path..."
git clone -q https://github.com/rust-lang/rustlings $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']);") 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" 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..." echo "Checking out version $Version..."
cd $Path git checkout -q ${Version}
git checkout -q tags/$Version
echo "Installing the 'rustlings' executable..." echo "Installing the 'rustlings' executable..."
cargo install --force --path . cargo install --force --path .

View File

@ -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 CONTEXT: usize = 2;
const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/clippy/Cargo.toml"; 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 { fn temp_file() -> String {
format!("./temp_{}", process::id()) format!("./temp_{}", process::id())
} }
// The mode of the exercise.
#[derive(Deserialize, Copy, Clone)] #[derive(Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Mode { pub enum Mode {
// Indicates that the exercise should be compiled as a binary
Compile, Compile,
// Indicates that the exercise should be compiled as a test harness
Test, Test,
// Indicates that the exercise should be linted with clippy
Clippy, Clippy,
} }
@ -28,41 +34,60 @@ pub struct ExerciseList {
pub exercises: Vec<Exercise>, pub exercises: Vec<Exercise>,
} }
// A representation of a rustlings exercise.
// This is deserialized from the accompanying info.toml file
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Exercise { pub struct Exercise {
// Name of the exercise
pub name: String, pub name: String,
// The path to the file containing the exercise's source code
pub path: PathBuf, pub path: PathBuf,
// The mode of the exercise (Test, Compile, or Clippy)
pub mode: Mode, pub mode: Mode,
// The hint text associated with the exercise
pub hint: String, pub hint: String,
} }
// An enum to track of the state of an Exercise.
// An Exercise can be either Done or Pending
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum State { pub enum State {
// The state of the exercise once it's been completed
Done, Done,
// The state of the exercise while it's not completed yet
Pending(Vec<ContextLine>), Pending(Vec<ContextLine>),
} }
// The context information of a pending exercise
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct ContextLine { pub struct ContextLine {
// The source code that is still pending completion
pub line: String, pub line: String,
// The line number of the source code still pending completion
pub number: usize, pub number: usize,
// Whether or not this is important
pub important: bool, pub important: bool,
} }
// The result of compiling an exercise
pub struct CompiledExercise<'a> { pub struct CompiledExercise<'a> {
exercise: &'a Exercise, exercise: &'a Exercise,
_handle: FileHandle, _handle: FileHandle,
} }
impl<'a> CompiledExercise<'a> { impl<'a> CompiledExercise<'a> {
// Run the compiled exercise
pub fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> { pub fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
self.exercise.run() self.exercise.run()
} }
} }
// A representation of an already executed binary
#[derive(Debug)] #[derive(Debug)]
pub struct ExerciseOutput { pub struct ExerciseOutput {
// The textual contents of the standard output of the binary
pub stdout: String, pub stdout: String,
// The textual contents of the standard error of the binary
pub stderr: String, pub stderr: String,
} }
@ -140,7 +165,11 @@ path = "{}.rs""#,
} }
fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> { fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
let cmd = Command::new(&temp_file()) let arg = match self.mode {
Mode::Test => "--show-output",
_ => ""
};
let cmd = Command::new(&temp_file()).arg(arg)
.output() .output()
.expect("Failed to run 'run' command"); .expect("Failed to run 'run' command");
@ -205,6 +234,7 @@ impl Display for Exercise {
} }
} }
#[inline]
fn clean() { fn clean() {
let _ignored = remove_file(&temp_file()); let _ignored = remove_file(&temp_file());
} }
@ -280,4 +310,16 @@ mod test {
assert_eq!(exercise.state(), State::Done); 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"));
}
} }

View File

@ -27,8 +27,21 @@ fn main() {
.version(crate_version!()) .version(crate_version!())
.author("Olivia Hugger, Carol Nichols") .author("Olivia Hugger, Carol Nichols")
.about("Rustlings is a collection of small exercises to get you used to writing and reading Rust code") .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")) .arg(
.subcommand(SubCommand::with_name("watch").alias("w").about("Reruns `verify` when files were edited")) Arg::with_name("nocapture")
.long("nocapture")
.help("Show outputs from the test exercises")
)
.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(
SubCommand::with_name("run") SubCommand::with_name("run")
.alias("r") .alias("r")
@ -43,7 +56,7 @@ fn main() {
) )
.get_matches(); .get_matches();
if None == matches.subcommand_name() { if matches.subcommand_name().is_none() {
println!(); println!();
println!(r#" welcome to... "#); println!(r#" welcome to... "#);
println!(r#" _ _ _ "#); println!(r#" _ _ _ "#);
@ -73,6 +86,7 @@ fn main() {
let toml_str = &fs::read_to_string("info.toml").unwrap(); let toml_str = &fs::read_to_string("info.toml").unwrap();
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises; let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
let verbose = matches.is_present("nocapture");
if let Some(ref matches) = matches.subcommand_matches("run") { if let Some(ref matches) = matches.subcommand_matches("run") {
let name = matches.value_of("name").unwrap(); let name = matches.value_of("name").unwrap();
@ -84,7 +98,7 @@ fn main() {
std::process::exit(1) 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") { if let Some(ref matches) = matches.subcommand_matches("hint") {
@ -102,25 +116,23 @@ fn main() {
} }
if matches.subcommand_matches("verify").is_some() { 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() { if matches.subcommand_matches("watch").is_some() && watch(&exercises, verbose).is_ok() {
if watch(&exercises).is_ok() { println!(
println!( "{emoji} All exercises completed! {emoji}",
"{emoji} All exercises completed! {emoji}", emoji = Emoji("🎉", "")
emoji = Emoji("🎉", "") );
); println!();
println!(""); println!("We hope you enjoyed learning about the various aspects of Rust!");
println!("We hope you enjoyed learning about the various aspects of Rust!"); println!(
println!( "If you noticed any issues, please don't hesitate to report them to our repo."
"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!("You can also contribute your own exercises to help the greater community!"); println!();
println!(""); println!("Before reporting an issue or contributing, please read our guidelines:");
println!("Before reporting an issue or contributing, please read our guidelines:"); println!("https://github.com/rust-lang/rustlings/blob/master/CONTRIBUTING.md");
println!("https://github.com/rust-lang/rustlings/blob/master/CONTRIBUTING.md");
}
} }
if matches.subcommand_name().is_none() { if matches.subcommand_name().is_none() {
@ -149,7 +161,7 @@ fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>) {
}); });
} }
fn watch(exercises: &[Exercise]) -> notify::Result<()> { fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
/* Clears the terminal with an ANSI escape code. /* Clears the terminal with an ANSI escape code.
Works in UNIX and newer Windows terminals. */ Works in UNIX and newer Windows terminals. */
fn clear_screen() { fn clear_screen() {
@ -164,7 +176,7 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> {
clear_screen(); clear_screen();
let to_owned_hint = |t: &Exercise| t.hint.to_owned(); 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(()), Ok(_) => return Ok(()),
Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))), Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))),
}; };
@ -179,7 +191,7 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> {
.iter() .iter()
.skip_while(|e| !filepath.ends_with(&e.path)); .skip_while(|e| !filepath.ends_with(&e.path));
clear_screen(); clear_screen();
match verify(pending_exercises) { match verify(pending_exercises, verbose) {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(exercise) => { Err(exercise) => {
let mut failed_exercise_hint = failed_exercise_hint.lock().unwrap(); let mut failed_exercise_hint = failed_exercise_hint.lock().unwrap();

View File

@ -2,15 +2,22 @@ use crate::exercise::{Exercise, Mode};
use crate::verify::test; use crate::verify::test;
use indicatif::ProgressBar; 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 { match exercise.mode {
Mode::Test => test(exercise)?, Mode::Test => test(exercise, verbose)?,
Mode::Compile => compile_and_run(exercise)?, Mode::Compile => compile_and_run(exercise)?,
Mode::Clippy => compile_and_run(exercise)?, Mode::Clippy => compile_and_run(exercise)?,
} }
Ok(()) 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<(), ()> { fn compile_and_run(exercise: &Exercise) -> Result<(), ()> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); progress_bar.set_message(format!("Compiling {}...", exercise).as_str());

View File

@ -2,10 +2,18 @@ use crate::exercise::{CompiledExercise, Exercise, Mode, State};
use console::style; use console::style;
use indicatif::ProgressBar; use indicatif::ProgressBar;
pub fn verify<'a>(start_at: impl IntoIterator<Item = &'a Exercise>) -> 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<Item = &'a Exercise>,
verbose: bool
) -> Result<(), &'a Exercise> {
for exercise in start_at { for exercise in start_at {
let compile_result = match exercise.mode { 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::Compile => compile_and_run_interactively(&exercise),
Mode::Clippy => compile_only(&exercise), Mode::Clippy => compile_only(&exercise),
}; };
@ -21,11 +29,13 @@ enum RunMode {
NonInteractive, NonInteractive,
} }
pub fn test(exercise: &Exercise) -> Result<(), ()> { // Compile and run the resulting test harness of the given Exercise
compile_and_test(exercise, RunMode::NonInteractive)?; pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> {
compile_and_test(exercise, RunMode::NonInteractive, verbose)?;
Ok(()) Ok(())
} }
// Invoke the rust compiler without running the resulting binary
fn compile_only(exercise: &Exercise) -> Result<bool, ()> { fn compile_only(exercise: &Exercise) -> Result<bool, ()> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); progress_bar.set_message(format!("Compiling {}...", exercise).as_str());
@ -38,6 +48,7 @@ fn compile_only(exercise: &Exercise) -> Result<bool, ()> {
Ok(prompt_for_completion(&exercise, None)) 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<bool, ()> { fn compile_and_run_interactively(exercise: &Exercise) -> Result<bool, ()> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {}...", exercise).as_str()); progress_bar.set_message(format!("Compiling {}...", exercise).as_str());
@ -63,7 +74,11 @@ fn compile_and_run_interactively(exercise: &Exercise) -> Result<bool, ()> {
Ok(prompt_for_completion(&exercise, Some(output.stdout))) Ok(prompt_for_completion(&exercise, Some(output.stdout)))
} }
fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result<bool, ()> { // 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<bool, ()> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Testing {}...", exercise).as_str()); progress_bar.set_message(format!("Testing {}...", exercise).as_str());
progress_bar.enable_steady_tick(100); progress_bar.enable_steady_tick(100);
@ -73,7 +88,10 @@ fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result<bool, ()>
progress_bar.finish_and_clear(); progress_bar.finish_and_clear();
match result { match result {
Ok(_) => { Ok(output) => {
if verbose {
println!("{}", output.stdout);
}
success!("Successfully tested {}", &exercise); success!("Successfully tested {}", &exercise);
if let RunMode::Interactive = run_mode { if let RunMode::Interactive = run_mode {
Ok(prompt_for_completion(&exercise, None)) Ok(prompt_for_completion(&exercise, None))
@ -92,6 +110,8 @@ fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result<bool, ()>
} }
} }
// Compile the given Exercise and return an object with information
// about the state of the compilation
fn compile<'a, 'b>( fn compile<'a, 'b>(
exercise: &'a Exercise, exercise: &'a Exercise,
progress_bar: &'b ProgressBar, progress_bar: &'b ProgressBar,
@ -124,16 +144,16 @@ fn prompt_for_completion(exercise: &Exercise, prompt_output: Option<String>) ->
Mode::Clippy => "The code is compiling, and 📎 Clippy 📎 is happy!", Mode::Clippy => "The code is compiling, and 📎 Clippy 📎 is happy!",
}; };
println!(""); println!();
println!("🎉 🎉 {} 🎉 🎉", success_msg); println!("🎉 🎉 {} 🎉 🎉", success_msg);
println!(""); println!();
if let Some(output) = prompt_output { if let Some(output) = prompt_output {
println!("Output:"); println!("Output:");
println!("{}", separator()); println!("{}", separator());
println!("{}", output); println!("{}", output);
println!("{}", separator()); println!("{}", separator());
println!(""); println!();
} }
println!("You can keep working on this exercise,"); println!("You can keep working on this exercise,");
@ -141,12 +161,12 @@ fn prompt_for_completion(exercise: &Exercise, prompt_output: Option<String>) ->
"or jump into the next one by removing the {} comment:", "or jump into the next one by removing the {} comment:",
style("`I AM NOT DONE`").bold() style("`I AM NOT DONE`").bold()
); );
println!(""); println!();
for context_line in context { for context_line in context {
let formatted_line = if context_line.important { let formatted_line = if context_line.important {
format!("{}", style(context_line.line).bold()) format!("{}", style(context_line.line).bold())
} else { } else {
format!("{}", context_line.line) context_line.line.to_string()
}; };
println!( println!(

View File

@ -1,4 +1,5 @@
#[test] #[test]
fn passing() { fn passing() {
println!("THIS TEST TOO SHALL PASS");
assert!(true); assert!(true);
} }

View File

@ -159,3 +159,25 @@ fn run_test_exercise_does_not_prompt() {
.code(0) .code(0)
.stdout(predicates::str::contains("I AM NOT DONE").not()); .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());
}